Synchronize and track bug bounty program scopes across YesWeHack, HackerOne, Intigriti and Bugcrowd.
Quick Start • Configuration • CLI • REST API • Notifications • Scope Processing
ScopesExtractor fetches the programs you have access to on each platform, normalizes and validates their scopes, and stores them in a local SQLite database. Every sync is diffed against the previous state, so new programs, removed programs and scope changes are recorded in a history and can be pushed to Discord or any HTTP endpoint. A REST API exposes the data to your recon tooling.
- Four platforms: YesWeHack, HackerOne, Intigriti and Bugcrowd, each enabled independently
- Change tracking: new and removed programs and scopes, with a configurable history retention
- Notifications: Discord webhooks and a generic JSON webhook, filterable by event and scope type
- REST API: scopes, wildcards, recent changes, exclusions and malformed scopes
- Scope normalization: platform-specific cleanup, type detection and validation before storage
- Background sync: run the API server with a built-in sync scheduler
The recommended way to run ScopesExtractor is Docker Compose. Database migrations run automatically on first start.
git clone https://github.com/JoshuaMart/ScopesExtractor.git
cd ScopesExtractor
cp .env.example .env # then fill in your platform credentials and API keyReview config/settings.yml to enable the platforms and notifications you need, then create a docker-compose.yml:
services:
scopes_extractor:
build: .
container_name: scopes_extractor
command: bundle exec bin/scopes_extractor serve --sync
ports:
- "4567:4567"
volumes:
- ./config/settings.yml:/app/config/settings.yml:ro
- ./.env:/app/.env:ro
- scopes_db:/app/db
restart: unless-stopped
volumes:
scopes_db:docker compose up -d
docker logs -f scopes_extractorThe API is now available on http://localhost:4567 and a sync runs every 3 hours by default.
Using docker run instead
docker build -t scopes_extractor .
docker volume create scopes_db
# One-off sync
docker run --rm \
-v "$(pwd)/config/settings.yml:/app/config/settings.yml:ro" \
-v "$(pwd)/.env:/app/.env:ro" \
-v scopes_db:/app/db \
scopes_extractor \
bundle exec bin/scopes_extractor sync
# API server with background sync
docker run -d --name scopes_extractor \
-v "$(pwd)/config/settings.yml:/app/config/settings.yml:ro" \
-v "$(pwd)/.env:/app/.env:ro" \
-v scopes_db:/app/db \
-p 4567:4567 \
scopes_extractor \
bundle exec bin/scopes_extractor serve --syncRunning without Docker
Requirements: Ruby 3.4+, SQLite3 and libcurl.
bundle install
bundle exec bin/scopes_extractor migrate
bundle exec bin/scopes_extractor serve --syncConfiguration is split between two files: secrets live in .env, everything else in config/settings.yml.
| Variable | Description |
|---|---|
YWH_EMAIL, YWH_PWD, YWH_OTP |
YesWeHack credentials and TOTP secret |
H1_USERNAME, H1_TOKEN |
HackerOne username and API token |
INTIGRITI_TOKEN |
Intigriti API bearer token |
BUGCROWD_EMAIL, BUGCROWD_PASSWORD, BUGCROWD_OTP |
Bugcrowd credentials and TOTP secret |
API_KEY |
Key expected in the X-API-KEY header of API requests |
Only the credentials of enabled platforms are required. Any other variable can be referenced from settings.yml with the ${VAR} syntax (see HTTP webhook).
| Key | Default | Description |
|---|---|---|
app.log_level |
INFO |
DEBUG, INFO, WARN or ERROR |
app.database_path |
db/scopes.db |
SQLite database location |
http.proxy |
null |
Optional proxy for outgoing requests |
http.timeout |
30 |
Request timeout in seconds |
api.port / api.bind |
4567 / 0.0.0.0 |
API listen address |
api.require_auth |
true |
Require the X-API-KEY header |
api.allowed_hosts |
[] |
Allowed Host headers, empty allows all |
platforms.<name>.enabled |
true |
Enable or disable a platform |
platforms.<name>.skip_vdp |
varies | Ignore programs without bounty |
platform_exclusions.<name> |
[] |
Program slugs to ignore |
sync.delay |
10800 |
Interval between background syncs, in seconds |
history_retention_days |
30 |
History entries older than this are purged |
validation.allow_private_suffixes |
false |
Accept domains under private public suffixes |
Notification settings are described in Notifications.
bundle exec bin/scopes_extractor <command> [options]| Command | Description |
|---|---|
sync [PLATFORM] |
Synchronize all enabled platforms, or a single one |
serve |
Start the REST API server |
migrate |
Run database migrations |
cleanup |
Purge history entries older than history_retention_days |
reset |
Delete all data from the database |
version |
Print the version |
help [COMMAND] |
Show help |
| Option | Applies to | Description |
|---|---|---|
-v, --verbose |
sync, serve |
Enable debug logging |
-p, --port |
serve |
Override api.port |
-b, --bind |
serve |
Override api.bind |
-s, --sync |
serve |
Run syncs in the background every sync.delay seconds |
-f, --force |
reset |
Skip the confirmation prompt |
Examples:
bundle exec bin/scopes_extractor sync hackerone -v
bundle exec bin/scopes_extractor serve -p 8080 -b 127.0.0.1 --syncAll endpoints return JSON. When api.require_auth is enabled, requests must include the X-API-KEY header matching the API_KEY environment variable.
curl -H "X-API-KEY: $API_KEY" "http://localhost:4567/?platform=hackerone&type=web&bounty=true"| Endpoint | Description |
|---|---|
GET / |
In-scope assets, with filters |
GET /wildcards |
Wildcard scopes only |
GET /changes |
Recent entries from the change history |
GET /exclusions |
Assets ignored during validation |
GET /malformed-scopes |
Assets rejected for an invalid format |
| Parameter | Type | Description |
|---|---|---|
platform |
string | Platform name, e.g. hackerone |
type |
string | Scope type, e.g. web, mobile, api |
bounty |
boolean | Only programs with (true) or without (false) bounty |
slug |
string | Program slug |
values_only |
boolean | Return a flat array of scope values |
Example response
{
"scopes": [
{
"slug": "example-program",
"platform": "hackerone",
"program_name": "Example Program",
"bounty": true,
"value": "*.example.com",
"type": "web",
"is_in_scope": true
}
],
"count": 1
}With values_only=true:
["*.example.com", "api.example.com"]| Parameter | Type | Description |
|---|---|---|
platform |
string | Platform name |
values_only |
boolean | Return a flat array of wildcard values |
The response has the same shape as GET /, under a wildcards key.
| Parameter | Type | Description |
|---|---|---|
hours |
integer | Look-back window in hours (default: 24) |
platform |
string | Platform name |
type |
string | Event type: add_program, remove_program, add_scope or remove_scope |
Example response
{
"changes": [
{
"id": 123,
"program_id": 45,
"program_slug": "example-program",
"platform_name": "bugcrowd",
"event_type": "add_scope",
"scope_value": "newapp.example.com",
"scope_type": "web",
"created_at": "2026-01-10T14:30:00Z"
}
],
"count": 1
}Returns every asset ignored during validation, most recent first.
Example response
{
"exclusions": [
{
"id": 1,
"platform": "hackerone",
"program_slug": "example-program",
"value": "example.com (production only)",
"reason": "Invalid format for web scope",
"created_at": "2026-01-09T10:00:00Z"
}
],
"count": 1
}Returns assets rejected with an Invalid format reason, limited to programs that still exist and sorted by program slug, platform, then value. The response has the same fields as GET /exclusions, under a malformed_scopes key.
Once a scope passes validation on a later sync, it is removed from this list.
Two notifiers are available and can be enabled together: Discord webhooks and a generic HTTP webhook.
| Event | Triggered when |
|---|---|
new_program |
A program is discovered |
removed_program |
A program is no longer available |
new_scope |
A scope is added to a program |
removed_scope |
A scope is removed from a program |
ignored_asset |
An asset fails validation |
error |
A synchronization fails (HTTP webhook only; Discord uses the dedicated errors webhook) |
Both notifiers accept new_scope_types to restrict new_scope notifications to certain scope types, e.g. ["web"]. Leave it empty or null to receive all types.
discord:
enabled: true
webhooks:
main:
url: "https://discord.com/api/webhooks/.../xxx"
events: ["new_program", "removed_program", "new_scope", "removed_scope", "ignored_asset"]
new_scope_types: ["web"]
errors:
url: "https://discord.com/api/webhooks/.../yyy"Each event is sent as a JSON POST request:
webhook:
enabled: true
url: "https://example.com/hooks/scopes"
headers:
Authorization: "Bearer ${WEBHOOK_TOKEN}"
events: ["new_program", "removed_program", "new_scope", "removed_scope", "ignored_asset", "error"]
new_scope_types: ["web"]${VAR} placeholders in the URL and header values are replaced with the matching environment variable, so tokens can stay in .env.
All payloads share the same envelope:
{
"event": "new_scope",
"timestamp": "2025-07-30T10:17:00Z",
"data": {
"platform": "yeswehack",
"program": "Example Program",
"value": "*.example.com",
"type": "web"
}
}| Event | data fields |
|---|---|
new_program |
platform, program, slug, scopes_count, scopes (count per type) |
removed_program |
platform, program, slug |
new_scope |
platform, program, value, type |
removed_scope |
platform, program, value |
ignored_asset |
platform, program, value, reason |
error |
title, message |
Scopes go through three steps before being stored: platform-specific normalization, global normalization, then type detection and validation. Assets that fail validation are recorded as exclusions and trigger an ignored_asset notification.
Platform-specific normalization
YesWeHack
- Expands hostname alternatives in parentheses or brackets:
(www|api).example.com→www.example.com,api.example.com - Preserves URL paths:
https://api-(eu|sg).example.com/connect→https://api-eu.example.com/connect,https://api-sg.example.com/connect - Removes soft hyphens and handles multi-part TLDs:
example.(com|co.uk) - Only expands the listed entries when a list contains
…
HackerOne
example.*→example.comexample.(TLD)→example.comdomain1.com,domain2.com→domain1.com,domain2.com
Intigriti
*.example.<tld>→*.example.comdomain1.com / domain2.com→domain1.com,domain2.com
Bugcrowd
example.com - Production→example.com
Global normalization
- Leading dots become wildcards:
.example.com→*.example.com - Trailing slashes and wildcards are removed:
example.com/*→example.com - Values are lowercased:
Example.COM→example.com - Escaped characters and extra spaces are cleaned up
Type detection
The platform-provided type is overridden when the value matches a known pattern:
| Pattern | Type | Example |
|---|---|---|
| GitHub / GitLab URL | source_code |
https://github.com/user/repo |
| Atlassian Marketplace | source_code |
https://marketplace.atlassian.com/apps/123 |
| App Store / Play Store URL | mobile |
https://apps.apple.com/app/id123 |
| Chrome Web Store | executable |
https://chrome.google.com/webstore/detail/ext |
| CIDR notation | cidr |
192.168.1.0/24 |
| Wildcard domain | web |
*.example.com |
Validation rules
Accepted: domains, subdomains, wildcards (*.example.com), URLs with or without paths, IP addresses and CIDR ranges.
Rejected:
- Values without a dot, unless they are IP addresses
- Multiple wildcards (
*.xyz.*.example.com) or misplaced wildcards (example*.com) - Template placeholders:
{id},<identifier>,[name] - Descriptions in parentheses:
example.com (production only) - Unexpected punctuation (periods, commas, semicolons)
- Spaces, except in URL query parameters
- Values shorter than 4 characters
#in the domain part (allowed in URL fragments)
bundle install
bundle exec rspec # test suite
bundle exec rubocop # lintingProject structure
ScopesExtractor/
├── bin/scopes_extractor # CLI entry point
├── config/settings.yml # Application settings
├── db/migrations/ # Sequel migrations
├── lib/scopes_extractor/
│ ├── api.rb # REST API (Sinatra)
│ ├── auto_sync.rb # Background sync scheduler
│ ├── cli.rb # CLI commands (Thor)
│ ├── config.rb # Configuration loader
│ ├── database.rb # Connection and migrations
│ ├── diff_engine.rb # Change detection
│ ├── http.rb # HTTP client
│ ├── normalizer.rb # Scope normalization
│ ├── sync_manager.rb # Sync orchestration
│ ├── validator.rb # Scope validation
│ ├── models/ # Data models
│ ├── notifiers/ # Discord and HTTP webhook
│ └── platforms/ # One directory per platform
└── spec/ # RSpec tests
Released under the MIT License. Upgrading from 1.x? See the CHANGELOG for breaking changes.
