bbot 2.0.1.4720rc0__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 (267) 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 -4
  6. bbot/core/engine.py +9 -8
  7. bbot/core/event/base.py +131 -52
  8. bbot/core/helpers/bloom.py +10 -3
  9. bbot/core/helpers/command.py +8 -7
  10. bbot/core/helpers/depsinstaller/installer.py +31 -13
  11. bbot/core/helpers/diff.py +10 -10
  12. bbot/core/helpers/dns/brute.py +7 -4
  13. bbot/core/helpers/dns/dns.py +1 -2
  14. bbot/core/helpers/dns/engine.py +4 -6
  15. bbot/core/helpers/dns/helpers.py +2 -2
  16. bbot/core/helpers/dns/mock.py +0 -1
  17. bbot/core/helpers/files.py +1 -1
  18. bbot/core/helpers/helper.py +7 -4
  19. bbot/core/helpers/interactsh.py +3 -3
  20. bbot/core/helpers/libmagic.py +65 -0
  21. bbot/core/helpers/misc.py +65 -22
  22. bbot/core/helpers/names_generator.py +17 -3
  23. bbot/core/helpers/process.py +0 -20
  24. bbot/core/helpers/regex.py +1 -1
  25. bbot/core/helpers/regexes.py +12 -6
  26. bbot/core/helpers/validators.py +1 -2
  27. bbot/core/helpers/web/client.py +1 -1
  28. bbot/core/helpers/web/engine.py +1 -2
  29. bbot/core/helpers/web/web.py +4 -114
  30. bbot/core/helpers/wordcloud.py +5 -5
  31. bbot/core/modules.py +36 -27
  32. bbot/core/multiprocess.py +58 -0
  33. bbot/core/shared_deps.py +46 -3
  34. bbot/db/sql/models.py +147 -0
  35. bbot/defaults.yml +11 -9
  36. bbot/modules/anubisdb.py +2 -2
  37. bbot/modules/apkpure.py +63 -0
  38. bbot/modules/azure_tenant.py +2 -2
  39. bbot/modules/baddns.py +35 -19
  40. bbot/modules/baddns_direct.py +92 -0
  41. bbot/modules/baddns_zone.py +3 -8
  42. bbot/modules/badsecrets.py +4 -3
  43. bbot/modules/base.py +195 -51
  44. bbot/modules/bevigil.py +7 -7
  45. bbot/modules/binaryedge.py +7 -4
  46. bbot/modules/bufferoverrun.py +47 -0
  47. bbot/modules/builtwith.py +6 -10
  48. bbot/modules/bypass403.py +5 -5
  49. bbot/modules/c99.py +10 -7
  50. bbot/modules/censys.py +9 -13
  51. bbot/modules/certspotter.py +5 -3
  52. bbot/modules/chaos.py +9 -7
  53. bbot/modules/code_repository.py +1 -0
  54. bbot/modules/columbus.py +3 -3
  55. bbot/modules/crt.py +5 -3
  56. bbot/modules/deadly/dastardly.py +1 -1
  57. bbot/modules/deadly/ffuf.py +9 -9
  58. bbot/modules/deadly/nuclei.py +3 -3
  59. bbot/modules/deadly/vhost.py +4 -3
  60. bbot/modules/dehashed.py +1 -1
  61. bbot/modules/digitorus.py +1 -1
  62. bbot/modules/dnsbimi.py +145 -0
  63. bbot/modules/dnscaa.py +3 -3
  64. bbot/modules/dnsdumpster.py +4 -4
  65. bbot/modules/dnstlsrpt.py +144 -0
  66. bbot/modules/docker_pull.py +7 -5
  67. bbot/modules/dockerhub.py +2 -2
  68. bbot/modules/dotnetnuke.py +18 -19
  69. bbot/modules/emailformat.py +1 -1
  70. bbot/modules/extractous.py +122 -0
  71. bbot/modules/filedownload.py +9 -7
  72. bbot/modules/fullhunt.py +7 -4
  73. bbot/modules/generic_ssrf.py +5 -5
  74. bbot/modules/github_codesearch.py +3 -2
  75. bbot/modules/github_org.py +4 -4
  76. bbot/modules/github_workflows.py +4 -4
  77. bbot/modules/gitlab.py +2 -5
  78. bbot/modules/google_playstore.py +93 -0
  79. bbot/modules/gowitness.py +48 -50
  80. bbot/modules/hackertarget.py +5 -3
  81. bbot/modules/host_header.py +5 -5
  82. bbot/modules/httpx.py +1 -4
  83. bbot/modules/hunterio.py +3 -9
  84. bbot/modules/iis_shortnames.py +19 -30
  85. bbot/modules/internal/cloudcheck.py +27 -12
  86. bbot/modules/internal/dnsresolve.py +22 -20
  87. bbot/modules/internal/excavate.py +85 -48
  88. bbot/modules/internal/speculate.py +41 -32
  89. bbot/modules/internetdb.py +4 -2
  90. bbot/modules/ip2location.py +3 -5
  91. bbot/modules/ipneighbor.py +1 -1
  92. bbot/modules/ipstack.py +3 -8
  93. bbot/modules/jadx.py +87 -0
  94. bbot/modules/leakix.py +11 -10
  95. bbot/modules/myssl.py +2 -2
  96. bbot/modules/newsletters.py +2 -2
  97. bbot/modules/otx.py +5 -3
  98. bbot/modules/output/asset_inventory.py +7 -7
  99. bbot/modules/output/base.py +1 -1
  100. bbot/modules/output/csv.py +1 -1
  101. bbot/modules/output/http.py +20 -14
  102. bbot/modules/output/mysql.py +51 -0
  103. bbot/modules/output/neo4j.py +7 -2
  104. bbot/modules/output/postgres.py +49 -0
  105. bbot/modules/output/slack.py +0 -1
  106. bbot/modules/output/sqlite.py +29 -0
  107. bbot/modules/output/stdout.py +2 -2
  108. bbot/modules/output/teams.py +107 -6
  109. bbot/modules/paramminer_headers.py +5 -8
  110. bbot/modules/passivetotal.py +13 -13
  111. bbot/modules/portscan.py +32 -6
  112. bbot/modules/postman.py +50 -126
  113. bbot/modules/postman_download.py +220 -0
  114. bbot/modules/rapiddns.py +3 -8
  115. bbot/modules/report/asn.py +11 -11
  116. bbot/modules/robots.py +3 -3
  117. bbot/modules/securitytrails.py +7 -10
  118. bbot/modules/securitytxt.py +1 -1
  119. bbot/modules/shodan_dns.py +7 -9
  120. bbot/modules/sitedossier.py +1 -1
  121. bbot/modules/skymem.py +2 -2
  122. bbot/modules/social.py +2 -1
  123. bbot/modules/subdomaincenter.py +1 -1
  124. bbot/modules/subdomainradar.py +160 -0
  125. bbot/modules/telerik.py +8 -8
  126. bbot/modules/templates/bucket.py +1 -1
  127. bbot/modules/templates/github.py +22 -14
  128. bbot/modules/templates/postman.py +21 -0
  129. bbot/modules/templates/shodan.py +14 -13
  130. bbot/modules/templates/sql.py +95 -0
  131. bbot/modules/templates/subdomain_enum.py +51 -16
  132. bbot/modules/templates/webhook.py +2 -4
  133. bbot/modules/trickest.py +8 -37
  134. bbot/modules/trufflehog.py +10 -12
  135. bbot/modules/url_manipulation.py +3 -3
  136. bbot/modules/urlscan.py +1 -1
  137. bbot/modules/viewdns.py +1 -1
  138. bbot/modules/virustotal.py +8 -30
  139. bbot/modules/wafw00f.py +1 -1
  140. bbot/modules/wayback.py +1 -1
  141. bbot/modules/wpscan.py +17 -11
  142. bbot/modules/zoomeye.py +11 -6
  143. bbot/presets/baddns-thorough.yml +12 -0
  144. bbot/presets/fast.yml +16 -0
  145. bbot/presets/kitchen-sink.yml +1 -0
  146. bbot/presets/spider.yml +4 -0
  147. bbot/presets/subdomain-enum.yml +7 -7
  148. bbot/scanner/manager.py +5 -16
  149. bbot/scanner/preset/args.py +44 -26
  150. bbot/scanner/preset/environ.py +7 -2
  151. bbot/scanner/preset/path.py +7 -4
  152. bbot/scanner/preset/preset.py +36 -23
  153. bbot/scanner/scanner.py +172 -62
  154. bbot/scanner/target.py +236 -434
  155. bbot/scripts/docs.py +1 -1
  156. bbot/test/bbot_fixtures.py +13 -3
  157. bbot/test/conftest.py +132 -100
  158. bbot/test/fastapi_test.py +17 -0
  159. bbot/test/owasp_mastg.apk +0 -0
  160. bbot/test/run_tests.sh +4 -4
  161. bbot/test/test.conf +2 -0
  162. bbot/test/test_step_1/test_bbot_fastapi.py +82 -0
  163. bbot/test/test_step_1/test_bloom_filter.py +2 -0
  164. bbot/test/test_step_1/test_cli.py +138 -64
  165. bbot/test/test_step_1/test_dns.py +62 -25
  166. bbot/test/test_step_1/test_engine.py +17 -17
  167. bbot/test/test_step_1/test_events.py +183 -28
  168. bbot/test/test_step_1/test_helpers.py +64 -28
  169. bbot/test/test_step_1/test_manager_deduplication.py +1 -1
  170. bbot/test/test_step_1/test_manager_scope_accuracy.py +333 -330
  171. bbot/test/test_step_1/test_modules_basic.py +68 -70
  172. bbot/test/test_step_1/test_presets.py +184 -96
  173. bbot/test/test_step_1/test_python_api.py +7 -2
  174. bbot/test/test_step_1/test_regexes.py +35 -5
  175. bbot/test/test_step_1/test_scan.py +39 -5
  176. bbot/test/test_step_1/test_scope.py +4 -3
  177. bbot/test/test_step_1/test_target.py +243 -145
  178. bbot/test/test_step_1/test_web.py +14 -8
  179. bbot/test/test_step_2/module_tests/base.py +15 -7
  180. bbot/test/test_step_2/module_tests/test_module_anubisdb.py +1 -1
  181. bbot/test/test_step_2/module_tests/test_module_apkpure.py +71 -0
  182. bbot/test/test_step_2/module_tests/test_module_asset_inventory.py +0 -1
  183. bbot/test/test_step_2/module_tests/test_module_azure_realm.py +1 -1
  184. bbot/test/test_step_2/module_tests/test_module_baddns.py +6 -6
  185. bbot/test/test_step_2/module_tests/test_module_baddns_direct.py +62 -0
  186. bbot/test/test_step_2/module_tests/test_module_bevigil.py +29 -2
  187. bbot/test/test_step_2/module_tests/test_module_binaryedge.py +4 -2
  188. bbot/test/test_step_2/module_tests/test_module_bucket_amazon.py +2 -2
  189. bbot/test/test_step_2/module_tests/test_module_bucket_azure.py +1 -1
  190. bbot/test/test_step_2/module_tests/test_module_bufferoverrun.py +35 -0
  191. bbot/test/test_step_2/module_tests/test_module_builtwith.py +2 -2
  192. bbot/test/test_step_2/module_tests/test_module_bypass403.py +1 -1
  193. bbot/test/test_step_2/module_tests/test_module_c99.py +126 -0
  194. bbot/test/test_step_2/module_tests/test_module_censys.py +4 -1
  195. bbot/test/test_step_2/module_tests/test_module_cloudcheck.py +4 -0
  196. bbot/test/test_step_2/module_tests/test_module_code_repository.py +11 -1
  197. bbot/test/test_step_2/module_tests/test_module_columbus.py +1 -1
  198. bbot/test/test_step_2/module_tests/test_module_credshed.py +3 -3
  199. bbot/test/test_step_2/module_tests/test_module_dastardly.py +2 -1
  200. bbot/test/test_step_2/module_tests/test_module_dehashed.py +2 -2
  201. bbot/test/test_step_2/module_tests/test_module_digitorus.py +1 -1
  202. bbot/test/test_step_2/module_tests/test_module_discord.py +1 -1
  203. bbot/test/test_step_2/module_tests/test_module_dnsbimi.py +103 -0
  204. bbot/test/test_step_2/module_tests/test_module_dnsbrute.py +9 -10
  205. bbot/test/test_step_2/module_tests/test_module_dnsbrute_mutations.py +1 -2
  206. bbot/test/test_step_2/module_tests/test_module_dnscommonsrv.py +1 -2
  207. bbot/test/test_step_2/module_tests/test_module_dnsdumpster.py +4 -4
  208. bbot/test/test_step_2/module_tests/test_module_dnstlsrpt.py +64 -0
  209. bbot/test/test_step_2/module_tests/test_module_dotnetnuke.py +0 -8
  210. bbot/test/test_step_2/module_tests/test_module_excavate.py +17 -37
  211. bbot/test/test_step_2/module_tests/test_module_extractous.py +54 -0
  212. bbot/test/test_step_2/module_tests/test_module_ffuf_shortnames.py +1 -1
  213. bbot/test/test_step_2/module_tests/test_module_filedownload.py +14 -14
  214. bbot/test/test_step_2/module_tests/test_module_git_clone.py +2 -2
  215. bbot/test/test_step_2/module_tests/test_module_github_org.py +19 -8
  216. bbot/test/test_step_2/module_tests/test_module_github_workflows.py +1 -1
  217. bbot/test/test_step_2/module_tests/test_module_gitlab.py +9 -4
  218. bbot/test/test_step_2/module_tests/test_module_google_playstore.py +83 -0
  219. bbot/test/test_step_2/module_tests/test_module_gowitness.py +4 -4
  220. bbot/test/test_step_2/module_tests/test_module_host_header.py +1 -1
  221. bbot/test/test_step_2/module_tests/test_module_http.py +4 -4
  222. bbot/test/test_step_2/module_tests/test_module_httpx.py +10 -8
  223. bbot/test/test_step_2/module_tests/test_module_hunterio.py +68 -4
  224. bbot/test/test_step_2/module_tests/test_module_jadx.py +55 -0
  225. bbot/test/test_step_2/module_tests/test_module_json.py +22 -9
  226. bbot/test/test_step_2/module_tests/test_module_leakix.py +7 -3
  227. bbot/test/test_step_2/module_tests/test_module_mysql.py +76 -0
  228. bbot/test/test_step_2/module_tests/test_module_myssl.py +1 -1
  229. bbot/test/test_step_2/module_tests/test_module_neo4j.py +1 -1
  230. bbot/test/test_step_2/module_tests/test_module_newsletters.py +6 -6
  231. bbot/test/test_step_2/module_tests/test_module_ntlm.py +7 -7
  232. bbot/test/test_step_2/module_tests/test_module_oauth.py +1 -1
  233. bbot/test/test_step_2/module_tests/test_module_otx.py +1 -1
  234. bbot/test/test_step_2/module_tests/test_module_paramminer_cookies.py +1 -2
  235. bbot/test/test_step_2/module_tests/test_module_paramminer_getparams.py +0 -6
  236. bbot/test/test_step_2/module_tests/test_module_paramminer_headers.py +2 -9
  237. bbot/test/test_step_2/module_tests/test_module_passivetotal.py +3 -1
  238. bbot/test/test_step_2/module_tests/test_module_portscan.py +9 -8
  239. bbot/test/test_step_2/module_tests/test_module_postgres.py +74 -0
  240. bbot/test/test_step_2/module_tests/test_module_postman.py +84 -253
  241. bbot/test/test_step_2/module_tests/test_module_postman_download.py +439 -0
  242. bbot/test/test_step_2/module_tests/test_module_rapiddns.py +93 -1
  243. bbot/test/test_step_2/module_tests/test_module_shodan_dns.py +20 -1
  244. bbot/test/test_step_2/module_tests/test_module_sitedossier.py +2 -2
  245. bbot/test/test_step_2/module_tests/test_module_smuggler.py +1 -1
  246. bbot/test/test_step_2/module_tests/test_module_social.py +11 -1
  247. bbot/test/test_step_2/module_tests/test_module_speculate.py +2 -6
  248. bbot/test/test_step_2/module_tests/test_module_splunk.py +4 -4
  249. bbot/test/test_step_2/module_tests/test_module_sqlite.py +18 -0
  250. bbot/test/test_step_2/module_tests/test_module_sslcert.py +1 -1
  251. bbot/test/test_step_2/module_tests/test_module_stdout.py +5 -3
  252. bbot/test/test_step_2/module_tests/test_module_subdomaincenter.py +1 -1
  253. bbot/test/test_step_2/module_tests/test_module_subdomainradar.py +208 -0
  254. bbot/test/test_step_2/module_tests/test_module_subdomains.py +1 -1
  255. bbot/test/test_step_2/module_tests/test_module_teams.py +8 -6
  256. bbot/test/test_step_2/module_tests/test_module_telerik.py +1 -1
  257. bbot/test/test_step_2/module_tests/test_module_trufflehog.py +317 -14
  258. bbot/test/test_step_2/module_tests/test_module_wayback.py +1 -1
  259. bbot/test/test_step_2/template_tests/test_template_subdomain_enum.py +2 -2
  260. {bbot-2.0.1.4720rc0.dist-info → bbot-2.3.0.5397rc0.dist-info}/METADATA +48 -18
  261. bbot-2.3.0.5397rc0.dist-info/RECORD +421 -0
  262. {bbot-2.0.1.4720rc0.dist-info → bbot-2.3.0.5397rc0.dist-info}/WHEEL +1 -1
  263. bbot/modules/unstructured.py +0 -163
  264. bbot/test/test_step_2/module_tests/test_module_unstructured.py +0 -102
  265. bbot-2.0.1.4720rc0.dist-info/RECORD +0 -387
  266. {bbot-2.0.1.4720rc0.dist-info → bbot-2.3.0.5397rc0.dist-info}/LICENSE +0 -0
  267. {bbot-2.0.1.4720rc0.dist-info → bbot-2.3.0.5397rc0.dist-info}/entry_points.txt +0 -0
