From ebb4eabf52b5efdc8793fc5d892dfb0561d3b5a4 Mon Sep 17 00:00:00 2001 From: incendiary Date: Fri, 31 Jul 2026 01:32:34 +0800 Subject: [PATCH] docs: state both purposes as co-equal, correct the review's premise MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The README led with dangling-CNAME detection and listed cloud matching as a bullet, which reads as though the first were the differentiator. It is the other way round: dangling-CNAME tooling is commodity, while identifying A records in claimable cloud space is what this tool adds. The opening now states both jobs, explains why the second is the less obvious one, and says plainly that the tool produces targets and never claims them — which is also why its output files are records rather than reports. REVIEW.md was written against that same misreading. It assessed cloud attribution as a supporting attribute and recorded it as met, at a time when the parser discarded every field that made a match actionable and the output said only "resolved to aws IPs". Rather than quietly rewrite the analysis, the mistaken premise is marked at the point it applies, the affected verdict is withdrawn, and a correction section records what the misframing caused the review to miss and what has since been fixed. Worth stating because it is a sharper version of the lesson already recorded there: that review was internally consistent, evidence-backed and thorough, while measuring against the wrong thing. Rigour applied to a wrong premise produces confident, well-supported, wrong conclusions. The run summary gave cloud matches a quiet line beneath the takeover banner. If the two jobs are co-equal the summary should show it, so cloud-hosted addresses are now announced in their own right, and a run that finds none says so rather than staying silent. Also corrects the wildcard wording. It read "not real hosts", which asserts non-existence that DNS cannot establish — a live site and a fabricated name in the same wildcarded zone return identical answers. It now says resolution proves nothing there, which is what is actually true. Co-Authored-By: Claude Opus 5 --- README.md | 37 ++++++++++++++----- REVIEW.md | 78 ++++++++++++++++++++++++++++++++++++++- classes/run_summary.py | 10 ++++- tests/test_run_summary.py | 23 ++++++++++++ 4 files changed, 137 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index af00828..8e1d4d5 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,33 @@ # DNSResolver -DNSResolver is a Python-based security tool for bulk DNS resolution and cloud infrastructure analysis. Given a list of domains it: +DNSResolver is a passive DNS reconnaissance tool for offensive security. Given a flat list of +candidate domains it **produces targets**, doing two jobs of equal weight: -- Resolves DNS records and matches resolved IPs against known IP ranges for **AWS, GCP, and Azure** -- Detects **dangling CNAME** records pointing to unclaimed cloud resources (potential subdomain takeover) -- Detects **NS takeover** opportunities where nameservers are unresolvable -- Flags **wildcard DNS** zones so catch-all answers are not mistaken for real hosts +**1. Dangling CNAME and NS takeover candidates.** CNAMEs pointing at service names nobody owns +any more, and domains whose nameservers no longer resolve. + +**2. A records resolving into cloud IP space.** The less obvious half, and the reason this tool +exists alongside the many that do job 1. + +A dangling CNAME names a service, so its intent can be read straight 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 the case**. Finding out means +churning allocation in that provider and region until the address comes back to you. + +So DNSResolver answers: *which A records land in cloud ranges worth pursuing, and where?* Each +match carries the provider's own published region, service and network border group, because an +address is only actionable if you know where it is allocated from and what it belongs to. + +It also: + +- Flags **wildcard DNS** zones, so catch-all answers are not mistaken for real hosts - Collects forensic evidence (dig/nslookup output) for flagged domains -All domain processing runs concurrently using `asyncio`, making it practical for large domain lists. +Domain processing runs concurrently using `asyncio`, making it practical for large domain lists. + +**This tool identifies targets — it does not claim them.** Acting on a finding is a separate +tool's job. That is why the output files are machine-readable records rather than prose reports, +and why DNSResolver reports what each provider publishes without judging what is worth pursuing. The tool is intentionally DNS-focused. It does not make active HTTP/HTTPS connections, probe TCP ports, validate TLS certificates, or take screenshots. These were deliberately excluded to keep the tool passive, dependency-light, and scoped to DNS reconnaissance. @@ -165,8 +184,8 @@ it reports what the provider states and leaves that decision to the operator. ## Wildcard DNS detection A zone serving a wildcard record (`*.example.com`) answers for **every** name beneath it. Against an -enumerated subdomain list that means thousands of "resolved" domains that are not real hosts, burying -the findings that matter. +enumerated subdomain list that means thousands of "resolved" domains whose resolution proves +nothing, burying the findings that matter. DNSResolver detects this automatically. For each zone it queries a couple of random labels that are almost certainly not real (`.example.com`). If they resolve, the zone answers for @@ -182,7 +201,7 @@ The end-of-run summary reports the count separately: ``` Resolved : 4 - of which wildcard : 2 (catch-all DNS — not real hosts) + of which wildcard : 2 (catch-all zone — resolution proves nothing) ``` Notes: diff --git a/REVIEW.md b/REVIEW.md index ede4d2e..da2339f 100644 --- a/REVIEW.md +++ b/REVIEW.md @@ -210,6 +210,12 @@ merely that a function "runs"; assert the classification/precedence/error-handli ## Phase 7 — Feature-vs-goals gap analysis (the "how good is good enough?" answer) +> ### ⚠️ This section was written against a misread goal — see [Correction](#correction-the-goal-this-review-assessed-was-wrong) at the end +> +> The analysis below treats cloud attribution as a supporting attribute of a dangling-CNAME +> tool. That is not what the tool is for. The verdicts in the table are sound for the goal +> stated, and the goal stated was wrong. Read the correction before relying on this. + **Stated goal (README):** passive, DNS-only bulk resolution + CSP attribution + dangling-CNAME and NS-takeover detection + forensic evidence, practical for large lists. Active probing (HTTP/TLS/ports/ screenshots) is *deliberately* out of scope. @@ -218,7 +224,7 @@ screenshots) is *deliberately* out of scope. |---|---|---| | Bulk async DNS resolution | asyncio + Semaphore + retry loop | ✅ Met | | Multi-IP fidelity (A records) | All A IPs captured, pipe-delimited, all fed to CSP match | ✅ Met | -| CSP attribution (AWS/GCP/Azure) | 3 providers, v4+v6, resilient Azure fetch | ✅ Met (perf caveat RA-C at scale) | +| CSP attribution (AWS/GCP/Azure) | 3 providers, v4+v6, resilient Azure fetch | ⚠️ **Verdict withdrawn** — assessed as an attribute, not as the product. See correction | | Dangling CNAME takeover detection | Depth-limited chain follow, 60 classification patterns, self-ref handling | ✅ Met, strong | | NS takeover detection | Unresolvable-NS check | ✅ Met | | Forensic evidence | Async dig/nslookup capture | ✅ Met (CLI only, by design) | @@ -331,3 +337,73 @@ and what it lacked was a green pipeline, three correctness fixes, and evidence. It is **not** finished — F1 is a genuine capability gap, and scale performance is unproven. Neither is a gap between promise and delivery: the README claims neither wildcard handling nor benchmarked throughput. + + +--- + +## Correction: the goal this review assessed was wrong + +*Added 2026-07-30, after the maintainer corrected the premise.* + +This review assessed DNSResolver as a dangling-CNAME detector with cloud attribution as a +supporting attribute. That is not what the tool is for, and the error runs through the whole +gap analysis in Phase 7. + +### The actual purpose + +Two jobs of **equal** weight: + +1. **Dangling CNAME / subdomain takeover candidates.** Commodity — other tooling covers this. +2. **A records resolving into claimable cloud IP space.** The differentiator. + +Job 2 is the one that is easy to under-value from the code. A dangling CNAME names a service, +so intent can be read straight from DNS. A bare A record cannot: if the 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**. The only way to find out is to churn allocation in +that provider and region until the address comes back to you. + +The tool identifies and hands off those targets. It never claims anything — that is a separate +tool. Which means the output files are **machine-readable interfaces**, not reports. + +### What the misframing caused this review to miss + +Phase 7 recorded CSP attribution as "✅ Met". At the time, `imports/cloud_ip_ranges.py` kept +only the CIDR string from each published prefix and discarded everything else: + +| Provider | Published | Kept | +|---|---|---| +| AWS | `ip_prefix`, `region`, `service`, `network_border_group` | prefix | +| GCP | `ipv4Prefix`/`ipv6Prefix`, `service`, `scope` | prefix | +| Azure | `addressPrefixes`, `region`, `systemService` | prefixes | + +The output read `example.com resolved to aws IPs: ['1.2.3.4']`. For a downstream consumer that +is close to useless: over half of AWS's ~10,500 prefixes carry the generic `AMAZON` tag, and +nothing distinguished a CloudFront edge from an EC2 address in a named region. The primary +product was, in effect, unfinished — and the review called it met. + +Two further defects followed from the same blind spot, both found only after the reframing: + +- **Wildcard resolutions were fed to the cloud matcher unmarked.** A wildcarded zone in cloud + space emitted one record per enumerated subdomain, each a distinct line, none deduplicated — + the hosting platform's own addresses burying real targets in the primary output. +- **`network_border_group` was discarded**, though it is the boundary an Elastic IP is actually + allocated from, and so the exact scope a consumer operates within. + +### Since fixed + +- Region, service and border group captured from all three providers. +- Handoff records: `[WILDCARD|]domain|ip|provider|region|service|prefix|border_group`. +- Summary groups by provider, region and service, and excludes wildcard records from target counts. +- Wildcard detection documented as best-effort against large rotating pools. + +### The lesson + +This is a sharper version of the verification lesson recorded above. There, tests passed while +the code was wrong. Here, the **review** was internally consistent, evidence-backed and +thorough — while measuring against the wrong thing. Rigour applied to a wrong premise produces +confident, well-supported, wrong conclusions. + +The premise came from the README, which led with dangling-CNAME detection and listed cloud +matching as a bullet. It was a reasonable misreading, available to anyone. That is why +[`AGENTS.md`](AGENTS.md) now states the purpose explicitly at repo root: so the next reader, +human or agent, does not have to infer it. diff --git a/classes/run_summary.py b/classes/run_summary.py index 3bc57e9..dfc261c 100644 --- a/classes/run_summary.py +++ b/classes/run_summary.py @@ -88,11 +88,19 @@ def display(self, total_input, failed_count): if wildcard_count: print( f" of which wildcard : {wildcard_count:>6,}" - " (catch-all DNS — not real hosts)" + " (catch-all zone — resolution proves nothing)" ) print(f" Unresolved errors : {unresolved_count:>6,}") print(f" Failed (all retries) : {failed_count:>6,}") print() + csp_target_count = aws_count + gcp_count + azure_count + if csp_target_count == 0: + print(" No cloud-hosted addresses found.") + else: + print( + f" [>] {csp_target_count} CLOUD-HOSTED ADDRESS(ES) — " + "candidate reclaim targets" + ) print( f" CSP matches — AWS: {aws_count} GCP: {gcp_count} Azure: {azure_count}" ) diff --git a/tests/test_run_summary.py b/tests/test_run_summary.py index bc9efe5..8c69379 100644 --- a/tests/test_run_summary.py +++ b/tests/test_run_summary.py @@ -261,3 +261,26 @@ def test_wildcard_cloud_records_are_excluded_from_targets(tmp_path, capsys): assert "AWS: 1" in out assert "GCP: 0" in out, "wildcard records must not count as GCP targets" assert "excluded (wildcard) : 2" in out + + +def test_cloud_targets_get_headline_billing(tmp_path, capsys): + """ + Cloud reclaim targets are one of the tool's two jobs, so they are announced + rather than reported as a footnote under the takeover banner. + """ + summary = _make_summary( + tmp_path, + {"csp": "a.com|1.1.1.1|aws|eu-west-2|EC2|1.1.0.0/16|eu-west-2\n"}, + ) + summary.display(total_input=1, failed_count=0) + + out = capsys.readouterr().out + assert "1 CLOUD-HOSTED ADDRESS(ES)" in out + + +def test_no_cloud_addresses_says_so_explicitly(tmp_path, capsys): + """Silence would read as 'not checked'; say the check ran and found nothing.""" + summary = _make_summary(tmp_path, {"csp": ""}) + summary.display(total_input=1, failed_count=0) + + assert "No cloud-hosted addresses found." in capsys.readouterr().out