sazzad1148/sazzadul-APRSRF

GitHub: sazzad1148/sazzadul-APRSRF

一款高性能被动子域名侦察框架,集成19个数据源、递归枚举、DNS验证、云资产指纹及情报数据库,解决大规模子域名发现的效率与深度问题。

Stars: 0 | Forks: 0

# sazzad007 -- passive + active subdomain recon Passive subdomain discovery from **19 production-ready sources**, smart multi-resolver wildcard filtering, **multi-source recursive enumeration** (crt.sh SAN chaining + Wayback + GitHub + optional active JS/CSP scraping), deep permutation (level 2-8), DNS validation enriched with **TTL / resolver used / response time / DNSSEC**, reverse DNS grouped by **ASN / organization / cloud provider**, cloud asset fingerprinting (AWS, Azure, GCP, Cloudflare, Fastly, Akamai, Vercel, Netlify, Heroku, GitHub Pages, ...), a **configurable, explainable confidence engine**, a **Provider Health Summary**, a queryable **SQLite intelligence DB**, **diff mode** against a previous run, and a clean `txt/ + json/ + reports/` output layout -- with resume support, caching, per-stage metrics, and an auto-discovering plugin architecture. See `ARCHITECTURE.md` for the pipeline diagram, full JSON schema, plugin SDK contract, and confidence-engine formula. See `CONTRIBUTING.md` for dev setup and how to add a new source. See `CHANGELOG.md` for version history. See `examples/` for sample output (all formats), sample config files (JSON/YAML/TOML), and a sample batch domain list. ### Quick start (installation) git clone cd # the folder containing run.py pip install -r requirements.txt --break-system-packages # (or, pip-installable form: pip install -e . -- gives you the # `passive-enum` command too, see section 2/17 below) cp .env.example .env # optional -- fill in any free API keys you have python3 run.py -d example.com --profile balanced Full detail (profiles, keys, CLI-tool sources, troubleshooting) is in section 1 below. ### What's new in v3.1 (this round) | Feature | Status | |---|---| | Modular plugin system | done | | Auto plugin discovery (drop a `.py` file, it's live) | done -- was already true, verified with a dedicated test | | Provider Health Summary (per-source ok/skipped/error + host counts + duplicates) | done | | Confidence engine (additive, explainable, configurable via `--config-file`) | done | | Source attribution | done | | Rich JSON schema (`confidence_breakdown`, discovery path, ASN, cloud, enrichment) | done | | TXT / CSV / HTML / Markdown reports | done | | SQLite intelligence DB (`intel.sqlite3`: run history, host history, search, cross-run duplicates) | done | | Diff mode (`--diff ` or `--diff auto`) | done | | Resume (per-stage checkpointing + graceful Ctrl+C message) | done, mid-stage resume out of scope (see ARCHITECTURE.md) | | Type hints on new/modified modules | done; full-codebase strict `mypy` is a work in progress, not a hard CI gate yet | | `black` / `ruff` / `mypy` / pre-commit | configured (`pyproject.toml`, `.pre-commit-config.yaml`) | | Tests | 25 tests across 5 files covering normalize, config, confidence engine, provider health, plugin discovery, intel DB, diff mode, exporters; no coverage-percentage claim made since `coverage.py` wasn't run in this sandbox (no network to install it) | | CI/CD (lint -> test matrix 3.11/3.12 -> build) | done | | pip packaging (`pyproject.toml`, `passive-enum` console script) | done | | Async engine | not done -- current concurrency is thread-pool based (`ThreadPoolExecutor`), which is adequate for I/O-bound HTTP/DNS work; a true async rewrite is a larger architectural change left for a future round | | HTML dashboard with charts/timeline | partial -- `report.html` is an interactive, sortable/filterable table with summary stat cards, not a charting dashboard | | Dedicated "Mr. Cool" startup banner (box-art logo + Author: Sazzadul / Version / Engine / Mode / Status panel) | done | | `--minimal` output mode (keep only `txt/final_hosts.txt` + `reports/report.json`, delete everything else) | done | | Auto-fresh output per run (2nd run never mixes with 1st run's leftovers; `--resume` is the only opt-out) | done | | Batch mode: `-dL/--domain-list example.txt` scans 2+ domains in one command, each into its own output subfolder | done | ### Per-source raw/normalized/rejected/duplicate breakdown Previously a source's log line only showed the final count (e.g. `assetfinder: 58 normalized hosts`) -- if a source returned real data that all got rejected somewhere in normalization/scope-checking, that was indistinguishable from the source legitimately finding nothing. Verified first that `normalize_hostname()` itself has no bug (tested against 18 realistic hostname patterns matching actual subfinder output for a real domain -- all normalized correctly), so this wasn't hiding a parsing bug -- but the *visibility* gap was real, so it's fixed: [+] subfinder: 428 normalized hosts (raw=558, rejected=6, dup=124) And if a source ever does return data that all gets rejected, that's now a `WARNING`, not a quiet `INFO` line, so it can't blend into a normal "found nothing" result: [!] some-source: got 558 raw line(s) but 0 normalized -- likely a real parsing/scope issue, not "found nothing". Run with --debug for detail. `raw`, `rejected`, and `duplicate_in_source` are also in each host's `provider_health` entry in `reports/report.json` and shown in the console Provider Health Summary. 2 new regression tests cover both the normal breakdown and the anomaly-flagging behavior. ### Second performance fix: passive source collection was also sequential The recursion-stage fix (below) wasn't the only sequential bottleneck. **Stage 1** -- querying all passive sources (21 at the time this fix was made, 19 now that C99/CertDB were removed) -- ran the exact same way: one source at a time, in a plain `for` loop, each blocking call (including slow/rate-limited free APIs like crt.sh, and subprocess-spawning CLI tools like `findomain`/`sublist3r`) fully serial before the next started. Against a realistic per-source latency profile, that alone costs minutes before DNS validation, recursion, or permutation even begin -- almost certainly the single biggest contributor to multi-hour total runtimes. Fixed the same way as recursion: a bounded parallel worker pool (`source_threads` -- 8/12/19 on fast/balanced/thorough, override with `--source-threads`). A simulation with a realistic latency profile (most sources 3-8s, a couple of slow ones up to 20s) went from **148s sequential to 22s parallel -- a 6.7x speedup** for this one stage alone; combined with the recursion fix, this is what actually closes the "1-3 hours" gap rather than adding more sources or features. Added 3 regression tests. ### Third performance fix: the actual root cause of "still stuck for hours" Parallelizing both stages above was necessary but **not sufficient** -- some real-world runs still stalled for hours afterward. The actual bug: both stages used `with concurrent.futures.ThreadPoolExecutor(...) as ex:`. Exiting that block calls `ex.shutdown(wait=True)`, which blocks until **every** submitted task finishes, no matter how many already completed. One abnormally slow response -- e.g. a crt.sh query for a host that shares a wildcard certificate with thousands of unrelated names, a genuinely common real-world case -- could hold an entire round hostage even though the other 100+ calls in that round finished in parallel within seconds. The work was genuinely parallel; the *exit path* silently re-serialized on whichever straggler was slowest. Fixed with a hard wall-clock ceiling per stage: python3 run.py -d example.com --recursion-round-timeout 120 # default: 60/180/400s by profile python3 run.py -d example.com --source-stage-timeout 120 # default: 60/180/400s by profile Both stages now wait via `as_completed(futures, timeout=)` instead of blocking indefinitely; on timeout, whatever completed is kept, the stragglers are logged by name, and `ex.shutdown(wait=False, cancel_futures=True)` returns immediately rather than waiting for already-running threads (Python can't forcibly kill a thread -- they finish in the background and their results are simply discarded). A single round can now never take longer than its configured ceiling, regardless of how slow the single worst response is. Covered by a dedicated regression test that fails if this ever regresses to blocking again. ### Fourth performance fix: permutation validation was 4+ seconds per host Reported: 5,005 permutations at `--max-depth 8`, ~4.3s/host, a ~6-hour ETA. Root cause was different from the three above -- not sequential execution, but **DNS resolution treating every failure identically**. A single nonexistent hostname (the large majority of speculative permutation guesses) could trigger up to `(retries+1) x len(resolvers) x 2` lookups -- 24 on the `thorough` profile -- before giving up, because NXDOMAIN (dnspython's "this name does not exist" exception) was caught by the same `except Exception: continue` as genuinely transient errors (timeouts, SERVFAIL) and retried the same way. **NXDOMAIN is authoritative at the name level** -- per the DNS spec, if a name doesn't exist, it doesn't exist for *any* record type, so retrying it, trying AAAA after A already came back NXDOMAIN, or asking a different resolver cannot produce a different answer. Fixed: the first NXDOMAIN now returns immediately -- 1 query instead of up to 24 for the common case. `NoAnswer` (name exists, just no record of *that* type) is left alone and still tries the other record type / other resolvers, since that case is genuinely worth a second opinion. No new flag needed -- this is always-on correct DNS handling, not a tunable. Covered by 4 tests, including one asserting the exact call count and one simulating 200 NXDOMAIN hosts to catch any regression. ### Additions from the maintainability/DX pass - **Module split**: `dns_utils.py` (445 lines) split into `dns_utils.py` (wildcard + core validation/PTR) and `enrichment.py` (ASN lookup, cloud fingerprinting, full DNS record collection). `pipeline.py`'s recursion expander methods moved out into `recursion_expanders.py` as plain, independently-testable functions. See `ARCHITECTURE.md`'s new "Module layout" table. (`pipeline.py` is still the largest file at ~630 lines -- that's the 11-stage orchestrator itself; further splitting it per-stage is possible but wasn't done this round.) - **`summary.json`**: a lean summary (counts, provider health, duplicates, ASN/cloud groupings, confidence weights) alongside the full `report.json`, for scripts/dashboards that only need the numbers. - **YAML/TOML config files**: `--config-file` now accepts `.yaml`/`.yml` (needs `pip install pyyaml`) and `.toml` (stdlib, no extra install) in addition to `.json`. - **Real file logging**: `output//logs/run_.log` is now actually created (it was documented but never wired up before) -- always captures full DEBUG-level detail regardless of console verbosity. New `--debug` (full tracebacks for source failures, both on console and in the log) and `--quiet` (WARNING-level console only, full detail still goes to the log file) flags. - **Progress display**: DNS validation, reverse DNS, cloud discovery, and DNS record collection now show live progress (a `tqdm` bar if installed, periodic log lines otherwise -- rate + ETA either way). Every pipeline stage also logs a `[stage N/11] ` marker. ### Fixes from the latest full-code revise pass - **Silent failures made visible (the "subfinder: 0 hosts but works fine by hand" bug):** every source -- CLI-tool-based (`subfinder`, `findomain`, `assetfinder`, `sublist3r`) and HTTP-based (`crt.sh` and all 13 others) -- used to swallow real failures (timeout, non-zero exit code, connection refused, non-2xx HTTP response, a crashed parse) into a bare `except Exception: return []`. That made "the tool actually failed" completely indistinguishable from "it ran fine and legitimately found zero subdomains" -- exactly what happened when `subfinder` reported 0 through the pipeline while running the identical `subfinder -d ...` command by hand found 558. Fixed: a clean run with zero real results (exit 0 / HTTP 2xx, empty output) still correctly returns an empty list -- that's a real answer. Everything else now raises (`CLISourceError` / `HTTPSourceError`), which `stage_passive_sources`'s existing per-source try/except already catches and reports as a proper `error` entry (with the real reason -- exit code, stderr snippet, HTTP status, connection failure) in the Provider Health Summary, instead of a misleading `✓ 0 hosts`. Added 15 tests covering both helpers and real source integration. - **Critical performance bug:** recursive enumeration ran fully sequential -- 425 hosts x 3 sources (a real-world case, `crypto.com`) meant 1275+ blocking HTTP calls one at a time against rate-limited APIs like crt.sh, which could stall for an hour or more on a single round. Fixed with a bounded parallel worker pool (`recursion_threads`, new per-profile tunable) plus an optional per-round frontier cap (`max_recursion_frontier_per_round`) for very large domains. See section 7 for the before/after and how to tune or disable either one. Added 3 regression tests, including one that fails if this ever goes sequential again. Went through the entire codebase end-to-end and fixed everything found: - **Bug:** `--diff --minimal` used together silently deleted `reports/diff.json` / `reports/diff.md` -- `--minimal`'s cleanup only kept `report.json`, so the one thing `--diff` was asked to produce got thrown away. Fixed: `cleanup_to_minimal()` now also preserves `diff.json`/`diff.md` when present. Added a regression test (`test_cleanup_to_minimal_keeps_final_report_and_diff_output`) and an end-to-end reproduction so this can't silently regress. - **Bug:** dead ternary in the Provider Health formatter (`"✓" if hosts > 0 else "✓"` -- both branches identical, clearly a leftover mistake). Simplified to just the checkmark. - **Cleanup:** removed an unused `import sys` in `cli.py`. - **Hardening:** the recursion-depth guard used when mapping permutation hosts back to their parent (for `discovery_path`) now matches `wordgen.generate_deep_permutations`'s bounds check exactly (`depth < 0 or depth >= max_depth`, not just the upper bound) -- defensive, since in practice every base host reaching that code is already in-scope. - Full pass also checked for: API keys ever being logged/printed anywhere (they aren't -- only ever passed as opaque dict values), unused imports across every module (heuristic + manual check -- clean), bare `except:` clauses (none -- every catch is `except Exception:`, so `KeyboardInterrupt`/`SystemExit` still propagate correctly). ### Installing (pip, optional) pip install -e . # editable install from this repo, or `pip install .` passive-enum -d example.com --profile balanced `python3 run.py -d example.com` keeps working exactly as before -- the pip console script is an additional entry point, not a replacement. **Scope:** only run this against domains you own or are explicitly authorized to test. It only queries public/passive data sources and performs standard DNS resolution -- no exploitation, no credential attacks. The optional `--active-recursion` flag fetches public pages on discovered hosts (to mine JS/CSP for more hostnames) -- still no exploitation, but it is a direct HTTP request to the target's own servers, so it's opt-in and off by default. **Project scope (deliberate):** this is a *subdomain reconnaissance* tool -- find the maximum number of valid subdomains and enrich them (sources, DNS records, ASN, cloud, confidence). It intentionally does NOT do port scanning, vulnerability scanning (nuclei), screenshots, JS secret scanning, directory brute-forcing, or exploitation -- those are different tools' jobs, and bolting them on would turn a fast, focused recon tool into a slow, sprawling one. If you need those, pipe this tool's `txt/final_hosts.txt` into `httpx`, `nuclei`, `gowitness`, etc. ## 1. Installation # from the project root (folder containing run.py) pip install -r requirements.txt --break-system-packages # optional external CLI tools (auto-detected, skipped if missing): # subfinder https://github.com/projectdiscovery/subfinder # assetfinder https://github.com/tomnomnom/assetfinder # findomain https://github.com/findomain/findomain # sublist3r https://github.com/aboul3la/Sublist3r Check what's available on your system: python3 run.py --list-plugins name confidence available ----------------------------------------------- alienvault_otx Medium yes anubisdb Medium yes assetfinder Medium no (missing key/binary) bufferover Medium yes censys High no (missing key/binary) certspotter High yes chaos High no (missing key/binary) crt.sh High yes findomain Medium no (missing key/binary) fullhunt Medium no (missing key/binary) github Medium no (missing key/binary) hackertarget Medium yes rapiddns Medium yes subfinder Medium no (missing key/binary) sublist3r Medium no (missing key/binary) threatminer Medium yes urlscan Medium yes virustotal Medium no (missing key/binary) wayback Medium yes Keyless out of the box (9): `crt.sh`, `bufferover`, `alienvault_otx`, `rapiddns`, `wayback`, `urlscan` (low volume), `certspotter` (low rate limit), `hackertarget` (tightly rate-limited free tier), `anubisdb`, `threatminer`. CLI-tool sources (installed separately, auto-detected on `PATH`): `subfinder`, `assetfinder`, `findomain`, `sublist3r`. Optional-API-key sources (5) -- all free signup, no payment required. The behavior is the same for every one of these: **key present -> the source runs; key absent -> it's auto-skipped with a clear reason in the Provider Health Summary, and the rest of the pipeline continues normally.** No exception, no crash, nothing else affected -- confirmed by `test_passive_sources_skipped_when_unavailable` and the "skipped" status path in `stage_passive_sources` (`Pipeline.provider_health`). | Source | Free signup at | CLI flag | |---|---|---| | GitHub | github.com (personal access token) | `--github-token` | | Censys | search.censys.io (Community tier) | `--censys-id` + `--censys-secret` (both needed) | | VirusTotal | virustotal.com | `--virustotal-key` | | FullHunt | fullhunt.io | `--fullhunt-key` | | Chaos | chaos.projectdiscovery.io (free, eligibility-based) | `--chaos-key` | `urlscan.io` and `CertSpotter` also accept an optional key (`--urlscan-key` / `--certspotter-key`) to raise their rate limit, but they're keyless-capable (listed above), not optional-only. **Removed from this project:** C99 (was a **paid** API -- didn't belong next to free/freemium sources) and CertDB (was a stub with no real endpoint -- there's no single standardized public "CertDB" service, so it never actually worked). Counting either of them made the source count inaccurate; 19 is the number of sources that actually run against real data. If you have a real paid-API key you want wired in (C99 or anything else), the plugin pattern in section 11 makes that a drop-in file, no core changes needed. Deliberately not included (paid-only, no usable free tier): Shodan, BinaryEdge. Sources deliberately not included and why (discontinued, ToS-violating scraping, or standalone tools already covered another way) are unchanged from the previous README section and still apply. ### Storing API keys cp .env.example .env # then edit .env and fill in whichever keys you have `.env` is already in `.gitignore`. Priority if a key is set in more than one place: `--flag-on-cli` > shell environment variable > `.env` file. ## 2. Basic usage python3 run.py -d example.com --profile balanced ### Auto-fresh output (default behavior) Every run starts from a clean slate for that domain -- unless you pass `--resume`, the domain's output directory is wiped automatically before the pipeline starts. A second run against the same domain never mixes with a first run's leftover files; you don't need to remember `--fresh` (it's kept as a no-op flag for backward compatibility, but wiping is now the default whenever you're not resuming). python3 run.py -d example.com --profile fast # run 1 -> output/... python3 run.py -d example.com --profile fast # run 2 -> output/ auto-wiped first, clean result python3 run.py -d example.com --profile fast --resume # only this one preserves the prior output ### Scanning multiple domains at once (`-dL` / `--domain-list`) # example.txt -- one domain per line, blank lines and #comments are skipped example.com another-domain.com # this-one-is-commented-out.com third-domain.com python3 run.py -dL example.txt --profile balanced Runs the full pipeline against every domain in the file (duplicates case-insensitively deduped). You can also combine `-d` with `-dL` -- both get scanned. Each domain gets its **own isolated output subfolder**: output/ example.com/ txt/ json/ reports/ ... another-domain.com/ txt/ json/ reports/ ... third-domain.com/ txt/ json/ reports/ ... (With a single `-d` and no `-dL`, output stays flat at `output/...` exactly as before -- the per-domain subfolder only kicks in for batch mode, so existing single-domain scripts/workflows aren't affected.) Auto-fresh applies per domain, and a Ctrl+C stops the whole batch (already-completed domains keep their results; the interrupted domain can be resumed individually with `--resume -d `). A batch summary (OK/INTERRUPTED/FAILED per domain) prints at the end. **Only want the final result, nothing else?** Use `--minimal` -- after the run, everything except the two final-report files is deleted automatically: python3 run.py -d example.com --profile fast --minimal Leaves exactly: output/ txt/final_hosts.txt # hostnames only, one per line reports/report.json # full structured report (IPs, sources, confidence, cloud, ...) Everything else -- per-stage `json/`, the other `txt/*.txt` files, `report.csv`/`.html`/`.md`, `cache.sqlite3`, `intel.sqlite3`, `metadata.json`, `checkpoints/`, `logs/` -- is removed. (Without `--minimal`, the default behavior is unchanged: only `checkpoints/` is auto-deleted, everything else stays -- see section 3 and section 18.) ## 3. Output structure output/ txt/ passive_hosts.txt # stage 01 -- raw candidates, pre-DNS-validation validated_hosts.txt # stage 03 -- initial DNS-validated hosts recursive_hosts.txt # stage 04 -- hosts after recursive expansion permutation_hosts.txt # stage 06 -- hosts found via permutation final_hosts.txt # FINAL validated hostnames -- ONLY hostnames, one per line json/ 01_passive_sources.json 02_wildcard_detection.json 03_initial_dns_validation.json 04_recursive_enumeration.json 05_word_extraction.json 06_permutation_validation.json 07_reverse_dns.json # PTR + ASN, per IP 08_cloud_discovery.json 09_dns_records.json 10_final_filter_validation.json # final hosts + TTL/resolver/RTT/DNSSEC reports/ report.json # full structured report: hostname -> IPs + everything else summary.json # lean summary only: counts, provider health, duplicates, # ASN/cloud groupings, confidence weights -- no per-host array report.csv report.html # dark-themed, sortable, filterable report.md metadata.json # first_seen/last_seen/sources/discovery_path per host intel.sqlite3 # run history (see section 15) -- separate from cache.sqlite3 cache.sqlite3 # API + DNS cache (persists across runs) logs/ run_.log # full DEBUG-level log for this run, regardless of # console verbosity (--quiet still gets a complete file) checkpoints/ # per-stage checkpoint files, auto-deleted after a # successful run (see "Auto-cleanup" below) **`txt/final_hosts.txt` contains ONLY validated hostnames** -- no IPs, no metadata -- exactly the "just hostname" list. Every other field you asked to track (sources, provider_count, confidence, records, cloud, wildcard, recursive_depth, discovery_path, tags, metadata) lives in `reports/report.json`, per host, e.g.: { "host": "login.example.com", "validated": true, "sources": ["crt.sh", "recursive-wayback"], "provider_count": 2, "confidence": 0.98, "confidence_label": "High", "records": { "ips": ["1.2.3.4"], "ttl": 300, "resolver_used": "8.8.8.8", "response_time_ms": 14.2, "dnssec": false, "dns_records": {"A": {"values": ["1.2.3.4"], "ttl": 300}, "MX": {...}} }, "cloud": {"provider": "AWS", "service": "CloudFront", "evidence": "d123.cloudfront.net"}, "wildcard": false, "recursive_depth": 1, "discovery_path": ["example.com"], "tags": ["recursive"], "metadata": { "first_seen": 1753350000.0, "last_seen": 1753350100.0, "validation_time": 1753350100.0, "ptr": {"1.2.3.4": "ec2-1-2-3-4.compute-1.amazonaws.com"}, "asn": {"1.2.3.4": {"asn": "16509", "prefix": "1.2.3.0/24", "country": "US", "registry": "arin", "org": "AMAZON-02"}} } } ### Auto-cleanup After a run finishes successfully, `output/checkpoints/` is deleted automatically (there's nothing left to resume once every stage completed) -- `txt/`, `json/`, `reports/`, `metadata.json`, `cache.sqlite3` and `logs/` are left in place; those are the actual deliverables. Pass `--keep-checkpoints` if you want to inspect them or resume-analyze a run later. ## 4. Configuration profiles Profile | threads | max_depth | perm/level | cache TTL | DNS resolvers checked ---|---|---|---|---|--- fast | 30 | 2 | 500 | 1h | system default balanced | 60 | 5 | 3,000 | 6h | 8.8.8.8, 1.1.1.1 thorough | 100 | 8 | 20,000 | 24h | 8.8.8.8, 1.1.1.1, 9.9.9.9 python3 run.py -d example.com --profile thorough --max-depth 8 python3 run.py -d example.com --profile balanced --threads 120 --perm-limit 8000 python3 run.py -d example.com --profile balanced --config-file myconfig.json `--config-file` accepts three formats, picked by extension: python3 run.py -d example.com --config-file myconfig.json # stdlib, always available python3 run.py -d example.com --config-file myconfig.toml # stdlib (tomllib, Python 3.11+), no extra install python3 run.py -d example.com --config-file myconfig.yaml # needs: pip install pyyaml --break-system-packages Same keys, any format -- e.g. `myconfig.yaml`: threads: 80 max_depth: 6 cache_ttl_seconds: 3600 confidence_weights: cloud: 20 permutation_penalty: -25 `--config-file` picks its parser from the extension -- `.json` (always available), `.toml` (stdlib `tomllib`, Python 3.11+, no extra install), or `.yaml`/`.yml` (needs `pip install pyyaml`, and raises a clear error telling you that if it's missing rather than a confusing traceback): // myconfig.json { "threads": 80, "max_depth": 6, "cache_ttl_seconds": 3600 } # myconfig.yaml threads: 80 max_depth: 6 cache_ttl_seconds: 3600 confidence_weights: cloud: 20 permutation_penalty: -25 # myconfig.toml threads = 80 max_depth = 6 cache_ttl_seconds = 3600 All three are equivalent -- pick whichever format you're already using elsewhere in your tooling. ## 5. Smarter wildcard detection Two problems with a naive single-probe wildcard check: (a) one resolver's stale cache or a transient answer can produce a false positive, and (b) a subdomain can have its **own** wildcard entry independent of the root (`*.dev.example.com` catching everything even though `*.example.com` does not). This version fixes both: - **Multi-probe, multi-resolver majority vote.** 4 random, essentially guaranteed-unregistered labels are probed across every resolver in the active profile (Google/Cloudflare/Quad9 on `balanced`/`thorough`). An IP only counts as part of the wildcard signature if it shows up for a **majority** of probe/resolver combinations -- a single fluke answer can't produce (or hide) a wildcard signal on its own. This is what fixes the "wildcard detection always returns 0" problem: the old version used a single static check that was too easy to miss transient wildcard responses, or too eager to call one stray IP a wildcard. - **Per-level detection**, unchanged in spirit from before: before the permutation stage expands deeper under an already-validated host, it separately probes `*.` for its own wildcard signature. Hosts found to be wildcarded are kept in the results (`"wildcard": true` in the report) but not used as a base for further permutation. ## 6. DNS validation enrichment Every final host's record in `reports/report.json` / `json/10_final_filter_validation.json` now carries, in addition to the resolved IPs: - **TTL** of the resolved record - **Resolver used** (which of the configured resolvers actually answered) - **Response time** in milliseconds - **DNSSEC** -- best-effort check for the Authenticated Data (AD) flag on a DNSSEC-aware re-query ## 7. Recursive enumeration -- multi-source expansion The old version only re-queried crt.sh per newly-discovered host. This version expands each round through **multiple independent sources per host**: developer.example.com | +--> crt.sh (certificate SAN chaining) +--> Wayback Machine (archived URLs under the host) +--> GitHub code search (only if --github-token is set) +--> [opt-in, --active-recursion] JS file links + CSP header hostnames Every newly-validated host records **which parent host it was discovered under**, in `discovery_path` -- so you can trace exactly how `vpn.internal.developer.example.com` was reached (e.g. `["example.com", "developer.example.com", "internal.developer.example.com"]`). ### Performance: parallelized, with a frontier cap (fixes hour-long stalls) An earlier version of this stage expanded `(host, source)` pairs **fully sequentially** -- one blocking HTTP call at a time, each carrying its own retry/backoff wait. Against a domain with hundreds of initially-validated hosts (e.g. `crypto.com` with 425), that's 1000+ sequential calls hitting a rate-limited free API like crt.sh, which could genuinely stall for an hour or more on a single round. Fixed two ways: 1. **Parallel expansion.** `(host, source)` pairs now run through a bounded thread pool (`recursion_threads` -- 10/20/30 on fast/balanced/thorough) instead of one at a time. Deliberately lower than the general `--threads` count so a big domain doesn't hammer a free-tier API hard enough to get IP-banned, while still overlapping every call's retry/backoff wait instead of paying it host-by-host. 2. **Frontier cap.** `max_recursion_frontier_per_round` (100/250/600 on fast/balanced/thorough) caps how many hosts get expanded in a single round on very large domains -- the rest stay validated and in your results, they just aren't recursed into during that run. Override or disable per run: python3 run.py -d crypto.com --profile balanced --recursion-threads 30 python3 run.py -d crypto.com --profile balanced --max-recursion-frontier 0 # disable the cap Or via `--config-file`: { "recursion_threads": 25, "max_recursion_frontier_per_round": null } ## 8. Reverse DNS -- grouped by ASN / provider / cloud `reports/report.json["reverse_dns_groups"]` (and the "Reverse DNS" sections of `report.md` / `summary` output) group every final host by: - **ASN** -- free, keyless lookup via Team Cymru's DNS-based whois service (`origin.asn.cymru.com` / `asn.cymru.com`; no API key, no rate-limited paid dependency) - **Cloud provider** -- see below Per-host PTR + ASN detail also lives under each host's `metadata.ptr` / `metadata.asn` in the full report. ## 9. Cloud/CDN discovery Each validated host's CNAME chain (up to 8 hops) is followed and matched against known provider fingerprints: CloudFront/S3/ELB -> **AWS**  |  azurefd/azurewebsites/blob.core.windows.net -> **Azure**  |  appspot/run.app/cloudfunctions -> **GCP**  |  cloudflare.net -> **Cloudflare**  |  fastly.net -> **Fastly**  |  akamai*/edgekey -> **Akamai**  |  vercel.app -> **Vercel**  |  netlify.app -> **Netlify**  |  heroku* -> **Heroku**  |  github.io -> **GitHub Pages** Matches show up per host as `"cloud": {"provider": "AWS", "service": "CloudFront", "evidence": "...", "cname_chain": [...]}` and are rolled up in `reverse_dns_groups.by_cloud_provider`. (It can only report what a CNAME chain actually reveals -- a host pointed straight at a bare IP with no CNAME won't be attributable this way; that's an inherent limit of passive CNAME fingerprinting, not a bug.) ## 10. Resume & checkpoints python3 run.py -d example.com --profile thorough --resume python3 run.py -d example.com --profile thorough --fresh # force a clean re-run Pipeline stages, in order: `01_passive_sources`, `02_wildcard_detection`, `03_initial_dns_validation`, `04_recursive_enumeration`, `05_word_extraction`, `06_permutation_validation`, `07_reverse_dns` (+ ASN), `08_cloud_discovery`, `09_dns_records`, `10_enrichment`, `11_final_filter_validation`. ## 11. Adding a new source (plugin architecture) No core pipeline code needs to change. Create a new file in `subdomain_recon/sources/`: # subdomain_recon/sources/my_source.py from .base import Source, SourceContext class MySource(Source): name = "my_source" confidence = "Medium" def fetch(self, domain, ctx: SourceContext): return [f"host1.{domain}", f"host2.{domain}"] It's picked up automatically -- verify with `python3 run.py --list-plugins`. ## 12. Running the test suite pip install pytest dnspython --break-system-packages pytest tests/ -v Covers: hostname normalization, scope/depth checks, config-profile loading and overrides, confidence scoring, and the full `txt/ + json/ + reports/` export layout (including the "auto-cleanup only removes checkpoints" behavior). ## 13. Provider Health Summary Every run ends with a per-source breakdown -- printed to the console and embedded in `reports/report.md`: Provider Summary alienvault_otx ✓ 114 hosts certspotter ✓ 98 hosts github ✗ Missing key (github) chaos ✗ Missing key (chaos) subfinder ✓ 31 hosts Unique Hosts : 421 Duplicates : 198 Errors : 2 `✗` distinguishes **skipped** (unavailable -- missing key or CLI binary, named explicitly) from **error** (a real failure -- non-2xx HTTP status, connection failure, non-zero CLI exit code, or timeout -- with the actual reason in the message; see the "Silent failures made visible" changelog entry above). A clean run that genuinely found zero hosts still correctly shows `ok, 0 hosts` -- that distinction is the whole point of the fix. ### Logging flags python3 run.py -d example.com --quiet # console: warnings/errors + final summary only python3 run.py -d example.com --verbose # console: DEBUG level python3 run.py -d example.com --debug # console: DEBUG + full tracebacks for source failures Regardless of console verbosity, a complete DEBUG-level log is always written to `output//logs/run_.log` -- `--quiet` only affects what scrolls past on screen, not what's kept for later debugging. ## 14. Confidence engine Full formula and JSON `confidence_breakdown` shape are in `ARCHITECTURE.md`. Quick version -- additive points, configurable via `--config-file`: { "confidence_weights": { "cloud": 20, "permutation_penalty": -25 } } Defaults: 2+ providers +20, DNS valid +20, recursive +15, cloud +10, GitHub +15, crt.sh +10, permutation-only -15. Score clamped to `[0, 100]`; High ## 15. SQLite intelligence DB + diff mode Every run is stored in `output/intel.sqlite3` (separate from the API/DNS `cache.sqlite3`) -- run history, per-host history across runs, cross-run duplicate detection, and hostname search are all plain SQL queries against it (see `subdomain_recon/intel_db.py`). Skip this with `--no-intel-db` if you don't want it. # diff against a specific saved report python3 run.py -d example.com --diff /path/to/old/reports/report.json # diff against the most recent prior run for this domain in intel.sqlite3 python3 run.py -d example.com --diff auto Writes `reports/diff.json` + `reports/diff.md` and logs a NEW/REMOVED summary. ## 16. Resume + Ctrl+C python3 run.py -d example.com --profile thorough --resume # the ONLY way to skip the auto-wipe python3 run.py -d example.com --profile thorough # every other invocation auto-wipes first (see section 2) `--fresh` still parses (so old scripts don't break) but is now a no-op -- wiping is the default whenever `--resume` isn't passed. Pressing Ctrl+C mid-run no longer just dies silently -- it logs exactly which stages were completed/checkpointed and prints the precise `--resume` command to continue. Resume granularity is per-stage (see `ARCHITECTURE.md` for why mid-stage resume isn't implemented). In batch mode (`-dL`), Ctrl+C stops the whole batch after the current domain; already-finished domains keep their results and the interrupted one can be resumed individually. ## 17. Developer tooling pip install -r requirements-dev.txt # pytest, black, ruff, mypy, pre-commit pre-commit install # run lint+format+tests before every commit black . ruff check . mypy . pytest tests/ -v CI (`.github/workflows/tests.yml`): lint (`ruff` + `black --check`) -> test matrix (Python 3.11, 3.12) -> package build, each gating the next. ## 18. Troubleshooting - **`pip install ... --break-system-packages` says "no such option"** -- you're on an older pip; either upgrade pip or drop the flag and use a virtualenv instead. - **dnspython not installed** -- basic A-record resolution falls back to Python's built-in `socket` module, but AAAA/CNAME/MX/TXT/NS/PTR, TTL, DNSSEC, and ASN lookups all require it: `pip install dnspython --break-system-packages`. - **`subfinder` / `assetfinder` / `findomain` / `sublist3r` show "no (missing key/binary)"** -- these are optional external CLI tools; install them and make sure they're on `PATH`, or ignore them. - **A source shows fewer hosts (or 0) here than running it standalone by hand** -- check the Provider Health Summary and `reports/report.json`'s `provider_health` section first: it now shows `error` with the real reason (timeout, exit code + stderr, HTTP status) whenever that's what actually happened, instead of a misleading `ok, 0 hosts` (see the fixes changelog above). If it genuinely says `ok, 0 hosts` with no error, the two runs likely just hit different upstream conditions (rate limits, transient network issues, or the standalone tool's own separate provider config/cache) -- re-run and compare, or run that one source standalone alongside this tool to compare directly. - **Running multiple domains at once is slower/flakier than expected** -- if you're launching several separate `python3 run.py -d ...` processes at the same time (one per domain) from the same machine, they all compete for the same free/rate-limited APIs and the same outbound connection pool simultaneously, which can trigger rate-limiting or timeouts across the board. Use `-dL/--domain-list` (section 2) instead -- it scans domains one at a time within a single process, so you get the same result without the self-inflicted concurrent load. - **Empty `txt/final_hosts.txt`** -- check `output/logs/run_*.log` and the `errors` list inside `reports/report.json["metrics"]` -- most commonly no sources were reachable (no network / no keys / no CLI tools), or the domain has aggressive wildcard DNS filtering out every candidate (check `report.json["wildcard_ips"]`). - **Cache seems stale** -- lower `cache_ttl_seconds` via `--config-file`, or delete `output/cache.sqlite3`. ## LICENSE MIT -- see `LICENSE`. This tool is intended for authorized security testing and research only. Only use it against domains and systems you own or have explicit written permission to test. The authors accept no liability for misuse.
标签:GitHub, Python, 域名侦查, 基础设施安全, 子域名枚举, 安全侦察, 实时处理, 无后门, 系统安全, 资产测绘, 逆向工具