#!/usr/bin/env python3
"""PeerCortex Daily Audit Mailer — detailed report via localhost:25"""
import os, json, smtplib
from datetime import datetime, timezone, timedelta
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import make_msgid, formatdate
SMTP_HOST = "localhost"
SMTP_PORT = 25
MAIL_FROM = "ctx-report@fichtmueller.org"
MAIL_TO = "rf@flexoptix.net"
def td(content, color=None, align="left", mono=False, bold=False):
ff = "monospace" if mono else "sans-serif"
fw = "font-weight:700;" if bold else ""
cl = "color:%s;" % color if color else ""
return '
%s | ' % (ff, align, cl, fw, content)
def load_report():
for delta in [0, 1]:
d = (datetime.now(timezone.utc) - timedelta(days=delta)).strftime("%Y-%m-%d")
try:
with open("/var/log/peercortex/audit-%s.json" % d) as f:
return json.load(f), d
except FileNotFoundError:
continue
return None, datetime.now(timezone.utc).strftime("%Y-%m-%d")
def build(report, date_str):
if not report:
return ("No report for %s
" % date_str,
"PeerCortex Audit %s — no report" % date_str)
counts = report.get("counts", {})
results = report.get("results", [])
elapsed = report.get("elapsed_s", 0)
pool = report.get("pool_size", 0)
ok = counts.get("ok", 0)
warn = counts.get("warning", 0)
crit = counts.get("critical", 0)
skip = counts.get("skip", 0)
total = len(results)
mins, secs = elapsed // 60, elapsed % 60
if crit == 0 and warn == 0:
sc, emoji, text, spfx = "#22c55e", "✅", "All Clear — no issues detected", "✅ All Clear"
elif crit == 0:
sc, emoji, text, spfx = "#f97316", "⚠️", "%d warning(s)" % warn, "⚠️ %d Warning(s)" % warn
else:
sc, emoji, text, spfx = "#ef4444", "🚨", "%d CRITICAL issue(s) — action required" % crit, "🚨 %d CRITICAL" % crit
subject = "PeerCortex Daily Audit %s — %s" % (date_str, spfx)
# Issues
issue_results = [r for r in results if r.get("status") in ("critical", "warning")]
issues_html = ""
if issue_results:
rows = ""
for r in issue_results:
asn = str(r["asn"])
name = (r.get("data", {}).get("name") or "?")[:28]
for sev, field, msg in r.get("issues", []):
c = "#ef4444" if sev == "CRITICAL" else "#f97316"
rows += "%s%s%s%s%s
" % (
td("[%s]" % sev[:4], c, mono=True),
td("AS" + asn, "#94a3b8", mono=True),
td(name, "#e2e8f0"),
td(field, "#64748b", mono=True),
td(msg[:60], "#94a3b8"))
issues_html = (
"⚠ Issues (%d ASNs)
" % len(issue_results) +
"" % rows
)
# Skipped
skip_results = [r for r in results if r.get("status") == "skip"]
skipped_html = ""
if skip_results:
rows = ""
for r in skip_results:
asn = str(r["asn"])
reason = r.get("issues", [["SKIP","","no response from PeerCortex"]])[0][2] if r.get("issues") else "no response from PeerCortex"
url = "https://peercortex.org/?asn=" + asn
rows += ("%s%s| "
"→ lookup |
") % (
td("AS" + asn, "#94a3b8", mono=True),
td(reason, "#64748b"),
url)
skipped_html = (
"⏭ Skipped ASNs (%d) — no PeerCortex response
" % skip +
"Possible causes: ASN not in any routing table, very large prefix count, or temporary API timeout.
" +
"" % rows
)
# Low RPKI
rpki_bad = sorted(
[r for r in results if r.get("status") != "skip" and r.get("data", {}).get("rpki_pct", 100) < 60],
key=lambda r: r.get("data", {}).get("rpki_pct", 100)
)[:8]
rpki_html = ""
if rpki_bad:
rows = ""
for r in rpki_bad:
d = r.get("data", {})
pct = d.get("rpki_pct", 0)
bar = ("") % max(2, pct)
rows += "%s%s%s| %s | %s
" % (
td("AS" + str(r["asn"]), "#94a3b8", mono=True),
td((d.get("name") or "?")[:28], "#e2e8f0"),
td(d.get("rir", "?"), "#64748b", "center"),
bar,
td(str(pct) + "%", "#ef4444", "right", bold=True))
rpki_html = (
"📉 Low RPKI Coverage (<60%%)
" +
"" % rows
)
# All ASNs table
sorted_results = sorted([r for r in results if r.get("status") != "skip"], key=lambda x: x["asn"])
header = ("%s%s%s%s%s%s%s%s%s%s%s
" % (
td("ASN", "#64748b", "right", mono=True),
td("Name", "#64748b"),
td("RIR", "#64748b", "center"),
td("CC", "#64748b", "center"),
td("Pfx", "#64748b", "right"),
td("RIPE Stat", "#64748b", "right"),
td("RPKI%", "#64748b", "right"),
td("IX", "#64748b", "right"),
td("PDB IX", "#64748b", "right"),
td("Fac", "#64748b", "right"),
td("Status", "#64748b", "center")))
rows = ""
for i, r in enumerate(sorted_results):
d = r.get("data", {})
bg = "#0f172a" if i % 2 == 0 else "#111827"
st = r.get("status", "ok")
if st == "critical":
st_html = '✗ CRIT'
elif st == "warning":
st_html = '⚠ WARN'
else:
st_html = '✓'
pct = d.get("rpki_pct", 100)
rpki_c = "#ef4444" if pct < 50 else "#f97316" if pct < 80 else "#94a3b8"
ripe = str(d.get("ripe_stat_pfx", "—"))
pdb = str(d.get("pdb_ix_cnt", "—"))
rows += "%s%s%s%s%s%s%s%s%s%s| %s |
" % (
bg,
td("AS" + str(r["asn"]), "#64748b", "right", mono=True),
td((d.get("name") or "?")[:28], "#e2e8f0"),
td(d.get("rir", "?"), "#94a3b8", "center"),
td(d.get("cc", "?"), "#94a3b8", "center"),
td(str(d.get("pfx_total", 0)), "#94a3b8", "right", mono=True),
td(ripe, "#64748b", "right", mono=True),
td(str(pct) + "%", rpki_c, "right", mono=True),
td(str(d.get("ix_cnt", 0)), "#94a3b8", "right", mono=True),
td(pdb, "#64748b", "right", mono=True),
td(str(d.get("fac_cnt", 0)), "#94a3b8", "right", mono=True),
st_html)
all_table = (
"All Tested ASNs (%d)
" % len(sorted_results) +
"" % (header, rows)
)
html = """
PEERCORTEX
Daily Audit Report · %(date)s
%(emoji)s %(text)s
%(total)d ASNs tested from pool of %(pool)d · Runtime: %(mins)dm %(secs)ds · Cross-validated: RIPE Stat + PeeringDB
|
%(ok)d PERFECT |
|
%(warn)d WARNINGS |
|
%(crit)d CRITICAL |
|
%(skip)d SKIPPED |
%(issues)s %(skipped)s %(rpki)s %(alltable)s
PeerCortex ·
peercortex.org
· 100 ASNs rotate daily from %(pool)d ASN pool · Audit at 02:00 UTC, report at 06:00 UTC
""" % dict(
date=date_str, sc=sc, emoji=emoji, text=text,
total=total, pool=pool, mins=mins, secs=secs,
ok=ok, warn=warn, crit=crit, skip=skip,
issues=issues_html, skipped=skipped_html,
rpki=rpki_html, alltable=all_table)
return html, subject
def send_email(subject, html_body):
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = MAIL_FROM
msg["To"] = MAIL_TO
msg["Date"] = formatdate(localtime=True)
msg["Message-ID"] = make_msgid(domain="fichtmueller.org")
msg.attach(MIMEText("PeerCortex Daily Audit — please view in an HTML-capable email client.\n\nhttps://peercortex.org", "plain", "utf-8"))
msg.attach(MIMEText(html_body, "html", "utf-8"))
with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=15) as s:
s.sendmail(MAIL_FROM, [MAIL_TO], msg.as_bytes())
print("Sent '%s' -> %s" % (subject, MAIL_TO))
if __name__ == "__main__":
report, date_str = load_report()
html, subject = build(report, date_str)
print("Email size: %d bytes" % len(html.encode()))
send_email(subject, html)