238 lines
11 KiB
Bash
238 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;">🌐 {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;">🛡 HAS — homeassistant</div>
|
||
|
|
<div style="font-size:13px;color:#94a3b8;margin-top:4px;">$NOW | 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;">🚨 LOCAL BANS — 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;">🚨 $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;">⚡ 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;">📌 Banned IPs — 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;">⛅ <strong>Cloud blocklist (CAPI):</strong> $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 | homeassistant (HAS) | 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 ---"
|