nas16-scripts/CrowdSec/cs_local_digest.sh
2026-07-26 21:21:03 -04:00

237 lines
11 KiB
Bash

#!/bin/bash
# =============================================================
# CrowdSec Local Attack — 4-Hour Digest Mailer
# Host: NAS16 (192.168.150.40)
# Schedule: Every 4 hours via OMV Scheduled Jobs
# Recommended cron: 0 0,4,8,12,16,20 * * *
#
# Reads: /var/log/crowdsec-notifier/local_pending.json
# Sends: One batched HTML email:
# - Dark header + red status banner
# - 3 stat cards: Local Bans / Origin / CAPI Today
# - Single table: IP Address / Scenario / Type /
# Duration / Scope / Detected At / Links
# - Amber CAPI footer note
# - Dark footer
# Clears: local_pending.json after a successful send.
# Skips: Send entirely if there are zero pending bans.
# =============================================================
SMTP_HOST="smtppro.zoho.com"
SMTP_PORT="465"
SMTP_USER="rufus.king@kingdezigns.com"
SMTP_PASS="Valerie8545"
FROM_EMAIL="rufus.king@kingdezigns.com"
TO_EMAIL="rufus.king@kingdezigns.com"
LOCAL_PENDING="/var/log/crowdsec-notifier/local_pending.json"
CAPI_DIGEST="/var/log/crowdsec-notifier/capi_digest.json"
LOG_FILE="/var/log/crowdsec-notifier/notifier.log"
mkdir -p /var/log/crowdsec-notifier
touch "$LOCAL_PENDING"
touch "$CAPI_DIGEST"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"; }
log "--- cs_local_digest run start ---"
# --- Count pending bans ---
PENDING_COUNT=$(grep -c . "$LOCAL_PENDING" 2>/dev/null || echo 0)
PENDING_COUNT=$(echo "$PENDING_COUNT" | tr -d ' ')
if [[ "$PENDING_COUNT" -eq 0 ]]; then
log "No local bans in this window. Skipping email."
log "--- cs_local_digest run complete (no bans) ---"
exit 0
fi
log "Building digest for $PENDING_COUNT local ban(s)."
NOW=$(date '+%Y-%m-%d %H:%M:%S UTC')
CAPI_TODAY=$(wc -l < "$CAPI_DIGEST" 2>/dev/null | tr -d ' ')
CAPI_TODAY=${CAPI_TODAY:-0}
# Snapshot the pending file before clearing
SNAPSHOT=$(cat "$LOCAL_PENDING")
# --- Build table rows ---
# NOTE: $SNAPSHOT is expanded inline inside the Python heredoc.
# Do NOT pipe into python3 - with a heredoc: the heredoc becomes stdin,
# silently replacing the pipe. Pass data as an inline variable instead.
TABLE_ROWS=$(python3 << PYEOF
import json
snapshot = """$SNAPSHOT"""
rows = []
for line in snapshot.strip().splitlines():
line = line.strip()
if not line:
continue
try:
d = json.loads(line)
except Exception:
continue
ip = d.get("value", "unknown")
scenario = d.get("scenario", "unknown")
duration = d.get("duration", "unknown")
ban_type = d.get("type", "ban").upper()
scope = d.get("scope", "Ip").upper()
ban_time = d.get("start_at", "")
if ban_time:
ban_time = ban_time[:19].replace("T", " ") + " UTC"
else:
ban_time = "unknown"
rows.append(f""" <tr style="background:#fff1f2;">
<td style="padding:10px 14px;font-size:13px;font-weight:700;color:#111827;white-space:nowrap;">&#127760; {ip}</td>
<td style="padding:10px 14px;font-size:12px;color:#374151;font-family:monospace;">{scenario}</td>
<td style="padding:10px 14px;font-size:12px;color:#6b7280;text-align:center;">{ban_type}</td>
<td style="padding:10px 14px;font-size:12px;color:#6b7280;text-align:center;">{duration}</td>
<td style="padding:10px 14px;font-size:12px;color:#6b7280;text-align:center;">{scope}</td>
<td style="padding:10px 14px;font-size:12px;color:#6b7280;white-space:nowrap;">{ban_time}</td>
<td style="padding:10px 14px;font-size:11px;white-space:nowrap;">
<a href="https://app.crowdsec.net/cti/{ip}" style="margin-right:6px;padding:3px 8px;background:#0f172a;color:white;border-radius:4px;text-decoration:none;font-size:11px;font-weight:600;">CTI</a>
<a href="https://www.whois.com/whois/{ip}" style="margin-right:6px;padding:3px 8px;background:#374151;color:white;border-radius:4px;text-decoration:none;font-size:11px;font-weight:600;">WHOIS</a>
<a href="https://www.abuseipdb.com/check/{ip}" style="padding:3px 8px;background:#374151;color:white;border-radius:4px;text-decoration:none;font-size:11px;font-weight:600;">Abuse</a>
</td>
</tr>""")
print("\n".join(rows))
PYEOF
)
log "Generated ${PENDING_COUNT} table row(s)."
# --- Write the full email HTML ---
HTML_FILE=$(mktemp /tmp/cs_local_digest_XXXXXX.html)
cat > "$HTML_FILE" << HTMLEOF
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
<body style="margin:0;padding:0;background:#f1f5f9;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
<table width="100%" cellspacing="0" cellpadding="0" style="background:#f1f5f9;padding:30px 0;">
<tr><td align="center">
<table width="720" cellspacing="0" cellpadding="0" style="max-width:720px;width:100%;">
<!-- HEADER -->
<tr><td style="background:#0f172a;border-radius:10px 10px 0 0;padding:28px 32px;">
<table width="100%" cellspacing="0" cellpadding="0"><tr>
<td>
<div style="font-size:11px;color:#94a3b8;letter-spacing:1px;text-transform:uppercase;">CrowdSec Intrusion Prevention</div>
<div style="font-size:26px;font-weight:800;color:white;margin-top:4px;">&#128737; HAS &mdash; homeassistant</div>
<div style="font-size:13px;color:#94a3b8;margin-top:4px;">$NOW &nbsp;|&nbsp; Machine: homeassistant</div>
</td>
<td align="right">
<div style="background:#b91c1c;color:white;padding:10px 20px;border-radius:8px;font-size:14px;font-weight:700;">&#128680; LOCAL BANS &mdash; 4H DIGEST</div>
</td>
</tr></table>
</td></tr>
<!-- STATUS BANNER -->
<tr><td style="background:#b91c1c;padding:12px 32px;">
<span style="color:white;font-size:14px;font-weight:600;">&#128680; &nbsp;$PENDING_COUNT locally-detected attack(s) banned in the past 4 hours.</span>
</td></tr>
<!-- STAT CARDS -->
<tr><td style="padding:24px 24px 8px;">
<table width="100%" cellspacing="0" cellpadding="0"><tr>
<td style="width:30%;padding:10px 14px;background:white;border-radius:8px;border:1px solid #e5e7eb;text-align:center;">
<div style="font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;">Local Bans</div>
<div style="font-size:32px;font-weight:800;color:#b91c1c;margin-top:4px;">$PENDING_COUNT</div>
<div style="font-size:11px;color:#6b7280;margin-top:2px;">this window</div>
</td>
<td style="width:5%;"></td>
<td style="width:30%;padding:10px 14px;background:white;border-radius:8px;border:1px solid #e5e7eb;text-align:center;">
<div style="font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;">Origin</div>
<div style="font-size:20px;font-weight:800;color:#b91c1c;margin-top:8px;">&#9889; LOCAL</div>
<div style="font-size:11px;color:#6b7280;margin-top:2px;">crowdsec detection</div>
</td>
<td style="width:5%;"></td>
<td style="width:30%;padding:10px 14px;background:#fffbeb;border-radius:8px;border:1px solid #fac775;text-align:center;">
<div style="font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;">CAPI Today</div>
<div style="font-size:32px;font-weight:800;color:#b45309;margin-top:4px;">$CAPI_TODAY</div>
<div style="font-size:11px;color:#6b7280;margin-top:2px;">cloud blocklist bans</div>
</td>
</tr></table>
</td></tr>
<!-- BAN TABLE -->
<tr><td style="padding:16px 24px 8px;">
<div style="background:white;border-radius:10px;border:1px solid #e5e7eb;overflow:hidden;">
<div style="padding:14px 18px;border-bottom:1px solid #e5e7eb;background:#f9fafb;">
<span style="font-size:13px;font-weight:700;color:#374151;text-transform:uppercase;letter-spacing:.5px;">&#128204; Banned IPs &mdash; Past 4 Hours</span>
</div>
<table width="100%" cellspacing="0" cellpadding="0">
<thead>
<tr style="background:#f9fafb;">
<th style="padding:8px 14px;font-size:10px;text-transform:uppercase;letter-spacing:.6px;color:#6b7280;font-weight:600;text-align:left;border-bottom:1px solid #e5e7eb;">IP Address</th>
<th style="padding:8px 14px;font-size:10px;text-transform:uppercase;letter-spacing:.6px;color:#6b7280;font-weight:600;text-align:left;border-bottom:1px solid #e5e7eb;">Scenario</th>
<th style="padding:8px 14px;font-size:10px;text-transform:uppercase;letter-spacing:.6px;color:#6b7280;font-weight:600;text-align:center;border-bottom:1px solid #e5e7eb;">Type</th>
<th style="padding:8px 14px;font-size:10px;text-transform:uppercase;letter-spacing:.6px;color:#6b7280;font-weight:600;text-align:center;border-bottom:1px solid #e5e7eb;">Duration</th>
<th style="padding:8px 14px;font-size:10px;text-transform:uppercase;letter-spacing:.6px;color:#6b7280;font-weight:600;text-align:center;border-bottom:1px solid #e5e7eb;">Scope</th>
<th style="padding:8px 14px;font-size:10px;text-transform:uppercase;letter-spacing:.6px;color:#6b7280;font-weight:600;text-align:left;border-bottom:1px solid #e5e7eb;">Detected At</th>
<th style="padding:8px 14px;font-size:10px;text-transform:uppercase;letter-spacing:.6px;color:#6b7280;font-weight:600;text-align:left;border-bottom:1px solid #e5e7eb;">Links</th>
</tr>
</thead>
<tbody>
$TABLE_ROWS
</tbody>
</table>
</div>
</td></tr>
<!-- CAPI NOTE -->
<tr><td style="padding:12px 24px 28px;">
<div style="background:#fffbeb;border-radius:10px;border:1px solid #fac775;padding:14px 22px;">
<span style="font-size:12px;color:#92400e;">&#9925; &nbsp;<strong>Cloud blocklist (CAPI):</strong> &nbsp;$CAPI_TODAY pre-emptive ban(s) recorded today. Full CAPI digest delivered at midnight by cs_digest.sh.</span>
</div>
</td></tr>
<!-- FOOTER -->
<tr><td style="background:#0f172a;border-radius:0 0 10px 10px;padding:16px 32px;text-align:center;">
<span style="font-size:11px;color:#64748b;">CrowdSec Intrusion Prevention &nbsp;|&nbsp; homeassistant (HAS) &nbsp;|&nbsp; KingDezigns Infrastructure</span>
</td></tr>
</table>
</td></tr>
</table>
</body>
</html>
HTMLEOF
# --- Send the email ---
python3 << PYEOF >> "$LOG_FILE" 2>&1
import smtplib, ssl
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
with open("$HTML_FILE") as f:
html_body = f.read()
msg = MIMEMultipart("alternative")
msg["Subject"] = "CrowdSec 4H Local Digest — $PENDING_COUNT Ban(s) on HAS"
msg["From"] = "CrowdSec KingDezigns <$FROM_EMAIL>"
msg["To"] = "$TO_EMAIL"
msg.attach(MIMEText(html_body, "html"))
ctx = ssl.create_default_context()
with smtplib.SMTP_SSL("$SMTP_HOST", $SMTP_PORT, context=ctx, timeout=15) as s:
s.login("$SMTP_USER", "$SMTP_PASS")
s.sendmail("$FROM_EMAIL", ["$TO_EMAIL"], msg.as_string())
print("sent")
PYEOF
SEND_STATUS=$?
rm -f "$HTML_FILE"
if [[ $SEND_STATUS -eq 0 ]]; then
log "4-hour digest sent. $PENDING_COUNT ban(s) reported. Clearing local_pending.json."
> "$LOCAL_PENDING"
else
log "ERROR: Digest email failed. local_pending.json NOT cleared — will retry next run."
fi
log "--- cs_local_digest run complete ---"