bbot 2.2.0.5279rc0__py3-none-any.whl → 2.2.0.5309rc0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of bbot might be problematic. Click here for more details.

Files changed (62) hide show
  1. bbot/__init__.py +1 -1
  2. bbot/cli.py +1 -1
  3. bbot/core/engine.py +2 -2
  4. bbot/core/event/base.py +23 -2
  5. bbot/core/helpers/bloom.py +8 -1
  6. bbot/core/helpers/dns/helpers.py +2 -2
  7. bbot/core/helpers/helper.py +4 -3
  8. bbot/core/helpers/misc.py +11 -5
  9. bbot/core/helpers/regexes.py +2 -1
  10. bbot/core/helpers/web/web.py +1 -1
  11. bbot/modules/anubisdb.py +1 -1
  12. bbot/modules/baddns.py +1 -1
  13. bbot/modules/bevigil.py +2 -2
  14. bbot/modules/binaryedge.py +1 -1
  15. bbot/modules/bufferoverrun.py +2 -3
  16. bbot/modules/builtwith.py +2 -2
  17. bbot/modules/c99.py +4 -2
  18. bbot/modules/certspotter.py +4 -2
  19. bbot/modules/chaos.py +4 -2
  20. bbot/modules/columbus.py +1 -1
  21. bbot/modules/crt.py +4 -2
  22. bbot/modules/digitorus.py +1 -1
  23. bbot/modules/dnscaa.py +3 -3
  24. bbot/modules/fullhunt.py +1 -1
  25. bbot/modules/hackertarget.py +4 -2
  26. bbot/modules/internal/excavate.py +2 -3
  27. bbot/modules/internal/speculate.py +1 -1
  28. bbot/modules/leakix.py +4 -2
  29. bbot/modules/myssl.py +1 -1
  30. bbot/modules/otx.py +4 -2
  31. bbot/modules/passivetotal.py +4 -2
  32. bbot/modules/rapiddns.py +2 -7
  33. bbot/modules/securitytrails.py +4 -2
  34. bbot/modules/shodan_dns.py +1 -1
  35. bbot/modules/subdomaincenter.py +1 -1
  36. bbot/modules/templates/subdomain_enum.py +3 -3
  37. bbot/modules/trickest.py +1 -1
  38. bbot/modules/virustotal.py +2 -7
  39. bbot/modules/zoomeye.py +5 -3
  40. bbot/presets/spider.yml +4 -0
  41. bbot/scanner/manager.py +1 -2
  42. bbot/scanner/preset/args.py +3 -3
  43. bbot/scanner/preset/path.py +3 -1
  44. bbot/scanner/preset/preset.py +10 -4
  45. bbot/scanner/scanner.py +7 -2
  46. bbot/scanner/target.py +236 -434
  47. bbot/test/test_step_1/test_bloom_filter.py +2 -0
  48. bbot/test/test_step_1/test_cli.py +7 -0
  49. bbot/test/test_step_1/test_dns.py +2 -1
  50. bbot/test/test_step_1/test_events.py +16 -2
  51. bbot/test/test_step_1/test_helpers.py +17 -0
  52. bbot/test/test_step_1/test_presets.py +50 -36
  53. bbot/test/test_step_1/test_python_api.py +4 -0
  54. bbot/test/test_step_1/test_scan.py +8 -2
  55. bbot/test/test_step_1/test_target.py +227 -129
  56. bbot/test/test_step_2/module_tests/test_module_dastardly.py +1 -1
  57. bbot/test/test_step_2/module_tests/test_module_ffuf_shortnames.py +1 -1
  58. {bbot-2.2.0.5279rc0.dist-info → bbot-2.2.0.5309rc0.dist-info}/METADATA +4 -4
  59. {bbot-2.2.0.5279rc0.dist-info → bbot-2.2.0.5309rc0.dist-info}/RECORD +62 -62
  60. {bbot-2.2.0.5279rc0.dist-info → bbot-2.2.0.5309rc0.dist-info}/LICENSE +0 -0
  61. {bbot-2.2.0.5279rc0.dist-info → bbot-2.2.0.5309rc0.dist-info}/WHEEL +0 -0
  62. {bbot-2.2.0.5279rc0.dist-info → bbot-2.2.0.5309rc0.dist-info}/entry_points.txt +0 -0
bbot/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # version placeholder (replaced by poetry-dynamic-versioning)
2
- __version__ = "v2.2.0.5279rc"
2
+ __version__ = "v2.2.0.5309rc"
3
3
 
4
4
  from .scanner import Scanner, Preset
bbot/cli.py CHANGED
@@ -174,7 +174,7 @@ async def _main():
174
174
  if sys.stdin.isatty():
175
175
 
176
176
  # warn if any targets belong directly to a cloud provider
177
- for event in scan.target.events:
177
+ for event in scan.target.seeds.events:
178
178
  if event.type == "DNS_NAME":
179
179
  cloudcheck_result = scan.helpers.cloudcheck(event.host)
180
180
  if cloudcheck_result:
bbot/core/engine.py CHANGED
@@ -641,7 +641,7 @@ class EngineServer(EngineBase):
641
641
  except BaseException as e:
642
642
  if isinstance(e, (TimeoutError, asyncio.exceptions.TimeoutError)):
643
643
  self.log.warning(f"{self.name}: Timeout after {timeout:,} seconds in finished_tasks({tasks})")
644
- for task in tasks:
644
+ for task in list(tasks):
645
645
  task.cancel()
646
646
  self._await_cancelled_task(task)
647
647
  else:
@@ -683,5 +683,5 @@ class EngineServer(EngineBase):
683
683
  for client_id in list(self.tasks):
684
684
  await self.cancel_task(client_id)
685
685
  for client_id, tasks in self.child_tasks.items():
686
- for task in tasks:
686
+ for task in list(tasks):
687
687
  await self._await_cancelled_task(task)
bbot/core/event/base.py CHANGED
@@ -341,6 +341,21 @@ class BaseEvent:
341
341
  return self.host
342
342
  return self._host_original
343
343
 
344
+ @property
345
+ def host_filterable(self):
346
+ """
347
+ A string version of the event that's used for regex-based blacklisting.
348
+
349
+ For example, the user can specify "REGEX:.*.evilcorp.com" in their blacklist, and this regex
350
+ will be applied against this property.
351
+ """
352
+ parsed_url = getattr(self, "parsed_url", None)
353
+ if parsed_url is not None:
354
+ return parsed_url.geturl()
355
+ if self.host is not None:
356
+ return str(self.host)
357
+ return ""
358
+
344
359
  @property
345
360
  def port(self):
346
361
  self.host
@@ -1114,8 +1129,7 @@ class DnsEvent(BaseEvent):
1114
1129
  class IP_RANGE(DnsEvent):
1115
1130
  def __init__(self, *args, **kwargs):
1116
1131
  super().__init__(*args, **kwargs)
1117
- net = ipaddress.ip_network(self.data, strict=False)
1118
- self.add_tag(f"ipv{net.version}")
1132
+ self.add_tag(f"ipv{self.host.version}")
1119
1133
 
1120
1134
  def sanitize_data(self, data):
1121
1135
  return str(ipaddress.ip_network(str(data), strict=False))
