Skip to content

ProfoundNetworks/dns-change-detector

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dns-change-detector

Async DNS scanner and change detector with provider inference and enterprise signal detection.

Features

  • Scans A, AAAA, MX, NS, TXT records plus _dmarc, _domainkey, _mta-sts subdomains and PTR records
  • Infers email provider from MX records (Google Workspace, Microsoft 365, Proofpoint, Mimecast, etc.)
  • Infers DNS provider from NS records (Cloudflare, AWS Route 53, Azure DNS, Vercel, Netlify, etc.)
  • Infers host provider from PTR records (AWS, GCP, Azure, Cloudflare, Fastly, DigitalOcean, etc.)
  • Detects enterprise TXT signals (Stripe, HubSpot, Atlassian, Salesforce, Zoom, DocuSign, and more)
  • Detects SPF and DMARC records with policy extraction
  • Diffs two DNS snapshots and emits typed, severity-ranked change events
  • Two backends: fast dany Go binary or pure dnspython fallback

Installation

pip install dns-change-detector

Quick Start

import asyncio
from dns_change_detector import scan_domain, extract_signals, detect_changes

async def main():
    # Take two snapshots over time
    old = extract_signals(await scan_domain("stripe.com"))
    # ... time passes ...
    new = extract_signals(await scan_domain("stripe.com"))

    for change in detect_changes(old, new):
        print(f"{change.severity:6}  {change.change_type}: {change.description}")

asyncio.run(main())

Scanning Multiple Domains

from dns_change_detector import scan_domains, extract_signals

async def scan_batch():
    responses = await scan_domains(
        ["stripe.com", "github.com", "cloudflare.com"],
        concurrency=50,
    )
    return [extract_signals(r) for r in responses]

Change Types

detect_changes(old, new) returns a list of DomainChange objects. Each has:

  • change_type — string key (see table below)
  • severity"high", "medium", or "low"
  • description — human-readable summary
  • old_value / new_value — the before/after values
change_type Severity Trigger
spf_added high SPF record appeared
spf_removed high SPF record disappeared
spf_changed medium SPF record content changed
dmarc_added high DMARC record appeared
dmarc_removed high DMARC record disappeared
dmarc_policy_changed high/medium Policy changed (high if jumps a level)
email_provider_changed high MX provider changed (e.g. Google → Microsoft)
email_provider_added medium Email provider first detected
email_provider_removed high Email provider removed
mx_changed low MX records changed within same provider
dns_provider_changed high NS provider changed (e.g. Cloudflare → Route 53)
dns_provider_detected medium DNS provider first detected
dns_provider_removed medium DNS provider removed
ns_changed low NS records changed within same provider
host_provider_changed high Host provider changed (e.g. AWS → GCP)
host_provider_detected low Host provider first detected
host_provider_removed medium Host provider removed
a_records_changed low A records changed within same host provider
cname_added medium CNAME target added
cname_removed medium CNAME target removed
cname_changed medium CNAME target changed
enterprise_signal_added medium Enterprise TXT signal appeared
enterprise_signal_removed medium Enterprise TXT signal removed

Provider Tables

The inference tables are public and extensible — mutate them before calling extract_signals:

from dns_change_detector import MX_PROVIDERS, NS_PROVIDERS, PTR_PROVIDERS, TXT_SIGNALS

# Add a custom email provider
MX_PROVIDERS.append(("mail.mycompany.com", "My Company Mail"))

# Add a custom enterprise TXT signal
TXT_SIGNALS.append(("myapp-domain-verification=", "myapp", "My App"))
Table Key format Infers
MX_PROVIDERS (hostname_substr, display_name) DNSSignals.email_provider
NS_PROVIDERS (hostname_substr, display_name) DNSSignals.dns_provider
PTR_PROVIDERS (hostname_substr, display_name) DNSSignals.host_provider
TXT_SIGNALS (txt_substr, signal_key, display_name) DNSSignals.enterprise_signals

CLI

# Scan a single domain (pretty-printed JSON)
dns-scan stripe.com

# Scan multiple domains (one JSON object per line)
dns-scan stripe.com github.com cloudflare.com

# Scan from a file (one domain per line, # comments ignored)
dns-scan --file domains.txt

# Custom resolver and concurrency
dns-scan --file domains.txt --server 1.1.1.1 --concurrency 100

dany Backend

By default the library uses pure Python (dnspython). For higher throughput, configure the optional dany Go backend:

import dns_change_detector.scanner as scanner

scanner.DANY_BINARY = "/usr/local/bin/dany"
scanner.DANY_NAMESERVER = "8.8.8.8"  # required on macOS (no /etc/resolv.conf)
scanner.DANY_TIMEOUT = 20.0          # seconds per domain

If dany is in your PATH it is detected and used automatically — no explicit configuration needed.

API Reference

scan_domain(domain, nameservers=None) → DNSResponse

Full DNS scan of a single domain. Uses the dany backend when available, otherwise falls back to dnspython.

scan_domains(domains, nameservers=None, concurrency=50) → list[DNSResponse]

Scan a list of domains with bounded concurrency. Safe to call with thousands of domains.

extract_signals(response, asn_reader=None) → DNSSignals

Extract normalized signals from a DNSResponse. The optional asn_reader is a geoip2.database.Reader opened against a GeoLite2-ASN database; when provided it serves as a fallback for host provider detection when PTR records are absent.

DNSSignals fields:

Field Type Description
domain str Domain name
email_provider str | None Inferred from MX records
dns_provider str | None Inferred from NS records
host_provider str | None Inferred from PTR records
has_spf bool SPF record present
spf_record str | None Raw SPF record
has_dmarc bool DMARC record present
dmarc_policy str | None none, quarantine, or reject
dmarc_record str | None Raw DMARC record
enterprise_signals frozenset[str] Signal keys (e.g. "stripe", "hubspot")
cname_target str | None First CNAME record
cname_service str | None Inferred from CNAME (e.g. "Shopify")
a_records frozenset[str] A record IPs
ns_records frozenset[str] NS hostnames
mx_records frozenset[str] Raw MX records
txt_records frozenset[str] Apex TXT records

detect_changes(old, new) → list[DomainChange]

Diff two DNSSignals snapshots. Both must be for the same domain. Returns an empty list when nothing changed.

DomainChange fields: domain, change_type, severity, description, old_value, new_value.

License

MIT

About

Async DNS scanner and change detector with provider inference and enterprise TXT signal detection

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages