bbot 2.0.1.4654rc0__py3-none-any.whl → 2.3.0.5397rc0__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 (270) hide show
  1. bbot/__init__.py +1 -1
  2. bbot/cli.py +3 -7
  3. bbot/core/config/files.py +0 -1
  4. bbot/core/config/logger.py +34 -4
  5. bbot/core/core.py +21 -6
  6. bbot/core/engine.py +9 -8
  7. bbot/core/event/base.py +162 -63
  8. bbot/core/helpers/bloom.py +10 -3
  9. bbot/core/helpers/command.py +9 -8
  10. bbot/core/helpers/depsinstaller/installer.py +89 -32
  11. bbot/core/helpers/depsinstaller/sudo_askpass.py +38 -2
  12. bbot/core/helpers/diff.py +10 -10
  13. bbot/core/helpers/dns/brute.py +18 -14
  14. bbot/core/helpers/dns/dns.py +16 -15
  15. bbot/core/helpers/dns/engine.py +159 -132
  16. bbot/core/helpers/dns/helpers.py +2 -2
  17. bbot/core/helpers/dns/mock.py +26 -8
  18. bbot/core/helpers/files.py +1 -1
  19. bbot/core/helpers/helper.py +7 -4
  20. bbot/core/helpers/interactsh.py +3 -3
  21. bbot/core/helpers/libmagic.py +65 -0
  22. bbot/core/helpers/misc.py +65 -22
  23. bbot/core/helpers/names_generator.py +17 -3
  24. bbot/core/helpers/process.py +0 -20
  25. bbot/core/helpers/regex.py +1 -1
  26. bbot/core/helpers/regexes.py +12 -6
  27. bbot/core/helpers/validators.py +1 -2
  28. bbot/core/helpers/web/client.py +1 -1
  29. bbot/core/helpers/web/engine.py +18 -13
  30. bbot/core/helpers/web/web.py +25 -116
  31. bbot/core/helpers/wordcloud.py +5 -5
  32. bbot/core/modules.py +36 -27
  33. bbot/core/multiprocess.py +58 -0
  34. bbot/core/shared_deps.py +46 -3
  35. bbot/db/sql/models.py +147 -0
  36. bbot/defaults.yml +15 -10
  37. bbot/errors.py +0 -8
  38. bbot/modules/anubisdb.py +2 -2
  39. bbot/modules/apkpure.py +63 -0
  40. bbot/modules/azure_tenant.py +2 -2
  41. bbot/modules/baddns.py +35 -19
  42. bbot/modules/baddns_direct.py +92 -0
  43. bbot/modules/baddns_zone.py +3 -8
  44. bbot/modules/badsecrets.py +4 -3
  45. bbot/modules/base.py +195 -51
  46. bbot/modules/bevigil.py +7 -7
  47. bbot/modules/binaryedge.py +7 -4
  48. bbot/modules/bufferoverrun.py +47 -0
  49. bbot/modules/builtwith.py +6 -10
  50. bbot/modules/bypass403.py +5 -5
  51. bbot/modules/c99.py +10 -7
  52. bbot/modules/censys.py +9 -13
  53. bbot/modules/certspotter.py +5 -3
  54. bbot/modules/chaos.py +9 -7
  55. bbot/modules/code_repository.py +1 -0
  56. bbot/modules/columbus.py +3 -3
  57. bbot/modules/crt.py +5 -3
  58. bbot/modules/deadly/dastardly.py +1 -1
  59. bbot/modules/deadly/ffuf.py +9 -9
  60. bbot/modules/deadly/nuclei.py +3 -3
  61. bbot/modules/deadly/vhost.py +4 -3
  62. bbot/modules/dehashed.py +1 -1
  63. bbot/modules/digitorus.py +1 -1
  64. bbot/modules/dnsbimi.py +145 -0
  65. bbot/modules/dnscaa.py +3 -3
  66. bbot/modules/dnsdumpster.py +4 -4
  67. bbot/modules/dnstlsrpt.py +144 -0
  68. bbot/modules/docker_pull.py +7 -5
  69. bbot/modules/dockerhub.py +2 -2
  70. bbot/modules/dotnetnuke.py +18 -19
  71. bbot/modules/emailformat.py +1 -1
  72. bbot/modules/extractous.py +122 -0
  73. bbot/modules/filedownload.py +9 -7
  74. bbot/modules/fullhunt.py +7 -4
  75. bbot/modules/generic_ssrf.py +5 -5
  76. bbot/modules/github_codesearch.py +3 -2
  77. bbot/modules/github_org.py +4 -4
  78. bbot/modules/github_workflows.py +4 -4
  79. bbot/modules/gitlab.py +2 -5
  80. bbot/modules/google_playstore.py +93 -0
  81. bbot/modules/gowitness.py +48 -50
  82. bbot/modules/hackertarget.py +5 -3
  83. bbot/modules/host_header.py +5 -5
  84. bbot/modules/httpx.py +1 -4
  85. bbot/modules/hunterio.py +3 -9
  86. bbot/modules/iis_shortnames.py +19 -30
  87. bbot/modules/internal/cloudcheck.py +27 -12
  88. bbot/modules/internal/dnsresolve.py +250 -276
  89. bbot/modules/internal/excavate.py +100 -64
  90. bbot/modules/internal/speculate.py +42 -33
  91. bbot/modules/internetdb.py +4 -2
  92. bbot/modules/ip2location.py +3 -5
  93. bbot/modules/ipneighbor.py +1 -1
  94. bbot/modules/ipstack.py +3 -8
  95. bbot/modules/jadx.py +87 -0
  96. bbot/modules/leakix.py +11 -10
  97. bbot/modules/myssl.py +2 -2
  98. bbot/modules/newsletters.py +2 -2
  99. bbot/modules/otx.py +5 -3
  100. bbot/modules/output/asset_inventory.py +7 -7
  101. bbot/modules/output/base.py +1 -1
  102. bbot/modules/output/csv.py +1 -2
  103. bbot/modules/output/http.py +20 -14
  104. bbot/modules/output/mysql.py +51 -0
  105. bbot/modules/output/neo4j.py +7 -2
  106. bbot/modules/output/postgres.py +49 -0
  107. bbot/modules/output/slack.py +0 -1
  108. bbot/modules/output/sqlite.py +29 -0
  109. bbot/modules/output/stdout.py +2 -2
  110. bbot/modules/output/teams.py +107 -6
  111. bbot/modules/paramminer_headers.py +5 -8
  112. bbot/modules/passivetotal.py +13 -13
  113. bbot/modules/portscan.py +32 -6
  114. bbot/modules/postman.py +50 -126
  115. bbot/modules/postman_download.py +220 -0
  116. bbot/modules/rapiddns.py +3 -8
  117. bbot/modules/report/asn.py +11 -11
  118. bbot/modules/robots.py +3 -3
  119. bbot/modules/securitytrails.py +7 -10
  120. bbot/modules/securitytxt.py +128 -0
  121. bbot/modules/shodan_dns.py +7 -9
  122. bbot/modules/sitedossier.py +1 -1
  123. bbot/modules/skymem.py +2 -2
  124. bbot/modules/social.py +2 -1
  125. bbot/modules/subdomaincenter.py +1 -1
  126. bbot/modules/subdomainradar.py +160 -0
  127. bbot/modules/telerik.py +8 -8
  128. bbot/modules/templates/bucket.py +1 -1
  129. bbot/modules/templates/github.py +22 -14
  130. bbot/modules/templates/postman.py +21 -0
  131. bbot/modules/templates/shodan.py +14 -13
  132. bbot/modules/templates/sql.py +95 -0
  133. bbot/modules/templates/subdomain_enum.py +53 -17
  134. bbot/modules/templates/webhook.py +2 -4
  135. bbot/modules/trickest.py +8 -37
  136. bbot/modules/trufflehog.py +18 -3
  137. bbot/modules/url_manipulation.py +3 -3
  138. bbot/modules/urlscan.py +1 -1
  139. bbot/modules/viewdns.py +1 -1
  140. bbot/modules/virustotal.py +8 -30
  141. bbot/modules/wafw00f.py +1 -1
  142. bbot/modules/wayback.py +1 -1
  143. bbot/modules/wpscan.py +17 -11
  144. bbot/modules/zoomeye.py +11 -6
  145. bbot/presets/baddns-thorough.yml +12 -0
  146. bbot/presets/fast.yml +16 -0
  147. bbot/presets/kitchen-sink.yml +1 -0
  148. bbot/presets/spider.yml +4 -0
  149. bbot/presets/subdomain-enum.yml +7 -7
  150. bbot/scanner/manager.py +5 -16
  151. bbot/scanner/preset/args.py +44 -26
  152. bbot/scanner/preset/environ.py +7 -2
  153. bbot/scanner/preset/path.py +7 -4
  154. bbot/scanner/preset/preset.py +36 -23
  155. bbot/scanner/scanner.py +176 -63
  156. bbot/scanner/target.py +236 -434
  157. bbot/scripts/docs.py +1 -1
  158. bbot/test/bbot_fixtures.py +22 -3
  159. bbot/test/conftest.py +132 -100
  160. bbot/test/fastapi_test.py +17 -0
  161. bbot/test/owasp_mastg.apk +0 -0
  162. bbot/test/run_tests.sh +4 -4
  163. bbot/test/test.conf +2 -0
  164. bbot/test/test_step_1/test_bbot_fastapi.py +82 -0
  165. bbot/test/test_step_1/test_bloom_filter.py +2 -0
  166. bbot/test/test_step_1/test_cli.py +138 -64
  167. bbot/test/test_step_1/test_dns.py +392 -70
  168. bbot/test/test_step_1/test_engine.py +17 -17
  169. bbot/test/test_step_1/test_events.py +203 -37
  170. bbot/test/test_step_1/test_helpers.py +64 -28
  171. bbot/test/test_step_1/test_manager_deduplication.py +1 -1
  172. bbot/test/test_step_1/test_manager_scope_accuracy.py +336 -338
  173. bbot/test/test_step_1/test_modules_basic.py +69 -71
  174. bbot/test/test_step_1/test_presets.py +184 -96
  175. bbot/test/test_step_1/test_python_api.py +7 -2
  176. bbot/test/test_step_1/test_regexes.py +35 -5
  177. bbot/test/test_step_1/test_scan.py +39 -5
  178. bbot/test/test_step_1/test_scope.py +5 -4
  179. bbot/test/test_step_1/test_target.py +243 -145
  180. bbot/test/test_step_1/test_web.py +48 -10
  181. bbot/test/test_step_2/module_tests/base.py +17 -20
  182. bbot/test/test_step_2/module_tests/test_module_anubisdb.py +1 -1
  183. bbot/test/test_step_2/module_tests/test_module_apkpure.py +71 -0
  184. bbot/test/test_step_2/module_tests/test_module_asset_inventory.py +0 -1
  185. bbot/test/test_step_2/module_tests/test_module_azure_realm.py +1 -1
  186. bbot/test/test_step_2/module_tests/test_module_baddns.py +6 -6
  187. bbot/test/test_step_2/module_tests/test_module_baddns_direct.py +62 -0
  188. bbot/test/test_step_2/module_tests/test_module_bevigil.py +29 -2
  189. bbot/test/test_step_2/module_tests/test_module_binaryedge.py +4 -2
  190. bbot/test/test_step_2/module_tests/test_module_bucket_amazon.py +2 -2
  191. bbot/test/test_step_2/module_tests/test_module_bucket_azure.py +1 -1
  192. bbot/test/test_step_2/module_tests/test_module_bufferoverrun.py +35 -0
  193. bbot/test/test_step_2/module_tests/test_module_builtwith.py +2 -2
  194. bbot/test/test_step_2/module_tests/test_module_bypass403.py +1 -1
  195. bbot/test/test_step_2/module_tests/test_module_c99.py +126 -0
  196. bbot/test/test_step_2/module_tests/test_module_censys.py +4 -1
  197. bbot/test/test_step_2/module_tests/test_module_cloudcheck.py +4 -0
  198. bbot/test/test_step_2/module_tests/test_module_code_repository.py +11 -1
  199. bbot/test/test_step_2/module_tests/test_module_columbus.py +1 -1
  200. bbot/test/test_step_2/module_tests/test_module_credshed.py +3 -3
  201. bbot/test/test_step_2/module_tests/test_module_dastardly.py +2 -1
  202. bbot/test/test_step_2/module_tests/test_module_dehashed.py +2 -2
  203. bbot/test/test_step_2/module_tests/test_module_digitorus.py +1 -1
  204. bbot/test/test_step_2/module_tests/test_module_discord.py +1 -1
  205. bbot/test/test_step_2/module_tests/test_module_dnsbimi.py +103 -0
  206. bbot/test/test_step_2/module_tests/test_module_dnsbrute.py +9 -10
  207. bbot/test/test_step_2/module_tests/test_module_dnsbrute_mutations.py +1 -2
  208. bbot/test/test_step_2/module_tests/test_module_dnscommonsrv.py +1 -2
  209. bbot/test/test_step_2/module_tests/test_module_dnsdumpster.py +4 -4
  210. bbot/test/test_step_2/module_tests/test_module_dnstlsrpt.py +64 -0
  211. bbot/test/test_step_2/module_tests/test_module_dotnetnuke.py +0 -8
  212. bbot/test/test_step_2/module_tests/test_module_excavate.py +17 -37
  213. bbot/test/test_step_2/module_tests/test_module_extractous.py +54 -0
  214. bbot/test/test_step_2/module_tests/test_module_ffuf_shortnames.py +1 -1
  215. bbot/test/test_step_2/module_tests/test_module_filedownload.py +14 -14
  216. bbot/test/test_step_2/module_tests/test_module_git_clone.py +2 -2
  217. bbot/test/test_step_2/module_tests/test_module_github_org.py +19 -8
  218. bbot/test/test_step_2/module_tests/test_module_github_workflows.py +1 -1
  219. bbot/test/test_step_2/module_tests/test_module_gitlab.py +9 -4
  220. bbot/test/test_step_2/module_tests/test_module_google_playstore.py +83 -0
  221. bbot/test/test_step_2/module_tests/test_module_gowitness.py +4 -4
  222. bbot/test/test_step_2/module_tests/test_module_host_header.py +1 -1
  223. bbot/test/test_step_2/module_tests/test_module_http.py +4 -4
  224. bbot/test/test_step_2/module_tests/test_module_httpx.py +10 -8
  225. bbot/test/test_step_2/module_tests/test_module_hunterio.py +68 -4
  226. bbot/test/test_step_2/module_tests/test_module_jadx.py +55 -0
  227. bbot/test/test_step_2/module_tests/test_module_json.py +24 -11
  228. bbot/test/test_step_2/module_tests/test_module_leakix.py +7 -3
  229. bbot/test/test_step_2/module_tests/test_module_mysql.py +76 -0
  230. bbot/test/test_step_2/module_tests/test_module_myssl.py +1 -1
  231. bbot/test/test_step_2/module_tests/test_module_neo4j.py +1 -1
  232. bbot/test/test_step_2/module_tests/test_module_newsletters.py +6 -6
  233. bbot/test/test_step_2/module_tests/test_module_ntlm.py +7 -7
  234. bbot/test/test_step_2/module_tests/test_module_oauth.py +1 -1
  235. bbot/test/test_step_2/module_tests/test_module_otx.py +1 -1
  236. bbot/test/test_step_2/module_tests/test_module_paramminer_cookies.py +1 -2
  237. bbot/test/test_step_2/module_tests/test_module_paramminer_getparams.py +0 -6
  238. bbot/test/test_step_2/module_tests/test_module_paramminer_headers.py +2 -9
  239. bbot/test/test_step_2/module_tests/test_module_passivetotal.py +3 -1
  240. bbot/test/test_step_2/module_tests/test_module_portscan.py +9 -8
  241. bbot/test/test_step_2/module_tests/test_module_postgres.py +74 -0
  242. bbot/test/test_step_2/module_tests/test_module_postman.py +84 -253
  243. bbot/test/test_step_2/module_tests/test_module_postman_download.py +439 -0
  244. bbot/test/test_step_2/module_tests/test_module_rapiddns.py +93 -1
  245. bbot/test/test_step_2/module_tests/test_module_securitytxt.py +50 -0
  246. bbot/test/test_step_2/module_tests/test_module_shodan_dns.py +20 -1
  247. bbot/test/test_step_2/module_tests/test_module_sitedossier.py +2 -2
  248. bbot/test/test_step_2/module_tests/test_module_smuggler.py +1 -1
  249. bbot/test/test_step_2/module_tests/test_module_social.py +11 -1
  250. bbot/test/test_step_2/module_tests/test_module_speculate.py +2 -6
  251. bbot/test/test_step_2/module_tests/test_module_splunk.py +4 -4
  252. bbot/test/test_step_2/module_tests/test_module_sqlite.py +18 -0
  253. bbot/test/test_step_2/module_tests/test_module_sslcert.py +1 -1
  254. bbot/test/test_step_2/module_tests/test_module_stdout.py +5 -3
  255. bbot/test/test_step_2/module_tests/test_module_subdomaincenter.py +1 -1
  256. bbot/test/test_step_2/module_tests/test_module_subdomainradar.py +208 -0
  257. bbot/test/test_step_2/module_tests/test_module_subdomains.py +1 -1
  258. bbot/test/test_step_2/module_tests/test_module_teams.py +8 -6
  259. bbot/test/test_step_2/module_tests/test_module_telerik.py +1 -1
  260. bbot/test/test_step_2/module_tests/test_module_trufflehog.py +317 -11
  261. bbot/test/test_step_2/module_tests/test_module_wayback.py +1 -1
  262. bbot/test/test_step_2/template_tests/test_template_subdomain_enum.py +135 -0
  263. {bbot-2.0.1.4654rc0.dist-info → bbot-2.3.0.5397rc0.dist-info}/METADATA +48 -18
  264. bbot-2.3.0.5397rc0.dist-info/RECORD +421 -0
  265. {bbot-2.0.1.4654rc0.dist-info → bbot-2.3.0.5397rc0.dist-info}/WHEEL +1 -1
  266. bbot/modules/unstructured.py +0 -163
  267. bbot/test/test_step_2/module_tests/test_module_unstructured.py +0 -102
  268. bbot-2.0.1.4654rc0.dist-info/RECORD +0 -385
  269. {bbot-2.0.1.4654rc0.dist-info → bbot-2.3.0.5397rc0.dist-info}/LICENSE +0 -0
  270. {bbot-2.0.1.4654rc0.dist-info → bbot-2.3.0.5397rc0.dist-info}/entry_points.txt +0 -0
