Skip to content

Releases: incendiary/DNSResolver

v3.0.1: the direnv template now exports its variables

Choose a tag to compare

@incendiary incendiary released this 17 Oct 19:02
bc83833

Patch release. No change to the tool's behaviour, outputs or contracts.

Fixed

  • .envrc.example now exports its variables. The template shipped in v3.0.0 had lines such as # DNSRESOLVER_TIMEOUT=1. direnv evaluates .envrc as 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 the export. 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

Choose a tag to compare

@incendiary incendiary released this 17 Oct 18:51
b6fe6b8

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.json and run-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 json skips 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.example is 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, aiofiles and boto3 are gone.
  • Installs in CI are hash-locked (requirements.lock, requirements-dev.lock), a weekly pip-audit runs, 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.md and REVIEW.md carry the current plan and findings.

Upgrading from v2.0.0

  1. If you parse takeover_candidates_*.txt, accept a fourth NS_TAKEOVER field.
  2. If you read external_ip from environment_results_*.json, pass --record-external-ip.
  3. If you deployed the Lambda entry point, stay on v2.0.0 or run the CLI instead.
  4. Optional: copy .envrc.example to .envrc and direnv 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

Choose a tag to compare

@incendiary incendiary released this 31 Jul 04:14
23a5533

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

Choose a tag to compare

@incendiary incendiary released this 29 Jul 14:02
6ffcc47

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

Choose a tag to compare

@incendiary incendiary released this 29 Jul 12:48
cf8e315

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

Choose a tag to compare

@incendiary incendiary released this 10 May 12:15

What changed

Documentation fixes only — no code changes.

  • Installation instructions: venv corrected to .venv
  • Options table: --version flag added
  • Stale v1.1.0 scope-change callout removed — replaced with a plain statement of what the tool does not do
  • Architecture diagram: TakeoverDetector and DomainCategoriser added
  • 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

Choose a tag to compare

@incendiary incendiary released this 10 May 12:05

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

Choose a tag to compare

@incendiary incendiary released this 10 May 12:04

What changed

  • python resolver.py --version now prints DNSResolver 1.10.2 and exits
  • DNSResolver v1.10.2 is 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

Choose a tag to compare

@incendiary incendiary released this 10 May 12:04

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

Choose a tag to compare

@incendiary incendiary released this 10 May 12:04

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.