bbot/scanner/scanner.py CHANGED
@@ -10,11 +10,11 @@ from datetime import datetime
10
10
  from collections import OrderedDict
11
11
 
12
12
  from bbot import __version__
13
-
14
13
  from bbot.core.event import make_event
15
14
  from .manager import ScanIngress, ScanEgress
16
15
  from bbot.core.helpers.misc import sha1, rand_string
17
16
  from bbot.core.helpers.names_generator import random_name
17
+ from bbot.core.multiprocess import SHARED_INTERPRETER_STATE
18
18
  from bbot.core.helpers.async_helpers import async_to_sync_gen
19
19
  from bbot.errors import BBOTError, ScanError, ValidationError
20
20
 
@@ -115,25 +115,37 @@ class Scanner:
115
115
  dispatcher (Dispatcher, optional): Dispatcher object to use. Defaults to new Dispatcher.
116
116
  **kwargs (list[str], optional): Additional keyword arguments (passed through to `Preset`).
117
117
  """
118
+ self._root_event = None
119
+ self._finish_event = None
120
+ self.start_time = None
121
+ self.end_time = None
122
+ self.duration = None
123
+ self.duration_human = None
124
+ self.duration_seconds = None
125
+
126
+ self._success = False
127
+
118
128
  if scan_id is not None:
119
- self.id = str(id)
129
+ self.id = str(scan_id)
120
130
  else:
121
131
  self.id = f"SCAN:{sha1(rand_string(20)).hexdigest()}"
122
132
 
123
- preset = kwargs.pop("preset", None)
133
+ custom_preset = kwargs.pop("preset", None)
124
134
  kwargs["_log"] = True
125
135
 
126
136
  from .preset import Preset
127
137
 
128
- if preset is None:
129
- preset = Preset(*targets, **kwargs)
130
- else:
131
- if not isinstance(preset, Preset):
132
- raise ValidationError(f'Preset must be of type Preset, not "{type(preset).__name__}"')
133
- self.preset = preset.bake(self)
138
+ base_preset = Preset(*targets, **kwargs)
139
+
140
+ if custom_preset is not None:
141
+ if not isinstance(custom_preset, Preset):
142
+ raise ValidationError(f'Preset must be of type Preset, not "{type(custom_preset).__name__}"')
143
+ base_preset.merge(custom_preset)
144
+
145
+ self.preset = base_preset.bake(self)
134
146
 
135
147
  # scan name
136
- if preset.scan_name is None:
148
+ if self.preset.scan_name is None:
137
149
  tries = 0
138
150
  while 1:
139
151
  if tries > 5:
@@ -148,12 +160,16 @@ class Scanner:
148
160
  break
149
161
  tries += 1
150
162
  else:
151
- scan_name = str(preset.scan_name)
152
- self.name = scan_name
163
+ scan_name = str(self.preset.scan_name)
164
+ self.name = scan_name.replace("/", "_")
165
+
166
+ # make sure the preset has a description
167
+ if not self.preset.description:
168
+ self.preset.description = self.name
153
169
 
154
170
  # scan output dir
155
- if preset.output_dir is not None:
156
- self.home = Path(preset.output_dir).resolve() / self.name
171
+ if self.preset.output_dir is not None:
172
+ self.home = Path(self.preset.output_dir).resolve() / self.name
157
173
  else:
158
174
  self.home = self.preset.bbot_home / "scans" / self.name
159
175
 
@@ -198,8 +214,8 @@ class Scanner:
198
214
  )
199
215
 
200
216
  # url file extensions
201
- self.url_extension_blacklist = set(e.lower() for e in self.config.get("url_extension_blacklist", []))
202
- self.url_extension_httpx_only = set(e.lower() for e in self.config.get("url_extension_httpx_only", []))
217
+ self.url_extension_blacklist = {e.lower() for e in self.config.get("url_extension_blacklist", [])}
218
+ self.url_extension_httpx_only = {e.lower() for e in self.config.get("url_extension_httpx_only", [])}
203
219
 
204
220
  # url querystring behavior
205
221
  self.url_querystring_remove = self.config.get("url_querystring_remove", True)
@@ -232,6 +248,8 @@ class Scanner:
232
248
  self._dns_strings = None
233
249
  self._dns_regexes = None
234
250
  self._dns_regexes_yara = None
251
+ self._dns_yara_rules_uncompiled = None
252
+ self._dns_yara_rules = None
235
253
 
236
254
  self.__log_handlers = None
237
255
  self._log_handler_backup = []
@@ -241,6 +259,9 @@ class Scanner:
241
259
  Creates the scan's output folder, loads its modules, and calls their .setup() methods.
242
260
  """
