A domain can look harmless today while its history tells a different story.

A website might have changed hosting providers several times. Its nameservers might have moved to a new DNS provider. Its registration details might have changed, or its DNS records might show connections to infrastructure that deserves further investigation.

These changes do not automatically mean a domain is malicious. However, they can provide useful clues when investigating phishing websites, suspicious redirects, typosquatting, and other domain-related security concerns.

In this guide, we will investigate a domain using historical DNS and WHOIS records. We will also build a Python script that collects the available data, extracts useful timestamps, and creates a timeline for further analysis.

We will use the WhoisFreaks Historical DNS API and Historical WHOIS API for the examples.

What You Will Learn

By the end of this tutorial, you will know how to:

  • Understand the difference between historical DNS and historical WHOIS.
  • Retrieve historical records through APIs.
  • Build a Python workflow for collecting domain history.
  • Create a timeline from DNS snapshots and WHOIS dates.
  • Interpret infrastructure changes without treating every change as evidence of malicious activity.
  • Save the collected data for further investigation.

1. Why Historical Domain Data Matters

A current DNS lookup tells you how a domain resolves now. A current WHOIS lookup provides registration information available at the time of the query.

Neither necessarily explains what happened in the past.

Imagine you are investigating a domain reported in a phishing email. Its current IP address points to a mainstream hosting provider. That alone does not tell you whether the domain used different infrastructure last week.

Historical records can help answer questions such as:

  • Which IP addresses has the domain pointed to?
  • When did its nameservers change?
  • Did its mail-related DNS records change?
  • What registration dates and registrar information appear in historical WHOIS records?
  • Do several infrastructure changes occur around the same time?
  • Which observations should be checked against other security sources?

The objective is to build a timeline of observable changes, not to label a domain malicious based on a single record.

2. Understand Historical DNS and WHOIS

Comparison of historical DNS snapshots and WHOIS registration records as complementary sources for a domain investigation.

Historical DNS and historical WHOIS provide different kinds of evidence. They are more useful when analyzed together.

Historical DNS records

DNS records describe how a domain is configured to resolve and provide different services.

Common record types include:

Record What it tells you
A IPv4 address associated with a hostname
AAAA IPv6 address associated with a hostname
NS Authoritative nameservers for a domain
MX Mail servers responsible for receiving email
CNAME Alias pointing to another hostname
TXT Text data used for verification, email authentication, and other purposes
SOA Information about the DNS zone, including its primary nameserver and administrative timing parameters

Historical DNS data can help reveal changes in hosting, nameservers, mail configuration, and other infrastructure.

For example, if a domain points to one IP address in an earlier snapshot and another IP address in a later snapshot, that is an observable infrastructure change. It does not, by itself, establish why the change happened.

Historical WHOIS records

WHOIS records provide information about domain registration. Depending on the domain, registry, registrar, privacy settings, and available historical data, records may include:

  • Domain creation date
  • Registration update date
  • Expiration date
  • Registrar information
  • Nameservers
  • Domain status
  • Registrant or administrative information, when available

Historical WHOIS records can help you compare registration information across different observation dates.

Keep one distinction in mind: a WHOIS record's query timestamp is not necessarily the date when a registration change occurred. Likewise, a domain's reported update date does not tell you exactly what changed.

Why use both?

Historical DNS helps you understand infrastructure. Historical WHOIS helps you understand the registration record.

Combining them can make an investigation more useful, especially when multiple observations support the same timeline.

3. Prepare Your API Access

For this tutorial, you need:

  • Python 3.10 or later
  • A WhoisFreaks API key with access to the relevant historical endpoints
  • A terminal or command prompt
  • An internet connection

You can review the provider's API documentation before running the examples. Check your plan's access and request limits because historical endpoint allowances vary.

Store your API key securely

Avoid putting API keys directly into source code or publishing them in a repository.

On Linux or macOS, set the key in your current terminal session:

read -rsp "Enter your WhoisFreaks API key: " WHOISFREAKS_API_KEY echo export WHOISFREAKS_API_KEY 

The key will not be displayed while you type it.

You can check that the environment variable exists without printing its value:

test -n "$WHOISFREAKS_API_KEY" && echo "API key is set" 

Keep the key private. Do not include it in screenshots, logs, or public code examples.

4. Retrieve Historical DNS Records

The Historical DNS endpoint used in this tutorial is:

https://api.whoisfreaks.com/v2.0/dns/historical 

A request can include the API key, domain name, record type, and page number.

For example, this command requests historical DNS data for example.com:

curl -G "https://api.whoisfreaks.com/v2.0/dns/historical" \ --data-urlencode "apiKey=$WHOISFREAKS_API_KEY" \ --data-urlencode "domainName=example.com" \ --data-urlencode "type=all" \ --data-urlencode "page=1" 

