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

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