@@ -1689,6 +1703,13 @@ def make_event(
1689
1703
  if event_type == "USERNAME" and validators.soft_validate(data, "email"):
1690
1704
  event_type = "EMAIL_ADDRESS"
1691
1705
  tags.add("affiliate")
1706
+ # Convert single-host IP_RANGE to IP_ADDRESS
1707
+ if event_type == "IP_RANGE":
1708
+ with suppress(Exception):
1709
+ net = ipaddress.ip_network(data, strict=False)
1710
+ if net.prefixlen == net.max_prefixlen:
1711
+ event_type = "IP_ADDRESS"
1712
+ data = net.network_address
1692
1713
 
1693
1714
  event_class = globals().get(event_type, DefaultEvent)
1694
1715
 
@@ -64,8 +64,15 @@ class BloomFilter:
64
64
  hash = (hash * 0x01000193) % 2**32 # 16777619
65
65
  return hash
66
66
 
67
- def __del__(self):
67
+ def close(self):
68
+ """Explicitly close the memory-mapped file."""
68
69
  self.mmap_file.close()
69
70
 
71
+ def __del__(self):
72
+ try:
73
+ self.close()
74
+ except Exception:
75
+ pass
76
+
70
77
  def __contains__(self, item):
71
78
  return self.check(item)
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
 
3
- from bbot.core.helpers.regexes import dns_name_regex
3
+ from bbot.core.helpers.regexes import dns_name_extraction_regex
4
4
  from bbot.core.helpers.misc import clean_dns_record, smart_decode
5
5
 
6
6
  log = logging.getLogger("bbot.core.helpers.dns")
@@ -198,7 +198,7 @@ def extract_targets(record):
198
198
  elif rdtype == "TXT":
199
199
  for s in record.strings:
200
200
  s = smart_decode(s)
201
- for match in dns_name_regex.finditer(s):
201
+ for match in dns_name_extraction_regex.finditer(s):
202
202
  start, end = match.span()
203
203
  host = s[start:end]
204
204
  add_result(rdtype, host)
@@ -12,10 +12,11 @@ from .diff import HttpCompare
12
12
  from .regex import RegexHelper
13
13
  from .wordcloud import WordCloud
14
14
  from .interactsh import Interactsh
15
- from ...scanner.target import Target
16
15
  from .depsinstaller import DepsInstaller
17
16
  from .async_helpers import get_event_loop
18
17
 
18
+ from bbot.scanner.target import BaseTarget
19
+
19
20
  log = logging.getLogger("bbot.core.helpers")
20
21
 
21
22
 
@@ -155,8 +156,8 @@ class ConfigAwareHelper:
155
156
  _filter = lambda x: x.is_dir() and self.regexes.scan_name_regex.match(x.name)
156
157
  self.clean_old(self.scans_dir, keep=self.keep_old_scans, filter=_filter)
157
158
 
158
- def make_target(self, *events, **kwargs):
159
- return Target(*events, **kwargs)
159
+ def make_target(self, *targets, **kwargs):
160
+ return BaseTarget(*targets, scan=self.scan, **kwargs)
160
161
 
161
162
  @property
162
163
  def config(self):
bbot/core/helpers/misc.py CHANGED
@@ -586,17 +586,18 @@ def is_dns_name(d, include_local=True):
586
586
  if include_local:
587
587
  if bbot_regexes.hostname_regex.match(d):
588
588
  return True
589
- if bbot_regexes.dns_name_regex.match(d):
589
+ if bbot_regexes.dns_name_validation_regex.match(d):
590
590
  return True
591
591
  return False
592
592
 
593
593
 
594
- def is_ip(d, version=None):
594
+ def is_ip(d, version=None, include_network=False):
595
595
  """
596
596
  Checks if the given string or object represents a valid IP address.
597
597
 
598
598
  Args:
599
599
  d (str or ipaddress.IPvXAddress): The IP address to check.
600
+ include_network (bool, optional): Whether to include network types (IPv4Network or IPv6Network). Defaults to False.
600
601
  version (int, optional): The IP version to validate (4 or 6). Default is None.
601
602
 
602
603
  Returns:
@@ -612,12 +613,17 @@ def is_ip(d, version=None):
612
613
  >>> is_ip('evilcorp.com')
613
614
  False
614
615
  """
616
+ ip = None
615
617
  try:
616
618
  ip = ipaddress.ip_address(d)
617
- if version is None or ip.version == version:
618
- return True
619
619
  except Exception:
620
- pass
620
+ if include_network:
621
+ try:
622
+ ip = ipaddress.ip_network(d, strict=False)
623
+ except Exception:
624
+ pass
625
+ if ip is not None and (version is None or ip.version == version):
626
+ return True
621
627
  return False
622
628
 
623
629
 
@@ -40,7 +40,8 @@ ip_range_regexes = list(re.compile(r, re.I) for r in _ip_range_regexes)
40
40
 
41
41
  # dns names with periods
42
42
  _dns_name_regex = r"(?:\w(?:[\w-]{0,100}\w)?\.)+(?:[xX][nN]--)?[^\W_]{1,63}\.?"
43
- dns_name_regex = re.compile(_dns_name_regex, re.I)
43
+ dns_name_extraction_regex = re.compile(_dns_name_regex, re.I)
44
+ dns_name_validation_regex = re.compile(r"^" + _dns_name_regex + r"$", re.I)
44
45
 
45
46
  # dns names without periods
46
47
  _hostname_regex = r"(?!\w*\.\w+)\w(?:[\w-]{0,100}\w)?"
@@ -58,7 +58,7 @@ class WebHelper(EngineClient):
58
58
  self.ssl_verify = self.config.get("ssl_verify", False)
59
59
  engine_debug = self.config.get("engine", {}).get("debug", False)
60
60
  super().__init__(
61
- server_kwargs={"config": self.config, "target": self.parent_helper.preset.target.radix_only},
61
+ server_kwargs={"config": self.config, "target": self.parent_helper.preset.target.minimal},
62
62
  debug=engine_debug,
63
63
  )
64
64
 
bbot/modules/anubisdb.py CHANGED
@@ -38,7 +38,7 @@ class anubisdb(subdomain_enum):
38
38
  return True, "DNS name is unresolved"
39
39
  return await super().abort_if(event)
40
40
 
41
- def parse_results(self, r, query):
41
+ async def parse_results(self, r, query):
42
42
  results = set()
43
43
  json = r.json()
44
44
  if json:
bbot/modules/baddns.py CHANGED
@@ -116,7 +116,7 @@ class baddns(BaseModule):
116
116
  context=f'{{module}}\'s "{r_dict["module"]}" module found {{event.type}}: {r_dict["description"]}',
117
117
  )
