Migrating from BBOT 2.x to 3.0
BBOT 3.0 is a major release with a wide-ranging cleanup of the CLI, preset syntax, event API, and module ecosystem. This page enumerates everything that changed in a backwards-incompatible way so existing scans, custom modules, and integrations can be updated.
If a change is missing here, please open an issue.
CLI / Targeting
The --whitelist concept was retired. BBOT now distinguishes between target
(what defines scope, i.e. what in_target() checks) and seeds (the events
that actually drive passive modules at scan start).
| 2.x | 3.0 |
|---|---|
-t / --targets (seed + scope) |
-t / --targets (scope only) |
-w / --whitelist |
removed (positional -t now plays this role) |
| (no equivalent) | -s / --seeds (optional override of seed events) |
-s / --silent |
-S / --silent (capitalized; -s reassigned to --seeds) |
--allow-deadly |
removed (see flag changes below) |
The -s flag changed meaning
In 2.x -s was --silent; in 3.0 it's --seeds, and silent moved to -S. A script still passing -s for quiet output will now be adding a seed instead, with no error.
Behavior:
- If
--seedsis omitted, seeds default to whatever was passed to--targets, so the simple case (bbot -t evilcorp.com -p subdomain-enum) is unchanged. --strict-scopenow means "only this exact host, no subdomains" against the target, not the whitelist.- The
Preset(...)Python API mirrors the CLI: positional args are the target, andseeds=is a keyword.whitelist=is gone.
Python API
# 2.x
preset = Preset("evilcorp.com", whitelist=["evilcorp.com", "evilcorp.net"])
scan.whitelist # Target object
scan.whitelisted(host)
# 3.0
preset = Preset("evilcorp.com", "evilcorp.net", seeds=["evilcorp.com"])
scan.target # BBOTTarget wrapper
scan.target.target # the radix-backed scope
scan.target.seeds # seed events
scan.in_target(host)
Scanner.whitelist and Scanner.whitelisted() have been removed.
BBOTTarget.whitelist is now BBOTTarget.target. Pickling of BBOTTarget was
also removed.
Flags
Renamed
| 2.x | 3.0 |
|---|---|
web-basic |
web |
web-thorough |
web-heavy |
Removed
| Removed | Replacement / notes |
|---|---|
aggressive |
use loud (network volume) and/or invasive (destructive) |
deadly |
dropped along with --allow-deadly; affected modules now live under regular flags |
Added
| Added | Meaning |
|---|---|
invasive |
Intrusive or potentially destructive |
safe |
Non-intrusive and non-destructive (now enforced on every module) |
download |
Modules that download files, apps, or repositories |
Every module must now declare at least one of passive / active and at
least one of safe, loud, or invasive. Custom modules carrying the removed
flags will fail validation.
Presets
Renamed
| 2.x | 3.0 |
|---|---|
web-basic |
web |
web-thorough |
web-heavy |
nuclei-intense |
nuclei-heavy |
spider-intense |
spider-heavy |
baddns-intense |
baddns-heavy |
dirbust-light |
webbrute |
dirbust-heavy |
webbrute-heavy |
lightfuzz-medium |
lightfuzz |
lightfuzz-superheavy |
lightfuzz-max |
Added
baddns, waf-bypass, wayback, wayback-heavy, web/paramminer-heavy,
web/virtualhost, web/virtualhost-heavy.
Syntax
Preset YAML files now accept the singular key target: in addition to
targets: (both are merged). The old whitelist: key is gone; use seeds: if
you need seeds that differ from the target. Lines beginning with # inside
target / seed / blacklist lists are stripped as comments. File paths in those
lists are resolved relative to the preset file via the new PresetPath
mechanism.
The top-level scan options that used to be implicit are now required to live
under config: inside a preset (this was previously documented behavior, now
enforced and called out in defaults.yml).
Presets and -c key=value options are now validated when loaded: unknown
keys, typos, and wrong value types are rejected up front (usually with a
closest-match hint, e.g. Did you mean "scope.strict"?) instead of being
silently ignored. The old ${env:VAR} interpolation inside config values is
gone too; inject secrets with shell expansion
(-c modules.shodan.api_key="$SHODAN_KEY") or keep them in secrets.yml.
Modules
Removed (no direct replacement)
smugglerwpscan
Removed and replaced
| 2.x | 3.0 replacement |
|---|---|
httpx |
http |
ffuf |
webbrute |
ffuf_shortnames |
webbrute_shortnames |
extractous |
kreuzberg |
output.http |
output.webhook |
vhost |
virtualhost (rebuilt; emits the new VIRTUAL_HOST event) |
If you used any of these in a custom preset or -m / -em invocation, update
the module name accordingly. The --list-modules output is the source of
truth.
The http name was reassigned
The module called http in 3.0 is not the same as the output module
called http in 2.x. The name was reassigned:
http(3.0, scan module) -- the replacement for the oldhttpxscan module. Probes URLs over the shared in-process blasthttp client.output.http(2.x, output module) -- the webhook-style output module that POSTed events to an arbitrary HTTP endpoint. This is nowoutput.webhookin 3.0.
Old scans that ran bbot -om http ... were emitting events to a webhook;
in 3.0 the equivalent is bbot -om webhook .... By contrast, bbot -m http
... enables the scan module, which probes URLs and is unrelated to the
former output module.
Late 2.x removals
These were dropped during the final 2.x releases, so they're already gone if you're on the latest 2.x, but may still surprise you if you're upgrading from an earlier 2.x version:
digitorus,passivetotal,sitedossier,wappalyzer(removed, no replacement)azure_realm(functionality merged intoazure_tenant)bucket_azure(nowbucket_microsoft)censys(split intocensys_dnsandcensys_ip)
New modules
- Scan:
bucket_hetzner,shodan_enterprise,trajan,waf_bypass - Output:
elastic,kafka,mongo,nats,rabbitmq,zeromq
Module API
BaseModuleno longer has_event_handler_watchdog_taskas a class attribute and the watchdog is owned by_setup().- New
BaseModule.update_event(event, **kwargs)andemit_event(existing_event, ...)flow. Passing an existing event tomake_event()now raises -- callupdate_event()instead. - New
BaseModule.setup_deps()lifecycle hook (runs alongsidesetup()for pure dependency installation like AI models or wordlists). - New
_disable_auto_module_deps = Trueopt-out so a module that watches URLs doesn't automatically pull inhttp/blasthttp. - New
accept_seedsattribute. Defaults toTruefor passive modules,Falseotherwise. Override explicitly if you want different behavior. default_discovery_contextnow uses{event.pretty_string}instead of{event.data}-- the latter is now a dict for URL-like events (see below).- New
BaseModule._is_http_wildcard_host(event)helper. ReturnsTruewhen the target responds identically to two random paths (catch-all / SPA router). Used bywebbrute,lightfuzz, andparamminerto skip hosts that would produce only false positives.
Events
Removed event types
VULNERABILITYis gone. Emit aFINDINGwithseverityset to"CRITICAL","HIGH","MEDIUM","LOW", or"INFO"instead. The pseudo event typeTARGETwas also retired in favor ofSEED.VHOSTwas renamed toVIRTUAL_HOST, emitted by the rebuiltvirtualhostmodule (formerlyvhost).
Class hierarchy
| 2.x | 3.0 |
|---|---|
URL_UNVERIFIED(BaseEvent) -- data is a string |
URL_UNVERIFIED(DictHostEvent) -- data is a dict with url, path, etc. |
URL(URL_UNVERIFIED) -- string data |
URL(URL_UNVERIFIED) -- dict data |
STORAGE_BUCKET(DictEvent, URL_UNVERIFIED) |
STORAGE_BUCKET(URL_UNVERIFIED) |
HTTP_RESPONSE(URL_UNVERIFIED, DictEvent) |
HTTP_RESPONSE(URL_UNVERIFIED) |
DictPathEvent(DictEvent) |
DictPathEvent(DictHostEvent) |
| (no UDP event) | OPEN_UDP_PORT(OPEN_TCP_PORT) |
URL events are now dict-backed. Reading .data on a URL / URL_UNVERIFIED /
HTTP_RESPONSE returns a dict, not a string. Use the new event.url property
to get the URL string, or event.pretty_string for a human-readable form.
Many built-in modules and most tests were updated accordingly; custom modules
that compared or formatted event.data for URL events must be updated.
Removed attributes
event.confidenceandevent.cumulative_confidenceno longer exist onBaseEvent. Confidence is now a per-finding value carried insideFINDING.data["confidence"]and validated against("UNKNOWN", "LOW", "MEDIUM", "HIGH", "CONFIRMED").source_domainhas been removed from events._always_emit_tags: the literal tag"target"was renamed to"seed".- Tags
ip-<address>,http-title-<title>, andcloud-<type>are no longer emitted as event tags by thehttp/ cloudcheck pipelines. Useevent.resolved_hosts,event.host_metadata, and the simplified cloud tags (cloud, plus a single provider tag) instead.
New event surface
event.url-- string URL property (works on URL-like events, returns""otherwise).event.pretty_string-- human-readable representation, used in logs and discovery context.event.host_metadata-- dict of structured per-host metadata (cloud providers, ASN info, etc.). Replaces the long-tail ofcloud-*tags.- Mutation helpers
add_resolved_host(),update_resolved_hosts(),add_dns_child(),set_raw_dns_record(). Direct assignment to the backing slots (event._resolved_hosts = ...,event.dns_children["A"].add(...)) is no longer the public contract: the slots are lazily initialized toNonefor memory reasons and exposed read-only via properties.
FINDING severity / confidence
FINDING.data is now validated against a fixed allowlist:
severity:"INFO" | "LOW" | "MEDIUM" | "HIGH" | "CRITICAL"(the old"INFORMATIONAL"and"MODERATE"values have been renamed toINFOandMEDIUM)confidence:"UNKNOWN" | "LOW" | "MEDIUM" | "HIGH" | "CONFIRMED"
Severity and confidence are recorded on the event as severity-<level> and
confidence-<level> tags. Lowercase tags like high or medium are no
longer added directly.
ASN as a target type
ASN:12345 (or AS12345) can now be used as a scan target -- the seed will be
expanded to its registered CIDRs via the asndb library at scan start. If the
lookup fails (network error, API unavailable), BBOT retries 3 times with a
3-second delay. If all retries fail, the scan aborts with a message suggesting
you pass CIDR ranges directly instead.
ASN lookups use the BLS API at api.bbot.io. No API key is required --
unauthenticated users can operate at a reasonable pace. If you want higher rate
limits, set the bbot_io_api_key config value (or BBOT_IO_API_KEY env var).
Currently the ASN API is the only api.bbot.io service BBOT uses, but any
future BLS APIs will work with the same key.
ASN enrichment (the ASN/subnet data attached to in-scope IPs via
host_metadata) degrades gracefully rather than aborting: after several
consecutive lookup failures, a circuit breaker disables ASN enrichment for the
rest of the scan and the scan continues without it. This is separate from the
ASN-as-target expansion above, which must abort because there would be nothing
to scan.
JSON output: before and after
If you have downstream tooling that parses BBOT's NDJSON output, the schema has changed in several ways. Here is a representative event from each version:
2.x -- URL event (string data)
{
"type": "URL",
"id": "URL:ab12cd34...",
"data": "https://www.evilcorp.com/login",
"host": "www.evilcorp.com",
"port": 443,
"resolved_hosts": ["1.2.3.4"],
"dns_children": {},
"scope_distance": 0,
"scan": "SCAN:deadbeef...",
"timestamp": 1719792000.0,
"parent": "DNS_NAME:ef56ab78...",
"tags": ["in-scope", "status-200", "ip-1.2.3.4", "http-title-Login"],
"module": "httpx",
"module_sequence": "httpx"
}
3.0 -- same URL event (dict data)
{
"type": "URL",
"id": "URL:ab12cd34...",
"uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"data_json": {
"url": "https://www.evilcorp.com/login",
"status_code": 200,
"http_title": "Login"
},
"host": "www.evilcorp.com",
"port": 443,
"resolved_hosts": ["1.2.3.4"],
"dns_children": {},
"scope_distance": 0,
"scan": "SCAN:deadbeef...",
"timestamp": 1719792000.0,
"parent": "DNS_NAME:ef56ab78...",
"parent_uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"tags": ["in-scope", "status-200"],
"module": "http",
"module_sequence": "http",
"discovery_context": "http probed www.evilcorp.com and found URL: https://www.evilcorp.com/login",
"discovery_path": ["SCAN:deadbeef", "DNS_NAME:www.evilcorp.com", "URL:https://www.evilcorp.com/login"],
"parent_chain": ["SCAN:deadbeef", "DNS_NAME:www.evilcorp.com"],
"host_metadata": {"cloud_provider": "amazon", "asn": 16509}
}
Key differences for parsers:
| What changed | 2.x | 3.0 |
|---|---|---|
| URL event data | "data": "https://..." (string) |
"data_json": {"url": "https://...", ...} (dict) |
| Non-URL event data | "data": "..." (string) |
"data": "..." (still a string) |
| Module name for HTTP probing | "module": "httpx" |
"module": "http" |
| Per-host cloud/IP tags | "ip-1.2.3.4", "http-title-Login", "cloud-amazon" in tags |
Moved to host_metadata dict and data_json fields; tags simplified |
| Event ID | "id" only |
"id" + new "uuid" (globally unique) |
| Parent reference | "parent" only |
"parent" + new "parent_uuid" |
| Discovery chain | not present | "discovery_context", "discovery_path", "parent_chain" |
The data vs data_json split
When an event's .data is a string (DNS_NAME, IP_ADDRESS, EMAIL_ADDRESS,
OPEN_TCP_PORT, etc.), the JSON key is "data". When .data is a dict
(URL, URL_UNVERIFIED, HTTP_RESPONSE, FINDING, STORAGE_BUCKET, etc.), the
JSON key is "data_json". In 2.x, URL events used "data" with a string
value. In 3.0, they use "data_json" with a dict. Parsers that key on
"data" for URL events will silently get nothing back.
2.x -- VULNERABILITY event
{
"type": "VULNERABILITY",
"data": {
"host": "www.evilcorp.com",
"severity": "HIGH",
"description": "SQL injection in login form",
"url": "https://www.evilcorp.com/login"
},
"tags": ["in-scope", "high"]
}
3.0 -- equivalent FINDING event
{
"type": "FINDING",
"data_json": {
"host": "www.evilcorp.com",
"severity": "HIGH",
"confidence": "HIGH",
"name": "SQL Injection",
"description": "SQL injection in login form",
"url": "https://www.evilcorp.com/login"
},
"tags": ["in-scope", "severity-high", "confidence-high"]
}
Changes:
VULNERABILITYtype is gone; useFINDINGinstead.confidenceis now required on every FINDING (one ofUNKNOWN,LOW,MEDIUM,HIGH,CONFIRMED).nameis now required (short label for the finding).- Severity strings changed:
INFORMATIONALis nowINFO,MODERATEis nowMEDIUM. - Tags changed from bare lowercase (
high) to prefixed (severity-high,confidence-high).
Config
bbot/defaults.yml saw a number of breaking renames and additions:
Renamed
| 2.x | 3.0 |
|---|---|
web.httpx_timeout |
web.http_timeout (target-directed traffic, default 10) |
| (no equivalent) | web.http_timeout_infrastructure (API calls, wordlist downloads, etc., default 10) |
web.httpx_retries |
web.http_retries |
dns.threads (global) |
dns.threads (now per-resolver; default lowered from 25 -> 10) |
web.ssl_verify |
web.ssl_verify_target (target-directed traffic, default false) |
| (no equivalent) | web.ssl_verify_infrastructure (API calls, wordlist downloads, etc., default true) |
Removed
deps.ffuf.version(ffuf is gone).- The top-level
modules.json.siem_friendlyflag (and thesiem_friendlyoutput module).
Added
max_mem_percent-- global ingress throttle when RSS exceeds the threshold.redact_secrets(defaulttrue) -- redacts secret values (API keys, tokens) when a resolved preset is serialized to YAML (e.g. the generatedbbot.yml). Set tofalseto include them.web.user_agent_suffix-- appended to the user agent (previously buried as a hidden CLI flag).web.http_rate_limit-- global rps cap across the shared blasthttp client.web.http_proxy_exclude-- hosts/CIDRs to exclude from the HTTP proxy (NO_PROXYequivalent).web.body_spill.{enabled,cache_mb,compress}-- disk-spill HTTP response bodies to keep them off the Python heap.dns.cache_size-- DNS LRU size.bbot_io_api_key-- optional API key forapi.bbot.ioservices. Currently only used by ASN lookups; no key is required for basic use. Future BLS APIs will share this key.dns.abort_thresholddefault lowered from50->10.dns.filter_ptrssemantics: PTR-derived hostnames are now treated as affiliates by default rather than being injected as in-scope DNS_NAME events during IP-range scans.
Stale config files and --reset-config / --reset-secrets
BBOT generates two config files the first time it runs and then never overwrites them:
~/.config/bbot/bbot.yml-- a fully-commented snapshot of all default options~/.config/bbot/secrets.yml-- the secret-bearing subset (API keys, etc.), written owner-only (0600)
Because they're written once and left alone, they drift out of sync with the
defaults as you upgrade. A bbot.yml generated under 2.x can still mention
web.httpx_timeout, web.ssl_verify, modules.json.siem_friendly, and other
keys that were renamed or removed in 3.0.
In 2.x this was harmless: unknown keys were silently ignored. In 3.0, config is
validated on load, so a leftover key in your generated file now produces a
validation error before the scan starts. When the offending key lives in one of
these generated files (rather than being a -c typo on the command line), BBOT
tells you which file it came from and points you at the matching reset flag.
Two new CLI flags regenerate the files from current defaults:
bbot --reset-config # regenerate bbot.yml
bbot --reset-secrets # regenerate secrets.yml
- Destructive. A regenerated file is a fresh, fully-commented template, so
any options you had uncommented are wiped. The existing file is backed up
first to
<name>.bak(then.bak.1,.bak.2, ... so earlier backups aren't clobbered). - Confirmation required. You're prompted before anything is overwritten;
pass
-y/--yesto skip the prompt. Non-interactive runs without--yesrefuse and exit rather than overwrite silently. - Independent. The two files are reset separately:
--reset-confignever touches the API keys insecrets.yml, and--reset-secretsnever touches the tuned options inbbot.yml. The regeneratedsecrets.ymlis written atomically and stays owner-only.
The usual fix after an in-place upgrade is to copy any customizations out of the old file, run the matching reset flag, then re-apply your changes to the fresh template (or move them into a preset).
DNS and HTTP internals
Both the DNS and HTTP engines were rewritten and the old subprocess architecture was deleted.
- DNS:
bbot/core/helpers/dns/engine.pyanddns/mock.pywere removed. TheDNSHelperno longer inherits fromEngineClient; resolution now goes through the native blastdns Rust client. Theself.helpers.dns.resolverdnspythonresolver is gone. - HTTP:
bbot/core/helpers/web/client.pyandweb/engine.pywere removed.WebHelperno longer inherits fromEngineClient. All HTTP goes through the shared blasthttp client (self.helpers.blasthttp). The httpx-basedrequest_batch/request_custom_batch/curlmethods were replaced byrequest(),request_batch_stream(urls, threads=10, **kwargs), anddownload(). - Engine framework removed: the
bbot.core.enginemodule (EngineBase/EngineClient/EngineServer) and theBBOTEngineErrorexception were deleted along with the subprocess architecture.WebErrorandDNSErrornow subclassBBOTErrordirectly, andCurlErrorwas removed. External code importingbbot.core.engine,BBOTEngineError, orCurlErrormust be updated. - The blasthttp dependency line is
blasthttp>=0.9.0.
Modules that previously instantiated their own httpx.AsyncClient or built
custom curl invocations must switch to self.helpers.request(...) /
self.helpers.blasthttp.
Output and DB models
- The pydantic / SQLModel models moved from
bbot.db.sql.modelstobbot.models.sql(plus a newbbot.models.pydantic/bbot.models.helperssplit). External importers must update. output.httpwas renamed tooutput.webhook.output.jsonlost thesiem_friendlyoption.- New output modules:
elastic,kafka,mongo,nats,rabbitmq,zeromq. - Neo4j output now also serializes
host_metadata.
Output modules are now purely additive
In 2.x, specifying -om could silently replace the default output modules
depending on whether any of the names overlapped with the defaults. This was
confusing and inconsistent.
In 3.0, -om is always additive: it adds modules on top of the defaults
(csv, txt, json, plus stdout from the CLI). To remove a default, use
the new -eom / --exclude-output-modules flag:
# defaults (csv, txt, json, stdout) are all enabled
bbot -t evilcorp.com -p subdomain-enum
# neo4j is added alongside all defaults
bbot -t evilcorp.com -p subdomain-enum -om neo4j
# json only -- explicitly exclude the other defaults
bbot -t evilcorp.com -p subdomain-enum -eom csv txt stdout
The same applies in preset YAML (exclude_output_modules:) and the Python API
(Preset(exclude_output_modules=[...])).
The python output module was also moved from output/ to internal/ -- it
was never a real output module, it is the Python API event bridge.
Omitted event types
Certain event types are excluded from output by default via the
omit_event_types config. These events are still processed by modules
internally, but they do not appear in JSON, CSV, or stdout output unless
you remove them from the list:
omit_event_types:
- HTTP_RESPONSE
- RAW_TEXT
- URL_UNVERIFIED
- DNS_NAME_UNRESOLVED
- FILESYSTEM
- WEB_PARAMETER
- RAW_DNS_RECORD
If your downstream tooling relied on seeing HTTP_RESPONSE or
URL_UNVERIFIED events in the output, you will need to remove them from
omit_event_types in your preset or config.
Dependencies and tooling
- Build system: migrated from Poetry to uv
with a hatchling build backend.
pyproject.tomlnow follows PEP 621, anduv.lockreplacespoetry.lock; install dev dependencies withuv sync --group dev. Versioning is static (version = "3.0.0"inpyproject.toml) rather than derived from git tags bypoetry-dynamic-versioning. - License: changed from
GPL-3.0toAGPL-3.0. - Python: minimum bumped from
3.9to3.10. Upper bound is now<3.15. - Lockstep deps:
radixtarget >=4.0.1,<5(composition pattern, no longer subclassed)cloudcheck >=11.1.0,<12blasthttp >=0.9.0(new)blastdns >=1.9.0,<2(new)asndb >=1.1.0(new)zstandard(new; used by HTTP body spill)httpxremoved as a runtime dep.
Custom modules that imported httpx, dns.asyncresolver, radixtarget
subclasses, or bbot.db.sql.models will need to be ported.