skills: add datto-edr (Datto EDR / Infocyte HUNT) + syncro-rmm memory

New /datto-edr skill — standalone CLI for the Datto EDR REST API (azcomp4587,
rebranded Infocyte HUNT, raw-token LoopBack). Live-verified reads across the whole
MSP fleet: orgs/sites/agents/detections/sweep + deploy-cmd. Scan/isolate gated
behind --confirm (shape-verified, not run against prod). Token vaulted at
msp-tools/datto-edr.sops.yaml.

Also: reference_syncro_rmm_api_gui_only memory (Syncro RMM policy mgmt is
GUI-only) and the guru-rmm submodule pointer bump (Feature 6 EDR research).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-25 12:39:42 -07:00
parent e61b39b5c8
commit bd1e84d32c
7 changed files with 377 additions and 30 deletions

View File

@@ -228,9 +228,23 @@ class DattoEDRClient:
"alertCount": True, "locationCount": True},
}) or []
def list_locations(self, org_id: Optional[str] = None, limit: int = 500) -> list[dict]:
"""List Locations (= client sites). VERIFIED LIVE. This is the per-client
grouping that agents belong to (Agent.locationId -> Location.id), and a
Location carries organizationId. Org -> Locations -> Agents is the
inventory hierarchy."""
filt: dict = {"limit": limit, "order": "name ASC",
"fields": {"id": True, "name": True, "organizationId": True,
"agentCount": True, "activeAgentCount": True,
"alertCount": True, "lastScannedOn": True}}
if org_id:
filt["where"] = {"organizationId": org_id}
return self._get("Locations", filt) or []
def list_targets(self, org_id: Optional[str] = None, limit: int = 500) -> list[dict]:
"""List Targets (= target groups / sites). VERIFIED LIVE.
Each Target belongs to an Organization via organizationId."""
"""List Targets (= SCAN target groups). VERIFIED LIVE. Distinct from
Locations: a Target is the scannable unit (POST targets/{id}/scan). Each
Target belongs to an Organization via organizationId."""
filt: dict = {"limit": limit, "order": "name ASC",
"fields": {"id": True, "name": True, "organizationId": True,
"agentCount": True, "activeAgentCount": True,
@@ -239,16 +253,17 @@ class DattoEDRClient:
filt["where"] = {"organizationId": org_id}
return self._get("Targets", filt) or []
def list_agents(self, org_id: Optional[str] = None,
target_group_id: Optional[str] = None,
def list_agents(self, location_id: Optional[str] = None,
location_ids: Optional[list[str]] = None,
limit: int = 500) -> list[dict]:
"""List Agents (endpoints). VERIFIED LIVE.
Filter by deviceGroupId (target group). Org filtering is done by the CLI
via the target-group->org map since Agents carry deviceGroupId, not orgId."""
"""List Agents (endpoints). VERIFIED LIVE. Filter by locationId (a single
site) or location_ids (an org's sites, via LoopBack `inq`)."""
filt: dict = {"limit": limit, "order": "hostname ASC"}
where: dict = {}
if target_group_id:
where["deviceGroupId"] = target_group_id
if location_id:
where["locationId"] = location_id
elif location_ids:
where["locationId"] = {"inq": location_ids}
if where:
filt["where"] = where
return self._get("Agents", filt) or []