Skip to content

Asdjdh/dnsaudit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dnsaudit

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.

Why

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.

What it checks

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.

Requirements

Python 3.10 or newer. Nothing else.

Usage

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)).

Options

--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

JSON

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": ["..."]
    }
  ]
}

Use in CI

Fail a pipeline if a domain's posture regresses below a bar:

python dnsaudit.py example.com --json --fail-under 80

Exit codes

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

As a library

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)

How resolution works

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.

Tests

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.

Scope and ethics

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.

License

MIT. See LICENSE.

About

Audit a domain email-security DNS (SPF/DKIM/DMARC/CAA/MX) over DNS-over-HTTPS and grade it A–F. Dependency-free Python.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages