DEV Community

Hải Hà Phan Thanh
Hải Hà Phan Thanh

Posted on

Bulk-check domain expiry, SSL expiry and email security (SPF/DMARC) with Python

Two outages everyone has had: a domain that expired because the renewal email went to someone who left, and a TLS certificate that expired on a Sunday. Both are cheap to prevent with a scheduled check. The same data also answers sales and research questions: which sites run Shopify, who has no DMARC policy, who uses Cloudflare.

DIY, one domain at a time

All of this uses open protocols and the standard library plus two small packages:

# pip install requests dnspython
import ssl, socket, datetime, requests, dns.resolver

def ssl_days_left(host):
    ctx = ssl.create_default_context()
    with socket.create_connection((host, 443), timeout=10) as s:
        with ctx.wrap_socket(s, server_hostname=host) as t:
            exp = ssl.cert_time_to_seconds(t.getpeercert()["notAfter"])
    return int((exp - datetime.datetime.now().timestamp()) / 86400)

def domain_expiry(domain):
    r = requests.get(f"https://rdap.org/domain/{domain}", timeout=20, allow_redirects=True)
    for ev in r.json().get("events", []):
        if ev["eventAction"] == "expiration":
            return ev["eventDate"]

resolver = dns.resolver.Resolver()
resolver.nameservers = ["1.1.1.1"]   # some local stub resolvers time out on big TXT sets

def txt(name):
    try:
        return [b"".join(r.strings).decode() for r in resolver.resolve(name, "TXT", lifetime=8)]
    except dns.resolver.NXDOMAIN:
        return []

d = "github.com"
print(ssl_days_left(d), domain_expiry(d))
print([t for t in txt(d) if t.startswith("v=spf1")], txt("_dmarc." + d))
Enter fullscreen mode Exit fullscreen mode

That works, but at scale you hit the usual friction: some TLDs have no RDAP (.io needs plain WHOIS over port 43), registries rate-limit, subdomains need stripping to the registrable name, and every field needs a parser.

Bulk: the actor

I built the Domain Lookup actor to do this for a list. One item per domain, with four optional sections: RDAP/WHOIS, DNS, TLS certificate, HTTP/tech detection.

No code: paste domains or URLs (hostnames are extracted, duplicates removed), pick checks, Start, export CSV.

Python:

# pip install apify-client
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("bachiembmt/domain-intel-whois-dns-ssl").call(run_input={
    "domains": ["example.com", "https://www.github.com/", "stripe.io"],
    "checks": ["rdap", "dns", "tls", "http"],
    "dkimSelectors": ["default", "google", "selector1"],
    "concurrency": 5,
})
for d in client.dataset(run.default_dataset_id).iterate_items():
    tls, rdap = d.get("tls") or {}, d.get("rdap") or {}
    dmarc = ((d.get("dns") or {}).get("emailAuth") or {}).get("dmarcPolicy")
    print(d["domain"], "| ssl days:", tls.get("daysToExpiry"),
          "| expires:", rdap.get("expiresAt"), "| dmarc:", dmarc,
          "| tech:", (d.get("http") or {}).get("tech"))
Enter fullscreen mode Exit fullscreen mode

A section that fails is null and explained in errors, while the rest of the item is still returned.

Expiry alert

items = list(client.dataset(run.default_dataset_id).iterate_items())
for d in items:
    days = (d.get("tls") or {}).get("daysToExpiry")
    if days is not None and days < 21:
        print("RENEW SSL:", d["domain"], days, "days")
Enter fullscreen mode Exit fullscreen mode

Schedule the actor weekly in the Console and add a webhook or email integration, and you have a basic expiry watcher for a portfolio of domains.

Email deliverability audit

dns.emailAuth gives spf, dmarc, dmarcPolicy and which of your dkimSelectors were found. Filter for dmarcPolicy of none or missing to list domains that can still be spoofed freely.

Honest limits

  • No personal data. Registrant names, emails and phones are never collected (they are mostly GDPR-redacted anyway); you get registrar, dates, status, nameservers and the registrant organisation when public.
  • Some ccTLDs publish neither RDAP nor WHOIS in usable form (for example .vn); the RDAP section errors, DNS/TLS/HTTP still work. TLDs without RDAP, like .io, use WHOIS with best-effort parsing.
  • Subdomain stripping has no public-suffix list, so names like a.example.co.uk may resolve to co.uk for RDAP.
  • DKIM cannot be enumerated: only the selectors you list are probed.
  • DNS goes through Cloudflare DNS-over-HTTPS, so it is a public-resolver view, not authoritative.
  • Tech detection is heuristic (headers and HTML markers), not exhaustive.
  • Use moderate concurrency on big lists because registries rate-limit.

Price

Pay per event, about $4 per 1,000 domains regardless of how many checks you select (check the Store page for the current price). 200 domains is about 80 cents. For a few domains, run the snippets above; the actor is for lists and for not maintaining the parsers. Disclosure: it is my project.

Try it: Domain Lookup actor

Top comments (0)