DEV Community

ULNIT
ULNIT

Posted on

Build a Bug Bounty Recon Pipeline in Pure Python (Runs 24/7 on a Raspberry Pi)

Most bug bounty hunters lose the hunt before it starts. Not because they lack skill, but because reconnaissance is tedious: enumerating subdomains, probing which hosts are actually alive, diffing today's results against yesterday's. Do it by hand and you'll spend 90% of your time on data plumbing and 10% actually hunting for bugs.

The fix is obvious: make the machine do the boring part. In this tutorial I'll walk through building a small recon pipeline in pure Python — no heavyweight dependencies, and it runs happily on a $35 Raspberry Pi. It's the same architecture that powers my Bug Bounty Automation Kit, so you can build the core yourself and decide if the full kit is worth grabbing later.

What we're building

A three-stage pipeline that runs unattended every morning:

  1. Enumerate — collect subdomains for a target from public sources
  2. Probe — check which hosts respond, record status codes
  3. Diff & report — compare against yesterday's snapshot and flag anything new

The whole thing fits in under 200 lines of Python.

Step 0: The environment

Keep it boring. A virtualenv and one dependency:

python3 -m venv ~/recon/.venv
source ~/recon/.venv/bin/activate
pip install requests
mkdir -p ~/recon/data ~/recon/logs
Enter fullscreen mode Exit fullscreen mode

This runs on any Linux box, but a Raspberry Pi is the sweet spot: it draws ~3W, runs 24/7, and nobody notices it humming away in a closet.

Step 1: Passive subdomain enumeration

Certificate Transparency logs are the highest-value free data source in recon. Every time a certificate is issued for *.example.com, it shows up in public logs — including subdomains the target probably forgot about. crt.sh exposes those logs over a simple JSON API:

import requests

def enumerate_subdomains(domain: str) -> set:
    resp = requests.get(
        "https://crt.sh/",
        params={"q": f"%.{domain}", "output": "json"},
        timeout=90,
    )
    resp.raise_for_status()
    names = set()
    for entry in resp.json():
        for name in entry.get("name_value", "").split("\n"):
            name = name.strip().lower().lstrip("*.")
            if name.endswith(domain):
                names.add(name)
    return names
Enter fullscreen mode Exit fullscreen mode

Two practical notes: crt.sh can take 30–60 seconds on large domains, so give the timeout room. And dedupe aggressively — wildcard certs will hand you the same host a dozen times.

Step 2: Probe for live hosts

A list of 4,000 subdomains is useless if you don't know which ones answer. Probe concurrently, but politely — 20 workers is plenty and won't melt anything:

import concurrent.futures

def probe(hosts, timeout=5):
    alive = {}

    def check(host):
        for scheme in ("https", "http"):
            try:
                r = requests.get(f"{scheme}://{host}",
                                 timeout=timeout, allow_redirects=False)
                return host, r.status_code
            except requests.RequestException:
                continue
        return host, None

    with concurrent.futures.ThreadPoolExecutor(max_workers=20) as pool:
        for host, status in pool.map(check, hosts):
            if status is not None:
                alive[host] = status
    return alive
Enter fullscreen mode Exit fullscreen mode

allow_redirects=False matters: you want the first response, because redirect chains to internal services are themselves interesting findings.

Step 3: Diff and report

Here's the part most tutorials skip, and it's where the actual value lives. New hosts are where bugs are. A subdomain that appeared overnight is often a staging deployment, a forgotten marketing site, or a half-finished API — exactly the kind of thing that hasn't been through a security review.

import json, datetime, pathlib

DATA = pathlib.Path.home() / "recon" / "data"

def diff_and_report(domain, alive):
    today = datetime.date.today().isoformat()
    previous = sorted(DATA.glob(f"{domain}-*.json"))
    known = set()
    if previous:
        known = set(json.loads(previous[-1].read_text()))

    (DATA / f"{domain}-{today}.json").write_text(json.dumps(alive))

    new_hosts = set(alive) - known
    if new_hosts:
        print(f"[!] {len(new_hosts)} NEW hosts for {domain}:")
        for h in sorted(new_hosts):
            print(f"    {h} -> {alive[h]}")
    else:
        print("No new hosts today.")
Enter fullscreen mode Exit fullscreen mode

Wire the three functions together in pipeline.py, and you've got a recon loop.

Step 4: Put it on a schedule

The whole point is that this runs whether you're awake or not:

0 5 * * * cd ~/recon && .venv/bin/python pipeline.py example.com >> logs/recon.log 2>&1
Enter fullscreen mode Exit fullscreen mode

At 5 AM every day, you get a fresh snapshot and a shortlist of new hosts. Check the log with your coffee; investigate anything interesting.

Where the kit comes in

What we built above is deliberately minimal — one target, one source, one script. The Bug Bounty Automation Kit is the production version of this pattern: multiple enumeration sources with cross-referencing, multi-target runs, rate-limit handling, notification hooks, and scheduling tuned for a Raspberry Pi. It's $15, one-time, and it's plain Python source you can read and modify — MIT licensed, no lock-in.

If you'd rather understand the pipeline than install a black box, this tutorial is your starting point. If you want the pipeline to cover ten targets while you sleep, the kit gets you there in an afternoon.

Wrapping up

Bug bounty automation isn't about replacing skill — it's about making sure your skill gets pointed at fresh targets instead of re-running the same nmap scan you ran last week. Enumerate, probe, diff, repeat. Everything else is a variation on that loop.

Happy hunting — and may your diffs always contain something interesting.

Top comments (0)