118
118
  else:
119
- self.warning(f"Got unrecognized confidence level: {r['confidence']}")
119
+ self.warning(f"Got unrecognized confidence level: {r_dict['confidence']}")
120
120
 
121
121
  found_domains = r_dict.get("found_domains", None)
122
122
  if found_domains:
bbot/modules/bevigil.py CHANGED
@@ -60,14 +60,14 @@ class bevigil(subdomain_enum_apikey):
60
60
  url = f"{self.base_url}/{self.helpers.quote(query)}/urls/"
61
61
  return await self.api_request(url)
62
62
 
63
- def parse_subdomains(self, r, query=None):
63
+ async def parse_subdomains(self, r, query=None):
64
64
  results = set()
65
65
  subdomains = r.json().get("subdomains")
66
66
  if subdomains:
67
67
  results.update(subdomains)
68
68
  return results
69
69
 
70
- def parse_urls(self, r, query=None):
70
+ async def parse_urls(self, r, query=None):
71
71
  results = set()
72
72
  urls = r.json().get("urls")
73
73
  if urls:
@@ -37,6 +37,6 @@ class binaryedge(subdomain_enum_apikey):
37
37
  url = f"{self.base_url}/query/domains/subdomain/{self.helpers.quote(query)}"
38
38
  return await self.api_request(url)
39
39
 
40
- def parse_results(self, r, query):
40
+ async def parse_results(self, r, query):
41
41
  j = r.json()
42
42
  return j.get("events", [])
@@ -33,7 +33,7 @@ class BufferOverrun(subdomain_enum_apikey):
33
33
  url = f"{self.commercial_base_url if self.commercial else self.base_url}?q=.{query}"
34
34
  return await self.api_request(url)
35
35
 
36
- def parse_results(self, r, query):
36
+ async def parse_results(self, r, query):
37
37
  j = r.json()
38
38
  subdomains_set = set()
39
39
  if isinstance(j, dict):
@@ -44,5 +44,4 @@ class BufferOverrun(subdomain_enum_apikey):
44
44
  subdomain = parts[4].strip()
45
45
  if subdomain and subdomain.endswith(f".{query}"):
46
46
  subdomains_set.add(subdomain)
47
- for subdomain in subdomains_set:
48
- yield subdomain
47
+ return subdomains_set
bbot/modules/builtwith.py CHANGED
@@ -62,7 +62,7 @@ class builtwith(subdomain_enum_apikey):
62
62
  url = f"{self.base_url}/redirect1/api.json?KEY={{api_key}}&LOOKUP={query}"
