What changed on our external attack surface since the last scan? Diffing discovery runs: asset identity, churn that is not change, and "disappeared" versus "unreachable"

DedSec Security Research ·

Monday's change report for example.com, an invented one worked through at the end of this post, lists 49 changes since Friday. Most are moved addresses, renewed certificates and unfamiliar names that all return the same page. Somewhere in the list, one CNAME now points at a different vendor, and a host that answered on Friday did not answer this morning. Nobody knows whether it was retired, is down, or was not reached.

A change report is a diff of two samples of something that changes constantly. What it shows depends on decisions it may not state: what counts as one asset, which properties are compared, which variation is noise, and what "not seen" means. Get those wrong and the report is either too noisy to read or silent about the change that matters.

Earlier posts cover the neighbouring questions: whether an asset is yours, what makes a dangling record exploitable, what certificate drift looks like and how findings are matched across scans. This one is about the diff itself, as general practice.

DNS behaviour is quoted from the RFCs named in each section, retrieved from rfc-editor.org on 24 October 2026; port states from the Nmap reference guide on nmap.org, and the certificate-lifetime schedule from the CA/Browser Forum's ballot page, both retrieved the same day. Names and addresses use example.com and the documentation ranges in RFC 5737 (192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24) and RFC 3849 (2001:db8::/32). All examples are illustrative.

What one asset is

To compare two runs, each observation needs a key that says which earlier observation it is. There are three natural candidates, and the choice decides what "changed" means.

The name. Key on www.example.com, and its addresses become attributes. A new address is a changed attribute, not a new asset, so the diff survives CDN and cloud address churn. It is blind to anything reachable only by address: a host with no DNS name, or a service still listening after the name pointing at it was removed.

The address. Key on 192.0.2.10, and the names pointing at it become attributes. This catches services with no name, but breaks down on shared infrastructure: a CDN edge address serves many unrelated names, and an address you release can be reassigned to someone else. An address-keyed diff of a CDN-fronted estate reports constant arrivals and departures of things that never changed.

The service: host, port and transport. Key on (vpn.example.com, 443, tcp), and the TLS configuration, banner and HTTP response become attributes. This is where exposure lives, since a new open port is a new attack path on an old host. It inherits the weaknesses of its host key and adds one: if the detected service type is part of the key, a fingerprint that misreads a listener on one run produces a "new" service and a "disappeared" one for a single unchanged port.

The practical answer is layered identity. Names, addresses, services and certificates are separate objects with separate keys, linked by observed relationships: this name resolved to this address at this time, this service presented this certificate. Each layer gets its own diff. A name whose addresses rotate has a quiet attribute change; a new service on an old name is a new object. The finding identity post describes the same trap for findings: a key that includes something volatile splits one thing into many.

Whatever the keys are, the report should state them. "12 new assets" means different things if an asset is a name, an address or a port.

Churn that is not change

Much of a raw diff is variation the owner did not cause and does not care about. Each source has its own mechanism and needs its own normalisation.

Address rotation

Round-robin answers. RFC 1034 section 3.6: "The order of RRs in a set is not significant, and need not be preserved by name servers, resolvers, or other parts of the DNS." A diff that compares answers as ordered lists reports a change whenever the order shuffles. Compare sets.

CDN, anycast and cloud load balancers. Here the set itself changes, from query to query or as the provider scales. None of it changes your exposure. Compare what you control, such as the CNAME target, rather than the addresses, and for addresses inside a provider's published ranges, record the provider instead of diffing the address. The attribution post explains why those addresses are the provider's.

Answers that depend on who asks. Geo-DNS and traffic steering answer different resolvers differently. RFC 7871 (EDNS Client Subnet) was written because authoritative servers tailor answers to the querier's location; it defines a "Tailored Response" as one "customized for the node that sent the query". Change the region or recursive resolver discovery runs from, and answers change with no change in the zone. Pin the vantage point, or record it with each observation.

Certificate renewal

RFC 5280 section 4.1.2.2 requires a certificate's serial number to be "unique for each certificate issued by a given CA". Serial and fingerprint therefore change on every renewal, and the public key often does too. A diff keyed on fingerprint reports every renewal as one certificate disappearing and another appearing.

Renewals are becoming more frequent. CA/Browser Forum ballot SC-081v3, adopted in April 2025, reduces the maximum validity of publicly trusted TLS certificates "from 398 days to 47 days", in steps "starting in March 2026 and concluding in March 2029".

Track a certificate through the service presenting it, and compare what carries meaning: issuer, names covered, key type and size, validity window, and whether the chain validates. A new serial from the same issuer for the same names is a renewal. A new issuer is not; the TLS drift post covers why.

Wildcard DNS

A wildcard answers for names that were never created, and further down than people expect. In RFC 4592's example zone, with *.example. present, a query for foo.bar.example. is answered from the wildcard "because bar.example. does not exist, but the wildcard does." Enumeration that guesses labels, or reads them from passive sources full of typos and scanner probes, finds an unlimited supply of "new" names that are all one record.

