#!/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"""
| 🌐 {ip} |
{scenario} |
{ban_type} |
{duration} |
{scope} |
{ban_time} |
CTI
WHOIS
Abuse
|
""")
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
|
CrowdSec Intrusion Prevention
🛡 HAS — homeassistant
$NOW | Machine: homeassistant
|
🚨 LOCAL BANS — 4H DIGEST
|
|
|
🚨 $PENDING_COUNT locally-detected attack(s) banned in the past 4 hours.
|
|
Local Bans
$PENDING_COUNT
this window
|
|
Origin
⚡ LOCAL
crowdsec detection
|
|
CAPI Today
$CAPI_TODAY
cloud blocklist bans
|
|
📌 Banned IPs — Past 4 Hours
| IP Address |
Scenario |
Type |
Duration |
Scope |
Detected At |
Links |
$TABLE_ROWS
|
|
⛅ Cloud blocklist (CAPI): $CAPI_TODAY pre-emptive ban(s) recorded today. Full CAPI digest delivered at midnight by cs_digest.sh.
|
|
CrowdSec Intrusion Prevention | homeassistant (HAS) | KingDezigns Infrastructure
|
|
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 ---"