Audit a domain's email-security DNS posture from the command line: SPF, DMARC, DKIM, CAA, and MX. It grades the result A–F and gives concrete fixes for each gap. Resolution is done over DNS-over-HTTPS, so it works the same on any network without a local resolver.
Pure Python standard library. No dependencies, no install step.
Email authentication is where domain spoofing is won or lost, and the failure
modes are quiet: an SPF record that silently exceeds the 10-lookup limit and
permerrors, a DMARC record stuck at p=none that monitors but never blocks, a
+all that authorizes the entire internet. These are easy to miss by eye.
dnsaudit reads the records, applies the rules from RFC 7208 (SPF), RFC 7489
(DMARC), and RFC 6376 (DKIM), and tells you exactly what to change.
| Check | What it looks at | Notable rules |
|---|---|---|
| SPF | apex TXT |
exactly one record; the 10 DNS-lookup limit; the terminal all qualifier (-all vs ~all vs +all vs none) |
| DMARC | _dmarc.<domain> TXT |
presence; policy p= (none/quarantine/reject); rua= reporting; pct=; subdomain policy sp= |
| DKIM | <selector>._domainkey.<domain> TXT |
probes 26 common provider selectors; a negative result is reported as inconclusive, not as failure |
| CAA | apex CAA |
presence (limits which CAs may issue certificates) |
| MX | apex MX |
presence; recognizes a null MX (RFC 7505) as a deliberate no-mail declaration |
The SPF lookup count is a count of the top-level terms in the record itself. It does not recurse into included policies, so it is a lower bound — which is what you want for catching records that are already at or over the limit before any nesting is counted.
Python 3.10 or newer. Nothing else.
python dnsaudit.py example.com
Real output against google.com:
dnsaudit google.com
Grade B (88/100, 88%)
[PASS] SPF (23/25)
SPF present, terminal qualifier ~all, 1 top-level DNS lookups.
fix: "~all" (softfail) is acceptable but "-all" (hardfail) is stronger once you are confident in your sending sources.
[PASS] DMARC (30/30)
DMARC present, p=reject.
[WARN] DKIM (10/20)
No DKIM key found at common selectors (inconclusive).
fix: No DKIM key answered at the common selectors probed (26 tried). If you sign mail, confirm the selector your provider uses and verify it resolves. ...
[PASS] MX (15/15)
MX present (1 host(s)).
[PASS] CAA (10/10)
CAA present (1 record(s)).
--json emit the report as JSON instead of text
--no-color disable ANSI color
--selectors a,b,c probe these DKIM selectors instead of the built-in list
--timeout SECONDS per-query timeout (default 10)
--fail-under PCT exit non-zero if the score is below PCT (for CI)
--version
python dnsaudit.py example.com --json
{
"domain": "example.com",
"score": 88,
"max_score": 100,
"percent": 88.0,
"grade": "B",
"checks": [
{
"name": "SPF",
"ok": true,
"score": 23,
"max_score": 25,
"summary": "SPF present, terminal qualifier ~all, 1 top-level DNS lookups.",
"details": { "record": "v=spf1 ...", "dns_lookups": 1, "all_qualifier": "~", "record_count": 1 },
"findings": ["..."]
}
]
}Fail a pipeline if a domain's posture regresses below a bar:
python dnsaudit.py example.com --json --fail-under 80
| Code | Meaning |
|---|---|
| 0 | audit ran (and met --fail-under if given) |
| 1 | audit ran but the score is below --fail-under |
| 2 | bad usage / argument error |
| 3 | DNS resolution failed |
The parsing and grading functions are pure and take already-fetched records, so you can use them without touching the network:
import dnsaudit
spf = dnsaudit.check_spf(["v=spf1 include:_spf.google.com -all"])
print(spf.ok, spf.score, spf.details["all_qualifier"])
# Full audit with your own resolver (any callable (name, rrtype) -> DoH JSON):
report = dnsaudit.audit_domain("example.com", resolver=my_resolver)
print(report.grade)Every lookup is an HTTPS GET to https://dns.google/resolve with
accept: application/dns-json, made with urllib. TXT records that the API
returns quoted and split into 255-byte chunks are unquoted and concatenated per
RFC 7208 before parsing. The resolver is injectable, which is how the test suite
runs entirely offline.
python tests.py
47 tests covering TXT chunk handling, SPF lookup counting and qualifier
detection, DMARC tag parsing and policy grading, DKIM detection, CAA/MX,
grading math, the DoH transport (via an injected opener), and the CLI. Prints
ALL TESTS PASSED on success. No network access required.
dnsaudit only performs public DNS reads. It sends no mail and makes no
connection to the audited domain's mail servers. Even so, run it only against
domains you own or are explicitly authorized to assess.
A negative DKIM result means none of the probed common selectors answered. It is not proof the domain lacks DKIM — the domain may use a selector not on the list. The report says so rather than overstating it.
MIT. See LICENSE.