Files
claudetools/.claude/scripts/edr-isolation-watch.sh
Howard Enos 3d28f9ea5d sync: auto-sync from HOWARD-HOME at 2026-07-07 11:15:40
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-07 11:15:40
2026-07-07 11:16:14 -07:00

72 lines
2.9 KiB
Bash

#!/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="${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 [ "${DRY_RUN:-0}" = "1" ]; then
printf '[DRY_RUN] would post to #%s:\n%s\n\n' "$TARGET" "$msg"
continue
fi
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"