Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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 (`<random-hex>.example.com`). If they resolve, the zone answers for
Expand All @@ -182,7 +201,7 @@ The end-of-run summary reports the count separately:

```
Resolved : 4
of which wildcard : 2 (catch-all DNSnot real hosts)
of which wildcard : 2 (catch-all zoneresolution proves nothing)
```

Notes:
Expand Down
78 changes: 77 additions & 1 deletion REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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) |
Expand Down Expand Up @@ -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.
10 changes: 9 additions & 1 deletion classes/run_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,19 @@ def display(self, total_input, failed_count):
if wildcard_count:
print(
f" of which wildcard : {wildcard_count:>6,}"
" (catch-all DNSnot real hosts)"
" (catch-all zoneresolution 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}"
)
Expand Down
23 changes: 23 additions & 0 deletions tests/test_run_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -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