Files
claudetools/.claude/skills/datto-edr/SKILL.md
Howard Enos 79bda6fab9 datto-edr: apply code-review fixes (gating + footgun hardening)
- deploy-cmd: require explicit --regkey or --group; never auto-pick an
  arbitrary cross-client registration key (would enroll into wrong org).
- raw: block POST to any */scan endpoint with no non-empty `where`
  (same tenant-wide footgun the scan command guards against).
- main(): catch-all for unexpected exceptions -> [ERROR] + errorlog,
  plus clean KeyboardInterrupt (130).
- isolate: forgiving extension-name match (exact, then substring),
  excludes the paired "Restore" ext; errors on ambiguous match.
- detections: --site -> --target-group; Alert.targetGroupId is a
  scan-target id, not a Location id (distinct from `agents --site`).
- status: relabel "Target groups (sites)" -> "Scan target groups".
- SKILL.md + docstrings updated to match.

Verified: py_compile clean, selftest green (216 agents), guards fire
on no-key/empty-where/no-agent, deploy-cmd --group picks the group's key.
2026-06-25 15:14:18 -07:00

8.9 KiB

name, description
name description
datto-edr Control the ACG Datto EDR / Datto AV tenant (azcomp4587 — Datto EDR is rebranded Infocyte HUNT; per-tenant LoopBack REST API). Read the whole MSP fleet from ONE token: per-client organizations, sites, agents (online/AV/isolation/version), detections (MITRE-tagged alerts), and a per-client security-posture sweep. Gated actions: trigger scans, invoke response extensions (host isolation), and emit the agent install one-liner for RMM-pushed deployment. Read-only by default; mutating ops require --confirm. Triggers: datto edr, datto av, infocyte, EDR detections, isolate endpoint, run a scan, deploy edr agent, edr security sweep, endpoint detection response, azcomp4587.

Datto EDR (Infocyte HUNT) Skill

Standalone CLI for the Datto EDR REST API against the live ACG tenant (azcomp4587.infocyte.com). Datto EDR is the rebranded Infocyte HUNT platform, so the API is a per-tenant LoopBack service: https://<instance>.infocyte.com/api/<Model>. Read-only by default; mutating operations are gated behind --confirm.

One API token sees the entire MSP fleet (13 client orgs, 40 scan groups, 215 agents, 1500+ alerts as of build) — segmented per-client by Organization.

Running the CLI

PY="bash $CLAUDETOOLS_ROOT/.claude/scripts/py.sh"
EDR="$PY C:/claudetools/.claude/skills/datto-edr/scripts/edr.py"

$EDR status                      # tenant rollup
$EDR orgs                        # clients (Organizations)
$EDR sites --org <orgId>         # client sites (Locations)
$EDR agents --org <orgId>        # endpoints for a client
$EDR detections --org <orgId> --days 7
$EDR sweep                       # per-client posture, sorted by recent detections

Transport auto-selects httpx if installed, else stdlib urllib (no hard dependency).

Credentials

The token is NEVER hardcoded. Loaded at runtime from the SOPS vault:

bash "$CLAUDETOOLS_ROOT/.claude/scripts/vault.sh" \
     get-field msp-tools/datto-edr.sops.yaml credentials.api_token

Auth model (critical): the token is passed in the raw Authorization header — NOT Bearer <token>, no prefix. (Verified live 2026-06-25.) For testing you can override with DATTO_EDR_TOKEN and DATTO_EDR_BASE_URL. The token expires ~1 year after creation (~2027-06-25) — refresh it in the console (username menu → Admin → Users & Tokens → API Tokens) and update the vault entry before it lapses.

Data model (how the tenant is structured)

  • Organization = a client/company (orgs). Carries agentCount/alertCount.
  • Location = a client site (sites). Carries organizationId. Agents belong to a Location via Agent.locationId. This is the inventory hierarchy: Organization → Location → Agent.
  • Target = a scan target group (scan-targets), the scannable unit for the scan command (POST targets/{id}/scan). Distinct from Location (though they may share an id when a site maps 1:1 to a scan group).
  • deviceGroup = a global category ("Servers", "Workstations") spanning all orgs — NOT a per-client grouping. Not used for client scoping.
  • Alert = a detection. Carries severity (0 info … 4 critical), mitreTactic/ mitreId, hostname, organizationName, targetGroupName, eventTime.

Command surface

Reads (run freely — always live, never cached)

$EDR status                                  # counts: orgs/sites/agents/active/isolated/alerts
$EDR orgs                                    # clients + agent/alert/site counts
$EDR sites [--org <orgId>]                   # Locations (client sites)
$EDR scan-targets [--org <orgId>]            # scan target groups (for `scan`)
$EDR agents [--org <orgId>] [--site <locationId>] [--limit N]
$EDR agent <agentId>                         # full agent detail
$EDR detections [--org <orgId>] [--site <locationId>] [--severity 0-4] [--days N] [--limit N]
$EDR detection <alertId>                     # full alert detail
$EDR sweep [--org <orgId>]                   # per-client posture rollup (headline view)
$EDR extensions                              # list response/collection extensions
$EDR tasks [--type "Scan - EDR"] [--limit N] # recent userTasks (scan/analysis jobs)
$EDR task <taskId>                           # one userTask (scan job) detail/status
$EDR deploy-cmd --regkey <key> | --group <targetId>   # emit the agent install one-liner (see Deployment)

