DEV Community

agenthustler
agenthustler

Posted on

Building a WHOIS and DNS History Tracker with Python

Domain intelligence is valuable for security research, brand protection, and competitive analysis. A WHOIS and DNS history tracker lets you monitor ownership changes, detect domain hijacking, and map infrastructure shifts over time.

In this guide, I'll build a complete domain intelligence tool that tracks WHOIS records and DNS changes using Python.

Why Track Domain History?

  • Security teams need to detect when domains are transferred to suspicious registrars
  • Brand protection requires monitoring for domain squatting and typosquatting
  • Competitive intelligence reveals when competitors acquire new domains or change hosting
  • Threat hunting uses DNS history to trace attacker infrastructure

Project Setup

pip install python-whois dnspython requests schedule
Enter fullscreen mode Exit fullscreen mode

Building the WHOIS Tracker

import whois
import dns.resolver
import json
import hashlib
from datetime import datetime
from pathlib import Path

class DomainTracker:
    def __init__(self, storage_dir: str = "./domain_data"):
        self.storage_dir = Path(storage_dir)
        self.storage_dir.mkdir(exist_ok=True)

    def get_whois(self, domain: str) -> dict:
        """Fetch current WHOIS data for a domain."""
        try:
            w = whois.whois(domain)
            return {
                "domain": domain,
                "registrar": w.registrar,
                "creation_date": str(w.creation_date),
                "expiration_date": str(w.expiration_date),
                "name_servers": sorted(w.name_servers or []),
                "registrant_country": w.country,
                "dnssec": w.dnssec,
                "updated_date": str(w.updated_date),
                "fetched_at": datetime.utcnow().isoformat()
            }
        except Exception as e:
            return {"domain": domain, "error": str(e)}

    def get_dns_records(self, domain: str) -> dict:
        """Fetch all DNS record types for a domain."""
        records = {"domain": domain, "fetched_at": datetime.utcnow().isoformat()}

        record_types = ["A", "AAAA", "MX", "NS", "TXT", "CNAME", "SOA"]

        for rtype in record_types:
            try:
                answers = dns.resolver.resolve(domain, rtype)
                records[rtype] = sorted([str(r) for r in answers])
            except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN,
                    dns.resolver.NoNameservers):
                records[rtype] = []

        return records
Enter fullscreen mode Exit fullscreen mode

Change Detection Engine

The core value is detecting changes over time:

    def compute_fingerprint(self, data: dict) -> str:
        """Create a hash fingerprint of record state."""
        canonical = json.dumps(data, sort_keys=True, default=str)
        return hashlib.sha256(canonical.encode()).hexdigest()[:16]

    def detect_changes(self, domain: str) -> list[dict]:
        """Compare current state against last known state."""
        history_file = self.storage_dir / f"{domain}.json"

        current_whois = self.get_whois(domain)
        current_dns = self.get_dns_records(domain)

        current_state = {
            "whois": current_whois,
            "dns": current_dns,
            "fingerprint": self.compute_fingerprint(
                {"whois": current_whois, "dns": current_dns}
            )
        }

        changes = []

        if history_file.exists():
            history = json.loads(history_file.read_text())
            last_state = history["snapshots"][-1]

            if current_state["fingerprint"] != last_state["fingerprint"]:
                diff = self._compute_diff(last_state, current_state)
                changes = diff
                history["snapshots"].append(current_state)
                history["change_log"].extend(changes)

        else:
            history = {
                "domain": domain,
                "first_seen": datetime.utcnow().isoformat(),
                "snapshots": [current_state],
                "change_log": []
            }

        history_file.write_text(json.dumps(history, indent=2, default=str))
        return changes

    def _compute_diff(self, old: dict, new: dict) -> list[dict]:
        """Find specific differences between two states."""
        changes = []
        timestamp = datetime.utcnow().isoformat()

        # Compare WHOIS fields
        for field in ["registrar", "name_servers", "expiration_date"]:
            old_val = old["whois"].get(field)
            new_val = new["whois"].get(field)
            if old_val != new_val:
                changes.append({
                    "timestamp": timestamp,
                    "type": "whois_change",
                    "field": field,
                    "old_value": old_val,
                    "new_value": new_val
                })

        # Compare DNS records
        for rtype in ["A", "AAAA", "MX", "NS", "TXT", "CNAME"]:
            old_records = set(old["dns"].get(rtype, []))
            new_records = set(new["dns"].get(rtype, []))

            added = new_records - old_records
            removed = old_records - new_records

            if added or removed:
                changes.append({
                    "timestamp": timestamp,
                    "type": "dns_change",
                    "record_type": rtype,
                    "added": list(added),
                    "removed": list(removed)
                })

        return changes
Enter fullscreen mode Exit fullscreen mode

Monitoring Multiple Domains

import schedule
import time

def monitor_domains(domains: list[str], interval_hours: int = 6):
    tracker = DomainTracker()

    def check_all():
        for domain in domains:
            changes = tracker.detect_changes(domain)
            if changes:
                print(f"\n[ALERT] Changes detected for {domain}:")
                for change in changes:
                    print(f"  - {change['type']}: {json.dumps(change, indent=4)}")
            else:
                print(f"[OK] {domain}: no changes")
            time.sleep(2)  # Rate limit

    check_all()  # Initial run
    schedule.every(interval_hours).hours.do(check_all)

    while True:
        schedule.run_pending()
        time.sleep(60)

# Track competitor domains
domains_to_watch = [
    "competitor1.com",
    "competitor2.io",
    "suspicious-domain.xyz"
]

monitor_domains(domains_to_watch, interval_hours=12)
Enter fullscreen mode Exit fullscreen mode

Adding Historical Lookups via Web Scraping

For historical WHOIS data not available through the python-whois library, you can scrape public archives. ScraperAPI handles the proxy rotation needed for these lookups.

import requests

def fetch_historical_whois(domain: str, api_key: str) -> str:
    """Fetch historical WHOIS data via web archive."""
    url = f"https://web.archive.org/web/2024*/whois.domaintools.com/{domain}"
    params = {"api_key": api_key, "url": url}
    response = requests.get("https://api.scraperapi.com", params=params)
    return response.text
Enter fullscreen mode Exit fullscreen mode

For high-volume DNS resolution with residential IPs, ThorData provides geo-distributed proxies that help avoid DNS-level blocking.

Use Cases in Production

  1. Brand monitoring: Track your brand domain and common typosquats daily
  2. M&A due diligence: Audit all domains owned by an acquisition target
  3. Threat intelligence: Map C2 infrastructure changes in real-time
  4. SEO monitoring: Detect when competitors change hosting or CDN providers

Monitor your scraping pipeline health with ScrapeOps dashboards to ensure you never miss a DNS change.

Wrapping Up

Domain intelligence is an underserved niche. Most tools charge $100+/month for what you can build with Python and a few libraries. The key is consistent monitoring — changes that slip by unnoticed for days can mean lost domains or security breaches.

Top comments (0)