bbot/modules/base.py CHANGED
@@ -63,7 +63,7 @@ class BaseModule:
63
63
 
64
64
  batch_wait (int): Seconds to wait before force-submitting a batch. Default is 10.
65
65
 
66
- failed_request_abort_threshold (int): Threshold for setting error state after failed HTTP requests (only takes effect when `request_with_fail_count()` is used. Default is 5.
66
+ api_failure_abort_threshold (int): Threshold for setting error state after failed HTTP requests (only takes effect when `api_request()` is used. Default is 5.
67
67
 
68
68
  _preserve_graph (bool): When set to True, accept events that may be duplicates but are necessary for construction of complete graph. Typically only enabled for output modules that need to maintain full chains of events, e.g. `neo4j` and `json`. Default is False.
69
69
 
@@ -103,7 +103,13 @@ class BaseModule:
103
103
  _module_threads = 1
104
104
  _batch_size = 1
105
105
  batch_wait = 10
106
- failed_request_abort_threshold = 5
106
+
107
+ # API retries, etc.
108
+ _api_retries = 2
109
+ # disable the module after this many failed attempts in a row
110
+ _api_failure_abort_threshold = 3
111
+ # sleep for this many seconds after being rate limited
112
+ _429_sleep_interval = 30
107
113
 
108
114
  default_discovery_context = "{module} discovered {event.type}: {event.data}"
109
115
 
@@ -148,8 +154,10 @@ class BaseModule:
148
154
  # string constant
149
155
  self._custom_filter_criteria_msg = "it did not meet custom filter criteria"
150
156
 
151
- # track number of failures (for .request_with_fail_count())
152
- self._request_failures = 0
157
+ self._api_keys = []
158
+
159
+ # track number of failures (for .api_request())
160
+ self._api_request_failures = 0
153
161
 
154
162
  self._tasks = []
155
163
  self._event_received = asyncio.Condition()
@@ -303,32 +311,76 @@ class BaseModule:
303
311
  if self.auth_secret:
304
312
  try:
305
313
  await self.ping()
306
- self.hugesuccess(f"API is ready")
307
- return True
314
+ self.hugesuccess("API is ready")
315
+ return True, ""
308
316
  except Exception as e:
317
+ self.trace(traceback.format_exc())
309
318
  return None, f"Error with API ({str(e).strip()})"
310
319
  else:
311
320
  return None, "No API key set"
312
321
 
313
- async def ping(self):
322
+ @property
323
+ def api_key(self):
324
+ if self._api_keys:
325
+ return self._api_keys[0]
326
+
327
+ @api_key.setter
328
+ def api_key(self, api_keys):
329
+ if isinstance(api_keys, str):
330
+ api_keys = [api_keys]
331
+ self._api_keys = list(api_keys)
332
+
333
+ def cycle_api_key(self):
334
+ if len(self._api_keys) > 1:
335
+ self.verbose("Cycling API key")
336
+ self._api_keys.insert(0, self._api_keys.pop())
337
+ else:
338
+ self.debug("No extra API keys to cycle")
339
+
340
+ @property
341
+ def api_retries(self):
342
+ return max(self._api_retries + 1, len(self._api_keys))
343
+
344
+ @property
345
+ def api_failure_abort_threshold(self):
346
+ return (self.api_retries * self._api_failure_abort_threshold) + 1
347
+
348
+ async def ping(self, url=None):
314
349
  """Asynchronously checks the health of the configured API.
315
350
 
316
- This method is used in conjunction with require_api_key() to verify that the API is not just configured, but also responsive. This method should include an assert statement to validate the API's health, typically by making a test request to a known endpoint.
351
+ This method is used in conjunction with require_api_key() to verify that the API is not just configured, but also responsive. It makes a test request to a known endpoint to validate the API's health.
317
352
 
318
- Example Usage:
319
- In your implementation, if the API has a "/ping" endpoint:
320
- async def ping(self):
321
- r = await self.request_with_fail_count(f"{self.base_url}/ping")
322
- resp_content = getattr(r, "text", "")
323
- assert getattr(r, "status_code", 0) == 200, resp_content
353
+ The method uses the `ping_url` attribute if defined, or falls back to a provided URL. If neither is available, no request is made.
354
+
355
+ Args:
356
+ url (str, optional): A specific URL to use for the ping request. If not provided, the method will use the `ping_url` attribute.
324
357
 
325
358
  Returns:
326
359
  None
327
360
 
328
361
  Raises:
329
- AssertionError: If the API does not respond as expected.
330
- """
331
- return
362
+ ValueError: If the API response is not successful (status code != 200).
363
+
364
+ Example Usage:
365
+ To use this method, simply define the `ping_url` attribute in your module:
366
+
367
+ class MyModule(BaseModule):
368
+ ping_url = "https://api.example.com/ping"
369
+
370
+ Alternatively, you can override this method for more complex health checks:
371
+
372
+ async def ping(self):
373
+ r = await self.api_request(f"{self.base_url}/complex-health-check")
374
+ if r.status_code != 200 or r.json().get('status') != 'healthy':
375
+ raise ValueError(f"API unhealthy: {r.text}")
376
+ """
377
+ if url is None:
378
+ url = getattr(self, "ping_url", "")
379
+ if url:
380
+ r = await self.api_request(url)
381
+ if getattr(r, "status_code", 0) != 200:
382
+ response_text = getattr(r, "text", "no response from server")
383
+ raise ValueError(response_text)
332
384
 
333
385
  @property
334
386
  def batch_size(self):
@@ -617,7 +669,7 @@ class BaseModule:
617
669
  if self.incoming_event_queue is not False:
618
670
  event = await self.incoming_event_queue.get()
619
671
  else:
620
- self.debug(f"Event queue is in bad state")
672
+ self.debug("Event queue is in bad state")
621
673
  break
622
674
  except asyncio.queues.QueueEmpty:
623
675
  continue
@@ -648,7 +700,7 @@ class BaseModule:
648
700
  else:
649
701
  self.error(f"Critical failure in module {self.name}: {e}")
650
702
  self.error(traceback.format_exc())
651
- self.log.trace(f"Worker stopped")
703
+ self.log.trace("Worker stopped")
652
704
 
653
705
  @property
654
706
  def max_scope_distance(self):
@@ -691,7 +743,7 @@ class BaseModule:
691
743
  if event.type in ("FINISHED",):
692
744
  return True, "its type is FINISHED"
693
745
  if self.errored:
694
- return False, f"module is in error state"
746
+ return False, "module is in error state"
695
747
  # exclude non-watched types
696
748
  if not any(t in self.get_watched_events() for t in ("*", event.type)):
697
749
  return False, "its type is not in watched_events"
@@ -718,7 +770,7 @@ class BaseModule:
718
770
  # check duplicates
719
771
  is_incoming_duplicate, reason = self.is_incoming_duplicate(event, add=True)
720
772
  if is_incoming_duplicate and not self.accept_dupes:
721
- return False, f"module has already seen it" + (f" ({reason})" if reason else "")
773
+ return False, "module has already seen it" + (f" ({reason})" if reason else "")
722
774
 
723
775
  return acceptable, reason
724
776
 
@@ -811,7 +863,7 @@ class BaseModule:
811
863
  """
812
864
  async with self._task_counter.count("queue_event()", _log=False):
813
865
  if self.incoming_event_queue is False:
814
- self.debug(f"Not in an acceptable state to queue incoming event")
866
+ self.debug("Not in an acceptable state to queue incoming event")
815
867
  return
816
868
  acceptable, reason = self._event_precheck(event)
817
869
  if not acceptable:
@@ -827,7 +879,7 @@ class BaseModule:
827
879
  if event.type != "FINISHED":
828
880
  self.scan._new_activity = True
829
881
  except AttributeError:
830
- self.debug(f"Not in an acceptable state to queue incoming event")
882
+ self.debug("Not in an acceptable state to queue incoming event")
831
883
 
832
884
  async def queue_outgoing_event(self, event, **kwargs):
833
885
  """
@@ -852,7 +904,7 @@ class BaseModule:
852
904
  try:
853
905
  await self.outgoing_event_queue.put((event, kwargs))
854
906
  except AttributeError:
855
- self.debug(f"Not in an acceptable state to queue outgoing event")
907
+ self.debug("Not in an acceptable state to queue outgoing event")
856
908
 
857
909
  def set_error_state(self, message=None, clear_outgoing_queue=False, critical=False):
858
910
  """
@@ -887,7 +939,7 @@ class BaseModule:
887
939
  self.errored = True
888
940
  # clear incoming queue
889
941
  if self.incoming_event_queue is not False:
890
- self.debug(f"Emptying event_queue")
942
+ self.debug("Emptying event_queue")
891
943
  with suppress(asyncio.queues.QueueEmpty):
892
944
  while 1:
893
945
  self.incoming_event_queue.get_nowait()
@@ -932,8 +984,11 @@ class BaseModule:
932
984
  def _outgoing_dedup_hash(self, event):
933
985
  """
934
986
  Determines the criteria for what is considered to be a duplicate event if `suppress_dupes` is True.
987
+
988
+ We take into account the `internal` attribute we don't want an internal event (which isn't distributed to output modules)
989
+ to inadvertently suppress a non-internal event.
935
990
  """
936
- return hash((event, self.name))
991
+ return hash((event, self.name, event.internal, event.always_emit))
937
992
 
938
993
  def get_per_host_hash(self, event):
939
994
  """
@@ -1065,32 +1120,122 @@ class BaseModule:
1065
1120
  async for line in self.helpers.run_live(*args, **kwargs):
1066
1121
  yield line
1067
1122
 
1068
- async def request_with_fail_count(self, *args, **kwargs):
1069
- """Asynchronously perform an HTTP request while keeping track of consecutive failures.
1123
+ def prepare_api_request(self, url, kwargs):
1124
+ """
1125
+ Prepare an API request by adding the necessary authentication - header, bearer token, etc.
1126
+ """
1127
+ if self.api_key:
1128
+ url = url.format(api_key=self.api_key)
1129
+ if "headers" not in kwargs:
1130
+ kwargs["headers"] = {}
1131
+ kwargs["headers"]["Authorization"] = f"Bearer {self.api_key}"
1132
+ return url, kwargs
1070
1133
 
1071
- This function wraps the `self.helpers.request` method, incrementing a failure counter if
1072
- the request returns None. When the failure counter exceeds `self.failed_request_abort_threshold`,
1073
- the module is set to an error state.
1134
+ async def api_request(self, *args, **kwargs):
1135
+ """
1136
+ Makes an HTTP request while automatically:
1137
+ - avoiding rate limits (sleep/retry)
1138
+ - cycling API keys
1139
+ - cancelling after too many failed attempts
1140
+ """
1141
+ url = args[0] if args else kwargs.pop("url", "")
1074
1142
 
1075
- Args:
1076
- *args: Positional arguments to pass to `self.helpers.request`.
1077
- **kwargs: Keyword arguments to pass to `self.helpers.request`.
1143
+ # loop until we have a successful request
1144
+ for _ in range(self.api_retries):
1145
+ if "headers" not in kwargs:
1146
+ kwargs["headers"] = {}
1147
+ new_url, kwargs = self.prepare_api_request(url, kwargs)
1148
+ kwargs["url"] = new_url
1078
1149
 
1079
- Returns:
1080
- Any: The response object or None if the request failed.
1150
+ r = await self.helpers.request(**kwargs)
1151
+ success = False if r is None else r.is_success
1152
+
1153
+ if success:
1154
+ self._api_request_failures = 0
1155
+ else:
1156
+ status_code = getattr(r, "status_code", 0)
1157
+ response_text = getattr(r, "text", "")
1158
+ self.trace(f"API response to {url} failed with status code {status_code}: {response_text}")
1159
+ self._api_request_failures += 1
1160
+ if self._api_request_failures >= self.api_failure_abort_threshold:
1161
+ self.set_error_state(
1162
+ f"Setting error state due to {self._api_request_failures:,} failed HTTP requests"
1163
+ )
1164
+ else:
1165
+ # sleep for a bit if we're being rate limited
1166
+ if status_code == 429:
1167
+ self.verbose(
1168
+ f"Sleeping for {self._429_sleep_interval:,} seconds due to rate limit (HTTP status: 429)"
1169
+ )
1170
+ await asyncio.sleep(self._429_sleep_interval)
1171
+ elif self._api_keys:
1172
+ # if request failed, cycle API keys and try again
1173
+ self.cycle_api_key()
1174
+ continue
1175
+ break
1081
1176
 
1082
- Raises:
1083
- None: Sets the module to an error state when the failure threshold is reached.
1084
- """
1085
- r = await self.helpers.request(*args, **kwargs)
1086
- if r is None:
1087
- self._request_failures += 1
1088
- else:
1089
- self._request_failures = 0
1090
- if self._request_failures >= self.failed_request_abort_threshold:
1091
- self.set_error_state(f"Setting error state due to {self._request_failures:,} failed HTTP requests")
1092
1177
  return r
1093
1178
 
1179
+ async def api_page_iter(self, url, page_size=100, json=True, next_key=None, **requests_kwargs):
1180
+ """
1181
+ An asynchronous generator function for iterating through paginated API data.
1182
+
1183
+ This function continuously makes requests to a specified API URL, incrementing the page number
1184
+ or applying a custom pagination function, and yields the received data one page at a time.
1185
+ It is well-suited for APIs that provide paginated results.
1186
+
1187
+ Args:
1188
+ url (str): The initial API URL. Can contain placeholders for 'page', 'page_size', and 'offset'.
1189
+ page_size (int, optional): The number of items per page. Defaults to 100.
1190
+ json (bool, optional): If True, attempts to deserialize the response content to a JSON object. Defaults to True.
1191
+ next_key (callable, optional): A function that takes the last page's data and returns the URL for the next page. Defaults to None.
1192
+ **requests_kwargs: Arbitrary keyword arguments that will be forwarded to the HTTP request function.
1193
+
1194
+ Yields:
1195
+ dict or httpx.Response: If 'json' is True, yields a dictionary containing the parsed JSON data. Otherwise, yields the raw HTTP response.
1196
+
1197
+ Note:
1198
+ The loop will continue indefinitely unless manually stopped. Make sure to break out of the loop once the last page has been received.
1199
+
1200
+ Examples:
1201
+ >>> agen = api_page_iter('https://api.example.com/data?page={page}&page_size={page_size}')
1202
+ >>> try:
1203
+ >>> async for page in agen:
1204
+ >>> subdomains = page["subdomains"]
1205
+ >>> self.hugesuccess(subdomains)
1206
+ >>> if not subdomains:
1207
+ >>> break
1208
+ >>> finally:
1209
+ >>> agen.aclose()
1210
+ """
1211
+ page = 1
1212
+ offset = 0
1213
+ result = None
1214
+ while 1:
1215
+ if result and callable(next_key):
1216
+ try:
1217
+ new_url = next_key(result)
1218
+ except Exception as e:
1219
+ self.debug(f"Failed to extract next page of results from {url}: {e}")
1220
+ self.debug(traceback.format_exc())
1221
+ else:
1222
+ new_url = self.helpers.safe_format(url, page=page, page_size=page_size, offset=offset)
1223
+ result = await self.api_request(new_url, **requests_kwargs)
1224
+ if result is None:
1225
+ self.verbose(f"api_page_iter() got no response for {url}")
1226
+ break
1227
+ try:
1228
+ if json:
1229
+ result = result.json()
1230
+ yield result
1231
+ except Exception:
1232
+ self.warning(f'Error in api_page_iter() for url: "{new_url}"')
1233
+ self.trace(traceback.format_exc())
1234
+ break
1235
+ finally:
1236
+ offset += page_size
1237
+ page += 1
1238
+
1094
1239
  @property
1095
1240
  def preset(self):
1096
1241
  return self.scan.preset
@@ -1417,7 +1562,7 @@ class BaseModule:
1417
1562
  self.trace()
1418
1563
 
1419
1564
 
1420
- class InterceptModule(BaseModule):
1565
+ class BaseInterceptModule(BaseModule):
1421
1566
  """
1422
1567
  An Intercept Module is a special type of high-priority module that gets early access to events.
1423
1568
 
@@ -1429,7 +1574,6 @@ class InterceptModule(BaseModule):
1429
1574
  """
1430
1575
 
1431
1576
  accept_dupes = True
1432
- suppress_dupes = False
1433
1577
  _intercept = True
1434
1578
 
1435
1579
  async def _worker(self):
@@ -1445,7 +1589,7 @@ class InterceptModule(BaseModule):
1445
1589
  event = incoming
1446
1590
  kwargs = {}
1447
1591
  else:
1448
- self.debug(f"Event queue is in bad state")
1592
+ self.debug("Event queue is in bad state")
1449
1593
  break
1450
1594
  except asyncio.queues.QueueEmpty:
1451
1595
  await asyncio.sleep(0.1)
@@ -1500,7 +1644,7 @@ class InterceptModule(BaseModule):
1500
1644
  else:
1501
1645
  self.critical(f"Critical failure in intercept module {self.name}: {e}")
1502
1646
  self.critical(traceback.format_exc())
1503
- self.log.trace(f"Worker stopped")
1647
+ self.log.trace("Worker stopped")
1504
1648
 
1505
1649
  async def get_incoming_event(self):
1506
1650
  """
@@ -1531,7 +1675,7 @@ class InterceptModule(BaseModule):
1531
1675
  try:
1532
1676
  self.incoming_event_queue.put_nowait((event, kwargs))
1533
1677
  except AttributeError:
1534
- self.debug(f"Not in an acceptable state to queue incoming event")
1678
+ self.debug("Not in an acceptable state to queue incoming event")
1535
1679
 
1536
1680
  async def _event_postcheck(self, event):
1537
1681
  return await self._event_postcheck_inner(event)
bbot/modules/bevigil.py CHANGED
@@ -22,12 +22,12 @@ class bevigil(subdomain_enum_apikey):
22
22
 
23
23
  async def setup(self):
24
24
  self.api_key = self.config.get("api_key", "")
25
- self.headers = {"X-Access-Token": self.api_key}
26
25
  self.urls = self.config.get("urls", False)
27
26
  return await super().setup()
28
27
 
29
- async def ping(self):
30
- pass
28
+ def prepare_api_request(self, url, kwargs):
29
+ kwargs["headers"]["X-Access-Token"] = self.api_key
30
+ return url, kwargs
31
31
 
32
32
  async def handle_event(self, event):
33
33
  query = self.make_query(event)
@@ -54,20 +54,20 @@ class bevigil(subdomain_enum_apikey):
54
54
 
55
55
  async def request_subdomains(self, query):
56
56
  url = f"{self.base_url}/{self.helpers.quote(query)}/subdomains/"
57
- return await self.request_with_fail_count(url, headers=self.headers)
57
+ return await self.api_request(url)
58
58
 
59
59
  async def request_urls(self, query):
60
60
  url = f"{self.base_url}/{self.helpers.quote(query)}/urls/"
61
- return await self.request_with_fail_count(url, headers=self.headers)
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:
@@ -21,19 +21,22 @@ class binaryedge(subdomain_enum_apikey):
21
21
 
22
22
  async def setup(self):
23
23
  self.max_records = self.config.get("max_records", 1000)
24
- self.headers = {"X-Key": self.config.get("api_key", "")}
25
24
  return await super().setup()
26
25
 
26
+ def prepare_api_request(self, url, kwargs):
27
+ kwargs["headers"]["X-Key"] = self.api_key
28
+ return url, kwargs
29
+
27
30
  async def ping(self):
28
31
  url = f"{self.base_url}/user/subscription"
29
- j = (await self.request_with_fail_count(url, headers=self.headers)).json()
32
+ j = (await self.api_request(url)).json()
30
33
  assert j.get("requests_left", 0) > 0
31
34
 
32
35
  async def request_url(self, query):
33
36
  # todo: host query (certs + services)
34
37
  url = f"{self.base_url}/query/domains/subdomain/{self.helpers.quote(query)}"
35
- return await self.request_with_fail_count(url, headers=self.headers)
38
+ return await self.api_request(url)
36
39
 
37
- def parse_results(self, r, query):
40
+ async def parse_results(self, r, query):
38
41
  j = r.json()
39
42
  return j.get("events", [])
@@ -0,0 +1,47 @@
1
+ from bbot.modules.templates.subdomain_enum import subdomain_enum_apikey
2
+
3
+
4
+ class BufferOverrun(subdomain_enum_apikey):
5
+ watched_events = ["DNS_NAME"]
6
+ produced_events = ["DNS_NAME"]
7
+ flags = ["subdomain-enum", "passive", "safe"]
8
+ meta = {
9
+ "description": "Query BufferOverrun's TLS API for subdomains",
10
+ "created_date": "2024-10-23",
11
+ "author": "@TheTechromancer",
12
+ "auth_required": True,
13
+ }
14
+ options = {"api_key": "", "commercial": False}
15
+ options_desc = {"api_key": "BufferOverrun API key", "commercial": "Use commercial API"}
16
+
17
+ base_url = "https://tls.bufferover.run/dns"
18
+ commercial_base_url = "https://bufferover-run-tls.p.rapidapi.com/ipv4/dns"
19
+
20
+ async def setup(self):
21
+ self.commercial = self.config.get("commercial", False)
22
+ return await super().setup()
23
+
24
+ def prepare_api_request(self, url, kwargs):
25
+ if self.commercial:
26
+ kwargs["headers"]["x-rapidapi-host"] = "bufferover-run-tls.p.rapidapi.com"
27
+ kwargs["headers"]["x-rapidapi-key"] = self.api_key
28
+ else:
29
+ kwargs["headers"]["x-api-key"] = self.api_key
30
+ return url, kwargs
31
+
32
+ async def request_url(self, query):
33
+ url = f"{self.commercial_base_url if self.commercial else self.base_url}?q=.{query}"
34
+ return await self.api_request(url)
35
+
36
+ async def parse_results(self, r, query):
37
+ j = r.json()
38
+ subdomains_set = set()
39
+ if isinstance(j, dict):
40
+ results = j.get("Results", [])
41
+ for result in results:
42
+ parts = result.split(",")
43
+ if len(parts) > 4:
44
+ subdomain = parts[4].strip()
45
+ if subdomain and subdomain.endswith(f".{query}"):
46
+ subdomains_set.add(subdomain)
47
+ return subdomains_set
bbot/modules/builtwith.py CHANGED
@@ -27,10 +27,6 @@ class builtwith(subdomain_enum_apikey):
27
27
  options_desc = {"api_key": "Builtwith API key", "redirects": "Also look up inbound and outbound redirects"}
28
28
  base_url = "https://api.builtwith.com"
29
29
 
30
- async def ping(self):
31
- # builtwith does not have a ping feature, so we skip it to save API credits
32
- return
33
-
34
30
  async def handle_event(self, event):
35
31
  query = self.make_query(event)
36
32
  # domains
@@ -59,14 +55,14 @@ class builtwith(subdomain_enum_apikey):
59
55
  )
60
56
 
61
57
  async def request_domains(self, query):
62
- url = f"{self.base_url}/v20/api.json?KEY={self.api_key}&LOOKUP={query}&NOMETA=yes&NOATTR=yes&HIDETEXT=yes&HIDEDL=yes"
63
- return await self.request_with_fail_count(url)
58
+ url = f"{self.base_url}/v20/api.json?KEY={{api_key}}&LOOKUP={query}&NOMETA=yes&NOATTR=yes&HIDETEXT=yes&HIDEDL=yes"
59
+ return await self.api_request(url)
64
60
 
65
61
  async def request_redirects(self, query):
66
- url = f"{self.base_url}/redirect1/api.json?KEY={self.api_key}&LOOKUP={query}"
67
- return await self.request_with_fail_count(url)
62
+ url = f"{self.base_url}/redirect1/api.json?KEY={{api_key}}&LOOKUP={query}"
63
+ return await self.api_request(url)
68
64
 
69
- def parse_domains(self, r, query):
65
+ async def parse_domains(self, r, query):
70
66
  """
71
67
  This method returns a set of subdomains.
72
68
  Each subdomain is an "FQDN" that was reported in the "Detailed Technology Profile" page on builtwith.com
@@ -96,7 +92,7 @@ class builtwith(subdomain_enum_apikey):
96
92
  self.verbose(f"No results for {query}: {error}")
97
93
  return results_set
98
94
 
99
- def parse_redirects(self, r, query):
95
+ async def parse_redirects(self, r, query):
100
96
  """
101
97
  This method creates a set.
102
98
  Each entry in the set is either an Inbound or Outbound Redirect reported in the "Redirect Profile" page on builtwith.com
bbot/modules/bypass403.py CHANGED
@@ -92,7 +92,7 @@ class bypass403(BaseModule):
92
92
  return None
93
93
 
94
94
  sig = self.format_signature(sig, event)
95
- if sig[2] != None:
95
+ if sig[2] is not None:
96
96
  headers = dict(sig[2])
97
97
  else:
98
98
  headers = None
@@ -106,13 +106,13 @@ class bypass403(BaseModule):
106
106
  continue
107
107
 
108
108
  # In some cases WAFs will respond with a 200 code which causes a false positive
109
- if subject_response != None:
109
+ if subject_response is not None:
110
110
  for ws in waf_strings:
111
111
  if ws in subject_response.text:
112
112
  self.debug("Rejecting result based on presence of WAF string")
113
113
  return
114
114
 
115
- if match == False:
115
+ if match is False:
116
116
  if str(subject_response.status_code)[0] != "4":
117
117
  if sig[2]:
118
118
  added_header_tuple = next(iter(sig[2].items()))
@@ -165,13 +165,13 @@ class bypass403(BaseModule):
165
165
  return False
166
166
 
167
167
  def format_signature(self, sig, event):
168
- if sig[3] == True:
168
+ if sig[3] is True:
169
169
  cleaned_path = event.parsed_url.path.strip("/")
170
170
  else:
171
171
  cleaned_path = event.parsed_url.path.lstrip("/")
172
172
  kwargs = {"scheme": event.parsed_url.scheme, "netloc": event.parsed_url.netloc, "path": cleaned_path}
173
173
  formatted_url = sig[1].format(**kwargs)
174
- if sig[2] != None:
174
+ if sig[2] is not None:
175
175
  formatted_headers = {k: v.format(**kwargs) for k, v in sig[2].items()}
176
176
  else:
177
177
  formatted_headers = None
bbot/modules/c99.py CHANGED
@@ -15,17 +15,19 @@ class c99(subdomain_enum_apikey):
15
15
  options_desc = {"api_key": "c99.nl API key"}
16
16
 
17
17
  base_url = "https://api.c99.nl"
18
+ ping_url = f"{base_url}/randomnumber?key={{api_key}}&between=1,100&json"
18
19
 
19
20
  async def ping(self):
20
- url = f"{self.base_url}/randomnumber?key={self.api_key}&between=1,100&json"
21
- response = await self.request_with_fail_count(url)
22
- assert response.json()["success"] == True
21
+ url = f"{self.base_url}/randomnumber?key={{api_key}}&between=1,100&json"
22
+ response = await self.api_request(url)
23
+ assert response.json()["success"] is True, getattr(response, "text", "no response from server")
23
24
 
24
25
  async def request_url(self, query):
25
- url = f"{self.base_url}/subdomainfinder?key={self.api_key}&domain={self.helpers.quote(query)}&json"
26
- return await self.request_with_fail_count(url)
26
+ url = f"{self.base_url}/subdomainfinder?key={{api_key}}&domain={self.helpers.quote(query)}&json"
27
+ return await self.api_request(url)
27
28
 
28
- def parse_results(self, r, query):
29
+ async def parse_results(self, r, query):
30
+ results = set()
29
31
  j = r.json()
30
32
  if isinstance(j, dict):
31
33
  subdomains = j.get("subdomains", [])
@@ -33,4 +35,5 @@ class c99(subdomain_enum_apikey):
33
35
  for s in subdomains:
34
36
  subdomain = s.get("subdomain", "")
35
37
  if subdomain:
36
- yield subdomain
38
+ results.add(subdomain)
39
+ return results