The standard defence is to resolve a random label first and treat every name whose answer matches as the wildcard. Two details complicate it in a diff:

Banners and response bodies

HTTP responses carry values that change on every request: Date headers, cookies, CSRF tokens, request IDs, CSP nonces, rendered timestamps. A hash of the body changes every run. Diff what describes exposure instead: status code, security-relevant headers, page title, the authentication challenge (login form, basic auth, an SSO redirect and its destination), and the detected technology and version. Strip volatile tokens before hashing anything.

"Disappeared" is several different results

The line that says something is gone is the most consequential in a change report, and the most likely to be wrong, because many different observations look like absence.

What the DNS can tell you

NXDOMAIN. RFC 1035 defines response code 3, Name Error, as "meaningful only for responses from an authoritative name server", signifying "that the domain name referenced in the query does not exist." RFC 2308 requires the zone's SOA in the response so the negative answer can be cached, for "the minimum of the MINIMUM field of the SOA record and the TTL of the SOA itself". RFC 8020 adds that after an NXDOMAIN, "all names and resource record sets (RRsets) at or below that node SHOULD be considered unreachable." An authoritative NXDOMAIN is the strongest removal evidence DNS offers, though a cached one can outlive a record being re-added.

NODATA. RFC 2308: "the name is valid, for the given class, but are no records of the given type." A name that lost its AAAA record but kept its A record has not disappeared, but a diff that treats an empty answer as a missing name will say it has.

SERVFAIL and timeouts. Response code 2 means the server "was unable to process this query due to a problem with the name server." Neither it nor a timeout says anything about whether the name exists.

Cached failures. RFC 2308 section 7 lets resolvers cache a server failure, or treat a server as dead, for at most five minutes. RFC 9520 (2023) makes it mandatory: "Resolvers MUST cache resolution failures for at least 1 second", and still for no more than five minutes. A run that sends many queries through one resolver can see one brief upstream problem repeated across every name in a zone.

Stale answers. RFC 8767 (serve-stale) lets a recursive resolver answer with expired data when it cannot refresh from the authoritative servers, with a suggested retention of "between 1 and 3 days" past expiry. An authoritative NoError or NXDOMAIN, with the AA bit set, counts as a refresh, so a deleted record does go away; what serve-stale can hide for days is a zone whose authoritative servers stopped answering. RFC 8914 Extended DNS Errors lets a resolver say which case applies, including code 3 (Stale Answer), 13 (Cached Error) and 22 (No Reachable Authority), when it sends them.

So for any name that seems to have disappeared, query the authoritative servers directly and record the response code.

What a port scan can tell you

The Nmap reference guide separates states that a seen-or-not diff collapses. A closed port "is accessible (it receives and responds to Nmap probe packets), but there is no application listening on it." Filtered means "Nmap cannot determine whether the port is open because packet filtering prevents its probes from reaching the port", and filters that silently drop probes "are far more common" than ones that answer with an error. Open to closed probably means the service stopped. Open to filtered may mean it was firewalled off from the internet, or only from your scanner by a rate limit, an intrusion-prevention rule, a geo-block or a provider's abuse protection.

The scanner itself may have changed: a new egress address, a shorter timeout, a smaller port list, a run cut short. The post on scans that pass without running covers that failure. Each run should report what it attempted and what completed, so a coverage gap is not read as the estate shrinking.

The dangerous case: it still resolves, but the target is gone

The most important disappearance is often one where the name stays. A CNAME still points at a cloud bucket, hosting app or CDN distribution and the name still resolves, but the provider now answers with an error meaning "no such resource". A name-keyed diff sees nothing. A content diff sees your site replaced by an error page. That is a dangling record, possibly claimable by anyone; the subdomain takeover post covers recognising and fixing one. It is the reason to diff the end of the CNAME chain, and what it serves, as well as the name.

Confirming absence: consecutive misses and tombstones

One miss is weak evidence, so absence should be a state an asset moves into:

A tombstone is also where a late takeover shows up. A name that was absent and returns pointing at a different provider, or serving content you did not publish, deserves more attention than a name seen for the first time.

Which changes matter

After normalisation, what remains is still uneven. Changes that usually alter exposure:

Usually cosmetic: address rotation within one provider, a same-issuer renewal, a reordered answer, a changed Date header or token, a wildcard name, a minor version bump in a supported branch. Log them for history. Alert on them and nobody reads the report.

The detection window

A diff sees state only at sample times. A bucket made public for an afternoon, a debug port opened during an incident and closed next morning, a staging host stood up for a weekend demo: if the exposure starts and ends between two runs, the diff never shows it, though anyone scanning in that window could have found it. Shorter intervals help but cost queries and load, and some changes will always fall between samples. What fills the gap is sources that record changes as they happen.

