Repository navigation
Releases: incendiary/DNSResolver
Release list
v3.0.1: the direnv template now exports its variables
Patch release. No change to the tool's behaviour, outputs or contracts.
Fixed
.envrc.examplenow exports its variables. The template shipped in v3.0.0 had lines such as# DNSRESOLVER_TIMEOUT=1. direnv evaluates.envrcas shell, so uncommenting one as written set an unexported variable that never reached the tool. Every example line is now# export DNSRESOLVER_X=default, and the README says to keep theexport. A new test uncomments the whole template, sources it in bash and checks that every variable the code reads is exported.
If you copied the v3.0.0 template, copy it again (cp .envrc.example .envrc && direnv allow) or add export to the lines you uncommented.
Everything else is unchanged from v3.0.0, including its breaking changes if you are upgrading from v2.
v3.0.0: versioned allocator handoff, fail-closed runs, and a much faster matcher
Major release. Read the breaking changes before upgrading.
v2.0.0 made cloud-address attribution a first-class product. v3.0.0 finishes that path: a versioned, schema-checked handoff document for the allocation pipeline, a run that fails closed instead of publishing partial results, a matcher that is about 64 times faster, and a leaner dependency set (three runtime dependencies instead of seven).
Breaking changes
| Area | Change |
|---|---|
takeover_candidates_*.txt |
NS_TAKEOVER records gain a fourth field: NS_TAKEOVER|origin|domain|nameserver. nameserver is the NS host whose address lookup failed, or - where the name has NS records but no address records. Parsers that expect exactly three fields will break. |
environment_results_*.json |
external_ip is now null unless --record-external-ip (or DNSRESOLVER_RECORD_EXTERNAL_IP=true) is set. The lookup called a third-party service on every start; it is now opt-in. |
| AWS Lambda | The Lambda entry point (lambda_handler.py), its environment manager, docs/LAMBDA.md, requirements-lambda.txt and the boto3 dependency are removed. The shared run() pipeline stays. Restorable from tag v2.0.0. |
| Configuration | Boolean options (verbose, extreme, evidence) in config.json now actually apply (they were silently ignored). The shipped config.json leaves all three off, so default behaviour is unchanged. Values in config.json and DNSRESOLVER_* variables are validated: a wrong type or an invalid boolean is an error that names the key, not a silent default. |
| Pipe file line order | The four pipe files are now written sorted by full line text, so identical input gives byte-identical files under any hash seed. Content is unchanged; do not depend on more than stability. |
For the allocation pipeline
allocator-targets-v1.json: a provider-aware JSON document of actionable cloud addresses, schema-checked (contracts/), grouped by provider, hostname, address and region with all published services, prefixes and network border groups preserved. Wildcard and uncertain results are excluded.dns-observations-v1.jsonandrun-manifest-v1.json: everything excluded, with reasons, and a manifest saying whether the run was complete, incomplete or failed. Actionable output is null for an incomplete or failed run.- Fails closed: a missing or stale AWS, GCP or Azure catalogue stops the run before any domain is processed; a required output that cannot be written is an error, not a swallowed one.
- Every attribution is preserved: overlapping prefixes, the same CIDR published by several providers, and one prefix under several services or regions all survive to the output.
New
run-report.md: a compact Markdown view of a run, on by default (--format md;--format jsonskips it). The JSON documents stay canonical and are identical either way. For the golden fixture the targets section is about a quarter of the size of the JSON, which makes a run cheap to read for an agent or a human.- Configuration from the environment: every option can come from a
DNSRESOLVER_*variable. A committed.envrc.exampleis the template for a git-ignored.envrc(direnv). Precedence: command-line flag, environment,config.json, built-in default. New negated flags:--no-verbose,--no-extreme,--no-evidence. - Much faster matching: prefixes are indexed by length instead of scanned one by one. On a 55,000-prefix catalogue, 10,000 hostnames take 1.3 s instead of 84.5 s, and 100,000 take 11 s. Output is identical; a property test checks the index against the original linear scan, including nested and duplicated prefixes.
bench/holds the harness and the baseline. - Reliable DNS resolution: timeouts and nameservers reach both resolver libraries, and A and AAAA fallback results survive.
Leaner and safer to build
- Runtime dependencies:
dnspython,tqdm,aiodns.requests,urllib3,aiofilesandboto3are gone. - Installs in CI are hash-locked (
requirements.lock,requirements-dev.lock), a weeklypip-auditruns, and CI enforces a 95% coverage floor (the suite is at about 97%). - A byte-exact end-to-end golden test now gates every change to the output files.
- Results are held as structured records and the JSON documents are built from them; the tool no longer re-parses its own output files.
- The delivered per-PR planning documents are archived;
ROADMAP.mdandREVIEW.mdcarry the current plan and findings.
Upgrading from v2.0.0
- If you parse
takeover_candidates_*.txt, accept a fourthNS_TAKEOVERfield. - If you read
external_ipfromenvironment_results_*.json, pass--record-external-ip. - If you deployed the Lambda entry point, stay on v2.0.0 or run the CLI instead.
- Optional: copy
.envrc.exampleto.envrcanddirenv allow.
Full detail is in README.md and AGENTS.md; the plan and decisions behind this release are in ROADMAP.md and REVIEW.md.
v2.0.0 — Cloud reclaim targets as a first-class product
Major release. Output formats have changed — see Breaking changes below before upgrading.
This release follows a correction in what the tool is understood to be. Cloud attribution had been treated as a supporting detail of a dangling-CNAME scanner. It is one of two jobs of equal weight, and the one without a commodity equivalent.
A dangling CNAME names a service, so its intent can be read from DNS. A bare A record cannot. If that address is a cloud IP the owner released, whoever allocates it next controls what is served for that hostname — and DNS gives no signal that this is so. Establishing which A records land in cloud ranges worth pursuing, and where, is what this tool adds.
Assessed against that purpose, the cloud path was unfinished.
Breaking changes
| File | Change |
|---|---|
csp_matches_*.txt |
Prose sentence replaced by structured fields: domain|ip|provider|region|service|prefix|border_group, one record per address |
resolution_results_*.txt |
May carry a WILDCARD_ZONE| prefix in addition to WILDCARD| |
takeover_candidates_*.txt |
Gained hop count and full CNAME chain path |
Anything parsing v1 output needs updating.
Cloud matches are now actionable
Region, service and AWS network border group are captured from all three providers and written with every match. Previously all of it was discarded at parse time and the output read resolved to aws IPs, which is close to no information: over half of AWS1s ~10,500 published prefixes carry the generic AMAZON tag.
The difference in practice — these are now distinguishable at a glance:
example.com|13.35.163.22|aws|GLOBAL|CLOUDFRONT|13.35.0.0/16|GLOBAL
example.com|3.11.53.7|aws|eu-west-2|EC2|3.8.0.0/14|eu-west-2
Both are "AWS". Only one is worth pursuing. The border group is the boundary an Elastic IP is actually allocated from, which usually mirrors the region but diverges for Local Zones and Wavelength.
Wildcard zones no longer inflate the target list
Two problems, both found by running against real DNS rather than by the test suite.
Catch-all answers were fed to the cloud matcher unmarked, so a wildcarded zone in cloud space emitted one record per enumerated subdomain — the hosting platform1s own addresses burying real findings.
The wildcard test itself asked only whether a resolution matched addresses a probe had observed. Against a large rotating fleet a probe sees a fraction of it, so a later name landed outside the sampled set and was reported as a clean result.
Wildcard status is now reported as two verdicts, because only one of the two questions is reliably answerable. Whether a zone answers for anything is always knowable; whether a particular answer came from that wildcard often is not.
WILDCARD— addresses matched what the probe observed; a confirmed catch-all.WILDCARD_ZONE— the zone answers for anything, but these addresses were not sampled. Could be a catch-all from a larger pool, or a genuine host.
Both are excluded from cloud target counts: in such a zone an address cannot be attributed to the domain rather than to the platform. On one live input this took cloud targets from eight to four, the four removed being a rotating catch-all that had counted as real.
Also
CNAME chain depth and full hop path are recorded on dangling output, so the claimable hop is visible without re-resolving. The end-of-run summary announces cloud-hosted addresses in their own right rather than as a footnote, and groups them by region and service.
AGENTS.md at the repository root states the tool1s purpose and conventions for anyone — human or AI agent — picking the project up, so the misreading this release corrects does not have to be rediscovered.
Note on how these were found
None of the defects fixed here were caught by the test suite, which stayed green throughout at 95% coverage. Two came from running the tool against real DNS, and the framing error came from the maintainer correcting the premise. Coverage measures exercise, not correctness — and a thorough review measuring the wrong thing produces confident, well-supported, wrong conclusions.
v1.12.0 — Wildcard detection and fewer false positives
Focused on making results trustworthy: two classes of false positive removed, wildcard zones detected, and the fingerprint set half again as large. Test count 186 to 243.
Added
Wildcard DNS detection. A zone serving *.example.com answers for every name beneath it, so an enumerated list returns thousands of "resolved" domains that are not real hosts. Each zone is now probed with a couple of random labels; if those resolve, the zone answers for anything and its addresses are recorded. Domains resolving only to that set are marked WILDCARD| and counted separately in the summary.
A host answering with any address outside the set is treated as real and left unflagged. Results are cached per zone, so a scan costs one probe round per zone rather than one per domain, and zones without a wildcard are untouched.
30 further takeover fingerprints (60 to 90), covering Webflow, Help Scout, Intercom, Campaign Monitor, Thinkific, Kajabi, Canny, Frontify, Aha!, ReadMe, SmugMug, Kinsta, Short.io and others. An unrecognised target is reported unknown, so a missing fingerprint turns a real finding into an unactionable line.
Fixed
Non-existent domains are no longer reported as takeover candidates. Any domain lacking A, AAAA, MX and NS records was written as a dangling CNAME whether or not a CNAME existed. A live run of 15 hosts announced "9 TAKEOVER CANDIDATE(S) DETECTED — REVIEW IMMEDIATELY"; all nine were plain NXDOMAIN names with no CNAME at all. On a real enumeration, where most candidate subdomains do not exist, that noise buries genuine findings. A CNAME must now actually be observed; names that simply do not resolve are reported as unresolved.
Housekeeping
The container build pipeline (Dockerfile, ECR workflow) has been removed — the Lambda image is maintained in a separate deployment project, and the workflow failed on every release tag for want of credentials this repository does not hold. lambda_handler.py remains as a reference entry point and stays covered by tests.
The README now links to the roadmap rather than duplicating it, states known limitations explicitly, and the certificate-transparency one-liner is a proper script (helper/crtsh_domains.sh) that retries, fails loudly rather than emitting a partial list, and filters out the certificate common names crt.sh returns alongside hostnames.
All outstanding dependency updates are merged.
Note on verification
Both false-positive fixes and the wildcard IPv6 gap were found by running the tool against real DNS, not by the test suite — which passed throughout. Coverage sits at 95% and did not catch any of them.
v1.11.0 — CI reliability, correctness, coverage, IPv6
Batch release covering the review-driven work. Test count 144 to 185, coverage 85% to 95%.
Fixed
CI on main was red and blocking every open PR. The version-sync baseline test asserted a git tag exists, but CI checks out without fetching tags, so it could never pass there — and it passed locally, so running CI locally did not catch it. The test now skips on a tagless checkout and accepts an unreleased version bump; CI fetches full history. ruff is pinned exactly so a new release cannot turn the build red on its own.
Dead subdomains were mislabelled as self-referential CNAMEs. The check compared the current domain against the original, which is trivially true on entry — so any NXDOMAIN name with no CNAME was reported as "the domain points to itself". Found by an authorised live run: 9 of 9 non-resolving hosts were mislabelled. The label is now gated on having actually followed a CNAME.
A network blip during Azure IP-range fetching could kill an entire scan. sys.exit(1) inside the fallback chain terminated the process and discarded all results gathered so far. It now returns empty and the caller skips Azure.
An empty timeout_results_*.txt was written on every run — a leftover from the output consolidation. Removed.
Added
IPv6 resolution. A and AAAA are now queried concurrently; a host serving only AAAA records was previously reported as unresolved. Verified live: ipv6.google.com now resolves to four IPv6 addresses. A domain counts as unresolved only when both record types fail, so takeover detection is unchanged.
Performance
CIDR ranges are parsed once per run rather than once per resolved IP (AWS and Azure publish roughly 67,000 prefixes between them). Duplicate cloud-provider matches are filtered through an in-memory set instead of re-reading the whole output file on every write — which also removes a blocking read from the async path and fixes a substring match that could suppress a legitimately different line.
Tests
Coverage rose from 85% to 95%. EnvironmentManager went from 35% to 86% after extracting its live HTTP call behind a seam so the class can be constructed offline; OutputManager reached 100%. Regression tests lock in each of the fixes above.
Documentation
The Lambda deployment walkthrough moved from the README to docs/LAMBDA.md, cutting the README from 406 to 232 lines, and the DNS-only scope boundary is now stated explicitly. REVIEW.md, ROADMAP.md and docs/roadmap/ record the review findings and the remaining plan.
v1.10.4 — README corrections
What changed
Documentation fixes only — no code changes.
- Installation instructions:
venvcorrected to.venv - Options table:
--versionflag added - Stale v1.1.0 scope-change callout removed — replaced with a plain statement of what the tool does not do
- Architecture diagram:
TakeoverDetectorandDomainCategoriseradded - Roadmap: pre-v1.10.0 version numbers removed (those tags no longer exist); v1.10.x entries retain version numbers
v1.10.3 — Self-referential CNAME classification
What changed
Self-referential CNAME fix
A domain with a CNAME pointing to itself was being surfaced as an unclassified takeover candidate with misleading risk text. It is a DNS misconfiguration — the target cannot be claimed by an attacker since it sits under a registered apex. It now appears in the summary under [self_referential]:
Classified dangling CNAMEs (1)
[self_referential]
atomic-uat.example.com.
Recommendation : Remove or correct the CNAME — the domain points to itself and will never resolve
Evidence : N/A
Latent bug fix
The NS query path inside check_dangling_cname_async was still writing to the removed ns_takeover output key (leftover from the v1.10.0 output consolidation). This path would have caused a KeyError at runtime for any domain whose CNAME target had NS records but no address records. Fixed to write NS_TAKEOVER| prefixed lines to the consolidated takeover key.
v1.10.2 — Version string
What changed
python resolver.py --versionnow printsDNSResolver 1.10.2and exitsDNSResolver v1.10.2is printed at startup before processing begins- The run summary header reads
DNSResolver v1.10.2 — Run Summary - Version is defined in one place:
version.py
v1.10.1 — Actionable run summary
What changed
The end-of-run summary now surfaces every takeover candidate with enough context to act without opening a file.
Unclassified dangling CNAMEs — before
Unclassified dangling CNAMEs : 2 (see takeover_candidates.txt)
Unclassified dangling CNAMEs — after
Unclassified dangling CNAMEs (2) — investigate manually
[?] accend.example.com
CNAME target : k8s-prod-1234.ap-southeast-1.elb.amazonaws.com.
Risk : Target does not resolve — if it can be claimed,
an attacker can serve content from accend.example.com
Action : Verify this CNAME is intentional; remove it if not
NS Takeover — after
NS Takeover candidates (1)
vuln.example.com -> NS: ns1.orphaned-registrar.com
Why : Nameserver does not resolve — registering it gives
an attacker DNS control over vuln.example.com and all its subdomains
The alert banner also now reads REVIEW IMMEDIATELY to make urgency explicit.
Note: the run shown in this release used pre-v1.9.2 code. With current code, accend.example.com → *.elb.amazonaws.com CNAMEs are classified as aws_elb and shown under the classified section with the ELB-specific takeover recommendation.
v1.10.0 — Consolidated output files
What changed
Output files reduced from 7 text files to 4 — cleaner working directory and simpler post-processing.
| Before (7 files) | After (4 files) |
|---|---|
dangling_cname_results_*.txt |
→ takeover_candidates_*.txt (prefixed `DANGLING |
ns_takeover_results_*.txt |
→ takeover_candidates_*.txt (prefixed `NS_TAKEOVER |
gcp_results_*.txt |
→ csp_matches_*.txt |
aws_results_*.txt |
→ csp_matches_*.txt |
azure_results_*.txt |
→ csp_matches_*.txt |
resolution_results_*.txt |
unchanged |
unresolved_results_*.txt |
unchanged |
takeover_candidates_*.txt format
DANGLING|<root_domain>|<cname_target>|<category>|<recommendation>|<evidence_url>
NS_TAKEOVER|<root_domain>|<ns_domain>
csp_matches_*.txt format
<domain> resolved to <vendor> IPs: ['<ip>', ...]
Breaking change
Any downstream tooling that reads the old dangling_cname_results_*.txt, ns_takeover_results_*.txt, gcp_results_*.txt, aws_results_*.txt, or azure_results_*.txt files must be updated to the new paths and line formats.