Automated discovery of self-hosted bug bounty and vulnerability disclosure programs, the ones companies run themselves rather than through HackerOne, Bugcrowd, Intigriti, or YesWeHack.
Everything found is scored, deduplicated against a SQLite state file, and pushed to a Google Sheet. Because state persists, every run tells you only what is genuinely new since last time.
| Source | What it finds | Cost |
|---|---|---|
security_txt |
Sweeps a domain list for /.well-known/security.txt (RFC 9116) |
free |
diodb |
disclose.io open program database, ~2,400 entries | free |
urlscan |
urlscan.io search for security.txt, date sorted | free key |
gcse |
Google Programmable Search, dork list with dateRestrict |
100 q/day free |
ctwatch.py |
Certificate transparency, catches security.* / vdp.* hosts |
free |
The core filter is an inversion: anything whose policy or contact points at a platform is discarded. What remains is your target set.
mkdir -p ~/bbdiscover && cd ~/bbdiscover
# drop bbdiscover.py, ctwatch.py, run.sh, requirements.txt, config.example.yaml here
pip install -r requirements.txt --break-system-packages
cp config.example.yaml config.yaml
mkdir -p lists outGet a domain list:
curl -sL https://tranco-list.eu/top-1m.csv.zip -o /tmp/t.zip
unzip -p /tmp/t.zip | cut -d, -f2 > lists/tranco.txtconsole.cloud.google.com→ new project → enable Google Sheets API- Create a Service Account, add a JSON key, save it as
service-account.json - Create a blank Sheet, copy the ID from the URL:
docs.google.com/spreadsheets/d/THIS_PART/edit - Share the Sheet with the service account email (Editor). This is the step everyone forgets, and without it you get a 403.
- Put
sheet_idandgoogle_credsinconfig.yaml
Two tabs are written: all_programs (everything tracked) and new_30d
(only what appeared in the last 30 days, sorted by score).
Set your contact address in the UA constant at the top of bbdiscover.py
before running the sweep. You are making a lot of requests, and a real contact
in the User-Agent is what keeps you off blocklists.
# Quick start, no crawling, populates ~1,300 programs in about 30 seconds
python3 bbdiscover.py --sources diodb
# Test the sweep on a small list first
head -500 lists/tranco.txt > /tmp/t.txt
python3 bbdiscover.py --sources security_txt --domains /tmp/t.txt --limit 500
# Full run
python3 bbdiscover.py --sources security_txt,diodb,urlscan --concurrency 150
# Only programs that pay and have real safe harbor
python3 bbdiscover.py --sources diodb --min-score 12Useful flags: --no-enrich (skip fetching policy pages, much faster),
--include-platform (keep HackerOne/Bugcrowd entries), --min-score N,
--quiet (for cron).
python3 ctwatch.py # stream certstream, writes lists/candidates.txt
python3 ctwatch.py --poll acme.com # one-shot crt.sh poll, cron friendlyThen feed the results back in:
cut -d, -f1 lists/candidates.txt > /tmp/ct.txt
python3 bbdiscover.py --sources security_txt --domains /tmp/ct.txt0 3 * * 1 /home/syed/bbdiscover/run.sh >> /home/syed/bbdiscover/cron.log 2>&1
A 200k-domain sweep at concurrency 150 takes roughly 40 minutes. Weekly is plenty; security.txt files do not appear that fast.
Higher means more worth your time.
| Signal | Points |
|---|---|
| Self-hosted (not on a platform) | +3 |
Pays a bounty (BBP) |
+4 |
| Monetary reward language in policy | +2 |
| Full safe harbor | +3 (partial +1, none -2) |
| Hall of fame / acknowledgements page | +1 |
Hiring: field present (staffed team, faster triage) |
+1 |
| Policy URL reachable | +1 (dead -4) |
Expired Expires: field |
-1 |
| Coordinated disclosure requires NDA | -1 |
Anything at 12 or above is self-hosted, pays, and has real legal cover. That is where to start.
The sweep is noisy by design. A security.txt with a Contact: but no
Policy: is still logged, scored low, and marked contact-only. Those are
often informal setups with no authorization language. Do not treat them as
open season.
Verify authorization before testing anything. A page saying "email us if
you find something" is not a scope grant. Read the policy, confirm it names
in-scope assets and includes non-prosecution language, and archive the policy
text as of the date you tested. program_type=VDP means no money, only credit.
Rate limiting. Default concurrency is 120. Going much higher gets you rate-limited by shared hosts and CDNs, and the results get worse, not better.
crt.sh is unreliable. ctwatch.py --poll retries with backoff and falls
back to Certspotter. The public certstream server also goes down regularly; if
you depend on streaming, self-host certstream-server-go.
diodb is community maintained, so entries can be stale. The policy_status
column carries the upstream alive/dead flag, and enrichment re-checks the URL
live, so dead links get penalized automatically.