63
63
  return await self.api_request(url)
64
64
 
65
- def parse_domains(self, r, query):
65
+ async def parse_domains(self, r, query):
66
66
  """
67
67
  This method returns a set of subdomains.
68
68
  Each subdomain is an "FQDN" that was reported in the "Detailed Technology Profile" page on builtwith.com
@@ -92,7 +92,7 @@ class builtwith(subdomain_enum_apikey):
92
92
  self.verbose(f"No results for {query}: {error}")
93
93
  return results_set
94
94
 
95
- def parse_redirects(self, r, query):
95
+ async def parse_redirects(self, r, query):
96
96
  """
97
97
  This method creates a set.
98
98
  Each entry in the set is either an Inbound or Outbound Redirect reported in the "Redirect Profile" page on builtwith.com
bbot/modules/c99.py CHANGED
@@ -26,7 +26,8 @@ class c99(subdomain_enum_apikey):
26
26
  url = f"{self.base_url}/subdomainfinder?key={{api_key}}&domain={self.helpers.quote(query)}&json"
27
27
  return await self.api_request(url)
28
28
 
29
- def parse_results(self, r, query):
29
+ async def parse_results(self, r, query):
30
+ results = set()
30
31
  j = r.json()
31
32
  if isinstance(j, dict):
32
33
  subdomains = j.get("subdomains", [])
@@ -34,4 +35,5 @@ class c99(subdomain_enum_apikey):
34
35
  for s in subdomains:
35
36
  subdomain = s.get("subdomain", "")
36
37
  if subdomain:
37
- yield subdomain
38
+ results.add(subdomain)
39
+ return results
@@ -17,9 +17,11 @@ class certspotter(subdomain_enum):
17
17
  url = f"{self.base_url}/issuances?domain={self.helpers.quote(query)}&include_subdomains=true&expand=dns_names"
18
18
  return self.api_request(url, timeout=self.http_timeout + 30)
19
19
 
20
- def parse_results(self, r, query):
20
+ async def parse_results(self, r, query):
21
+ results = set()
21
22
  json = r.json()
22
23
  if json:
23
24
  for r in json:
24
25
  for dns_name in r.get("dns_names", []):
25
- yield dns_name.lstrip(".*").rstrip(".")
26
+ results.add(dns_name.lstrip(".*").rstrip("."))
27
+ return results
bbot/modules/chaos.py CHANGED
@@ -26,7 +26,8 @@ class chaos(subdomain_enum_apikey):
26
26
  url = f"{self.base_url}/{domain}/subdomains"
27
27
  return await self.api_request(url)
28
28
 
29
- def parse_results(self, r, query):
29
+ async def parse_results(self, r, query):
30
+ results = set()
30
31
  j = r.json()
31
32
  subdomains_set = set()
32
33
  if isinstance(j, dict):
@@ -39,4 +40,5 @@ class chaos(subdomain_enum_apikey):
39
40
  for s in subdomains_set:
40
41
  full_subdomain = f"{s}.{domain}"
41
42
  if full_subdomain and full_subdomain.endswith(f".{query}"):
42
- yield full_subdomain
43
+ results.add(full_subdomain)
44
+ return results
bbot/modules/columbus.py CHANGED
@@ -17,7 +17,7 @@ class columbus(subdomain_enum):
17
17
  url = f"{self.base_url}/{self.helpers.quote(query)}?days=365"
18
18
  return await self.api_request(url)
19
19
 
20
- def parse_results(self, r, query):
20
+ async def parse_results(self, r, query):
21
21
  results = set()
22
22
  json = r.json()
23
23
  if json and isinstance(json, list):
bbot/modules/crt.py CHANGED
@@ -23,7 +23,8 @@ class crt(subdomain_enum):
23
23
  url = self.helpers.add_get_params(self.base_url, params).geturl()
24
24
  return await self.api_request(url, timeout=self.http_timeout + 30)
25
25
 
26
- def parse_results(self, r, query):
26
+ async def parse_results(self, r, query):
27
+ results = set()
27
28
  j = r.json()
28
29
  for cert_info in j:
