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/core/event/base.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import io
2
2
  import re
3
+ import uuid
3
4
  import json
4
5
  import base64
5
6
  import logging
@@ -24,6 +25,7 @@ from bbot.core.helpers import (
24
25
  is_domain,
25
26
  is_subdomain,
26
27
  is_ip,
28
+ is_ip_type,
27
29
  is_ptr,
28
30
  is_uri,
29
31
  url_depth,
@@ -58,9 +60,10 @@ class BaseEvent:
58
60
 
59
61
  Attributes:
60
62
  type (str): Specifies the type of the event, e.g., `IP_ADDRESS`, `DNS_NAME`.
61
- id (str): A unique identifier for the event.
63
+ id (str): An identifier for the event (event type + sha1 hash of data). NOT universally unique.
64
+ uuid (UUID): A universally unique identifier for the event.
62
65
  data (str or dict): The main data for the event, e.g., a URL or IP address.
63
- data_graph (str): Representation of `self.data` for Neo4j graph nodes.
66
+ data_graph (str): Representation of `self.data` for graph nodes (e.g. Neo4j).
64
67
  data_human (str): Representation of `self.data` for human output.
65
68
  data_id (str): Representation of `self.data` used to calculate the event's ID (and ultimately its hash, which is used for deduplication)
66
69
  data_json (str): Representation of `self.data` to be used in JSON serialization.
@@ -75,6 +78,7 @@ class BaseEvent:
75
78
  resolved_hosts (list of str): List of hosts to which the event data resolves, applicable for URLs and DNS names.
76
79
  parent (BaseEvent): The parent event that led to the discovery of this event.
77
80
  parent_id (str): The `id` attribute of the parent event.
81
+ parent_uuid (str): The `uuid` attribute of the parent event.
78
82
  tags (set of str): Descriptive tags for the event, e.g., `mx-record`, `in-scope`.
79
83
  module (BaseModule): The module that discovered the event.
80
84
  module_sequence (str): The sequence of modules that participated in the discovery.
@@ -154,7 +158,7 @@ class BaseEvent:
154
158
  Raises:
155
159
  ValidationError: If either `scan` or `parent` are not specified and `_dummy` is False.
156
160
  """
157
-
161
+ self._uuid = uuid.uuid4()
158
162
  self._id = None
159
163
  self._hash = None
160
164
  self._data = None
@@ -166,11 +170,13 @@ class BaseEvent:
166
170
  self._parent = None
167
171
  self._priority = None
168
172
  self._parent_id = None
173
+ self._parent_uuid = None
169
174
  self._host_original = None
170
175
  self._scope_distance = None
171
176
  self._module_priority = None
172
177
  self._resolved_hosts = set()
173
- self.dns_children = dict()
178
+ self.dns_children = {}
179
+ self.raw_dns_records = {}
174
180
  self._discovery_context = ""
175
181
  self._discovery_context_regex = re.compile(r"\{(?:event|module)[^}]*\}")
176
182
  self.web_spider_distance = 0
@@ -197,7 +203,7 @@ class BaseEvent:
197
203
  # self.scan holds the instantiated scan object (for helpers, etc.)
198
204
  self.scan = scan
199
205
  if (not self.scan) and (not self._dummy):
200
- raise ValidationError(f"Must specify scan")
206
+ raise ValidationError("Must specify scan")
201
207
  # self.scans holds a list of scan IDs from scans that encountered this event
202
208
  self.scans = []
203
209
  if scans is not None:
@@ -216,7 +222,7 @@ class BaseEvent:
216
222
 
217
223
  self.parent = parent
218
224
  if (not self.parent) and (not self._dummy):
219
- raise ValidationError(f"Must specify event parent")
225
+ raise ValidationError("Must specify event parent")
220
226
 
221
227
  if tags is not None:
222
228
  for tag in tags:
@@ -295,9 +301,9 @@ class BaseEvent:
295
301
  The purpose of internal events is to enable speculative/explorative discovery without cluttering
296
302
  the console with irrelevant or uninteresting events.
297
303
  """
298
- if not value in (True, False):
304
+ if value not in (True, False):
299
305
  raise ValueError(f'"internal" must be boolean, not {type(value)}')
300
- if value == True:
306
+ if value is True:
301
307
  self.add_tag("internal")
302
308
  else:
303
309
  self.remove_tag("internal")
@@ -335,6 +341,21 @@ class BaseEvent:
335
341
  return self.host
336
342
  return self._host_original
337
343
 
344
+ @property
345
+ def host_filterable(self):
346
+ """
347
+ A string version of the event that's used for regex-based blacklisting.
348
+
349
+ For example, the user can specify "REGEX:.*.evilcorp.com" in their blacklist, and this regex
350
+ will be applied against this property.
351
+ """
352
+ parsed_url = getattr(self, "parsed_url", None)
353
+ if parsed_url is not None:
354
+ return parsed_url.geturl()
355
+ if self.host is not None:
356
+ return str(self.host)
357
+ return ""
358
+
338
359
  @property
339
360
  def port(self):
340
361
  self.host
@@ -347,6 +368,12 @@ class BaseEvent:
347
368
  return 80
348
369
  return self._port
349
370
 
371
+ @property
372
+ def netloc(self):
373
+ if self.host and is_ip_type(self.host, network=False):
374
+ return make_netloc(self.host, self.port)
375
+ return None
376
+
350
377
  @property
351
378
  def host_stem(self):
352
379
  """
@@ -379,10 +406,20 @@ class BaseEvent:
379
406
  """
380
407
  This event's full discovery context, including those of all its parents
381
408
  """
382
- parent_path = []
409
+ discovery_path = []
410
+ if self.parent is not None and self.parent is not self:
411
+ discovery_path = self.parent.discovery_path
412
+ return discovery_path + [self.discovery_context]
413
+
414
+ @property
415
+ def parent_chain(self):
416
+ """
417
+ This event's full discovery context, including those of all its parents
418
+ """
419
+ parent_chain = []
383
420
  if self.parent is not None and self.parent is not self:
384
- parent_path = self.parent.discovery_path
385
- return parent_path + [[self.id, self.discovery_context]]
421
+ parent_chain = self.parent.parent_chain
422
+ return parent_chain + [str(self.uuid)]
386
423
 
387
424
  @property
388
425
  def words(self):
@@ -425,11 +462,6 @@ class BaseEvent:
425
462
  no_host_information = not bool(self.host)
426
463
  return self._always_emit or always_emit_tags or no_host_information
427
464
 
428
- @property
429
- def quick_emit(self):
430
- no_host_information = not bool(self.host)
431
- return self._quick_emit or no_host_information
432
-
433
465
  @property
434
466
  def id(self):
435
467
  """
@@ -439,6 +471,13 @@ class BaseEvent:
439
471
  self._id = f"{self.type}:{self.data_hash.hex()}"
440
472
  return self._id
441
473
 
474
+ @property
475
+ def uuid(self):
476
+ """
477
+ A universally unique identifier for the event
478
+ """
479
+ return f"{self.type}:{self._uuid}"
480
+
442
481
  @property
443
482
  def data_hash(self):
444
483
  """
@@ -479,12 +518,13 @@ class BaseEvent:
479
518
  for t in list(self.tags):
480
519
  if t.startswith("distance-"):
481
520
  self.remove_tag(t)
482
- if scope_distance == 0:
483
- self.add_tag("in-scope")
484
- self.remove_tag("affiliate")
485
- else:
486
- self.remove_tag("in-scope")
487
- self.add_tag(f"distance-{new_scope_distance}")
521
+ if self.host:
522
+ if scope_distance == 0:
523
+ self.add_tag("in-scope")
524
+ self.remove_tag("affiliate")
525
+ else:
526
+ self.remove_tag("in-scope")
527
+ self.add_tag(f"distance-{new_scope_distance}")
488
528
  self._scope_distance = new_scope_distance
489
529
  # apply recursively to parent events
490
530
  parent_scope_distance = getattr(self.parent, "scope_distance", None)
@@ -556,6 +596,13 @@ class BaseEvent:
556
596
  return parent_id
557
597
  return self._parent_id
558
598
 
599
+ @property
600
+ def parent_uuid(self):
601
+ parent_uuid = getattr(self.get_parent(), "uuid", None)
602
+ if parent_uuid is not None:
603
+ return parent_uuid
604
+ return self._parent_uuid
605
+
559
606
  @property
560
607
  def validators(self):
561
608
  """
@@ -722,12 +769,12 @@ class BaseEvent:
722
769
  Returns:
723
770
  dict: JSON-serializable dictionary representation of the event object.
724
771
  """
772
+ j = {}
725
773
  # type, ID, scope description
726
- j = dict()
727
- for i in ("type", "id", "scope_description"):
774
+ for i in ("type", "id", "uuid", "scope_description", "netloc"):
728
775
  v = getattr(self, i, "")
729
776
  if v:
730
- j.update({i: v})
777
+ j.update({i: str(v)})
731
778
  # event data
732
779
  data_attr = getattr(self, f"data_{mode}", None)
733
780
  if data_attr is not None:
@@ -743,6 +790,8 @@ class BaseEvent:
743
790
  j["host"] = str(self.host)
744
791
  j["resolved_hosts"] = sorted(str(h) for h in self.resolved_hosts)
745
792
  j["dns_children"] = {k: list(v) for k, v in self.dns_children.items()}
793
+ if isinstance(self.port, int):
794
+ j["port"] = self.port
746
795
  # web spider distance
747
796
  web_spider_distance = getattr(self, "web_spider_distance", None)
748
797
  if web_spider_distance is not None:
@@ -758,6 +807,9 @@ class BaseEvent:
758
807
  parent_id = self.parent_id
759
808
  if parent_id:
760
809
  j["parent"] = parent_id
810
+ parent_uuid = self.parent_uuid
811
+ if parent_uuid:
812
+ j["parent_uuid"] = parent_uuid
761
813
  # tags
762
814
  if self.tags:
763
815
  j.update({"tags": list(self.tags)})
@@ -770,6 +822,7 @@ class BaseEvent:
770
822
  # discovery context
771
823
  j["discovery_context"] = self.discovery_context
772
824
  j["discovery_path"] = self.discovery_path
825
+ j["parent_chain"] = self.parent_chain
773
826
 
774
827
  # normalize non-primitive python objects
775
828
  for k, v in list(j.items()):
@@ -909,6 +962,10 @@ class SCAN(BaseEvent):
909
962
  def discovery_path(self):
910
963
  return []
911
964
 
965
+ @property
966
+ def parent_chain(self):
967
+ return []
968
+
912
969
 
913
970
  class FINISHED(BaseEvent):
914
971
  """
@@ -956,18 +1013,20 @@ class ClosestHostEvent(DictHostEvent):
956
1013
  if not self.host:
957
1014
  for parent in self.get_parents(include_self=True):
958
1015
  # inherit closest URL
959
- if not "url" in self.data:
1016
+ if "url" not in self.data:
960
1017
  parent_url = getattr(parent, "parsed_url", None)
961
1018
  if parent_url is not None:
962
1019
  self.data["url"] = parent_url.geturl()
963
1020
  # inherit closest path
964
- if not "path" in self.data and isinstance(parent.data, dict):
1021
+ if "path" not in self.data and isinstance(parent.data, dict) and not parent.type == "HTTP_RESPONSE":
965
1022
  parent_path = parent.data.get("path", None)
966
1023
  if parent_path is not None:
967
1024
  self.data["path"] = parent_path
968
1025
  # inherit closest host
969
1026
  if parent.host:
970
1027
  self.data["host"] = str(parent.host)
1028
+ # we do this to refresh the hash
1029
+ self.data = self.data
971
1030
  break
972
1031
  # die if we still haven't found a host
973
1032
  if not self.host:
@@ -977,20 +1036,21 @@ class ClosestHostEvent(DictHostEvent):
977
1036
  class DictPathEvent(DictEvent):
978
1037
  def sanitize_data(self, data):
979
1038
  new_data = dict(data)
1039
+ new_data["path"] = str(new_data["path"])
980
1040
  file_blobs = getattr(self.scan, "_file_blobs", False)
981
1041
  folder_blobs = getattr(self.scan, "_folder_blobs", False)
982
1042
  blob = None
983
1043
  try:
984
- data_path = Path(data["path"])
985
- if data_path.is_file():
1044
+ self._data_path = Path(data["path"])
1045
+ if self._data_path.is_file():
986
1046
  self.add_tag("file")
987
1047
  if file_blobs:
988
- with open(data_path, "rb") as file:
1048
+ with open(self._data_path, "rb") as file:
989
1049
  blob = file.read()
990
- elif data_path.is_dir():
1050
+ elif self._data_path.is_dir():
991
1051
  self.add_tag("folder")
992
1052
  if folder_blobs:
993
- blob = self._tar_directory(data_path)
1053
+ blob = self._tar_directory(self._data_path)
994
1054
  except KeyError:
995
1055
  pass
996
1056
  if blob:
@@ -1053,13 +1113,23 @@ class DnsEvent(BaseEvent):
1053
1113
  if parent_module_type == "DNS":
1054
1114
  self.dns_resolve_distance += 1
1055
1115
  # self.add_tag(f"resolve-distance-{self.dns_resolve_distance}")
1116
+ # tag subdomain / domain
1117
+ if is_subdomain(self.host):
1118
+ self.add_tag("subdomain")
1119
+ elif is_domain(self.host):
1120
+ self.add_tag("domain")
1121
+ # tag private IP
1122
+ try:
1123
+ if self.host.is_private:
1124
+ self.add_tag("private-ip")
1125
+ except AttributeError:
1126
+ pass
1056
1127
 
1057
1128
 
1058
1129
  class IP_RANGE(DnsEvent):
1059
1130
  def __init__(self, *args, **kwargs):
1060
1131
  super().__init__(*args, **kwargs)
1061
- net = ipaddress.ip_network(self.data, strict=False)
1062
- self.add_tag(f"ipv{net.version}")
1132
+ self.add_tag(f"ipv{self.host.version}")
1063
1133
 
1064
1134
  def sanitize_data(self, data):
1065
1135
  return str(ipaddress.ip_network(str(data), strict=False))
@@ -1069,13 +1139,6 @@ class IP_RANGE(DnsEvent):
1069
1139
 
1070
1140
 
1071
1141
  class DNS_NAME(DnsEvent):
1072
- def __init__(self, *args, **kwargs):
1073
- super().__init__(*args, **kwargs)
1074
- if is_subdomain(self.data):
1075
- self.add_tag("subdomain")
1076
- elif is_domain(self.data):
1077
- self.add_tag("domain")
1078
-
1079
1142
  def sanitize_data(self, data):
1080
1143
  return validators.validate_host(data)
1081
1144
 
@@ -1117,7 +1180,6 @@ class URL_UNVERIFIED(BaseEvent):
1117
1180
  self.num_redirects = getattr(self.parent, "num_redirects", 0)
1118
1181
 
1119
1182
  def _data_id(self):
1120
-
1121
1183
  data = super()._data_id()
1122
1184
 
1123
1185
  # remove the querystring for URL/URL_UNVERIFIED events, because we will conditionally add it back in (based on settings)
@@ -1165,7 +1227,7 @@ class URL_UNVERIFIED(BaseEvent):
1165
1227
 
1166
1228
  def add_tag(self, tag):
1167
1229
  host_same_as_parent = self.parent and self.host == self.parent.host
1168
- if tag == "spider-danger" and host_same_as_parent and not "spider-danger" in self.tags:
1230
+ if tag == "spider-danger" and host_same_as_parent and "spider-danger" not in self.tags:
1169
1231
  # increment the web spider distance
1170
1232
  if self.type == "URL_UNVERIFIED":
1171
1233
  self.web_spider_distance += 1
@@ -1187,7 +1249,7 @@ class URL_UNVERIFIED(BaseEvent):
1187
1249
 
1188
1250
  def _words(self):
1189
1251
  first_elem = self.parsed_url.path.lstrip("/").split("/")[0]
1190
- if not "." in first_elem:
1252
+ if "." not in first_elem:
1191
1253
  return extract_words(first_elem)
1192
1254
  return set()
1193
1255
 
@@ -1204,7 +1266,6 @@ class URL_UNVERIFIED(BaseEvent):
1204
1266
 
1205
1267
 
1206
1268
  class URL(URL_UNVERIFIED):
1207
-
1208
1269
  def __init__(self, *args, **kwargs):
1209
1270
  super().__init__(*args, **kwargs)
1210
1271
 
@@ -1216,7 +1277,7 @@ class URL(URL_UNVERIFIED):
1216
1277
  @property
1217
1278
  def resolved_hosts(self):
1218
1279
  # TODO: remove this when we rip out httpx
1219
- return set(".".join(i.split("-")[1:]) for i in self.tags if i.startswith("ip-"))
1280
+ return {".".join(i.split("-")[1:]) for i in self.tags if i.startswith("ip-")}
1220
1281
 
1221
1282
  @property
1222
1283
  def pretty_string(self):
@@ -1246,7 +1307,6 @@ class URL_HINT(URL_UNVERIFIED):
1246
1307
 
1247
1308
 
1248
1309
  class WEB_PARAMETER(DictHostEvent):
1249
-
1250
1310
  def _data_id(self):
1251
1311
  # dedupe by url:name:param_type
1252
1312
  url = self.data.get("url", "")
@@ -1495,14 +1555,39 @@ class WAF(DictHostEvent):
1495
1555
 
1496
1556
 
1497
1557
  class FILESYSTEM(DictPathEvent):
1498
- pass
1499
-
1500
-
1501
- class RAW_DNS_RECORD(DictHostEvent):
1558
+ def __init__(self, *args, **kwargs):
1559
+ super().__init__(*args, **kwargs)
1560
+ if self._data_path.is_file():
1561
+ # detect type of file content using magic
1562
+ from bbot.core.helpers.libmagic import get_magic_info, get_compression
1563
+
1564
+ extension, mime_type, description, confidence = get_magic_info(self.data["path"])
1565
+ self.data["magic_extension"] = extension
1566
+ self.data["magic_mime_type"] = mime_type
1567
+ self.data["magic_description"] = description
1568
+ self.data["magic_confidence"] = confidence
1569
+ # detection compression
1570
+ compression = get_compression(mime_type)
1571
+ if compression:
1572
+ self.add_tag("compressed")
1573
+ self.add_tag(f"{compression}-archive")
1574
+ self.data["compression"] = compression
1575
+ # refresh hash
1576
+ self.data = self.data
1577
+
1578
+
1579
+ class RAW_DNS_RECORD(DictHostEvent, DnsEvent):
1502
1580
  # don't emit raw DNS records for affiliates
1503
1581
  _always_emit_tags = ["target"]
1504
1582
 
1505
1583
 
1584
+ class MOBILE_APP(DictEvent):
1585
+ _always_emit = True
1586
+
1587
+ def _pretty_string(self):
1588
+ return self.data["url"]
1589
+
1590
+
1506
1591
  def make_event(
1507
1592
  data,
1508
1593
  event_type=None,
@@ -1569,23 +1654,23 @@ def make_event(
1569
1654
  tags = set(tags)
1570
1655
 
1571
1656
  if is_event(data):
1572
- data = copy(data)
1573
- if scan is not None and not data.scan:
1574
- data.scan = scan
1575
- if scans is not None and not data.scans:
1576
- data.scans = scans
1657
+ event = copy(data)
1658
+ if scan is not None and not event.scan:
1659
+ event.scan = scan
1660
+ if scans is not None and not event.scans:
1661
+ event.scans = scans
1577
1662
  if module is not None:
1578
- data.module = module
1663
+ event.module = module
1579
1664
  if parent is not None:
1580
- data.parent = parent
1665
+ event.parent = parent
1581
1666
  if context is not None:
1582
- data.discovery_context = context
1583
- if internal == True:
1584
- data.internal = True
1667
+ event.discovery_context = context
1668
+ if internal is True:
1669
+ event.internal = True
1585
1670
  if tags:
1586
- data.tags = tags.union(data.tags)
1671
+ event.tags = tags.union(event.tags)
1587
1672
  event_type = data.type
1588
- return data
1673
+ return event
1589
1674
  else:
1590
1675
  if event_type is None:
1591
1676
  event_type, data = get_event_type(data)
@@ -1615,6 +1700,13 @@ def make_event(
1615
1700
  if event_type == "USERNAME" and validators.soft_validate(data, "email"):
1616
1701
  event_type = "EMAIL_ADDRESS"
1617
1702
  tags.add("affiliate")
1703
+ # Convert single-host IP_RANGE to IP_ADDRESS
1704
+ if event_type == "IP_RANGE":
1705
+ with suppress(Exception):
1706
+ net = ipaddress.ip_network(data, strict=False)
1707
+ if net.prefixlen == net.max_prefixlen:
1708
+ event_type = "IP_ADDRESS"
1709
+ data = net.network_address
1618
1710
 
1619
1711
  event_class = globals().get(event_type, DefaultEvent)
1620
1712
 
@@ -1671,6 +1763,9 @@ def event_from_json(j, siem_friendly=False):
1671
1763
  data = j["data"]
1672
1764
  kwargs["data"] = data
1673
1765
  event = make_event(**kwargs)
1766
+ event_uuid = j.get("uuid", None)
1767
+ if event_uuid is not None:
1768
+ event._uuid = uuid.UUID(event_uuid.split(":")[-1])
1674
1769
 
1675
1770
  resolved_hosts = j.get("resolved_hosts", [])
1676
1771
  event._resolved_hosts = set(resolved_hosts)
@@ -1680,6 +1775,10 @@ def event_from_json(j, siem_friendly=False):
1680
1775
  parent_id = j.get("parent", None)
1681
1776
  if parent_id is not None:
1682
1777
  event._parent_id = parent_id
1778
+ parent_uuid = j.get("parent_uuid", None)
1779
+ if parent_uuid is not None:
1780
+ parent_type, parent_uuid = parent_uuid.split(":", 1)
1781
+ event._parent_uuid = parent_type + ":" + str(uuid.UUID(parent_uuid))
1683
1782
  return event
1684
1783
  except KeyError as e:
1685
1784
  raise ValidationError(f"Event missing required field: {e}")
@@ -5,14 +5,14 @@ import mmap
5
5
 
6
6
  class BloomFilter:
7
7
  """
8
- Simple bloom filter implementation capable of rougly 400K lookups/s.
8
+ Simple bloom filter implementation capable of roughly 400K lookups/s.
9
9
 
10
10
  BBOT uses bloom filters in scenarios like DNS brute-forcing, where it's useful to keep track
11
11
  of which mutations have been tried so far.
12
12
 
13
13
  A 100-megabyte bloom filter (800M bits) can store 10M entries with a .01% false-positive rate.
14
14
  A python hash is 36 bytes. So if you wanted to store these in a set, this would take up
15
- 36 * 10M * 2 (key+value) == 720 megabytes. So we save rougly 7 times the space.
15
+ 36 * 10M * 2 (key+value) == 720 megabytes. So we save roughly 7 times the space.
16
16
  """
17
17
 
18
18
  def __init__(self, size=8000000):
@@ -64,8 +64,15 @@ class BloomFilter:
64
64
  hash = (hash * 0x01000193) % 2**32 # 16777619
65
65
  return hash
66
66
 
67
- def __del__(self):
67
+ def close(self):
68
+ """Explicitly close the memory-mapped file."""
68
69
  self.mmap_file.close()
69
70
 
71
+ def __del__(self):
72
+ try:
73
+ self.close()
74
+ except Exception:
75
+ pass
76
+
70
77
  def __contains__(self, item):
71
78
  return self.check(item)
@@ -210,9 +210,10 @@ async def _write_proc_line(proc, chunk):
210
210
  return True
211
211
  except Exception as e:
212
212
  proc_args = [str(s) for s in getattr(proc, "args", [])]
213
- command = " ".join(proc_args)
214
- log.warning(f"Error writing line to stdin for command: {command}: {e}")
215
- log.trace(traceback.format_exc())
213
+ command = " ".join(proc_args).strip()
214
+ if command:
215
+ log.warning(f"Error writing line to stdin for command: {command}: {e}")
216
+ log.trace(traceback.format_exc())
216
217
  return False
217
218
 
218
219
 
@@ -268,11 +269,11 @@ def _prepare_command_kwargs(self, command, kwargs):
268
269
  (['sudo', '-E', '-A', 'LD_LIBRARY_PATH=...', 'PATH=...', 'ls', '-l'], {'limit': 104857600, 'stdout': -1, 'stderr': -1, 'env': environ(...)})
269
270
  """
270
271
  # limit = 100MB (this is needed for cases like httpx that are sending large JSON blobs over stdout)
271
- if not "limit" in kwargs:
272
+ if "limit" not in kwargs:
272
273
  kwargs["limit"] = 1024 * 1024 * 100
273
- if not "stdout" in kwargs:
274
+ if "stdout" not in kwargs:
274
275
  kwargs["stdout"] = asyncio.subprocess.PIPE
275
- if not "stderr" in kwargs:
276
+ if "stderr" not in kwargs:
276
277
  kwargs["stderr"] = asyncio.subprocess.PIPE
277
278
  sudo = kwargs.pop("sudo", False)
278
279
 
@@ -285,7 +286,7 @@ def _prepare_command_kwargs(self, command, kwargs):
285
286
 
286
287
  # use full path of binary, if not already specified
287
288
  binary = command[0]
288
- if not "/" in binary:
289
+ if "/" not in binary:
289
290
  binary_full_path = which(binary)
290
291
  if binary_full_path is None:
291
292
  raise SubprocessError(f'Command "{binary}" was not found')
@@ -295,7 +296,7 @@ def _prepare_command_kwargs(self, command, kwargs):
295
296
  if sudo and os.geteuid() != 0:
296
297
  self.depsinstaller.ensure_root()
297
298
  env["SUDO_ASKPASS"] = str((self.tools_dir / self.depsinstaller.askpass_filename).resolve())
298
- env["BBOT_SUDO_PASS"] = self.depsinstaller._sudo_password
299
+ env["BBOT_SUDO_PASS"] = self.depsinstaller.encrypted_sudo_pw
299
300
  kwargs["env"] = env
300
301
 
301
302
  PATH = os.environ.get("PATH", "")