🛡️ Proactive Defense: Automating Threat Intelligence

August 14, 2025

Turning raw ban logs into a live, self‑updating proof of resilience - without leaking a single op‑sec detail.

The Invisible Shield

I wanted the world to see who was trying to get in - not just me via CrowdSec or Fail2Ban logs.

Attackers hammer away at a few of my sites, from my portfolio (beane.me) to maggie.lol, a tribute to my two Golden Retrievers. This post turns that invisible defense into something legible.

The Problem With Raw Logs

Unfiltered cscli alerts are great for operators, but most readers need a clean, scannable summary. A public dashboard makes the defense real - at a glance - without exposing implementation details.

Designing the Pipeline

  1. Detection: CrowdSec + Fail2Ban
  2. Fetcher: Python → JSON (sanitized, deduped)
  3. Renderer: Python → HTML (atomic writes)
  4. Scheduler: cron (details redacted for op‑sec)

Fetching and Sanitizing Alerts

Using Python, I shell out to cscli, parse JSON, normalize timestamps, and cap to the most recent 25 decisions to keep the feed fast and readable.

import json, subprocess

def fetch_all_alerts():
    """Call cscli and return parsed alerts JSON."""
    res = subprocess.run(
        ["cscli", "alerts", "list", "-o", "json"],
        capture_output=True, text=True, check=True
    )
    return json.loads(res.stdout) if res.stdout else []

def dedup_merge(existing, recent, cap=25):
    keys = {(d["ip"], d["start_at"]) for d in existing if "ip" in d and "start_at" in d}
    new_items = [d for d in recent if (d["ip"], d["start_at"]) not in keys]
    combined = new_items + existing
    combined.sort(key=lambda d: d["start_at"], reverse=True)
    return combined[:cap]

We call cscli as a subprocess to retrieve the current alerts, output them in JSON, and feed them into our pipeline. In production, a simple try/except guards against malformed output - omitted here for brevity.

Rendering the Dashboard

A scheduled Python job parses the refreshed JSON and publishes updates to threats.beane.me every 15 minutes.

Security-Aware Storytelling

A good public dashboard shows the impact, not the implementation. I’ve omitted cron lines, usernames, and absolute paths - proving you can be transparent without handing out a blueprint.

Before → After

Raw cscli output

ID     value             reason                              country  as                             decisions  created_at
          5119   Ip:4.190.198.107  http-admin-interface-probing         JP       MICROSOFT-CORP-MSN-AS-BLOCK     ban:1      2025-08-14 13:19 UTC

Dashboard view

threats.beane.me view of IP 4.190.198.107 ban

Click to view the full‑size image.

What's Next

Next up: intel.beane.me - taking the same automation‑driven clarity you see here and aiming it at the broader threat landscape, so every signal tells a story.

For anyone watching intrusion alerts stack up, wondering if the system will hold - it will. Keep building.