243
261
 
262
+ # update the master PID
263
+ SHARED_INTERPRETER_STATE.update_scan_pid()
264
+
244
265
  self.helpers.mkdir(self.home)
245
266
  if not self._prepped:
246
267
  # save scan preset
@@ -248,7 +269,7 @@ class Scanner:
248
269
  f.write(self.preset.to_yaml())
249
270
 
250
271
  # log scan overview
251
- start_msg = f"Scan with {len(self.preset.scan_modules):,} modules seeded with {len(self.target):,} targets"
272
+ start_msg = f"Scan seeded with {len(self.seeds):,} targets"
252
273
  details = []
253
274
  if self.whitelist != self.target:
254
275
  details.append(f"{len(self.whitelist):,} in whitelist")
@@ -272,7 +293,9 @@ class Scanner:
272
293
  self.debug(
273
294
  f"Setting intercept module {intercept_module.name}._incoming_event_queue to previous intercept module {prev_intercept_module.name}.outgoing_event_queue"
274
295
  )
275
- intercept_module._incoming_event_queue = prev_intercept_module.outgoing_event_queue
296
+ interqueue = asyncio.Queue()
297
+ intercept_module._incoming_event_queue = interqueue
298
+ prev_intercept_module._outgoing_event_queue = interqueue
276
299
 
