sync: auto-sync from HOWARD-HOME at 2026-07-07 11:01:18
Author: Howard Enos Machine: HOWARD-HOME Timestamp: 2026-07-07 11:01:18
This commit is contained in:
67
.claude/scripts/edr-isolation-watch.sh
Normal file
67
.claude/scripts/edr-isolation-watch.sh
Normal file
@@ -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"
|
||||
@@ -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:
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user