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>
This commit is contained in:
2026-06-25 13:55:22 -07:00
parent 778e12d83e
commit 1e80fb24db
4 changed files with 263 additions and 117 deletions

View File

@@ -70,30 +70,59 @@ sourceName, responseData, signed, managed, archived`.
| Alerts | `GET /Alerts` (filter `where` org/severity/createdOn) |
| Alert detail | `GET /Alerts/{id}` |
| Extensions | `GET /Extensions` |
| Agent reg keys | `GET /agentKeys` (for deploy one-liner) |
| 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 (shape-verified from the InfocyteHUNTAPI module; gate behind --confirm)
## 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 a target group | `POST /targets/{targetGroupId}/scan` | `{"options":{...ScanOptions}, "where":{...}?}` |
| Scan a single target | `POST /targets/scan` | `{"target":"ip/host","targetGroup":{"id":"..."},"options":{...}}` |
| Response extension (isolate/kill/quarantine) | `POST /targets/scan` | `{"target":"...","targetGroup":{"id":"..."},"options":{"extensions":[{"id"|"name":...}]}}` |
| **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>"}` |
**ScanOptions** booleans: `process, module, driver, memory, account, artifact,
autostart, application, installed, hook, network, events`, plus
`extensions:[{id,args,order}]`.
**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).
These were **not executed** against the prod tenant during build (they scan/isolate
real client endpoints). Confirm the response-extension id/name via `GET /Extensions`
and run one deliberate test on an ACG-internal machine before relying on them.
**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 (`Install-EDR -InstanceName <cname> -RegKey <key>`). Pull
a live key from `GET /agentKeys`. Push via GuruRMM `/rmm` script execution or any
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