Most of the bugs I report are not clever. They are ordinary bugs on hosts the target forgot about: a staging API behind a wildcard DNS record, an old admin panel on a non-standard port, a JavaScript bundle that still references a retired endpoint. The hard part is not exploitation. The hard part is being the first person to look at that host after it appeared.
That is a systems problem, not a skill problem. If you run recon by hand every time, you look at the same popular targets everyone else looks at, on the same day they do. If you run recon as a pipeline, you look at everything in scope continuously, and you get told when something changes.
This post describes the four-stage pipeline I use for both bug bounty and client VAPT (vulnerability assessment and penetration testing) work: seeding, enumeration, resolution and change detection. I will also be honest about where large language models (LLMs) help in that pipeline and where they actively make it worse.
Stage 1: seeding
Seeding answers one question: what does the organisation actually own? Every later stage depends on it, so this is where I spend disproportionate time. A missed seed is a whole branch of attack surface you will never enumerate.
The inputs I collect, in rough order of reliability:
- ASNs. An Autonomous System Number identifies a block of IP space routed by one organisation. Large companies often own their own; mid-sized ones usually rent from a cloud provider, which means the ASN tells you the provider, not the target. My tool AsN_GrApEr automates the lookup, but any WHOIS or BGP data source gives you the same thing: organisation name in, IP ranges out.
- Organisation names and legal entities. The marketing brand is rarely the registrant name. Search for the legal entity, past names and regional subsidiaries. Corporate registries and SEC-style filings list these.
- Certificate Transparency (CT). CT logs are public, append-only records of every TLS certificate issued by participating certificate authorities. Searching them for a domain returns every hostname someone has requested a certificate for, including internal-sounding ones that were never meant to be public. This is the single highest-yield seed source for subdomains.
- Acquisitions. When a company buys another company, the acquired infrastructure usually joins the scope months before anyone hardens it. MergerHunt is my helper for pulling acquisition lists; press releases and the target's own investor pages work too.
- The program's scope page. Read it twice. Scope often lists apex domains and wildcards that the brand itself never advertises.
The common misconception at this stage is that seeding is "just the root domain". People put example.com into a subdomain tool and call it done. In practice the interesting hosts live under example-corp.net, exampleinternal.io or a subsidiary brand, and only CT logs, ASN data or acquisition history will surface them.
Stage 2: enumeration
Enumeration expands each seed into candidate hosts, ports and endpoints. It should be wide and cheap. Accuracy comes in the next stage.
Subdomains
I combine passive sources (CT, DNS datasets, search engines, code hosting) with active brute force against a wordlist and permutation generation (api-staging, staging-api, api2). SubSpyder wraps this for me; the widely used open-source tools do the same job. The point is to merge every source and dedupe, because each source misses something the others catch.
One technique worth understanding rather than just running: DNS resolution of a wildcard. If *.example.com resolves to the same address, brute forcing will "find" thousands of fake hosts. Detect the wildcard first by resolving a random label, record its answer, and discard any brute-force hit with the same answer. DigIT does this check for me before anything else runs.
Ports and services
For each resolved IP, a fast port scan on the common web and management ports, then a fuller scan on hosts that look interesting. Non-standard web ports (8080, 8443, 8000, 9000, 3000) are where staging apps and dashboards live. A port is not a finding; a service version is not a finding either. They are leads. What matters is whether the service on that port is reachable from the internet, whether it should be, and what it exposes once you talk to it.
JavaScript endpoints
Modern front-ends ship their API map in the JavaScript bundle. For every live web host I pull the scripts, extract absolute and relative paths, and add every /api/..., /v2/... or GraphQL route to the endpoint list. APIScout is my wrapper for this; it is a regex pass over the bundle followed by a request to each path to see which ones answer. Retired endpoints that still answer are a recurring source of authorization bugs, because nobody maintains the access checks on code they think is dead.
# the idea, not the tool: collect scripts, extract paths, keep the ones that answer
cat live-hosts.txt | while read h; do
curl -s "$h" | grep -oE 'src="[^"]+\.js[^"]*"' | sed 's/src="//;s/"$//' \
| sed "s#^/#$h/#" >> scripts.txt
done
sort -u scripts.txt | xargs -n1 curl -s \
| grep -oE '"/(api|v[0-9]+|graphql)[^"]*"' | tr -d '"' | sort -u > endpoints.txt
Stage 3: resolution and fingerprinting
Enumeration produces noise on purpose. Resolution turns it into a list of things that are actually alive, and fingerprinting tells you what each one is.
- Live-host check. Resolve every candidate, then probe HTTP and HTTPS on the ports found earlier. Record status code, title, content length, redirect target and the final URL after redirects. A host that redirects to the main site is usually uninteresting; a host that returns a 401, a 403 or a login page is interesting.
- Technology fingerprinting. Response headers (
Server,X-Powered-By), cookie names, favicon hashes and framework-specific paths tell you the stack. This matters because the next step for a Next.js app, a Sitefinity CMS and a Jenkins instance are three completely different test plans. - Screenshots. One screenshot per live host, viewed as a grid. Humans spot "that one looks like an internal admin panel" in a second; grepping titles takes longer and misses more.
- Cluster, do not list. Group hosts by stack, by certificate, by response hash. A thousand hosts collapse into twenty clusters, and you test one representative of each cluster first.
The misconception here is that more hosts means more findings. It does not. Resolution is where you throw away ninety per cent of what enumeration produced, and that is the point. The goal is a short list you will actually test, not a long list you will admire.
Stage 4: change detection
This stage is the whole reason the pipeline exists. Everything above runs on a schedule. Each run's output is diffed against the previous run, and only the delta reaches me.
The deltas, ranked by how often they turn into a report:
- New live host. The highest-signal event in bug bounty. A host that appeared last night has had zero people look at it.
- New open port on a known host. Usually a deploy exposed something: a debug port, a database admin UI, a metrics endpoint.
- Changed JavaScript bundle. New endpoints, new feature flags, sometimes new credentials someone committed by mistake.
- Changed response on a known endpoint. A 403 that became a 200 is an authorization regression. These are my favourite, because they are real bugs that will be fixed quickly once reported.
- New certificate in CT. Often the earliest signal that a host is about to exist. By the time DNS points at it, you already know the name.
Diffing is simple to implement and easy to get wrong. Normalise before you compare: strip timestamps, request IDs and CSRF tokens from responses, sort lists, and ignore ordering. Otherwise every run produces a hundred fake changes and you stop reading the alerts, which is the same as not having a pipeline.
# nightly: run the stages, then diff against yesterday and alert on the delta
#!/usr/bin/env bash
set -euo pipefail
DAY=$(date +%F); PREV=$(date -d yesterday +%F)
mkdir -p runs/$DAY
seed targets.txt > runs/$DAY/seeds.txt
enumerate runs/$DAY/seeds.txt > runs/$DAY/candidates.txt
resolve runs/$DAY/candidates.txt > runs/$DAY/live.txt
comm -13 <(sort runs/$PREV/live.txt) <(sort runs/$DAY/live.txt) > runs/$DAY/new-hosts.txt
[ -s runs/$DAY/new-hosts.txt ] && notify "new hosts for $DAY" runs/$DAY/new-hosts.txt
The commands are placeholders for whatever tools you prefer. The structure is what matters: each stage reads the previous stage's file and writes its own, so any stage can be re-run or replaced without touching the rest.
Where AI helps, and where it hurts
I use LLMs inside this pipeline in three places, all of them after the data has been collected and all of them as a reader, not a decision-maker.
Where it helps
- Triaging JavaScript. A bundle can be megabytes of minified code. Asking a model "list every API path, every third-party hostname and anything that looks like a credential or key" and then verifying the output by hand is faster than reading it all. The verification step is not optional; the model will occasionally invent a path that does not exist.
- Clustering screenshots and titles. Given a few hundred page titles and short descriptions, a model groups them into "login pages", "marketing pages", "error pages", "admin-looking pages" reliably enough that I review the admin-looking group first.
- Summarising diffs. A nightly diff with forty changed responses is tedious. A one-paragraph summary ("three new hosts, all on the staging wildcard; one endpoint changed from 401 to 200") lets me decide in seconds whether tonight is worth opening a terminal.
Where it hurts
- Generating hostnames or scope. Models will confidently produce subdomains that do not exist and, worse, hosts that exist but are not in scope. Scope is a legal boundary, not a creative exercise. Nothing an LLM outputs gets probed until it has been resolved against the program's scope list by deterministic code.
- Deciding what to test. The model does not know what the program paid for last quarter, what is a duplicate, or which host is a honeypot. That judgement stays with me.
- Anything that sends requests. I do not let a model drive the scanner. Rate limits, scope and safety are enforced in code that does not change its mind.
The misconception to avoid: "AI recon" as a product category mostly means an LLM wrapped around the same tools, with the scope check removed. The tools were never the bottleneck. Reading the output was. Use the model for reading.
Scope and ethics
A pipeline that runs unattended can do damage unattended. Three rules I enforce in code, not in my head:
- Scope is an allow-list, checked at every stage. Seeds, candidates and live hosts are all filtered against the program's in-scope patterns before the next stage touches them. Out-of-scope hosts are logged, never probed.
- Rate limits per host and per program. Recon should be indistinguishable from a slow, polite crawler. If a program specifies a request rate or a
User-Agentheader, that is configuration, not a suggestion. - Read-only by default. The pipeline resolves, fetches and fingerprints. It does not log in, submit forms or test payloads. Exploitation is a manual, scoped, documented step that happens after I have decided a host is worth it.
The same pipeline, pointed at a client's assets under a signed VAPT engagement, becomes continuous attack-surface monitoring. The only thing that changes is the scope file.
Checklist
- Seeds: ASNs, legal entity names, CT logs, acquisitions and the scope page, merged and deduplicated.
- Wildcard DNS detected and filtered before any brute force.
- Subdomains from passive sources plus active brute force and permutations.
- Common and non-standard web ports probed on every resolved IP.
- JavaScript bundles harvested for endpoints on every live host.
- Live-host data recorded: status, title, length, redirect chain, stack fingerprint, screenshot.
- Hosts clustered by stack and response hash before testing.
- Every stage writes a file; every run is diffed against the last; only the delta is reported.
- Responses normalised before diffing so alerts stay meaningful.
- Scope enforced as an allow-list at every stage; rate limits and read-only behaviour enforced in code.
- LLMs used only to read collected data, with outputs verified before anything acts on them.
Takeaways
- Recon is a systems problem. The person who looks at a new host first usually finds the bug; a scheduled pipeline makes that person you.
- Seeding deserves more time than it gets. Subsidiaries, old brands and CT logs are where the forgotten hosts live.
- Enumeration should be wide; resolution should be ruthless. Test twenty clusters, not two thousand hosts.
- Change detection is the product. If you only build one stage well, build the diff.
- Use AI to read, never to decide or to send. Scope and rate limits belong in deterministic code.
Learn more
- OWASP Web Security Testing Guide, "Information Gathering" section (owasp.org)
- OWASP Amass project documentation for attack-surface mapping concepts (owasp.org)
- Certificate Transparency overview at certificate.transparency.dev
- My open-source recon tools on the projects page
Related: Bug Bounty in 2026 · Testing AI Agents and MCP Servers. Want continuous attack-surface monitoring on your own assets? Book a free 30-minute scoping call.



