DMARC aggregate-report analyzer (Flask + SQLite) on ps1raf
  • Python 70.1%
  • HTML 25.2%
  • Shell 3.6%
  • Dockerfile 1.1%
Find a file
raf b14eae0775 chore: pin Flask>=3.1.3 in requirements.txt (reflect apt python3-flask; silence osv false-positive)
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 06:48:12 +02:00
nginx DMARC analyzer: Flask+SQLite, gz/zip/xml import, webhook, stats, TOP-10, alarms 2026-06-10 09:35:49 +02:00
templates feat: IMAP fetch — import DMARC reports from a mailbox 2026-06-13 09:35:59 +02:00
tests fix(parser): accept namespaced DMARC reports (GMX/web.de urn:...:dmarc-2.0) 2026-06-13 10:54:03 +02:00
.gitignore DMARC analyzer: Flask+SQLite, gz/zip/xml import, webhook, stats, TOP-10, alarms 2026-06-10 09:35:49 +02:00
app.py fix(imap): UID-based + batched fetch (max_per_run), single expunge at end 2026-06-13 10:28:44 +02:00
Containerfile feat: IMAP fetch — import DMARC reports from a mailbox 2026-06-13 09:35:59 +02:00
dmarc_imap.py fix(imap): only delete successfully-imported msgs; mark failures \Seen (no loop, no data loss) 2026-06-13 10:31:01 +02:00
dmarc_parser.py fix(parser): accept namespaced DMARC reports (GMX/web.de urn:...:dmarc-2.0) 2026-06-13 10:54:03 +02:00
drain-imap.sh add drain-imap.sh: one-shot IMAP backlog drainer (loops /cron/fetch-imap until empty) 2026-06-13 10:55:53 +02:00
README.md feat: IMAP fetch — import DMARC reports from a mailbox 2026-06-13 09:35:59 +02:00
requirements.txt chore: pin Flask>=3.1.3 in requirements.txt (reflect apt python3-flask; silence osv false-positive) 2026-06-15 06:48:12 +02:00
schema.sql DMARC analyzer: Flask+SQLite, gz/zip/xml import, webhook, stats, TOP-10, alarms 2026-06-10 09:35:49 +02:00

dmarc-analyzer

DMARC aggregate-report (RUA) analyzer for ps1raf — Flask + SQLite, rootless podman quadlet.

Parses DMARC aggregate reports (.xml, .xml.gz, .zip), stores them in SQLite, and gives you filterable/searchable statistics, TOP-10 lists, and threshold alarms. Background on what the fields mean: https://www.mailreach.co/de/blog/how-to-read-a-dmarc-report.

Access

Import paths

  1. Web UIImport page, drag in one or more .xml / .xml.gz / .zip files.

  2. WebhookPOST /webhook/dmarc with Authorization: Bearer <WEBHOOK_TOKEN> (or ?token=…). Raw body (--data-binary @report.xml.gz, set X-Filename) or multipart (-F report=@report.zip). Imports are idempotent on the report id.

    curl -sk -X POST https://ps1raf.tn.ps1.at:8470/webhook/dmarc \
      -H "Authorization: Bearer $WEBHOOK_TOKEN" \
      -H "X-Filename: report.xml.gz" --data-binary @report.xml.gz
    
  3. IMAP mailbox — pull report attachments straight from a mailbox (DMARC reporters email .xml.gz/.zip/.xml attachments). Configure in the container .env:

    IMAP_HOST=imap.example.com    # empty = feature disabled
    IMAP_PORT=993                 # IMAP_SSL=1
    IMAP_USER=dmarc@example.com
    IMAP_PASS=...
    IMAP_FOLDER=INBOX            # IMAP_SEARCH=UNSEEN
    IMAP_POST_ACTION=seen        # seen | delete | move:<folder>
    

    Trigger from the Import page ("Fetch now"), via POST /cron/fetch-imap, or let the dmarc-imap-fetch.timer (every 15 min) poll. Stdlib imaplib; only report-looking attachments are taken (signatures / inline text ignored); imports are idempotent on the report id; processed messages get the post-action (default: mark \Seen).

Statistics

  • Dashboard — totals, DMARC aligned-pass / unaligned-fail %, disposition breakdown, and TOP-10 lists (source IPs, failing source IPs, sending domains, reporting orgs, failing from-domains). All respect the filter bar.
  • Records — full searchable/filterable table (domain, result pass/fail, disposition, IP search, date range, free-text search), paginated.
  • Reports — one row per imported report.

DMARC "pass" = the message is aligned (policy-evaluated DKIM or SPF passed).

Alarms (sensible defaults — override in .env)

knob default meaning
ALARM_WINDOW_DAYS 7 analysis window
ALARM_FAIL_RATE_WARN / _CRIT 5% / 15% unaligned-message fraction
ALARM_MIN_VOLUME 100 ignore fail-rate alarm below this many msgs
ALARM_SOURCE_FAIL_MIN 50 one source IP sending ≥N unaligned msgs
ALARM_DISPOSITION_MIN 20 quarantine+reject msgs in window

Breaches are pushed to ntfy (note: prefix), de-duplicated per kind+subject within 24h. Evaluated on every import and by a daily timer hitting /cron/check-alarms.

Architecture

  • app.py — Flask app (routes, ingest, filters, stats, alarms).
  • dmarc_parser.py — stdlib decompress + XML parse.
  • schema.sql — SQLite schema (reports, records with a generated aligned_pass, alarm_log).
  • Containerfile — Debian 13 + python3-flask + gunicorn. DB at /data/dmarc.db (mounted).
  • Quadlet: ~/.config/containers/systemd/dmarc-analyzer.container. Secrets in .env (gitignored).
systemctl --user status dmarc-analyzer.service
podman logs dmarc-analyzer --tail 50
python3 tests/test_parser.py