Skip to content

Commit 5ac89e7

Browse files
committed
restore cloudcheck tag propagation to URL/OPEN_TCP_PORT children
`_minimize()` wiped `_resolved_hosts` on the parent DNS_NAME once its consumers finished, which starved `dnsresolve` line 156 when child URL / OPEN_TCP_PORT events later reached it. Without the parent's IPs on the child, cloudcheck's per-host inheritance filter had nothing to match against and the cloud tags never propagated. Stop wiping `_resolved_hosts` in `_minimize()`: it is a small frozenset that the memory optimization was not really targeting. Also updates the modules still checking the pre-f561bda1e composed tags (`cdn-cloudflare`, `waf-cloudflare`, `cdn-imperva`, `cdn-akamai`, `cdn-cloudfront`) to key off the flat provider name that cloudcheck now emits. Regression tests cover both sides: cloudcheck asserts `_minimize()` preserves resolved_hosts and that URL / OPEN_TCP_PORT children inherit cloud tags; waf_bypass asserts both cloudflare and imperva tags trigger protection detection; virtualhost asserts each of the four flat provider names causes filter_event to skip the URL.
1 parent 6f62e97 commit 5ac89e7

6 files changed

Lines changed: 148 additions & 8 deletions

File tree

bbot/core/event/base.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -790,7 +790,6 @@ def _minimize(self):
790790
# release container slots; lazy-init pattern means None == empty
791791
self._dns_children = None
792792
self._raw_dns_records = None
793-
self._resolved_hosts = None
794793

795794
def clone(self):
796795
# Create a shallow copy of the event first

bbot/modules/virtualhost.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,10 +1019,10 @@ async def finish(self):
10191019

10201020
async def filter_event(self, event):
10211021
if (
1022-
"cdn-cloudflare" in event.tags
1023-
or "cdn-imperva" in event.tags
1024-
or "cdn-akamai" in event.tags
1025-
or "cdn-cloudfront" in event.tags
1022+
"cloudflare" in event.tags
1023+
or "imperva" in event.tags
1024+
or "akamai" in event.tags
1025+
or "cloudfront" in event.tags
10261026
):
10271027
self.debug(f"Not processing URL {event.url} because it's behind a WAF or CDN, and that's pointless")
10281028
return False

bbot/modules/waf_bypass.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,9 @@ async def handle_event(self, event):
8383

8484
# Detect WAF/CDN protection based on tags
8585
provider_name = None
86-
if "cdn-cloudflare" in event.tags or "waf-cloudflare" in event.tags:
86+
if "cloudflare" in event.tags:
8787
provider_name = "CloudFlare"
88-
elif "cdn-imperva" in event.tags:
88+
elif "imperva" in event.tags:
8989
provider_name = "Imperva"
9090

9191
is_protected = provider_name is not None

bbot/test/test_step_2/module_tests/test_module_cloudcheck.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,59 @@ async def setup_after_prep(self, module_test):
123123
assert "google" in child_disjoint.tags
124124
assert "cloud" in child_disjoint.tags
125125

126+
# ── _minimize() must not wipe resolved_hosts ───────────────────
127+
# Real scan flow: a DNS_NAME gets processed by dnsresolve + cloudcheck,
128+
# then Event._minimize() decrements its consumer count. Later, child
129+
# events (URL, OPEN_TCP_PORT) that share the DNS_NAME's host reach
130+
# dnsresolve and copy the parent's resolved_hosts (line 156). If
131+
# _minimize() wiped resolved_hosts, that copy carries nothing and
132+
# cloudcheck's per-host filter has no IPs to match against, so the
133+
# cloud tag never inherits.
134+
minimized = scan.make_event("minimize.evilcorp.com", parent=scan.root_event)
135+
minimized.resolved_hosts = ("asdf.amazonaws.com", "10.0.0.1")
136+
await module.handle_event(minimized)
137+
# drive _module_consumers below 0 so the wipe block runs
138+
minimized._minimize()
139+
minimized._minimize()
140+
assert minimized.resolved_hosts, (
141+
"_minimize() must preserve resolved_hosts — dnsresolve line 156 copies it to child "
142+
"URL/OPEN_TCP_PORT events, and cloudcheck's per-host inheritance needs those IPs"
143+
)
144+
# cloud host_metadata already survives minimization; confirm
145+
assert "asdf.amazonaws.com" in minimized.host_metadata
146+
147+
# ── URL/OPEN_TCP_PORT propagation end-to-end ──────────────────
148+
# Together with the _minimize() preservation above, this exercises
149+
# the real flow: parent DNS_NAME cloud-tagged, then minimized, then a
150+
# URL child gets its resolved_hosts populated (mirroring dnsresolve),
151+
# and cloudcheck's existing subset-gate + per-host filter should copy
152+
# the cloud tag onto the child.
153+
url_child = scan.make_event(
154+
"http://minimize.evilcorp.com/",
155+
"URL",
156+
parent=minimized,
157+
tags=["status-200", "in-scope"],
158+
)
159+
# simulate dnsresolve intercept: child inherits parent's resolved_hosts
160+
url_child._resolved_hosts = minimized.resolved_hosts
161+
await module.handle_event(url_child)
162+
assert "amazon" in url_child.tags, (
163+
"URL child of amazon-tagged DNS_NAME must inherit the cloud tag "
164+
"once its resolved_hosts carries the parent's IPs"
165+
)
166+
assert "cloud" in url_child.tags
167+
assert "asdf.amazonaws.com" in url_child.host_metadata
168+
169+
port_child = scan.make_event(
170+
"minimize.evilcorp.com:443",
171+
"OPEN_TCP_PORT",
172+
parent=minimized,
173+
)
174+
port_child._resolved_hosts = minimized.resolved_hosts
175+
await module.handle_event(port_child)
176+
assert "amazon" in port_child.tags, "OPEN_TCP_PORT child must inherit cloud tag from parent"
177+
assert "asdf.amazonaws.com" in port_child.host_metadata
178+
126179
# ── YARA prefilter short-circuit ───────────────────────────────
127180
# When no host could possibly match any bucket regex (mismatched
128181
# suffix) the prefilter returns no matches and we skip the Python

