PostureAudit

Auditing email authentication across every domain you manage

Checking one domain is a five-minute job with any lookup tool. Checking eighty, every week, and noticing the one where somebody edited DNS on Tuesday and quietly broke SPF — that is a different problem, and it is the one that actually costs an agency a client.

Why portfolios drift

Email authentication is not a set-and-forget configuration, because most of it depends on other people's records. Three things change underneath you:

None of these bounce. None raise an alert. They surface weeks later as "our invoices are going to spam," by which point the reputation damage is already done.

The useful cadence is weekly, not continuous. DNS records change on human timescales. A weekly sweep catches drift long before it becomes a deliverability complaint, and eighty domains checked weekly is about 350 requests a month.

A working portfolio sweep

Put your domains in a text file, one per line. This script audits each one, prints a table, and exits non-zero if anything fails — so you can run it from cron or CI and only hear about it when something is wrong.

#!/usr/bin/env python3
"""Audit a portfolio of domains. Exits 1 if any domain has a failing check."""
import json, sys, urllib.parse, urllib.request

API = "https://api.postureaudit.com/v1/audit"
KEY = "YOUR_API_KEY"          # omit for the free anonymous allowance

def audit(domain):
    url = f"{API}?domain={urllib.parse.quote(domain)}"
    req = urllib.request.Request(url, headers={"X-API-Key": KEY} if KEY else {})
    with urllib.request.urlopen(req, timeout=45) as r:
        return json.load(r)

def main(path):
    domains = [l.strip() for l in open(path) if l.strip()
               and not l.startswith("#")]
    problems = 0

    for domain in domains:
        try:
            result = audit(domain)
        except Exception as exc:
            print(f"{domain:<32} ERROR    {exc}")
            problems += 1
            continue

        bad = [f for s in result["sections"] for f in s["findings"]
               if f["status"] == "fail"]
        warn = [f for s in result["sections"] for f in s["findings"]
                if f["status"] == "warn"]

        flag = "FAIL" if bad else ("warn" if warn else "ok")
        print(f"{result['domain']:<32} {result['grade']:<2} "
              f"{result['score']:>3}/100  {flag}")

        for f in bad:
            print(f"    {f['title']}: {f['message']}")
            if f.get("fix"):
                print(f"      fix: {f['fix']}")
        problems += len(bad)

    print(f"\n{len(domains)} domains, {problems} failing checks")
    return 1 if problems else 0

if __name__ == "__main__":
    sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "domains.txt"))

Run it:

$ python3 sweep.py clients.txt
acme-corp.com                    B  85/100  warn
initech.example                  F  45/100  FAIL
    SPF DNS lookup limit: SPF requires more than the 10 DNS lookups allowed
      fix: Reduce nested include: entries, or flatten rarely-changing includes
globex.example                   A  95/100  ok

3 domains, 1 failing checks

Alert on change, not on state

The version above tells you the current state every time it runs, which means a domain stuck at a warning generates identical output forever. People stop reading that within two weeks.

Store the previous score per domain and report only differences:

import pathlib

STATE = pathlib.Path("scores.json")
previous = json.loads(STATE.read_text()) if STATE.exists() else {}
current = {}

for domain in domains:
    result = audit(domain)
    current[domain] = result["score"]
    was = previous.get(domain)
    if was is None:
        print(f"NEW      {domain}: {result['score']}")
    elif result["score"] < was:
        print(f"DROPPED  {domain}: {was} -> {result['score']}")
    elif result["score"] > was:
        print(f"IMPROVED {domain}: {was} -> {result['score']}")

STATE.write_text(json.dumps(current))

Now a silent run means nothing changed, and any output is worth reading. That is the difference between a report someone acts on and one they filter to a folder.

What to look at first across a portfolio

Finding idWhy it comes first
spf.too_many_lookups SPF is failing right now and the record looks valid. Nothing else on this list is actively broken while appearing fine.
spf.all_permissive +all authorises the entire internet to send as the domain. Worse than publishing no SPF.
dmarc.missing No policy at all, and bulk senders to Gmail and Yahoo are required to publish one.
spf.lookups_near_limit Nine or ten of ten. Not broken yet; the next vendor breaks it. This is the one worth fixing while it is still cheap.
mtasts.policy_unreachable Advertised but unusable, so TLS is not actually being enforced.

Finding ids are stable, so you can key alerting logic on them rather than matching on message text.

Volume and cost

A sweep costs one request per domain. Some realistic figures:

PortfolioCadenceRequests / month
30 domainsweekly~130
80 domainsweekly~350
150 domainsdaily~4,500
1,000 domainsdaily~30,000

The first two fit inside the free tier. Nothing here requires a paid plan until you are running daily sweeps across a hundred-plus domains — see pricing.