Skip to content

Commit 0539889

Browse files
dns mock cleanup
1 parent 697ef7d commit 0539889

17 files changed

+160
-184
lines changed

bbot/core/helpers/dns.py

-25
Original file line numberDiff line numberDiff line change
@@ -1040,28 +1040,3 @@ def _get_dummy_module(self, name):
10401040
dummy_module = self.parent_helper._make_dummy_module(name=name, _type="DNS")
10411041
self._dummy_modules[name] = dummy_module
10421042
return dummy_module
1043-
1044-
def mock_dns(self, dns_dict):
1045-
if self._orig_resolve_raw is None:
1046-
self._orig_resolve_raw = self.resolve_raw
1047-
1048-
async def mock_resolve_raw(query, **kwargs):
1049-
results = []
1050-
errors = []
1051-
types = self._parse_rdtype(kwargs.get("type", ["A", "AAAA"]))
1052-
for t in types:
1053-
with suppress(KeyError):
1054-
results += self._mock_table[(query, t)]
1055-
return results, errors
1056-
1057-
for (query, rdtype), answers in dns_dict.items():
1058-
if isinstance(answers, str):
1059-
answers = [answers]
1060-
for answer in answers:
1061-
rdata = dns.rdata.from_text("IN", rdtype, answer)
1062-
try:
1063-
self._mock_table[(query, rdtype)].append((rdtype, rdata))
1064-
except KeyError:
1065-
self._mock_table[(query, rdtype)] = [(rdtype, [rdata])]
1066-
1067-
self.resolve_raw = mock_resolve_raw

bbot/test/bbot_fixtures.py

+65
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import dns
23
import sys
34
import pytest
45
import asyncio # noqa
@@ -252,3 +253,67 @@ def install_all_python_deps():
252253
for module in module_loader.preloaded().values():
253254
deps_pip.update(set(module.get("deps", {}).get("pip", [])))
254255
subprocess.run([sys.executable, "-m", "pip", "install"] + list(deps_pip))
256+
257+
258+
class MockResolver:
259+
import dns
260+
261+
def __init__(self, mock_data=None):
262+
self.mock_data = mock_data if mock_data else {}
263+
self.nameservers = ["127.0.0.1"]
264+
265+
async def resolve_address(self, ipaddr, *args, **kwargs):
266+
modified_kwargs = {}
267+
modified_kwargs.update(kwargs)
268+
modified_kwargs["rdtype"] = "PTR"
269+
return await self.resolve(str(dns.reversename.from_address(ipaddr)), *args, **modified_kwargs)
270+
271+
def create_dns_response(self, query_name, rdtype):
272+
query_name = query_name.strip(".")
273+
answers = self.mock_data.get(query_name, {}).get(rdtype, [])
274+
if not answers:
275+
raise self.dns.resolver.NXDOMAIN(f"No answer found for {query_name} {rdtype}")
276+
277+
message_text = f"""id 1234
278+
opcode QUERY
279+
rcode NOERROR
280+
flags QR AA RD
281+
;QUESTION
282+
{query_name}. IN {rdtype}
283+
;ANSWER"""
284+
for answer in answers:
285+
message_text += f"\n{query_name}. 1 IN {rdtype} {answer}"
286+
287+
message_text += "\n;AUTHORITY\n;ADDITIONAL\n"
288+
message = self.dns.message.from_text(message_text)
289+
return message
290+
291+
async def resolve(self, query_name, rdtype=None):
292+
if rdtype is None:
293+
rdtype = "A"
294+
elif isinstance(rdtype, str):
295+
rdtype = rdtype.upper()
296+
else:
297+
rdtype = str(rdtype.name).upper()
298+
299+
domain_name = self.dns.name.from_text(query_name)
300+
rdtype_obj = self.dns.rdatatype.from_text(rdtype)
301+
302+
if "_NXDOMAIN" in self.mock_data and query_name in self.mock_data["_NXDOMAIN"]:
303+
# Simulate the NXDOMAIN exception
304+
raise self.dns.resolver.NXDOMAIN
305+
306+
try:
307+
response = self.create_dns_response(query_name, rdtype)
308+
answer = self.dns.resolver.Answer(domain_name, rdtype_obj, self.dns.rdataclass.IN, response)
309+
return answer
310+
except self.dns.resolver.NXDOMAIN:
311+
return []
312+
313+
314+
@pytest.fixture()
315+
def mock_dns():
316+
def _mock_dns(scan, mock_data):
317+
scan.helpers.dns.resolver = MockResolver(mock_data)
318+
319+
return _mock_dns

