diff --git a/.claude/scripts/edr-isolation-watch.sh b/.claude/scripts/edr-isolation-watch.sh new file mode 100644 index 00000000..d2c8d460 --- /dev/null +++ b/.claude/scripts/edr-isolation-watch.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# edr-isolation-watch.sh — interim EDR auto-isolation alerter. +# +# Polls Datto EDR for detections whose automated response included `isolate-host` +# (i.e. a machine was auto-isolated / "quarantined") and posts each NEW one to the +# private #dev-alerts channel (Mike + Howard). Runs as a plain scheduled job — no +# LLM, no tokens. Retire once the GuruRMM EDR webhook integration (Feature 6) lands. +# +# Schedule (every 10 min), e.g. cron: +# */10 * * * * bash /path/to/claudetools/.claude/scripts/edr-isolation-watch.sh >/dev/null 2>&1 +# +# State: .edr-watch-state.json (gitignored) holds already-alerted alert IDs so each +# isolation is announced exactly once, even though Mike may un-isolate before the poll. + +set -uo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +EDR="bash $ROOT/.claude/scripts/py.sh $ROOT/.claude/skills/datto-edr/scripts/edr.py" +DM="bash $ROOT/.claude/scripts/discord-dm.sh" +export STATE="$ROOT/.claude/scripts/.edr-watch-state.json" +TARGET="dev" # #dev-alerts = Mike + Howard, private + +# Pull last 24h of detections (list includes responseData) and emit any NEW +# isolate-host events as one pipe-delimited line each; update state in-place. +new_events="$($EDR --json detections --days 1 --limit 500 2>/dev/null | python -c ' +import sys, json, os +state_path = os.environ["STATE"] +try: + rows = json.load(sys.stdin) + rows = rows if isinstance(rows, list) else rows.get("data", rows.get("alerts", [])) +except Exception: + sys.exit(0) # API hiccup: emit nothing, do not churn state +try: + seen = set(json.load(open(state_path))) +except Exception: + seen = set() +fresh = [] +for a in rows: + rd = a.get("responseData") or [] + names = [r.get("name") for r in rd] if isinstance(rd, list) else [] + if "isolate-host" not in names: + continue + aid = a.get("id") + if not aid or aid in seen: + continue + seen.add(aid) + fresh.append("|".join(str(a.get(k, "")) for k in + ("id", "hostname", "organizationName", "severity", "sourceName", "eventTime"))) +# keep state bounded +json.dump(sorted(seen)[-500:], open(state_path, "w")) +print("\n".join(fresh)) +' 2>/dev/null)" + +[ -z "$new_events" ] && exit 0 + +while IFS='|' read -r aid host org sev rule ts; do + [ -z "$aid" ] && continue + msg="[EDR AUTO-ISOLATION] **$host** was auto-isolated by Datto EDR +- Client: $org +- Rule: $rule (severity: $sev) +- Time: $ts +- This machine was cut off the network by an EDR automated response. Verify it is intended before restoring. +- Console: https://azcomp4587.infocyte.com (alert id: $aid)" + if ! printf '%s' "$msg" | $DM "$TARGET" 2>/dev/null; then + bash "$ROOT/.claude/scripts/log-skill-error.sh" "edr-isolation-watch" "discord post failed for $host ($aid)" 2>/dev/null || true + fi +done <<< "$new_events" diff --git a/.claude/skills/datto-edr/references/api-reference.md b/.claude/skills/datto-edr/references/api-reference.md index e46f71a2..272bba41 100644 --- a/.claude/skills/datto-edr/references/api-reference.md +++ b/.claude/skills/datto-edr/references/api-reference.md @@ -108,6 +108,46 @@ installed, hook, network, events`, plus `extensions:[{id,args,order}]`. the extension id via `GET /Extensions` (e.g. `Host Isolation [Win/Linux]`) and test on an ACG-internal box first. +## Alert Suppression Rules (verified live 2026-07-07) + +Suppress a false-positive detection so it stops firing (and stops triggering any +attached auto-response like `isolate-host`). LoopBack models are API-reachable: + +| Op | Method/path | +|---|---| +| List rules | `GET /SuppressionRules` (`[id, alertId, name, versionCount, description, organizationId, locationId, active, deleted]`) | +| Count | `GET /SuppressionRules/count` | +| Match criteria (per rule) | `GET /SuppressionRules/{id}/versions` → `[{id, suppressionRuleId, name, description, metadata:{...}}]` | + +**`metadata`** is the full match-field map; each key is `{value, active, display, dataType, +operator}`. A field participates in the match ONLY when `active:true`; all active fields are +**AND**-ed. Available fields (`display`): Alert Type, Item Type, Organization Name, Location +Name, Hostname, IP Address, Name, File Path, File SHA1, File SHA256, File Signature Issuer, +**Process Command Line**, AV Threat Name, Operating System, Threat Status, Severity, Rule +Name, EPP Type, Threat Category, Process Owner, Process Owner UID, AV Hits, Parent Process +Name, Grand Parent Process Name. + +**Scoping guidance (learned from the vwp-qbs RMM false-positive incident):** the tightest +safe suppression matches on **Process Command Line** (a unique fingerprint of the exact +script) plus **Grand Parent Process Name** (the launching agent, e.g. `gururmm-agent.exe`). +That trusts one exact known-good automation without blinding a whole binary. NEVER suppress +on `Name`/`File Path`/`File SHA*` of a LOLBin (powershell/rundll32/etc.) alone — it disables +the rule for that binary everywhere. Add `Rule Name` to limit to one rule; add `Hostname`/ +`Organization Name`/`Location Name` to limit scope (empty `organizationId`/`locationId` on the +rule = fleet-wide, gated by the metadata match). **Do NOT blanket-whitelist an RMM agent as +grandparent by itself** — the RMM is a SYSTEM-level RCE channel and the top MSP attack path; +whitelisting all of it blinds EDR exactly where it matters. + +**Create — CONSOLE-observed, API create RUN-unverified.** Console flow: open the alert → +**Create Suppression Rule** → check the desired Match fields → Save. This POSTs a +`SuppressionRule` (carrying `alertId`, `name`, `description`, `organizationId`, `locationId`, +`active`) plus a version whose `metadata` has the chosen fields flipped to `active:true`. To +replicate via API, POST `/SuppressionRules` then the version to `/SuppressionRules/{id}/ +versions` with that metadata shape — verify the exact envelope on the next real create (build +→ read back via the GET above → delete if wrong) before relying on it. Example on this tenant: +rule `e4dd55bf-…` (name "Exfiltration Over HTTP Protocol", alertId `5d5f39b1-…`) matches +Process Command Line + Grand Parent = `gururmm-agent.exe`. + ## Deployment (not a REST call) The agent installs by running the binary on the endpoint: diff --git a/.gitignore b/.gitignore index f60282aa..6936a1c9 100644 --- a/.gitignore +++ b/.gitignore @@ -150,3 +150,6 @@ temp/ # Scratch output from ocu/openclaw tooling (empty error/json artifacts) .ocu.* + +# EDR isolation watcher local state (no secrets, per-host) +.claude/scripts/.edr-watch-state.json