29
30
  if not type(cert_info) == dict:
@@ -35,4 +36,5 @@ class crt(subdomain_enum):
35
36
  domain = cert_info.get("name_value")
36
37
  if domain:
37
38
  for d in domain.splitlines():
38
- yield d.lower()
39
+ results.add(d.lower())
40
+ return results
bbot/modules/digitorus.py CHANGED
@@ -19,7 +19,7 @@ class digitorus(subdomain_enum):
19
19
  url = f"{self.base_url}/{self.helpers.quote(query)}"
20
20
  return await self.helpers.request(url)
21
21
 
22
- def parse_results(self, r, query):
22
+ async def parse_results(self, r, query):
23
23
  results = set()
24
24
  content = getattr(r, "text", "")
25
25
  extract_regex = re.compile(r"[\w.-]+\." + query, re.I)
bbot/modules/dnscaa.py CHANGED
@@ -2,7 +2,7 @@
2
2
  #
3
3
  # Checks for and parses CAA DNS TXT records for IODEF reporting destination email addresses and/or URL's.
4
4
  #
5
- # NOTE: when the target domain is initially resolved basic "dns_name_regex" matched targets will be extracted so we do not perform that again here.
5
+ # NOTE: when the target domain is initially resolved basic "dns_name_extraction_regex" matched targets will be extracted so we do not perform that again here.
6
6
  #
7
7
  # Example CAA records,
8
8
  # 0 iodef "mailto:dnsadmin@example.com"
@@ -23,7 +23,7 @@ from bbot.modules.base import BaseModule
23
23
 
24
24
  import re
25
25
 
26
- from bbot.core.helpers.regexes import dns_name_regex, email_regex, url_regexes
26
+ from bbot.core.helpers.regexes import dns_name_extraction_regex, email_regex, url_regexes
27
27
 
28
28
  # Handle '0 iodef "mailto:support@hcaptcha.com"'
29
29
  # Handle '1 iodef "https://some.host.tld/caa;"'
@@ -109,7 +109,7 @@ class dnscaa(BaseModule):
109
109
 
110
110
  elif caa_match.group("property").lower().startswith("issue"):
111
111
  if self._dns_names:
112
- for match in dns_name_regex.finditer(caa_match.group("text")):
112
+ for match in dns_name_extraction_regex.finditer(caa_match.group("text")):
113
113
  start, end = match.span()
114
114
  name = caa_match.group("text")[start:end]
115
115
 
bbot/modules/fullhunt.py CHANGED
@@ -35,5 +35,5 @@ class fullhunt(subdomain_enum_apikey):
35
35
  response = await self.api_request(url)
36
36
  return response
37
37
 
38
- def parse_results(self, r, query):
38
+ async def parse_results(self, r, query):
39
39
  return r.json().get("hosts", [])
@@ -18,12 +18,14 @@ class hackertarget(subdomain_enum):
18
18
  response = await self.api_request(url)
19
19
  return response
20
20
 
21
- def parse_results(self, r, query):
21
+ async def parse_results(self, r, query):
22
+ results = set()
22
23
  for line in r.text.splitlines():
23
24
  host = line.split(",")[0]
24
25
  try:
25
26
  self.helpers.validators.validate_host(host)
26
- yield host
27
+ results.add(host)
27
28
  except ValueError:
28
29
  self.debug(f"Error validating API result: {line}")
29
30
  continue
31
+ return results
@@ -527,9 +527,8 @@ class excavate(BaseInternalModule, BaseInterceptModule):
527
527
  async def process(self, yara_results, event, yara_rule_settings, discovery_context):
528
528
  for identifier in yara_results.keys():
529
529
  for csp_str in yara_results[identifier]:
530
- domains = await self.helpers.re.findall(bbot_regexes.dns_name_regex, csp_str)
531
- unique_domains = set(domains)
532
- for domain in unique_domains:
530
+ domains = await self.excavate.scan.extract_in_scope_hostnames(csp_str)
531
+ for domain in domains:
533
532
  await self.report(domain, event, yara_rule_settings, discovery_context, event_type="DNS_NAME")
534
533
 
535
534
  class EmailExtractor(ExcavateRule):
