diff --git a/bbot/scanner/preset/preset.py b/bbot/scanner/preset/preset.py index c6ab3a8220..4550028c10 100644 --- a/bbot/scanner/preset/preset.py +++ b/bbot/scanner/preset/preset.py @@ -478,6 +478,33 @@ 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 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( + 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()