An IP is a lease, not an identity
When we see an IP in an alert, we treat it like a name — "that's the attacker." But public IPv4 space is constantly reassigned, recycled, NAT'd and rotated. Today's malware command-and-control server is, a month later, a legitimate cloud VM rented by a small business. The IP didn't change; who holds the lease changed.
Your instinct in the SOC — "don't block it permanently, it might be clean soon" — is exactly right, and it's the whole point. Two consequences follow:
- Blocks should decay. A permanent block on a reassigned IP eventually punishes an innocent party and clutters your firewall.
- Trust should decay too. An IP you allow-listed for a partner can quietly change hands — and nobody watches that side.
Two blind spots: block rot and allowlist rot
Block rot is the one you already feel: a block list grows forever, and old entries quietly start hitting clean, reassigned space — false positives you never revisit.
Allowlist rot is the dangerous one almost nobody watches. You add a partner's or vendor's IP to a firewall allow rule. Two years later that IP has been released and re-leased to someone else — and your firewall is now trusting a stranger, with a permit rule straight into your network. The scary part: nothing alerts, because the IP never changed. Only the owner did.
What to track over time
For a watchlist of IPs and the domains you care about, snapshot these fields on a schedule and diff each run against the last:
| Field | Source | Why a change matters |
|---|---|---|
| A / AAAA records | DNS | A service moved IPs — migration, failover, or fast-flux malware. |
| ASN + netblock owner | RIPEstat / RDAP | The lease changed hands — the highest-signal event. |
| Reverse DNS (PTR) | DNS | The name an IP claims changed. |
| Geolocation | IP geo APIs | Country/region drift on a "known" IP. |
| Reputation / exposure | Blocklists, Shodan tags | Newly listed (badness arrived) or delisted (block can expire). |
| Open ports / services | Shodan InternetDB | Exposure changed on your own IPs. |
| TLS certificate | CT logs | A new certificate appeared on the host. |
Store one row per observation, so every field has a history you can graph and alert on. That's the whole trick: collect → diff → alert.
Hands-on lab: snapshot & diff an IP
Let's build the smallest useful version by hand. All of this is passive — it reads public registries, never probes the target. Run it in PowerShell on Windows (the ideas are identical on macOS/Linux with dig/curl).
What does the domain resolve to today?
Resolve-DnsName example.com -Type A | Select -Expand IPAddress
Who holds the lease on that IP right now?
RIPEstat (free, no key) maps an IP to its ASN and prefix, and the ASN to its holder:
# IP -> ASN + prefix
curl "https://stat.ripe.net/data/network-info/data.json?resource=93.184.216.34"
# ASN -> holder name
curl "https://stat.ripe.net/data/as-overview/data.json?resource=AS15133"
Reverse DNS and exposure
Resolve-DnsName 93.184.216.34 -Type PTR
# Shodan InternetDB (passive): ports, tags, CVEs already known for this IP
curl "https://internetdb.shodan.io/93.184.216.34"
Snapshot + diff in ~30 lines of Node
Save this as ipwatch.js. First run stores a baseline; every later run prints what changed.
const fs = require("fs");
const dns = require("dns").promises;
async function getJson(u){ const r = await fetch(u); return r.ok ? r.json() : null; }
async function snapshot(domain){
const ips = await dns.resolve4(domain).catch(()=>[]);
const out = {};
for (const ip of ips){
const ni = await getJson(`https://stat.ripe.net/data/network-info/data.json?resource=${ip}`);
const asn = ni?.data?.asns?.[0] ? "AS"+ni.data.asns[0] : "";
const ov = asn && await getJson(`https://stat.ripe.net/data/as-overview/data.json?resource=${asn}`);
const ptr = (await dns.reverse(ip).catch(()=>[]))[0] || "";
const idb = await getJson(`https://internetdb.shodan.io/${ip}`);
out[ip] = { asn, holder: ov?.data?.holder || "", ptr,
ports:(idb?.ports||[]).join(","), tags:(idb?.tags||[]).join(",") };
}
return out;
}
(async () => {
const domain = process.argv[2] || "example.com";
const file = `ipwatch_${domain}.json`;
const now = await snapshot(domain);
const prev = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file)) : null;
if (!prev){ console.log("Baseline saved for", domain); }
else {
for (const ip of new Set([...Object.keys(prev), ...Object.keys(now)])){
const a = prev[ip], b = now[ip];
if (!a) console.log(`NEW IP ${ip} ${b.holder}`);
else if (!b) console.log(`GONE IP ${ip} (was ${a.holder})`);
else for (const k of Object.keys(b))
if (a[k] !== b[k]) console.log(`CHANGED ${ip} ${k}: "${a[k]}" -> "${b[k]}"`);
}
}
fs.writeFileSync(file, JSON.stringify(now, null, 2));
})();
node ipwatch.js example.com # run 1: baseline
node ipwatch.js example.com # run later: prints only what changed
# CHANGED 93.184.216.34 holder: "EDGECAST" -> "EDGIO" <- the lease changed hands
That's the entire concept in one file: resolve, record owner/PTR/ports, and diff. Everything else — scheduling, a watchlist, alerting, scoring — is scaffolding around this core.
Building the tracker (and wiring it to Sentinel)
To make it real, wrap that loop in the same shape as any monitoring pipeline:
- Schedule: a timer job (cron, or an Azure Function timer like our DNS Audit monitor) runs the snapshot every hour/day.
- Store: append each observation to a table (Azure Table Storage, a database, or straight into a Log Analytics custom table).
- Diff & alert: compare to the previous snapshot; on a change, emit an event.
- Sentinel: send change events into Log Analytics and write analytics rules — now you get a timeline and alerting for free.
- Confidence decay: give every indicator a score that drops on a schedule and drops further when the owner changes, so both blocks and allows age out automatically.
The change events worth alerting on
| Event | Why it matters |
|---|---|
| Allow-listed IP changed ASN/owner | You're now trusting whoever got the lease. Re-verify the rule. (The one nobody watches.) |
| Blocked IP delisted / owner changed | Review and expire the block so you stop over-blocking clean space. |
| Your own asset IP changed unexpectedly | Cloud misconfig, failover, or — worst case — a BGP hijack. |
| Domain A-record flux > N/hour across ASNs | Fast-flux: malware / phishing infrastructure, not a CDN. |
| Monitored IP newly listed on a blocklist | A trusted neighbour just went bad on shared hosting. |
Video walkthrough
ipwatch.js catch an ownership change.Want this built for your environment?
We build lightweight monitoring like this — IP and DNS change tracking wired into Sentinel — for Melbourne businesses.
Try DNS AuditTalk to Yogesh