@@ -65,7 +65,7 @@ class speculate(BaseInternalModule):
65
65
  if not self.portscanner_enabled:
66
66
  self.info(f"No portscanner enabled. Assuming open ports: {', '.join(str(x) for x in self.ports)}")
67
67
 
68
- target_len = len(self.scan.target)
68
+ target_len = len(self.scan.target.seeds)
69
69
  if target_len > self.config.get("max_hosts", 65536):
70
70
  if not self.portscanner_enabled:
71
71
  self.hugewarning(
bbot/modules/leakix.py CHANGED
@@ -35,10 +35,12 @@ class leakix(subdomain_enum_apikey):
35
35
  response = await self.api_request(url)
36
36
  return response
37
37
 
38
- def parse_results(self, r, query=None):
38
+ async def parse_results(self, r, query=None):
39
+ results = set()
39
40
  json = r.json()
40
41
  if json:
41
42
  for entry in json:
42
43
  subdomain = entry.get("subdomain", "")
43
44
  if subdomain:
44
- yield subdomain
45
+ results.add(subdomain)
46
+ return results
bbot/modules/myssl.py CHANGED
@@ -17,7 +17,7 @@ class myssl(subdomain_enum):
17
17
  url = f"{self.base_url}?domain={self.helpers.quote(query)}"
18
18
  return await self.api_request(url)
19
19
 
20
- def parse_results(self, r, query):
20
+ async def parse_results(self, r, query):
21
21
  results = set()
22
22
  json = r.json()
23
23
  if json and isinstance(json, dict):
bbot/modules/otx.py CHANGED
@@ -17,10 +17,12 @@ class otx(subdomain_enum):
17
17
  url = f"{self.base_url}/api/v1/indicators/domain/{self.helpers.quote(query)}/passive_dns"
18
18
  return self.api_request(url)
19
19
 
20
- def parse_results(self, r, query):
20
+ async def parse_results(self, r, query):
21
+ results = set()
21
22
  j = r.json()
22
23
  if isinstance(j, dict):
23
24
  for entry in j.get("passive_dns", []):
24
25
  subdomain = entry.get("hostname", "")
25
26
  if subdomain:
26
- yield subdomain
27
+ results.add(subdomain)
28
+ return results
@@ -39,6 +39,8 @@ class passivetotal(subdomain_enum_apikey):
39
39
  url = f"{self.base_url}/enrichment/subdomains?query={self.helpers.quote(query)}"
40
40
  return await self.api_request(url)
41
41
 
42
- def parse_results(self, r, query):
42
+ async def parse_results(self, r, query):
43
+ results = set()
43
44
  for subdomain in r.json().get("subdomains", []):
44
- yield f"{subdomain}.{query}"
45
+ results.add(f"{subdomain}.{query}")
46
+ return results
bbot/modules/rapiddns.py CHANGED
@@ -18,11 +18,6 @@ class rapiddns(subdomain_enum):
18
18
  response = await self.api_request(url, timeout=self.http_timeout + 10)
19
19
  return response
20
20
 
21
- def parse_results(self, r, query):
22
- results = set()
21
+ async def parse_results(self, r, query):
23
22
  text = getattr(r, "text", "")
24
- for match in self.helpers.regexes.dns_name_regex.findall(text):
25
- match = match.lower()
26
- if match.endswith(query):
27
- results.add(match)
28
- return results
23
+ return await self.scan.extract_in_scope_hostnames(text)
@@ -26,8 +26,10 @@ class securitytrails(subdomain_enum_apikey):
26
26
  response = await self.api_request(url)
27
27
  return response
28
28
 
29
- def parse_results(self, r, query):
29
+ async def parse_results(self, r, query):
30
+ results = set()
30
31
  j = r.json()
31
32
  if isinstance(j, dict):
32
33
  for host in j.get("subdomains", []):
33
- yield f"{host}.{query}"
34
+ results.add(f"{host}.{query}")
35
+ return results
@@ -22,5 +22,5 @@ class shodan_dns(shodan):
22
22
  def make_url(self, query):
23
23
  return f"{self.base_url}/dns/domain/{self.helpers.quote(query)}?key={{api_key}}&page={{page}}"
24
24
 
25
- def parse_results(self, json, query):
25
+ async def parse_results(self, json, query):
26
26
  return [f"{sub}.{query}" for sub in json.get("subdomains", [])]
@@ -33,7 +33,7 @@ class subdomaincenter(subdomain_enum):
33
33
  break
34
34
  return response
35
35
 
36
- def parse_results(self, r, query):
36
+ async def parse_results(self, r, query):
37
37
  results = set()
38
38
  json = r.json()
39
39
  if json and isinstance(json, list):
@@ -106,7 +106,7 @@ class subdomain_enum(BaseModule):
106
106
  break
107
107
  return ".".join([s for s in query.split(".") if s != "_wildcard"])
108
108
 
109
- def parse_results(self, r, query=None):
109
+ async def parse_results(self, r, query=None):
110
110
  json = r.json()
111
111
  if json:
112
112
  for hostname in json:
@@ -123,7 +123,7 @@ class subdomain_enum(BaseModule):
123
123
  self.info(f'Query "{query}" failed (no response)')
124
124
  return []
125
125
  try:
126
- results = list(parse_fn(response, query))
126
+ results = list(await parse_fn(response, query))
127
127
  except Exception as e:
128
128
  if response:
129
129
  self.info(
@@ -144,7 +144,7 @@ class subdomain_enum(BaseModule):
144
144
  agen = self.api_page_iter(url, page_size=self.page_size, **self.api_page_iter_kwargs)
145
145
  try:
146
146
  async for response in agen:
147
- subdomains = self.parse_results(response, query)
147
+ subdomains = await self.parse_results(response, query)
148
148
  self.verbose(f'Got {len(subdomains):,} subdomains for "{query}"')
149
149
  if not subdomains:
150
150
  break
bbot/modules/trickest.py CHANGED
@@ -36,7 +36,7 @@ class Trickest(subdomain_enum_apikey):
36
36
  url += "&limit={page_size}&offset={offset}&select=hostname&orderby=hostname"
37
37
  return url
38
38
 
39
- def parse_results(self, j, query):
39
+ async def parse_results(self, j, query):
40
40
  results = j.get("results", [])
41
41
  subdomains = set()
42
42
  for item in results:
@@ -24,11 +24,6 @@ class virustotal(subdomain_enum_apikey):
24
24
  kwargs["headers"]["x-apikey"] = self.api_key
25
25
  return url, kwargs
26
26
 
27
- def parse_results(self, r, query):
28
- results = set()
27
+ async def parse_results(self, r, query):
29
28
  text = getattr(r, "text", "")
30
- for match in self.helpers.regexes.dns_name_regex.findall(text):
31
- match = match.lower()
32
- if match.endswith(query):
33
- results.add(match)
34
- return results
29
+ return await self.scan.extract_in_scope_hostnames(text)
bbot/modules/zoomeye.py CHANGED
@@ -60,7 +60,7 @@ class zoomeye(subdomain_enum_apikey):
60
60
  agen = self.api_page_iter(url)
61
61
  try:
62
62
  async for j in agen:
63
- r = list(self.parse_results(j))
63
+ r = list(await self.parse_results(j))
64
64
  if r:
65
65
  results.update(set(r))
66
66
  if not r or i >= (self.max_pages - 1):
@@ -70,6 +70,8 @@ class zoomeye(subdomain_enum_apikey):
70
70
  agen.aclose()
71
71
  return results
72
72
 
73
- def parse_results(self, r):
73
+ async def parse_results(self, r):
74
+ results = set()
74
75
  for entry in r.get("list", []):
75
- yield entry["name"]
76
+ results.add(entry["name"])
77
+ return results
bbot/presets/spider.yml CHANGED
@@ -3,6 +3,10 @@ description: Recursive web spider
3
3
  modules:
4
4
  - httpx
5
5
 
6
+ blacklist:
7
+ # Prevent spider from invalidating sessions by logging out
8
+ - "RE:/.*(sign|log)[_-]?out"
9
+
6
10
  config:
7
11
  web:
8
12
  # how many links to follow in a row