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.
This commit is contained in:
@@ -308,16 +308,20 @@ class DattoEDRClient:
|
||||
orgs = self.list_organizations()
|
||||
if org_id:
|
||||
orgs = [o for o in orgs if o.get("id") == org_id]
|
||||
since = (datetime.now(timezone.utc) - timedelta(days=7)).isoformat()
|
||||
out = []
|
||||
for o in orgs:
|
||||
oid = o.get("id")
|
||||
recent = self.list_alerts(org_id=oid, days=7, limit=500)
|
||||
# _count (not list+len) so a >500-alert client isn't silently capped
|
||||
# and mis-sorted, and we don't transfer 500 full records per org.
|
||||
recent = self._count("Alerts", {"organizationId": oid,
|
||||
"createdOn": {"gt": since}})
|
||||
out.append({
|
||||
"organizationId": oid,
|
||||
"organization": o.get("name"),
|
||||
"agents": o.get("agentCount", 0),
|
||||
"alertsTotal": o.get("alertCount", 0),
|
||||
"alertsLast7d": len(recent),
|
||||
"alertsLast7d": recent,
|
||||
})
|
||||
out.sort(key=lambda r: (-r["alertsLast7d"], (r["organization"] or "").lower()))
|
||||
return {"clients": out}
|
||||
@@ -334,15 +338,26 @@ class DattoEDRClient:
|
||||
# Response extensions (isolate/kill/quarantine) ride the same call via
|
||||
# options.extensions. (Verified live 2026-06-25.)
|
||||
# ======================================================================
|
||||
# Full EDR forensic collection - the default so a bare `scan` is a real sweep
|
||||
# (an empty options body risks a thin scan that returns a false all-clear on a
|
||||
# compromised host). Pass options={} explicitly to override with a lean scan.
|
||||
DEFAULT_SCAN_OPTIONS = {
|
||||
"process": True, "module": True, "driver": True, "memory": True,
|
||||
"account": True, "artifact": True, "autostart": True, "application": True,
|
||||
"installed": True, "events": True,
|
||||
}
|
||||
|
||||
def scan_agents(self, agent_ids: list[str],
|
||||
options: Optional[dict] = None,
|
||||
task_name: str = "Scan - EDR") -> Any:
|
||||
"""Trigger an EDR scan on specific agents (POST agents/scan).
|
||||
|
||||
Targets by Agent.id via a LoopBack where filter:
|
||||
{"where":{"id":{"inq":[ids]}}, "options":{}, "taskName":"Scan - EDR"}.
|
||||
REFUSES an empty agent_ids list - an empty where scans the whole tenant.
|
||||
`options` may carry EDR forensic toggles + extensions; empty {} is valid.
|
||||
Targets by Agent.id via the AND-wrapped LoopBack where the console uses:
|
||||
{"where":{"and":[{"id":[ids]}]}, "options":{...}, "taskName":"Scan - EDR"}.
|
||||
A bare {"id":{"inq":[...]}} returns HTTP 412 "column reference id is
|
||||
ambiguous" (the scan query joins tables) - the and-wrap disambiguates.
|
||||
REFUSES an empty agent_ids list (an absent where scans the whole tenant).
|
||||
`options` defaults to DEFAULT_SCAN_OPTIONS (full forensic); pass {} for lean.
|
||||
STATE-CHANGING - gate behind --confirm at the call site."""
|
||||
ids = [i for i in (agent_ids or []) if i]
|
||||
if not ids:
|
||||
@@ -350,12 +365,9 @@ class DattoEDRClient:
|
||||
"scan_agents refused: empty agent list. An absent `where` scans "
|
||||
"the ENTIRE tenant (156-host footgun) - pass explicit agent id(s)."
|
||||
)
|
||||
# AND-wrapped form is REQUIRED: a bare {id:{inq:[...]}} returns HTTP 412
|
||||
# "column reference id is ambiguous" (the backend joins tables). The
|
||||
# console builds {and:[{id:[...]}]}; verified live -> "Scanning 1 host".
|
||||
body = {
|
||||
"where": {"and": [{"id": ids}]},
|
||||
"options": options or {},
|
||||
"options": self.DEFAULT_SCAN_OPTIONS if options is None else options,
|
||||
"taskName": task_name,
|
||||
}
|
||||
return self._request("POST", "Agents/scan", body=body)
|
||||
|
||||
Reference in New Issue
Block a user