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
|
the extension id via `GET /Extensions` (e.g. `Host Isolation [Win/Linux]`) and test on
|
||||||
an ACG-internal box first.
|
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)
|
## Deployment (not a REST call)
|
||||||
|
|
||||||
The agent installs by running the binary on the endpoint:
|
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)
|
# Scratch output from ocu/openclaw tooling (empty error/json artifacts)
|
||||||
.ocu.*
|
.ocu.*
|
||||||
|
|
||||||
|
# EDR isolation watcher local state (no secrets, per-host)
|
||||||
|
.claude/scripts/.edr-watch-state.json
|
||||||
|
|||||||
Reference in New Issue
Block a user