bbot/test/conftest.py

-67
Original file line numberDiff line numberDiff line change
@@ -179,70 +179,3 @@ def proxy_server():
179179
# Stop the server.
180180
server.shutdown()
181181
server_thread.join()
182-
183-
184-
class MockResolver:
185-
import dns
186-
187-
def __init__(self, mock_data=None):
188-
self.mock_data = mock_data if mock_data else {}
189-
self.nameservers = ["127.0.0.1"]
190-
191-
async def resolve_address(self, host):
192-
try:
193-
from dns.asyncresolver import resolve_address
194-
195-
result = await resolve_address(host)
196-
return result
197-
except ImportError:
198-
raise ImportError("dns.asyncresolver.Resolver.resolve_address not found")
199-
200-
def create_dns_response(self, query_name, rdtype):
201-
answers = self.mock_data.get(query_name, {}).get(rdtype, [])
202-
if not answers:
203-
raise self.dns.resolver.NXDOMAIN(f"No answer found for {query_name} {rdtype}")
204-
205-
message_text = f"""id 1234
206-
opcode QUERY
207-
rcode NOERROR
208-
flags QR AA RD
209-
;QUESTION
210-
{query_name}. IN {rdtype}
211-
;ANSWER"""
212-
for answer in answers:
213-
message_text += f"\n{query_name}. 1 IN {rdtype} {answer}"
214-
215-
message_text += "\n;AUTHORITY\n;ADDITIONAL\n"
216-
message = self.dns.message.from_text(message_text)
217-
return message
218-
219-
async def resolve(self, query_name, rdtype=None):
220-
if rdtype is None:
221-
rdtype = "A"
222-
elif isinstance(rdtype, str):
223-
rdtype = rdtype.upper()
224-
else:
225-
rdtype = str(rdtype.name).upper()
226-
227-
domain_name = self.dns.name.from_text(query_name)
228-
rdtype_obj = self.dns.rdatatype.from_text(rdtype)
229-
230-
if "_NXDOMAIN" in self.mock_data and query_name in self.mock_data["_NXDOMAIN"]:
231-
# Simulate the NXDOMAIN exception
232-
raise self.dns.resolver.NXDOMAIN
233-
234-
try:
235-
response = self.create_dns_response(query_name, rdtype)
236-
answer = self.dns.resolver.Answer(domain_name, rdtype_obj, self.dns.rdataclass.IN, response)
237-
return answer
238-
except self.dns.resolver.NXDOMAIN:
239-
return []
240-
241-
242-
@pytest.fixture()
243-
def configure_mock_resolver(monkeypatch):
244-
def _configure(mock_data):
245-
mock_resolver = MockResolver(mock_data)
246-
return mock_resolver
247-
248-
return _configure

bbot/test/test_step_1/test_dns.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33

44
@pytest.mark.asyncio
5-
async def test_dns(bbot_scanner, bbot_config):
5+
async def test_dns(bbot_scanner, bbot_config, mock_dns):
66
scan = bbot_scanner("1.1.1.1", config=bbot_config)
77
helpers = scan.helpers
88