277
300
  # abort if there are no output modules
278
301
  num_output_modules = len([m for m in self.modules.values() if m._type == "output"])
@@ -304,18 +327,18 @@ class Scanner:
304
327
 
305
328
  async def async_start(self):
306
329
  """ """
307
- failed = True
308
- scan_start_time = datetime.now()
330
+ self.start_time = datetime.now()
331
+ self.root_event.data["started_at"] = self.start_time.isoformat()
309
332
  try:
310
333
  await self._prep()
311
334
 
312
335
  self._start_log_handlers()
313
- self.trace(f'Ran BBOT {__version__} at {scan_start_time}, command: {" ".join(sys.argv)}')
336
+ self.trace(f'Ran BBOT {__version__} at {self.start_time}, command: {" ".join(sys.argv)}')
314
337
  self.trace(f"Target: {self.preset.target.json}")
315
338
  self.trace(f"Preset: {self.preset.to_dict(redact_secrets=True)}")
316
339
 
317
340
  if not self.target:
318
- self.warning(f"No scan targets specified")
341
+ self.warning("No scan targets specified")
319
342
 
320
343
  # start status ticker
321
344
  self.ticker_task = asyncio.create_task(
@@ -325,7 +348,7 @@ class Scanner:
325
348
  self.status = "STARTING"
326
349
 
327
350
  if not self.modules:
328
- self.error(f"No modules loaded")
351
+ self.error("No modules loaded")
329
352
  self.status = "FAILED"
330
353
  return
331
354
  else:
@@ -339,7 +362,8 @@ class Scanner:
339
362
 
340
363
  # distribute seed events
341
364
  self.init_events_task = asyncio.create_task(
342
- self.ingress_module.init_events(self.target.events), name=f"{self.name}.ingress_module.init_events()"
365
+ self.ingress_module.init_events(self.target.seeds.events),
366
+ name=f"{self.name}.ingress_module.init_events()",
343
367
  )
344
368
 
345
369
  # main scan loop
@@ -361,16 +385,19 @@ class Scanner:
361
385
  if self._finished_init and self.modules_finished:
362
386
  new_activity = await self.finish()
363
387
  if not new_activity:
388
+ self._success = True
389
+ scan_finish_event = await self._mark_finished()
390
+ yield scan_finish_event
364
391
  break
365
392
 
366
393
  await asyncio.sleep(0.1)
367
394
 
368
- failed = False
395
+ self._success = True
369
396
 
370
397
  except BaseException as e:
371
398
  if self.helpers.in_exception_chain(e, (KeyboardInterrupt, asyncio.CancelledError)):
372
399
  self.stop()
373
- failed = False
400
+ self._success = True
374
401
  else:
375
402
  try:
376
403
  raise
@@ -394,26 +421,47 @@ class Scanner:
394
421
  await self._report()
395
422
  await self._cleanup()
396
423
 
397
- log_fn = self.hugesuccess
398
- if self.status == "ABORTING":
399
- self.status = "ABORTED"
400
- log_fn = self.hugewarning
401
- elif failed:
402
- self.status = "FAILED"
403
- log_fn = self.critical
404
- else:
405
- self.status = "FINISHED"
406
-
407
- scan_run_time = datetime.now() - scan_start_time
408
- scan_run_time = self.helpers.human_timedelta(scan_run_time)
409
- log_fn(f"Scan {self.name} completed in {scan_run_time} with status {self.status}")
410
-
411
424
  await self.dispatcher.on_finish(self)
412
425
 
413
426
  self._stop_log_handlers()
414
427
 
428
+ async def _mark_finished(self):
429
+ log_fn = self.hugesuccess
430
+ if self.status == "ABORTING":
431
+ status = "ABORTED"
432
+ log_fn = self.hugewarning
433
+ elif not self._success:
434
+ status = "FAILED"
435
+ log_fn = self.critical
436
+ else:
437
+ status = "FINISHED"
438
+
439
+ self.end_time = datetime.now()
440
+ self.duration = self.end_time - self.start_time
441
+ self.duration_seconds = self.duration.total_seconds()
442
+ self.duration_human = self.helpers.human_timedelta(self.duration)
443
+
444
+ status_message = f"Scan {self.name} completed in {self.duration_human} with status {status}"
445
+
446
+ scan_finish_event = self.finish_event(status_message, status)
447
+
448
+ # queue final scan event with output modules
449
+ output_modules = [m for m in self.modules.values() if m._type == "output" and m.name != "python"]
450
+ for m in output_modules:
451
+ await m.queue_event(scan_finish_event)
452
+ # wait until output modules are flushed
453
+ while 1:
454
+ modules_finished = all(m.finished for m in output_modules)
455
+ if modules_finished:
456
+ break
457
+ await asyncio.sleep(0.05)
458
+
459
+ self.status = status
460
+ log_fn(status_message)
461
+ return scan_finish_event
462
+
415
463
  def _start_modules(self):
416
- self.verbose(f"Starting module worker loops")
464
+ self.verbose("Starting module worker loops")
417
465
  for module in self.modules.values():
418
466
  module.start()
419
467
 
@@ -437,17 +485,17 @@ class Scanner:
437
485
  Soft-failed modules are not set to an error state but are also removed if `remove_failed` is True.
438
486
  """
439
487
  await self.load_modules()
440
- self.verbose(f"Setting up modules")
488
+ self.verbose("Setting up modules")
441
489
  succeeded = []
442
490
  hard_failed = []
443
491
  soft_failed = []
444
492
 
445
493
  async for task in self.helpers.as_completed([m._setup() for m in self.modules.values()]):
446
494
  module, status, msg = await task
447
- if status == True:
495
+ if status is True:
448
496
  self.debug(f"Setup succeeded for {module.name} ({msg})")
449
497
  succeeded.append(module.name)
450
- elif status == False:
498
+ elif status is False:
451
499
  self.warning(f"Setup hard-failed for {module.name}: {msg}")
452
500
  self.modules[module.name].set_error_state()
453
501
  hard_failed.append(module.name)
@@ -489,11 +537,11 @@ class Scanner:
489
537
  """
490
538
  if not self._modules_loaded:
491
539
  if not self.preset.modules:
492
- self.warning(f"No modules to load")
540
+ self.warning("No modules to load")
493
541
  return
494
542
 
495
543
  if not self.preset.scan_modules:
496
- self.warning(f"No scan modules to load")
544
+ self.warning("No scan modules to load")
497
545
 
498
546
  # install module dependencies
499
547
  succeeded, failed = await self.helpers.depsinstaller.install(*self.preset.modules)
@@ -637,7 +685,7 @@ class Scanner:
637
685
 
638
686
  if modules_errored:
639
687
  self.verbose(
640
- f'{self.name}: Modules errored: {len(modules_errored):,} ({", ".join([m for m in modules_errored])})'
688
+ f'{self.name}: Modules errored: {len(modules_errored):,} ({", ".join(list(modules_errored))})'
641
689
  )
642
690
 
643
691
  num_queued_events = self.num_queued_events
@@ -674,7 +722,7 @@ class Scanner:
674
722
  memory_usage = module.memory_usage
675
723
  module_memory_usage.append((module.name, memory_usage))
676
724
  module_memory_usage.sort(key=lambda x: x[-1], reverse=True)
677
- self.debug(f"MODULE MEMORY USAGE:")
725
+ self.debug("MODULE MEMORY USAGE:")
678
726
  for module_name, usage in module_memory_usage:
679
727
  self.debug(f" - {module_name}: {self.helpers.bytes_to_human(usage)}")
680
728
 
@@ -721,12 +769,12 @@ class Scanner:
721
769
  # Trigger .finished() on every module and start over
722
770
  log.info("Finishing scan")
723
771
  for module in self.modules.values():
724
- finished_event = self.make_event(f"FINISHED", "FINISHED", dummy=True, tags={module.name})
772
+ finished_event = self.make_event("FINISHED", "FINISHED", dummy=True, tags={module.name})
725
773
  await module.queue_event(finished_event)
726
774
  self.verbose("Completed finish()")
727
775
  return True
728
- # Return False if no new events were generated since last time
729
776
  self.verbose("Completed final finish()")
777
+ # Return False if no new events were generated since last time
730
778
  return False
731
779
 
732
780
  def _drain_queues(self):
@@ -849,6 +897,10 @@ class Scanner:
849
897
  def target(self):
850
898
  return self.preset.target
851
899
 
900
+ @property
901
+ def seeds(self):
902
+ return self.preset.seeds
903
+
852
904
  @property
853
905
  def whitelist(self):
854
906
  return self.preset.whitelist
@@ -945,12 +997,25 @@ class Scanner:
945
997
  }
