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/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)
@@ -231,6 +247,9 @@ class Scanner:
231
247
 
232
248
  self._dns_strings = None
233
249
  self._dns_regexes = None
250
+ self._dns_regexes_yara = None
251
+ self._dns_yara_rules_uncompiled = None
252
+ self._dns_yara_rules = None
234
253
 
235
254
  self.__log_handlers = None
236
255
  self._log_handler_backup = []
@@ -240,6 +259,9 @@ class Scanner:
240
259
  Creates the scan's output folder, loads its modules, and calls their .setup() methods.
241
260
  """
242
261
 
262
+ # update the master PID
263
+ SHARED_INTERPRETER_STATE.update_scan_pid()
264
+
243
265
  self.helpers.mkdir(self.home)
244
266
  if not self._prepped:
245
267
  # save scan preset
@@ -247,7 +269,7 @@ class Scanner:
247
269
  f.write(self.preset.to_yaml())
248
270
 
249
271
  # log scan overview
250
- 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"
251
273
  details = []
252
274
  if self.whitelist != self.target:
253
275
  details.append(f"{len(self.whitelist):,} in whitelist")
@@ -271,7 +293,9 @@ class Scanner:
271
293
  self.debug(
272
294
  f"Setting intercept module {intercept_module.name}._incoming_event_queue to previous intercept module {prev_intercept_module.name}.outgoing_event_queue"
273
295
  )
274
- 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
275
299
 
276
300
  # abort if there are no output modules
277
301
  num_output_modules = len([m for m in self.modules.values() if m._type == "output"])
@@ -303,18 +327,18 @@ class Scanner:
303
327
 
304
328
  async def async_start(self):
305
329
  """ """
306
- failed = True
307
- scan_start_time = datetime.now()
330
+ self.start_time = datetime.now()
331
+ self.root_event.data["started_at"] = self.start_time.isoformat()
308
332
  try:
309
333
  await self._prep()
310
334
 
311
335
  self._start_log_handlers()
312
- 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)}')
313
337
  self.trace(f"Target: {self.preset.target.json}")
314
338
  self.trace(f"Preset: {self.preset.to_dict(redact_secrets=True)}")
315
339
 
316
340
  if not self.target:
317
- self.warning(f"No scan targets specified")
341
+ self.warning("No scan targets specified")
318
342
 
319
343
  # start status ticker
320
344
  self.ticker_task = asyncio.create_task(
@@ -324,7 +348,7 @@ class Scanner:
324
348
  self.status = "STARTING"
325
349
 
326
350
  if not self.modules:
327
- self.error(f"No modules loaded")
351
+ self.error("No modules loaded")
328
352
  self.status = "FAILED"
329
353
  return
330
354
  else:
@@ -338,7 +362,8 @@ class Scanner:
338
362
 
339
363
  # distribute seed events
340
364
  self.init_events_task = asyncio.create_task(
341
- 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()",
342
367
  )
343
368
 
344
369
  # main scan loop
@@ -360,16 +385,19 @@ class Scanner:
360
385
  if self._finished_init and self.modules_finished:
361
386
  new_activity = await self.finish()
362
387
  if not new_activity:
388
+ self._success = True
389
+ scan_finish_event = await self._mark_finished()
390
+ yield scan_finish_event
363
391
  break
364
392
 
365
393
  await asyncio.sleep(0.1)
366
394
 
367
- failed = False
395
+ self._success = True
368
396
 
369
397
  except BaseException as e:
370
398
  if self.helpers.in_exception_chain(e, (KeyboardInterrupt, asyncio.CancelledError)):
371
399
  self.stop()
372
- failed = False
400
+ self._success = True
373
401
  else:
374
402
  try:
375
403
  raise
@@ -393,26 +421,47 @@ class Scanner:
393
421
  await self._report()
394
422
  await self._cleanup()
395
423
 
396
- log_fn = self.hugesuccess
397
- if self.status == "ABORTING":
398
- self.status = "ABORTED"
399
- log_fn = self.hugewarning
400
- elif failed:
401
- self.status = "FAILED"
402
- log_fn = self.critical
403
- else:
404
- self.status = "FINISHED"
405
-
406
- scan_run_time = datetime.now() - scan_start_time
407
- scan_run_time = self.helpers.human_timedelta(scan_run_time)
408
- log_fn(f"Scan {self.name} completed in {scan_run_time} with status {self.status}")
409
-
410
424
  await self.dispatcher.on_finish(self)
411
425
 
412
426
  self._stop_log_handlers()
413
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
+
414
463
  def _start_modules(self):
415
- self.verbose(f"Starting module worker loops")
464
+ self.verbose("Starting module worker loops")
416
465
  for module in self.modules.values():
417
466
  module.start()
418
467
 
@@ -436,17 +485,17 @@ class Scanner:
436
485
  Soft-failed modules are not set to an error state but are also removed if `remove_failed` is True.
437
486
  """
438
487
  await self.load_modules()
439
- self.verbose(f"Setting up modules")
488
+ self.verbose("Setting up modules")
440
489
  succeeded = []
441
490
  hard_failed = []
442
491
  soft_failed = []
443
492
 
444
493
  async for task in self.helpers.as_completed([m._setup() for m in self.modules.values()]):
445
494
  module, status, msg = await task
446
- if status == True:
495
+ if status is True:
447
496
  self.debug(f"Setup succeeded for {module.name} ({msg})")
448
497
  succeeded.append(module.name)
449
- elif status == False:
498
+ elif status is False:
450
499
  self.warning(f"Setup hard-failed for {module.name}: {msg}")
451
500
  self.modules[module.name].set_error_state()
452
501
  hard_failed.append(module.name)
