From 48313d75309bfa36e424ce468650957c3d767bbd Mon Sep 17 00:00:00 2001 From: liquidsec Date: Mon, 18 May 2026 10:06:23 -0400 Subject: [PATCH 1/2] require dnsresolve when DNS_NAME consumers are enabled Catches all three opt-out paths in one preset-bake check: - `-em dnsresolve` - top-level `dnsresolve: false` - `dns.disable: true` Points users at `dns.minimal: true` for the reduce-DNS-overhead case. --- bbot/scanner/preset/preset.py | 22 ++++++++++++++++++++++ bbot/test/test_step_1/test_presets.py | 17 +++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/bbot/scanner/preset/preset.py b/bbot/scanner/preset/preset.py index c6ab3a8220..1fd84f324d 100644 --- a/bbot/scanner/preset/preset.py +++ b/bbot/scanner/preset/preset.py @@ -478,6 +478,28 @@ def bake(self, scan=None): for output_module in self.default_output_modules: baked_preset.add_module(output_module, module_type="output", raise_error=False) + # dnsresolve is the intercept module that resolves every event, tags wildcards/unresolved, + # and rewrites wildcard hits to `_wildcard.parent`. Modules that watch DNS_NAME depend on + # those tags to filter false positives; without it, brute-force and passive-enum modules + # emit unverified and wildcard-tainted names. This catches all three opt-out paths: + # `-em dnsresolve`, top-level `dnsresolve: false`, and `dns.disable: true`. + dnsresolve_disabled = "dnsresolve" not in baked_preset.modules or baked_preset.config.get("dns", {}).get( + "disable", False + ) + if dnsresolve_disabled: + dns_name_consumers = sorted( + m + for m in baked_preset.modules + if "DNS_NAME" in baked_preset.preloaded_module(m).get("watched_events", []) + ) + if dns_name_consumers: + raise ValidationError( + f"dnsresolve is required by these enabled modules but is disabled: {', '.join(dns_name_consumers)}. " + "Use `dns.minimal: true` (resolves A/AAAA only, skips MX/NS/SRV expansion) " + "if you want to reduce DNS overhead while keeping wildcard and unresolved tagging. " + "If you genuinely want no DNS at all, also disable the modules listed above." + ) + # create target object from bbot.scanner.target import BBOTTarget diff --git a/bbot/test/test_step_1/test_presets.py b/bbot/test/test_step_1/test_presets.py index 4dfb0a7fc3..c861a05a66 100644 --- a/bbot/test/test_step_1/test_presets.py +++ b/bbot/test/test_step_1/test_presets.py @@ -1274,3 +1274,20 @@ def test_preset_file_targets(tmp_path): target_inputs5 = set(preset5._target_list) assert "nested.evilcorp.com" in target_inputs5 assert "my_targets.txt" not in target_inputs5 + + +def test_preset_dnsresolve_required_by_dns_name_consumers(): + # gate must fire for all three opt-out paths when any DNS_NAME-watching module is enabled + for opt_out in ( + {"exclude_modules": ["dnsresolve"], "flags": ["subdomain-enum"]}, + {"config": {"dnsresolve": False}, "flags": ["subdomain-enum"]}, + {"config": {"dns": {"disable": True}}, "flags": ["subdomain-enum"]}, + ): + with pytest.raises(ValidationError, match="dnsresolve is required"): + Preset(**opt_out).bake() + + # dns.minimal keeps dnsresolve in the pipeline -- must NOT fire + Preset(flags=["subdomain-enum"], config={"dns": {"minimal": True}}).bake() + + # disabling dnsresolve with no DNS_NAME consumers enabled is allowed + Preset(exclude_modules=["dnsresolve"]).bake() From 9b739ef625f11d91fa779fbe6b28c3848aa70a6a Mon Sep 17 00:00:00 2001 From: liquidsec Date: Mon, 18 May 2026 10:27:00 -0400 Subject: [PATCH 2/2] check explicit opt-outs instead of dnsresolve absence The previous "dnsresolve not in modules" check tripped on --list-module-options, which intentionally sets _default_internal_modules to [] to suppress internal modules from the listing. Switch to checking the three explicit user actions directly so inspection-mode code paths that strip internal modules don't trigger the gate. --- bbot/scanner/preset/preset.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/bbot/scanner/preset/preset.py b/bbot/scanner/preset/preset.py index 1fd84f324d..4550028c10 100644 --- a/bbot/scanner/preset/preset.py +++ b/bbot/scanner/preset/preset.py @@ -481,10 +481,15 @@ def bake(self, scan=None): # dnsresolve is the intercept module that resolves every event, tags wildcards/unresolved, # and rewrites wildcard hits to `_wildcard.parent`. Modules that watch DNS_NAME depend on # those tags to filter false positives; without it, brute-force and passive-enum modules - # emit unverified and wildcard-tainted names. This catches all three opt-out paths: - # `-em dnsresolve`, top-level `dnsresolve: false`, and `dns.disable: true`. - dnsresolve_disabled = "dnsresolve" not in baked_preset.modules or baked_preset.config.get("dns", {}).get( - "disable", False + # emit unverified and wildcard-tainted names. This catches all three explicit opt-outs: + # `-em dnsresolve`, top-level `dnsresolve: false`, and `dns.disable: true`. We check the + # user actions rather than "dnsresolve in modules" so that internal-module-trimming code + # paths (e.g. `--list-module-options` setting `_default_internal_modules = []`) don't trip + # the gate -- those aren't real scans, and the user hasn't asked to disable anything. + dnsresolve_disabled = ( + "dnsresolve" in baked_preset.exclude_modules + or baked_preset.config.get("dnsresolve", True) is False + or baked_preset.config.get("dns", {}).get("disable", False) ) if dnsresolve_disabled: dns_name_consumers = sorted(