@@ -85,8 +85,12 @@ async def test_dns(bbot_scanner, bbot_config):
8585
dns_config = OmegaConf.create({"dns_resolution": True})
8686
dns_config = OmegaConf.merge(bbot_config, dns_config)
8787
scan2 = bbot_scanner("evilcorp.com", config=dns_config)
88-
scan2.helpers.dns.mock_dns(
89-
{("evilcorp.com", "TXT"): '"v=spf1 include:cloudprovider.com ~all"', ("cloudprovider.com", "A"): "1.2.3.4"}
88+
mock_dns(
89+
scan2,
90+
{
91+
"evilcorp.com": {"TXT": ['"v=spf1 include:cloudprovider.com ~all"']},
92+
"cloudprovider.com": {"A": ["1.2.3.4"]},
93+
},
9094
)
9195
events = [e async for e in scan2.async_start()]
9296
assert 1 == len(

bbot/test/test_step_1/test_manager_deduplication.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44

55
@pytest.mark.asyncio
6-
async def test_manager_deduplication(bbot_config, bbot_scanner):
6+
async def test_manager_deduplication(bbot_config, bbot_scanner, mock_dns):
77

88
class DefaultModule(BaseModule):
99
_name = "default_module"
@@ -62,7 +62,7 @@ async def do_scan(*args, _config={}, _dns_mock={}, scan_callback=None, **kwargs)
6262
scan.modules["per_hostport_only"] = per_hostport_only
6363
scan.modules["per_domain_only"] = per_domain_only
6464
if _dns_mock:
65-
scan.helpers.dns.mock_dns(_dns_mock)
65+
mock_dns(scan, _dns_mock)
6666
if scan_callback is not None:
6767
scan_callback(scan)
6868
return (
@@ -76,12 +76,12 @@ async def do_scan(*args, _config={}, _dns_mock={}, scan_callback=None, **kwargs)
7676
)
7777

7878
dns_mock_chain = {
79-
("default_module.test.notreal", "A"): "127.0.0.3",
80-
("everything_module.test.notreal", "A"): "127.0.0.4",
81-
("no_suppress_dupes.test.notreal", "A"): "127.0.0.5",
82-
("accept_dupes.test.notreal", "A"): "127.0.0.6",
83-
("per_hostport_only.test.notreal", "A"): "127.0.0.7",
84-
("per_domain_only.test.notreal", "A"): "127.0.0.8",
79+
"default_module.test.notreal": {"A": ["127.0.0.3"]},
80+
"everything_module.test.notreal": {"A": ["127.0.0.4"]},
81+
"no_suppress_dupes.test.notreal": {"A": ["127.0.0.5"]},
82+
"accept_dupes.test.notreal": {"A": ["127.0.0.6"]},
83+
"per_hostport_only.test.notreal": {"A": ["127.0.0.7"]},
84+
"per_domain_only.test.notreal": {"A": ["127.0.0.8"]},
8585
}
8686

8787
# dns search distance = 1, report distance = 0

bbot/test/test_step_1/test_manager_scope_accuracy.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -777,7 +777,7 @@ def custom_setup(scan):
777777

778778

779779
@pytest.mark.asyncio
780-
async def test_manager_blacklist(bbot_config, bbot_scanner, bbot_httpserver, caplog):
780+
async def test_manager_blacklist(bbot_config, bbot_scanner, bbot_httpserver, caplog, mock_dns):
781781

782782
bbot_httpserver.expect_request(uri="/").respond_with_data(response_data="<a href='http://www-prod.test.notreal:8888'/><a href='http://www-dev.test.notreal:8888'/>")
783783

@@ -791,9 +791,9 @@ async def test_manager_blacklist(bbot_config, bbot_scanner, bbot_httpserver, cap
791791
whitelist=["127.0.0.0/29", "test.notreal"],
792792
blacklist=["127.0.0.64/29"],
793793
)
794-
scan.helpers.dns.mock_dns({
795-
("www-prod.test.notreal", "A"): "127.0.0.66",
796-
("www-dev.test.notreal", "A"): "127.0.0.22",
794+
mock_dns(scan, {
795+
"www-prod.test.notreal": {"A": ["127.0.0.66"]},
796+
"www-dev.test.notreal": {"A": ["127.0.0.22"]},
797797
})
798798

799799
events = [e async for e in scan.async_start()]

bbot/test/test_step_1/test_modules_basic.py

+32-14
Original file line numberDiff line numberDiff line change
@@ -303,31 +303,51 @@ async def test_modules_basic_perdomainonly(scan, helpers, events, bbot_config, b
303303

304304

305305
@pytest.mark.asyncio
306-
async def test_modules_basic_stats(helpers, events, bbot_config, bbot_scanner, httpx_mock, monkeypatch):
306+
async def test_modules_basic_stats(helpers, events, bbot_config, bbot_scanner, httpx_mock, monkeypatch, mock_dns):
307307
from bbot.modules.base import BaseModule
308308

309309
class dummy(BaseModule):
310310
_name = "dummy"
311311
watched_events = ["*"]
312312

313313
async def handle_event(self, event):
314+
# quick emit events like FINDINGS behave differently than normal ones
315+
# hosts are not speculated from them
314316
await self.emit_event(
315317
{"host": "www.evilcorp.com", "url": "http://www.evilcorp.com", "description": "asdf"}, "FINDING", event
316318
)
319+
await self.emit_event("https://asdf.evilcorp.com", "URL", event, tags=["status-200"])
317320

318321
scan = bbot_scanner(
319322
"evilcorp.com",
323+
modules=["speculate"],
320324
config=bbot_config,
321325
force_start=True,
322326
)
323-
scan.helpers.dns.mock_dns({("evilcorp.com", "A"): "127.0.254.1", ("www.evilcorp.com", "A"): "127.0.254.2"})
327+
mock_dns(
328+
scan,
329+
{
330+
"evilcorp.com": {"A": ["127.0.254.1"]},
331+
"www.evilcorp.com": {"A": ["127.0.254.2"]},
332+
"asdf.evilcorp.com": {"A": ["127.0.254.3"]},
333+
},
334+
)
324335

325336
scan.modules["dummy"] = dummy(scan)
326337
events = [e async for e in scan.async_start()]
327338

328-
assert len(events) == 3
339+
assert len(events) == 6
340+
341+
assert scan.stats.events_emitted_by_type == {
342+
"SCAN": 1,
343+
"DNS_NAME": 3,
344+
"URL": 1,
345+
"FINDING": 1,
346+
"ORG_STUB": 1,
347+
"OPEN_TCP_PORT": 1,
348+
}
329349

330-
assert set(scan.stats.module_stats) == {"dummy", "python", "TARGET"}
350+
assert set(scan.stats.module_stats) == {"host", "speculate", "python", "dummy", "TARGET"}
331351

332352
target_stats = scan.stats.module_stats["TARGET"]
333353
assert target_stats.emitted == {"SCAN": 1, "DNS_NAME": 1}
@@ -338,19 +358,17 @@ async def handle_event(self, event):
338358
assert target_stats.consumed_total == 0
339359

340360
dummy_stats = scan.stats.module_stats["dummy"]
341-
assert dummy_stats.emitted == {"FINDING": 1}
342-
assert dummy_stats.emitted_total == 1
343-
assert dummy_stats.produced == {"FINDING": 1}
344-
assert dummy_stats.produced_total == 1
345-
assert dummy_stats.consumed == {"SCAN": 1, "DNS_NAME": 1}
346-
assert dummy_stats.consumed_total == 2
361+
assert dummy_stats.emitted == {"FINDING": 1, "URL": 1}
362+
assert dummy_stats.emitted_total == 2
363+
assert dummy_stats.produced == {"FINDING": 1, "URL": 1}
364+
assert dummy_stats.produced_total == 2
365+
assert dummy_stats.consumed == {"DNS_NAME": 2, "OPEN_TCP_PORT": 1, "SCAN": 1, "URL": 1}
366+
assert dummy_stats.consumed_total == 5
347367

348368
python_stats = scan.stats.module_stats["python"]
349369
assert python_stats.emitted == {}
350370
assert python_stats.emitted_total == 0
351371
assert python_stats.produced == {}
352372
assert python_stats.produced_total == 0
353-
assert python_stats.consumed == {"SCAN": 1, "FINDING": 1, "DNS_NAME": 1}
354-
assert python_stats.consumed_total == 3
355-
356-
assert scan.stats.events_emitted_by_type == {"SCAN": 1, "FINDING": 1, "DNS_NAME": 1}
373+
assert python_stats.consumed == {"DNS_NAME": 2, "FINDING": 1, "ORG_STUB": 1, "SCAN": 1, "URL": 1}
374+
assert python_stats.consumed_total == 6

bbot/test/test_step_1/test_scan.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ async def test_scan(
99
neograph,
1010
monkeypatch,
1111
bbot_scanner,
12+
mock_dns,
1213
):
1314
scan0 = bbot_scanner(
1415
"1.1.1.1/31",
@@ -55,15 +56,15 @@ async def test_scan(
5556
assert not scan2.in_scope("1.0.0.1")
5657

5758
dns_table = {
58-
("1.1.1.1", "PTR"): "one.one.one.one",
59-
("one.one.one.one", "A"): "1.1.1.1",
59+
"1.1.1.1": {"PTR": ["one.one.one.one"]},
60+
"one.one.one.one": {"A": ["1.1.1.1"]},
6061
}
6162

6263
# make sure DNS resolution works
6364
dns_config = OmegaConf.create({"dns_resolution": True})
6465
dns_config = OmegaConf.merge(bbot_config, dns_config)
6566
scan4 = bbot_scanner("1.1.1.1", config=dns_config)
66-
scan4.helpers.dns.mock_dns(dns_table)
67+
mock_dns(scan4, dns_table)
6768
events = []
6869
async for event in scan4.async_start():
6970
events.append(event)
@@ -74,7 +75,7 @@ async def test_scan(
7475
no_dns_config = OmegaConf.create({"dns_resolution": False})
7576
no_dns_config = OmegaConf.merge(bbot_config, no_dns_config)
7677
scan5 = bbot_scanner("1.1.1.1", config=no_dns_config)
77-
scan5.helpers.dns.mock_dns(dns_table)
78+
mock_dns(scan5, dns_table)
7879
events = []
7980
async for event in scan5.async_start():
8081
events.append(event)

0 commit comments

Comments
 (0)