@@ -488,11 +537,11 @@ class Scanner:
488
537
  """
489
538
  if not self._modules_loaded:
490
539
  if not self.preset.modules:
491
- self.warning(f"No modules to load")
540
+ self.warning("No modules to load")
492
541
  return
493
542
 
494
543
  if not self.preset.scan_modules:
495
- self.warning(f"No scan modules to load")
544
+ self.warning("No scan modules to load")
496
545
 
497
546
  # install module dependencies
498
547
  succeeded, failed = await self.helpers.depsinstaller.install(*self.preset.modules)
@@ -636,7 +685,7 @@ class Scanner:
636
685
 
637
686
  if modules_errored:
638
687
  self.verbose(
639
- 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))})'
640
689
  )
641
690
 
642
691
  num_queued_events = self.num_queued_events
@@ -673,7 +722,7 @@ class Scanner:
673
722
  memory_usage = module.memory_usage
674
723
  module_memory_usage.append((module.name, memory_usage))
675
724
  module_memory_usage.sort(key=lambda x: x[-1], reverse=True)
676
- self.debug(f"MODULE MEMORY USAGE:")
725
+ self.debug("MODULE MEMORY USAGE:")
677
726
  for module_name, usage in module_memory_usage:
678
727
  self.debug(f" - {module_name}: {self.helpers.bytes_to_human(usage)}")
679
728
 
@@ -720,12 +769,12 @@ class Scanner:
720
769
  # Trigger .finished() on every module and start over
721
770
  log.info("Finishing scan")
722
771
  for module in self.modules.values():
723
- 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})
724
773
  await module.queue_event(finished_event)
725
774
  self.verbose("Completed finish()")
726
775
  return True
727
- # Return False if no new events were generated since last time
728
776
  self.verbose("Completed final finish()")
777
+ # Return False if no new events were generated since last time
729
778
  return False
730
779
 
731
780
  def _drain_queues(self):
@@ -848,6 +897,10 @@ class Scanner:
848
897
  def target(self):
849
898
  return self.preset.target
850
899
 
900
+ @property
901
+ def seeds(self):
902
+ return self.preset.seeds
903
+
851
904
  @property
852
905
  def whitelist(self):
853
906
  return self.preset.whitelist
@@ -944,12 +997,25 @@ class Scanner:
944
997
  }
945
998
  ```
946
999
  """
947
- 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)
948
1015
  root_event._id = self.id
949
1016
  root_event.scope_distance = 0
950
1017
  root_event.parent = root_event
951
1018
  root_event.module = self._make_dummy_module(name="TARGET", _type="TARGET")
952
- root_event.discovery_context = f"Scan {self.name} started at {root_event.timestamp}"
953
1019
  return root_event
954
1020
 
955
1021
  @property
@@ -958,14 +1024,13 @@ class Scanner:
958
1024
  A list of DNS hostname strings generated from the scan target
959
1025
  """
960
1026
  if self._dns_strings is None:
961
- dns_targets = set(t.host for t in self.target if t.host and isinstance(t.host, str))
962
- dns_whitelist = set(t.host for t in self.whitelist if t.host and isinstance(t.host, str))
963
- dns_targets.update(dns_whitelist)
964
- dns_targets = sorted(dns_targets, key=len)
965
- 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()
966
1030
  dns_strings = []
967
- for t in dns_targets:
968
- 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)
969
1034
  dns_strings.append(t)
970
1035
  self._dns_strings = dns_strings
971
1036
  return self._dns_strings
@@ -1000,7 +1065,7 @@ class Scanner:
1000
1065
  ... for match in regex.finditer(response.text):
1001
1066
  ... hostname = match.group().lower()
1002
1067
  """
1003
- if not self._dns_regexes:
1068
+ if self._dns_regexes is None:
1004
1069
  self._dns_regexes = self._generate_dns_regexes(r"((?:(?:[\w-]+)\.)+")
1005
1070
  return self._dns_regexes
1006
1071
 
@@ -1009,20 +1074,68 @@ class Scanner:
1009
1074
  """
1010
1075
  Returns a list of DNS hostname regexes formatted specifically for compatibility with YARA rules.
1011
1076
  """
1012
- return self._generate_dns_regexes(r"(([a-z0-9-]+\.)+")
1077
+ if self._dns_regexes_yara is None:
1078
+ self._dns_regexes_yara = self._generate_dns_regexes(r"(([a-z0-9-]+\.)*")
1079
+ return self._dns_regexes_yara
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
1013
1118
 
1014
1119
  @property
1015
1120
  def json(self):
1016
1121
  """
1017
1122
  A dictionary representation of the scan including its name, ID, targets, whitelist, blacklist, and modules
1018
1123
  """
1019
- j = dict()
1124
+ j = {}
1020
1125
  for i in ("id", "name"):
1021
1126
  v = getattr(self, i, "")
1022
1127
  if v:
1023
1128
  j.update({i: v})
1024
1129
  j["target"] = self.preset.target.json
1025
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
1026
1139
  return j
1027
1140
 
1028
1141
  def debug(self, *args, trace=False, **kwargs):
@@ -1178,7 +1291,7 @@ class Scanner:
1178
1291
  context = f"{context.__qualname__}()"
1179
1292
  filename, lineno, funcname = self.helpers.get_traceback_details(e)
1180
1293
  if self.helpers.in_exception_chain(e, (KeyboardInterrupt,)):
1181
- log.debug(f"Interrupted")
1294
+ log.debug("Interrupted")
1182
1295
  self.stop()
1183
1296
  elif isinstance(e, BrokenPipeError):
1184
1297
  log.debug(f"BrokenPipeError in {filename}:{lineno}:{funcname}(): {e}")