--json is a global flag (place before the subcommand): $EDR --json sweep.

Mutating (gated — refuse without --confirm)

Scan/isolate go through POST Agents/scan with a where filter selecting the agents. The CLI REQUIRES explicit --agent id(s): an absent where scans the ENTIRE tenant (the "156-host" footgun), so the client refuses an empty list.

# EDR-scan specific agent(s). Repeat --agent for several. POST Agents/scan
#   {"where":{"and":[{"id":[ids]}]}, "options":{}, "taskName":"Scan - EDR"}
$EDR scan --agent <agentId> [--agent <agentId2> ...] --confirm

# Response extension (Host Isolation, etc.) on agent(s) — rides Agents/scan w/ options.extensions
$EDR isolate --agent <agentId> [--extension-name "Host Isolation [Win/Linux]"] --confirm
$EDR isolate --agent <agentId> --extension-id <extId> --confirm

# Cancel a running scan/task; create a group; mint a group registration key
$EDR cancel <taskId> --confirm
$EDR create-group --name "[TEST] Foo" --org <organizationId> --confirm
$EDR mint-key --group <targetId> --key <10char> --confirm

# Power tool — any endpoint. Non-GET requires --confirm.
$EDR raw GET Agents --filter '{"limit":1}'
$EDR raw POST Agents/scan --data '{"where":{"and":[{"id":["<id>"]}]},"options":{},"taskName":"Scan - EDR"}' --confirm

The single-agent scan body that works: {"where":{"and":[{"id":["<agentId>"]}]}, "options":{}, "taskName":"Scan - EDR"}. A bare {"id":{"inq":[...]}} returns HTTP 412 "column reference id is ambiguous" — the and-wrap is required. AV scans (Scan - AV Quick/Full) are policy-driven, NOT callable via this endpoint.

Deployment

Agent install is not a REST call — it's the agent binary run on the endpoint. deploy-cmd emits the official one-liner. It needs an explicit key: either --regkey <key> (used verbatim) or --group <targetId> (looks up THAT group's key from /agentKeys). It will not auto-pick an arbitrary key — keys are per-target-group and cross-client, so a wrong key lands the agent in the wrong org. To land an agent in a SPECIFIC group: create-groupmint-key (the key id is caller-supplied) → deploy-cmd --group <targetId> → install with that -RegKey.

Install-EDR -URL "https://azcomp4587.infocyte.com" -RegKey <key>

Pass the full -URL, not -InstanceName azcomp4587 — the install script's loose .com regex matches "zcom" inside the cname, leaving --url empty and the agent erroring a value is required for '--url'. Push via GuruRMM (/rmm) or any remote-exec channel. RegKey auto-approves + adds to the key's target group; without it the agent registers but awaits manual approval.

Safety gating

  • Reads never mutate and run without confirmation.
  • scan, isolate, cancel, create-group, mint-key, and any non-GET raw print a [DRY-RUN] line and exit non-zero unless --confirm is passed.
  • scan/isolate additionally REFUSE an empty --agent list (would scan the whole tenant). Never scan/isolate casually — these hit live client production endpoints. Confirm the agent id with the read commands first.

Verified vs. unverified

Live-verified 2026-06-25 (exercised end-to-end on the tenant): all reads (status, orgs, sites, scan-targets, agents incl. per-org Location mapping, agent, detections, detection, sweep, extensions, tasks, task); the full deploy lifecycle (create-groupmint-key → install via /rmm → agent registered into the group); scan (targeted single-agent — confirmed "Scanning 1 host"); cancel; deploy-cmd; raw; and the --confirm + empty---agent guards.

RUN-unverified (shape correct, not fired at prod): isolate — it rides the same Agents/scan call with the Host Isolation extension in options.extensions; confirm the extension id via $EDR extensions and test on an ACG-internal box before relying on it (it cuts the endpoint off the network). AV scans are policy-driven (not callable).

Relationship to GuruRMM (Feature 6)

This skill is the prototype for GuruRMM security-connector #2 (Datto EDR), exactly as the bitdefender skill prototyped connector #1. Its read logic (per-client agents + detections + AV/isolation state) ports into the GuruRMM SecurityVendor trait; the live "EDR add-on" dashboard is best fed by EDR webhooks (Admin → Webhooks) into a GuruRMM receiver, complemented by REST polling here. See projects/msp-tools/guru-rmm/docs/RMM_THOUGHTS.md Feature 6.

Reference

Full endpoint map, auth detail, LoopBack filter syntax, and the scan/response payload shapes: references/api-reference.md.