Certificate Transparency. Publicly trusted CAs log certificates to public append-only logs, because browsers require it for trust (RFC 6962, obsoleted by version 2 in RFC 9162). A CA typically logs a precertificate before issuing the certificate, and certificates are usually obtained before a service goes live, so a new name can appear in CT before its host serves anything. Delivery is bounded but not instant: a log promises to incorporate an accepted submission within its Maximum Merge Delay (RFC 6962 section 3, carried into RFC 9162), a parameter each log sets. The limits in the TLS drift post apply: names covered only by wildcard certificates or private CAs never appear, and a logged name is not necessarily deployed. Treat a CT entry as a reason to look at a name now, not as an inventory record.

Your own change records. DNS provider audit logs, infrastructure-as-code plans, cloud configuration history and certificate requests record changes when they happen. A change in these with no counterpart in the outside-in diff, or the reverse, is worth investigating.

The first run hides what is already there

A change report shows differences from a baseline, and the first run sets the baseline. Whatever was exposed when monitoring began, whether the forgotten admin panel, the dangling CNAME or the open database port, is part of it and never appears as "new". The same happens whenever the baseline is reset: after a tool migration, or when a newly acquired domain is enrolled. Two related effects:

The fix is to review the inventory itself, with an owner per asset: on the first run, after each baseline reset, and periodically after that. The change report is how you keep up with the estate, not how you learn what is in it.

An illustrative diff

The invented example.com report from the opening, after normalisation:

  1. www.example.com: CNAME target unchanged; the addresses it resolves to changed from 192.0.2.10 and 192.0.2.11 to 192.0.2.11 and 192.0.2.12, all inside the CDN's published range. Churn.
  2. api.example.com: new serial and fingerprint, same issuer, same names, later expiry. Renewal.
  3. qz81k.example.com and 40 more names, all answering with the same CNAME as a random-label probe. Wildcard.
  4. legacy.example.com: SERVFAIL through the recursive resolver; the authoritative servers time out. Missing, cause unknown. Check the delegation before calling it retired.
  5. shop.example.com: CNAME target moved from one hosting provider's domain to another's. Matters: a new dependency, and the old resource should be released only after the move.
  6. vpn.example.com (198.51.100.7): port 8443 newly open, serving a login page. Matters.
  7. promo.example.com: still resolves; the chain ends at a storage provider now returning its "no such bucket" error. Matters, urgently: a probable dangling record.
  8. webmail.example.com: TLS 1.0 now accepted where only 1.2 and 1.3 were, and the certificate is self-signed. Matters: probably a different backend.
  9. dev.example.com: new AAAA record 2001:db8::5, with SSH open on IPv6 and filtered on IPv4. Matters.

Forty-nine raw changes: five that need someone today, one that needs a second look, and forty-three that belong in the history.

Where Maphra fits

Maphra starts from a domain name and works outward, with no agent and no credential into your estate. Its discovery includes subdomain and host enumeration, service and technology fingerprinting, and certificate transparency monitoring. It re-runs discovery on a schedule, diffs the result against the last known surface, and tells you what appeared, what changed and what quietly disappeared (how any tool decides that is one of the questions below). Findings are deduplicated across scans and tracked through new, reopened and fixed states, and each carries the evidence that produced it.

That is the whole claim. This post does not claim which keys Maphra uses for an asset, which fields it compares between runs, how it normalises address, certificate, wildcard or response-body churn, how many consecutive misses it requires before reporting something as gone, whether it keeps tombstones, how it distinguishes NXDOMAIN from SERVFAIL or filtered from closed, how often its schedule runs, or that it uses Certificate Transparency as an event source between runs. The mechanisms above are general practice, written so you can judge any change report, ours included.

Questions to ask of any change report

  1. What is the unit: name, address, service, certificate, or a layered model? Does the report say?
  2. Which fields are compared for each unit, and which are deliberately ignored?
  3. How is address churn handled for CDN, anycast, load-balancer and round-robin answers? Are answers compared as sets?
  4. Is the vantage point fixed, and are the resolver and region recorded with each observation?
  5. Is a renewal distinguished from a replacement, with an issuer change reported separately?
  6. How are wildcards detected, including rotating wildcard answers and names that leave a wildcard's coverage when a real record is added?
  7. What does "disappeared" mean? Is the reason recorded: NXDOMAIN, NODATA, SERVFAIL, timeout, closed, filtered or not attempted?
  8. How many misses confirm absence, and does a returning asset come back with its history?
  9. Does it diff the end of the CNAME chain and what is served there?
  10. Does each run report its own coverage, so a scanner failure is not read as the estate shrinking?
  11. What covers the time between runs, such as Certificate Transparency, and how long can an exposure go unsampled?
  12. What was in the baseline, when was the inventory last reviewed, and are a new source's first sightings marked as new to the tool rather than new on the internet?

A change report is only as useful as its definitions of "same", "different" and "gone". With those explicit, the 49-line Monday report shrinks to the lines someone has to act on, and the host that did not answer this morning comes with the reason it did not.

More about Maphra · All posts