Replace example.com with the domain you are authorized to investigate.

The response can include fields such as:

  • totalRecords
  • totalPages
  • currentPage
  • historicalDnsRecords

Each historical snapshot can contain a query timestamp, the domain name, DNS types, and associated DNS records.

The totalPages field matters. A response containing one page is not necessarily the complete historical dataset.

If the API returns multiple pages, retrieve them according to the response metadata and your plan's rate limits.

Respect rate limits

Do not assume that a short fixed delay is sufficient for every plan.

Check the API response headers, especially:

  • X-RateLimit-Allowed-Requests
  • X-RateLimit-Remaining-Requests
  • X-RateLimit-Remaining-Time

The remaining-time header is expressed in nanoseconds in the responses described by the provider's rate-limit documentation. Convert it to seconds before using it as a delay.

If you receive HTTP 429, wait for the indicated reset period when available, then retry according to the API's guidance. Avoid repeatedly sending requests while the limit is exhausted.

5. Retrieve Historical WHOIS Records

The Historical WHOIS endpoint used here is:

https://api.whoisfreaks.com/v2.0/whois/history 

You can request historical WHOIS data using:

curl -G "https://api.whoisfreaks.com/v2.0/whois/history" \ --data-urlencode "apiKey=$WHOISFREAKS_API_KEY" \ --data-urlencode "domainName=example.com" \ --data-urlencode "whois=historical" 

The response may contain fields such as whois_domains_historical, along with registration details and observation timestamps.

Pay attention to the structure of the actual response. The provider's documentation and product examples may use different summary-field names, and the available metadata can vary. In one test of the endpoint, the response included total_records and whois_domains_historical, but did not include pagination fields.

Do not assume that every domain returns the same number of historical records or that one request always returns a complete history. Check the response and the provider's documentation for your account and endpoint.

For the script below, we make one historical WHOIS request and save the complete response returned by that request. We do not assume that WHOIS pagination is available.

6. Build a Python Domain Investigation Script

Now we can combine the two sources in a small Python workflow.

The script will:

  1. Request historical DNS records.
  2. Follow DNS pagination when the response provides page information.
  3. Respect available rate-limit reset headers.
  4. Request historical WHOIS data.
  5. Extract useful timestamps and IP addresses where their record fields are available.
  6. Create a chronological timeline.
  7. Save both the raw API responses and the timeline in a JSON report.

Five-step domain investigation workflow: select a domain, fetch historical DNS, fetch historical WHOIS, normalize timestamps, and build a timeline.

The script uses Python's standard library, so you do not need to install additional packages.

Create a file named investigate_domain.py:

import argparse import ipaddress import json import os import time import urllib.error import urllib.parse import urllib.request from datetime import datetime, timezone DNS_URL = "https://api.whoisfreaks.com/v2.0/dns/historical" WHOIS_URL = "https://api.whoisfreaks.com/v2.0/whois/history" TIMEOUT_SECONDS = 30 MAX_DNS_PAGES = 20 # The next request will wait if a previous response indicates # that the current rate-limit allowance has been exhausted. NEXT_REQUEST_AT = 0.0 def request_json(url, params): """Make a GET request and return the decoded JSON response.""" global NEXT_REQUEST_AT for attempt in range(2): wait_seconds = NEXT_REQUEST_AT - time.monotonic() if wait_seconds > 0: print( f"Rate limit reached. Waiting " f"{wait_seconds:.1f} seconds." ) time.sleep(wait_seconds) query = urllib.parse.urlencode(params) request = urllib.request.Request( f"{url}?{query}", headers={"Accept": "application/json"}, ) try: with urllib.request.urlopen( request, timeout=TIMEOUT_SECONDS, ) as response: body = response.read().decode("utf-8") data = json.loads(body) headers = response.headers except urllib.error.HTTPError as exc: if exc.code == 429 and attempt == 0: reset_ns = exc.headers.get( "X-RateLimit-Remaining-Time" ) if reset_ns: try: delay = int(reset_ns) / 1_000_000_000 except ValueError: delay = 0 if delay > 0: print( "HTTP 429 received. Waiting for the " "reported reset period." ) NEXT_REQUEST_AT = ( time.monotonic() + delay + 2 ) continue detail = exc.read().decode( "utf-8", errors="replace" ) raise RuntimeError( f"API request failed with HTTP {exc.code}: " f"{detail[:500]}" ) from exc except urllib.error.URLError as exc: raise RuntimeError( f"Network request failed: {exc.reason}" ) from exc remaining = headers.get( "X-RateLimit-Remaining-Requests" ) reset_ns = headers.get( "X-RateLimit-Remaining-Time" ) if remaining is not None and reset_ns is not None: try: if int(remaining) <= 0: reset_seconds = int(reset_ns) / 1_000_000_000 if reset_seconds > 0: NEXT_REQUEST_AT = ( time.monotonic() + reset_seconds + 2 ) except ValueError: pass return data raise RuntimeError( "The API request could not be completed after retrying." ) def fetch_historical_dns(api_key, domain): """Retrieve historical DNS pages, up to a safety limit.""" snapshots = [] for page in range(1, MAX_DNS_PAGES + 1): print(f"Fetching historical DNS page {page}...") data = request_json( DNS_URL, { "apiKey": api_key, "domainName": domain, "type": "all", "page": page, }, ) records = data.get("historicalDnsRecords", []) if not isinstance(records, list): raise RuntimeError( "Unexpected DNS response: " "historicalDnsRecords is not a list." ) snapshots.extend(records) total_pages = data.get("totalPages") if total_pages is None: # Without pagination metadata, do not guess  # whether more pages exist.  break try: if page >= int(total_pages): break except (TypeError, ValueError): break else: print( "Warning: reached the DNS page safety limit. " "The returned dataset may be incomplete." ) return snapshots def fetch_historical_whois(api_key, domain): """Retrieve the historical WHOIS response.""" print("Fetching historical WHOIS data...") return request_json( WHOIS_URL, { "apiKey": api_key, "domainName": domain, "whois": "historical", }, ) def parse_datetime(value): """Parse common ISO-style timestamps and date strings.""" if value is None: return None if not isinstance(value, (str, int, float)): return None text = str(value).strip() if not text: return None try: parsed = datetime.fromisoformat( text.replace("Z", "+00:00") ) if parsed.tzinfo is None: parsed = parsed.replace(tzinfo=timezone.utc) return parsed except ValueError: return None def add_event(events, value, event_type, details): """Add an event only when its timestamp can be parsed.""" parsed = parse_datetime(value) if parsed is None: return events.append( { "timestamp": parsed.isoformat(), "type": event_type, "details": details, } ) def collect_ip_values(value): """Find valid IP addresses in common nested response fields.""" found = [] if isinstance(value, dict): for key, item in value.items(): if key.lower() in { "address", "ip_address", "ip", } and isinstance(item, str): try: found.append( str(ipaddress.ip_address(item.strip())) ) except ValueError: pass else: found.extend(collect_ip_values(item)) elif isinstance(value, list): for item in value: found.extend(collect_ip_values(item)) return found def find_ip_addresses(snapshot): """Extract IPv4/IPv6 addresses from A and AAAA records.""" found = [] records = snapshot.get("dnsRecords", []) if not isinstance(records, list): return [] for record in records: if not isinstance(record, dict): continue record_type = str( record.get("dnsType", "") ).upper() if record_type not in {"A", "AAAA"}: continue # DNS API responses may place address data inside  # record-type-specific nested fields.  found.extend(collect_ip_values(record)) return sorted(set(found)) def build_timeline(dns_snapshots, whois_data): """Create a timeline from available DNS and WHOIS fields.""" events = [] for snapshot in dns_snapshots: if not isinstance(snapshot, dict): continue domain = snapshot.get("domainName", "unknown domain") query_time = snapshot.get("queryTime") ip_addresses = find_ip_addresses(snapshot) add_event( events, query_time, "dns_snapshot", { "domain": domain, "ip_addresses": ip_addresses, "dns_types": snapshot.get("dnsTypes", []), }, ) whois_records = whois_data.get( "whois_domains_historical", [] ) if isinstance(whois_records, list): for record in whois_records: if not isinstance(record, dict): continue domain = record.get("domain_name", "unknown domain") query_time = record.get("query_time") registrar = record.get("domain_registrar") if isinstance(registrar, dict): registrar = registrar.get("registrar_name") add_event( events, query_time, "whois_snapshot", { "domain": domain, "registrar": registrar, "name_servers": record.get("name_servers"), "domain_status": record.get("domain_status"), }, ) for field, label in [ ("create_date", "reported_creation_date"), ("update_date", "reported_update_date"), ("expiry_date", "reported_expiry_date"), ]: add_event( events, record.get(field), label, { "domain": domain, "source_query_time": query_time, }, ) events.sort(key=lambda event: event["timestamp"]) return events def main(): parser = argparse.ArgumentParser( description=( "Collect historical DNS and WHOIS data " "for a domain investigation." ) ) parser.add_argument( "domain", help="Domain to investigate, such as example.com", ) parser.add_argument( "--output", default<...
Source:https://dev.to/furqan_ashraf/investigate-a-suspicious-domain-historical-dns-and-whois-1pj3