bbot/test/test_step_2/module_tests/test_module_virtualhost.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,3 +1070,36 @@ def check(self, module_test, events):
10701070
f"SAN analyzer received {type(san_arg).__name__}, expected str. Value: {san_arg!r}"
10711071
)
10721072
assert san_arg.startswith("https://"), f"Expected HTTPS URL, got {san_arg!r}"
1073+
1074+
1075+
class TestVirtualhostSkipsCdnWaf(VirtualhostTestBase):
1076+
"""filter_event must reject URLs tagged with any of the flat cloud-provider tags
1077+
that cloudcheck emits (post-`Migrate cloudcheck to host_metadata` refactor)."""
1078+
1079+
targets = ["http://localhost:8888"]
1080+
modules_overrides = ["virtualhost"]
1081+
1082+
async def setup_after_prep(self, module_test):
1083+
pass
1084+
1085+
async def check(self, module_test, events):
1086+
vh_module = module_test.scan.modules["virtualhost"]
1087+
1088+
for tag in ("cloudflare", "imperva", "akamai", "cloudfront"):
1089+
url_event = module_test.scan.make_event(
1090+
"http://cdn-test.local:8888/",
1091+
"URL",
1092+
parent=module_test.scan.root_event,
1093+
tags=[tag, "in-scope", "status-200"],
1094+
)
1095+
result = await vh_module.filter_event(url_event)
1096+
assert result is False, f"virtualhost must skip URL tagged {tag!r} (got {result!r})"
1097+
1098+
untagged_event = module_test.scan.make_event(
1099+
"http://plain.local:8888/",
1100+
"URL",
1101+
parent=module_test.scan.root_event,
1102+
tags=["in-scope", "status-200"],
1103+
)
1104+
result = await vh_module.filter_event(untagged_event)
1105+
assert result is True, f"virtualhost must not skip an untagged URL (got {result!r})"

bbot/test/test_step_2/module_tests/test_module_waf_bypass.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ async def handle_event(self, event):
4343
self.events_seen.append(event.data)
4444
url = "http://protected.test:8888/"
4545
url_event = self.scan.make_event(
46-
url, "URL", parent=self.scan.root_event, tags=["cdn-cloudflare", "in-scope", "status-200"]
46+
url, "URL", parent=self.scan.root_event, tags=["cloudflare", "in-scope", "status-200"]
4747
)
4848
if url_event is not None:
4949
await self.emit_event(url_event)
@@ -135,3 +135,58 @@ def check(self, module_test, events):
135135
in e.data["description"]
136136
]
137137
assert correct_description, "Incorrect description"
138+
139+
140+
class TestWAFBypassTagRecognition(ModuleTestBase):
141+
"""Regression: waf_bypass keys off the flat cloud-provider tags cloudcheck emits
142+
post-`Migrate cloudcheck to host_metadata` refactor. Both `cloudflare` and `imperva`
143+
tags must trigger protected-domain detection."""
144+
145+
targets = ["evilcorp.com"]
146+
modules_overrides = ["waf_bypass"]
147+
148+
async def setup_after_prep(self, module_test):
149+
pass
150+
151+
async def check(self, module_test, events):
152+
waf_module = module_test.scan.modules["waf_bypass"]
153+
154+
async def fake_resolve(host):
155+
return []
156+
157+
module_test.monkeypatch.setattr(waf_module.helpers.dns, "resolve", fake_resolve)
158+
159+
async def fake_get_url_content(url, ip=None):
160+
return None
161+
162+
import types
163+
164+
module_test.monkeypatch.setattr(
165+
waf_module, "get_url_content", types.MethodType(fake_get_url_content, waf_module), raising=True
166+
)
167+
168+
for tag in ("cloudflare", "imperva"):
169+
waf_module.protected_domains = {}
170+
url_event = module_test.scan.make_event(
171+
f"http://{tag}-protected.test/",
172+
"URL",
173+
parent=module_test.scan.root_event,
174+
tags=[tag, "in-scope", "status-200"],
175+
)
176+
await waf_module.handle_event(url_event)
177+
assert f"{tag}-protected.test" in waf_module.protected_domains, (
178+
f"waf_bypass did not recognize {tag!r}-tagged URL as protected"
179+
)
180+
181+
# sanity: an untagged URL is not treated as protected
182+
waf_module.protected_domains = {}
183+
untagged = module_test.scan.make_event(
184+
"http://plain.test/",
185+
"URL",
186+
parent=module_test.scan.root_event,
187+
tags=["in-scope", "status-200"],
188+
)
189+
await waf_module.handle_event(untagged)
190+
assert "plain.test" not in waf_module.protected_domains, (
191+
"waf_bypass should not flag untagged URLs as protected"
192+
)

0 commit comments

Comments
 (0)