946
998
  ```
947
999
  """
948
- root_event = self.make_event(data=self.json, event_type="SCAN", dummy=True)
1000
+ if self._root_event is None:
1001
+ self._root_event = self.make_root_event(f"Scan {self.name} started at {self.start_time}")
1002
+ self._root_event.data["status"] = self.status
1003
+ return self._root_event
1004
+
1005
+ def finish_event(self, context=None, status=None):
1006
+ if self._finish_event is None:
1007
+ if context is None or status is None:
1008
+ raise ValueError("Must specify context and status")
1009
+ self._finish_event = self.make_root_event(context)
1010
+ self._finish_event.data["status"] = status
1011
+ return self._finish_event
1012
+
1013
+ def make_root_event(self, context):
1014
+ root_event = self.make_event(data=self.json, event_type="SCAN", dummy=True, context=context)
949
1015
  root_event._id = self.id
950
1016
  root_event.scope_distance = 0
951
1017
  root_event.parent = root_event
952
1018
  root_event.module = self._make_dummy_module(name="TARGET", _type="TARGET")
953
- root_event.discovery_context = f"Scan {self.name} started at {root_event.timestamp}"
954
1019
  return root_event
955
1020
 
956
1021
  @property
@@ -959,14 +1024,13 @@ class Scanner:
959
1024
  A list of DNS hostname strings generated from the scan target
960
1025
  """
961
1026
  if self._dns_strings is None:
962
- dns_targets = set(t.host for t in self.target if t.host and isinstance(t.host, str))
963
- dns_whitelist = set(t.host for t in self.whitelist if t.host and isinstance(t.host, str))
964
- dns_targets.update(dns_whitelist)
965
- dns_targets = sorted(dns_targets, key=len)
966
- dns_targets_set = set()
1027
+ dns_whitelist = {t.host for t in self.whitelist if t.host and isinstance(t.host, str)}
1028
+ dns_whitelist = sorted(dns_whitelist, key=len)
1029
+ dns_whitelist_set = set()
967
1030
  dns_strings = []
968
- for t in dns_targets:
969
- if not any(x in dns_targets_set for x in self.helpers.domain_parents(t, include_self=True)):
1031
+ for t in dns_whitelist:
1032
+ if not any(x in dns_whitelist_set for x in self.helpers.domain_parents(t, include_self=True)):
1033
+ dns_whitelist_set.add(t)
970
1034
  dns_strings.append(t)
971
1035
  self._dns_strings = dns_strings
972
1036
  return self._dns_strings
@@ -1011,21 +1075,67 @@ class Scanner:
1011
1075
  Returns a list of DNS hostname regexes formatted specifically for compatibility with YARA rules.
1012
1076
  """
1013
1077
  if self._dns_regexes_yara is None:
1014
- self._dns_regexes_yara = self._generate_dns_regexes(r"(([a-z0-9-]+\.)+")
1078
+ self._dns_regexes_yara = self._generate_dns_regexes(r"(([a-z0-9-]+\.)*")
1015
1079
  return self._dns_regexes_yara
1016
1080
 
1081
+ @property
1082
+ def dns_yara_rules_uncompiled(self):
1083
+ if self._dns_yara_rules_uncompiled is None:
1084
+ regexes_component_list = []
1085
+ for i, r in enumerate(self.dns_regexes_yara):
1086
+ regexes_component_list.append(rf"$dns_name_{i} = /\b{r.pattern}/ nocase")
1087
+ if regexes_component_list:
1088
+ regexes_component = " ".join(regexes_component_list)
1089
+ self._dns_yara_rules_uncompiled = f'rule hostname_extraction {{meta: description = "matches DNS hostname pattern derived from target(s)" strings: {regexes_component} condition: any of them}}'
1090
+ return self._dns_yara_rules_uncompiled
1091
+
1092
+ async def dns_yara_rules(self):
1093
+ if self._dns_yara_rules is None:
1094
+ if self.dns_yara_rules_uncompiled is not None:
1095
+ import yara
1096
+
1097
+ self._dns_yara_rules = await self.helpers.run_in_executor(
1098
+ yara.compile, source=self.dns_yara_rules_uncompiled
1099
+ )
1100
+ return self._dns_yara_rules
1101
+
1102
+ async def extract_in_scope_hostnames(self, s):
1103
+ """
1104
+ Given a string, uses yara to extract hostnames matching scan targets
1105
+
1106
+ Examples:
1107
+ >>> await self.scan.extract_in_scope_hostnames("http://www.evilcorp.com")
1108
+ ... {"www.evilcorp.com"}
1109
+ """
1110
+ matches = set()
1111
+ dns_yara_rules = await self.dns_yara_rules()
1112
+ if dns_yara_rules is not None:
1113
+ for match in await self.helpers.run_in_executor(dns_yara_rules.match, data=s):
1114
+ for string in match.strings:
1115
+ for instance in string.instances:
1116
+ matches.add(str(instance))
1117
+ return matches
1118
+
1017
1119
  @property
1018
1120
  def json(self):
1019
1121
  """
1020
1122
  A dictionary representation of the scan including its name, ID, targets, whitelist, blacklist, and modules
1021
1123
  """
1022
- j = dict()
1124
+ j = {}
1023
1125
  for i in ("id", "name"):
1024
1126
  v = getattr(self, i, "")
1025
1127
  if v:
1026
1128
  j.update({i: v})
1027
1129
  j["target"] = self.preset.target.json
1028
1130
  j["preset"] = self.preset.to_dict(redact_secrets=True)
1131
+ if self.start_time is not None:
1132
+ j["started_at"] = self.start_time.isoformat()
1133
+ if self.end_time is not None:
1134
+ j["finished_at"] = self.end_time.isoformat()
1135
+ if self.duration is not None:
1136
+ j["duration_seconds"] = self.duration_seconds
1137
+ if self.duration_human is not None:
1138
+ j["duration"] = self.duration_human
1029
1139
  return j
1030
1140
 
1031
1141
  def debug(self, *args, trace=False, **kwargs):
@@ -1181,7 +1291,7 @@ class Scanner:
1181
1291
  context = f"{context.__qualname__}()"
1182
1292
  filename, lineno, funcname = self.helpers.get_traceback_details(e)
1183
1293
  if self.helpers.in_exception_chain(e, (KeyboardInterrupt,)):
1184
- log.debug(f"Interrupted")
1294
+ log.debug("Interrupted")
1185
1295
  self.stop()
1186
1296
  elif isinstance(e, BrokenPipeError):
1187
1297
  log.debug(f"BrokenPipeError in {filename}:{lineno}:{funcname}(): {e}")