bbot 2.0.1.4720rc0__py3-none-any.whl → 2.3.0.5401rc0__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 (278) 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 +12 -10
  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 +20 -21
  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 +29 -12
  86. bbot/modules/internal/dnsresolve.py +22 -22
  87. bbot/modules/internal/excavate.py +97 -59
  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 +8 -11
  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 +18 -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 -2
  146. bbot/presets/spider.yml +4 -0
  147. bbot/presets/subdomain-enum.yml +7 -7
  148. bbot/presets/web/dotnet-audit.yml +0 -1
  149. bbot/scanner/manager.py +5 -16
  150. bbot/scanner/preset/args.py +46 -26
  151. bbot/scanner/preset/environ.py +7 -2
  152. bbot/scanner/preset/path.py +7 -4
  153. bbot/scanner/preset/preset.py +36 -23
  154. bbot/scanner/scanner.py +172 -62
  155. bbot/scanner/target.py +236 -434
  156. bbot/scripts/docs.py +1 -1
  157. bbot/test/bbot_fixtures.py +13 -3
  158. bbot/test/conftest.py +132 -100
  159. bbot/test/fastapi_test.py +17 -0
  160. bbot/test/owasp_mastg.apk +0 -0
  161. bbot/test/run_tests.sh +4 -4
  162. bbot/test/test.conf +2 -0
  163. bbot/test/test_step_1/test__module__tests.py +0 -1
  164. bbot/test/test_step_1/test_bbot_fastapi.py +79 -0
  165. bbot/test/test_step_1/test_bloom_filter.py +2 -1
  166. bbot/test/test_step_1/test_cli.py +138 -64
  167. bbot/test/test_step_1/test_dns.py +61 -27
  168. bbot/test/test_step_1/test_engine.py +17 -19
  169. bbot/test/test_step_1/test_events.py +183 -30
  170. bbot/test/test_step_1/test_helpers.py +64 -29
  171. bbot/test/test_step_1/test_manager_deduplication.py +1 -1
  172. bbot/test/test_step_1/test_manager_scope_accuracy.py +333 -330
  173. bbot/test/test_step_1/test_modules_basic.py +68 -70
  174. bbot/test/test_step_1/test_presets.py +183 -100
  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 +4 -3
  179. bbot/test/test_step_1/test_target.py +242 -145
  180. bbot/test/test_step_1/test_web.py +14 -10
  181. bbot/test/test_step_2/module_tests/base.py +15 -7
  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 +28 -48
  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 -6
  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 +22 -9
  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 +16 -16
  233. bbot/test/test_step_2/module_tests/test_module_ntlm.py +8 -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_pgp.py +2 -2
  241. bbot/test/test_step_2/module_tests/test_module_portscan.py +9 -8
  242. bbot/test/test_step_2/module_tests/test_module_postgres.py +74 -0
  243. bbot/test/test_step_2/module_tests/test_module_postman.py +84 -253
  244. bbot/test/test_step_2/module_tests/test_module_postman_download.py +439 -0
  245. bbot/test/test_step_2/module_tests/test_module_rapiddns.py +93 -1
  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 +14 -14
  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 +4 -8
  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 -14
  261. bbot/test/test_step_2/module_tests/test_module_viewdns.py +1 -1
  262. bbot/test/test_step_2/module_tests/test_module_wayback.py +1 -1
  263. bbot/test/test_step_2/template_tests/test_template_subdomain_enum.py +2 -2
  264. bbot/wordlists/devops_mutations.txt +1 -1
  265. bbot/wordlists/ffuf_shortname_candidates.txt +1 -1
  266. bbot/wordlists/nameservers.txt +1 -1
  267. bbot/wordlists/paramminer_headers.txt +1 -1
  268. bbot/wordlists/paramminer_parameters.txt +1 -1
  269. bbot/wordlists/raft-small-extensions-lowercase_CLEANED.txt +1 -1
  270. bbot/wordlists/valid_url_schemes.txt +1 -1
  271. {bbot-2.0.1.4720rc0.dist-info → bbot-2.3.0.5401rc0.dist-info}/METADATA +48 -18
  272. bbot-2.3.0.5401rc0.dist-info/RECORD +421 -0
  273. {bbot-2.0.1.4720rc0.dist-info → bbot-2.3.0.5401rc0.dist-info}/WHEEL +1 -1
  274. bbot/modules/unstructured.py +0 -163
  275. bbot/test/test_step_2/module_tests/test_module_unstructured.py +0 -102
  276. bbot-2.0.1.4720rc0.dist-info/RECORD +0 -387
  277. {bbot-2.0.1.4720rc0.dist-info → bbot-2.3.0.5401rc0.dist-info}/LICENSE +0 -0
  278. {bbot-2.0.1.4720rc0.dist-info → bbot-2.3.0.5401rc0.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,12 +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()
174
- self.raw_dns_records = dict()
178
+ self.dns_children = {}
179
+ self.raw_dns_records = {}
175
180
  self._discovery_context = ""
176
181
  self._discovery_context_regex = re.compile(r"\{(?:event|module)[^}]*\}")
177
182
  self.web_spider_distance = 0
@@ -198,7 +203,7 @@ class BaseEvent:
198
203
  # self.scan holds the instantiated scan object (for helpers, etc.)
199
204
  self.scan = scan
200
205
  if (not self.scan) and (not self._dummy):
201
- raise ValidationError(f"Must specify scan")
206
+ raise ValidationError("Must specify scan")
202
207
  # self.scans holds a list of scan IDs from scans that encountered this event
203
208
  self.scans = []
204
209
  if scans is not None:
@@ -217,7 +222,7 @@ class BaseEvent:
217
222
 
218
223
  self.parent = parent
219
224
  if (not self.parent) and (not self._dummy):
220
- raise ValidationError(f"Must specify event parent")
225
+ raise ValidationError("Must specify event parent")
221
226
 
222
227
  if tags is not None:
223
228
  for tag in tags:
@@ -296,9 +301,9 @@ class BaseEvent:
296
301
  The purpose of internal events is to enable speculative/explorative discovery without cluttering
297
302
  the console with irrelevant or uninteresting events.
298
303
  """
299
- if not value in (True, False):
304
+ if value not in (True, False):
300
305
  raise ValueError(f'"internal" must be boolean, not {type(value)}')
301
- if value == True:
306
+ if value is True:
302
307
  self.add_tag("internal")
303
308
  else:
304
309
  self.remove_tag("internal")
@@ -336,6 +341,21 @@ class BaseEvent:
336
341
  return self.host
337
342
  return self._host_original
338
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
+
339
359
  @property
340
360
  def port(self):
341
361
  self.host
@@ -348,6 +368,12 @@ class BaseEvent:
348
368
  return 80
349
369
  return self._port
350
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
+
351
377
  @property
352
378
  def host_stem(self):
353
379
  """
@@ -393,7 +419,7 @@ class BaseEvent:
393
419
  parent_chain = []
394
420
  if self.parent is not None and self.parent is not self:
395
421
  parent_chain = self.parent.parent_chain
396
- return parent_chain + [self.id]
422
+ return parent_chain + [str(self.uuid)]
397
423
 
398
424
  @property
399
425
  def words(self):
@@ -436,11 +462,6 @@ class BaseEvent:
436
462
  no_host_information = not bool(self.host)
437
463
  return self._always_emit or always_emit_tags or no_host_information
438
464
 
439
- @property
440
- def quick_emit(self):
441
- no_host_information = not bool(self.host)
442
- return self._quick_emit or no_host_information
443
-
444
465
  @property
445
466
  def id(self):
446
467
  """
@@ -450,6 +471,13 @@ class BaseEvent:
450
471
  self._id = f"{self.type}:{self.data_hash.hex()}"
451
472
  return self._id
452
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
+
453
481
  @property
454
482
  def data_hash(self):
455
483
  """
@@ -490,12 +518,13 @@ class BaseEvent:
490
518
  for t in list(self.tags):
491
519
  if t.startswith("distance-"):
492
520
  self.remove_tag(t)
493
- if scope_distance == 0:
494
- self.add_tag("in-scope")
495
- self.remove_tag("affiliate")
496
- else:
497
- self.remove_tag("in-scope")
498
- 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}")
499
528
  self._scope_distance = new_scope_distance
500
529
  # apply recursively to parent events
501
530
  parent_scope_distance = getattr(self.parent, "scope_distance", None)
@@ -567,6 +596,13 @@ class BaseEvent:
567
596
  return parent_id
568
597
  return self._parent_id
569
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
+
570
606
  @property
571
607
  def validators(self):
572
608
  """
@@ -733,12 +769,12 @@ class BaseEvent:
733
769
  Returns:
734
770
  dict: JSON-serializable dictionary representation of the event object.
735
771
  """
772
+ j = {}
736
773
  # type, ID, scope description
737
- j = dict()
738
- for i in ("type", "id", "scope_description"):
774
+ for i in ("type", "id", "uuid", "scope_description", "netloc"):
739
775
  v = getattr(self, i, "")
740
776
  if v:
741
- j.update({i: v})
777
+ j.update({i: str(v)})
742
778
  # event data
743
779
  data_attr = getattr(self, f"data_{mode}", None)
744
780
  if data_attr is not None:
@@ -754,6 +790,8 @@ class BaseEvent:
754
790
  j["host"] = str(self.host)
755
791
  j["resolved_hosts"] = sorted(str(h) for h in self.resolved_hosts)
756
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
757
795
  # web spider distance
758
796
  web_spider_distance = getattr(self, "web_spider_distance", None)
759
797
  if web_spider_distance is not None:
@@ -769,6 +807,9 @@ class BaseEvent:
769
807
  parent_id = self.parent_id
770
808
  if parent_id:
771
809
  j["parent"] = parent_id
810
+ parent_uuid = self.parent_uuid
811
+ if parent_uuid:
812
+ j["parent_uuid"] = parent_uuid
772
813
  # tags
773
814
  if self.tags:
774
815
  j.update({"tags": list(self.tags)})
@@ -972,18 +1013,20 @@ class ClosestHostEvent(DictHostEvent):
972
1013
  if not self.host:
973
1014
  for parent in self.get_parents(include_self=True):
974
1015
  # inherit closest URL
975
- if not "url" in self.data:
1016
+ if "url" not in self.data:
976
1017
  parent_url = getattr(parent, "parsed_url", None)
977
1018
  if parent_url is not None:
978
1019
  self.data["url"] = parent_url.geturl()
979
1020
  # inherit closest path
980
- 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":
981
1022
  parent_path = parent.data.get("path", None)
982
1023
  if parent_path is not None:
983
1024
  self.data["path"] = parent_path
984
1025
  # inherit closest host
985
1026
  if parent.host:
986
1027
  self.data["host"] = str(parent.host)
1028
+ # we do this to refresh the hash
1029
+ self.data = self.data
987
1030
  break
988
1031
  # die if we still haven't found a host
989
1032
  if not self.host:
@@ -993,20 +1036,21 @@ class ClosestHostEvent(DictHostEvent):
993
1036
  class DictPathEvent(DictEvent):
994
1037
  def sanitize_data(self, data):
995
1038
  new_data = dict(data)
1039
+ new_data["path"] = str(new_data["path"])
996
1040
  file_blobs = getattr(self.scan, "_file_blobs", False)
997
1041
  folder_blobs = getattr(self.scan, "_folder_blobs", False)
998
1042
  blob = None
999
1043
  try:
1000
- data_path = Path(data["path"])
1001
- if data_path.is_file():
1044
+ self._data_path = Path(data["path"])
1045
+ if self._data_path.is_file():
1002
1046
  self.add_tag("file")
1003
1047
  if file_blobs:
1004
- with open(data_path, "rb") as file:
1048
+ with open(self._data_path, "rb") as file:
1005
1049
  blob = file.read()
1006
- elif data_path.is_dir():
1050
+ elif self._data_path.is_dir():
1007
1051
  self.add_tag("folder")
1008
1052
  if folder_blobs:
1009
- blob = self._tar_directory(data_path)
1053
+ blob = self._tar_directory(self._data_path)
1010
1054
  except KeyError:
1011
1055
  pass
1012
1056
  if blob:
@@ -1085,8 +1129,7 @@ class DnsEvent(BaseEvent):
1085
1129
  class IP_RANGE(DnsEvent):
1086
1130
  def __init__(self, *args, **kwargs):
1087
1131
  super().__init__(*args, **kwargs)
1088
- net = ipaddress.ip_network(self.data, strict=False)
1089
- self.add_tag(f"ipv{net.version}")
1132
+ self.add_tag(f"ipv{self.host.version}")
1090
1133
 
1091
1134
  def sanitize_data(self, data):
1092
1135
  return str(ipaddress.ip_network(str(data), strict=False))
@@ -1137,7 +1180,6 @@ class URL_UNVERIFIED(BaseEvent):
1137
1180
  self.num_redirects = getattr(self.parent, "num_redirects", 0)
1138
1181
 
1139
1182
  def _data_id(self):
1140
-
1141
1183
  data = super()._data_id()
1142
1184
 
1143
1185
  # remove the querystring for URL/URL_UNVERIFIED events, because we will conditionally add it back in (based on settings)
@@ -1185,7 +1227,7 @@ class URL_UNVERIFIED(BaseEvent):
1185
1227
 
1186
1228
  def add_tag(self, tag):
1187
1229
  host_same_as_parent = self.parent and self.host == self.parent.host
1188
- 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:
1189
1231
  # increment the web spider distance
1190
1232
  if self.type == "URL_UNVERIFIED":
1191
1233
  self.web_spider_distance += 1
@@ -1207,7 +1249,7 @@ class URL_UNVERIFIED(BaseEvent):
1207
1249
 
1208
1250
  def _words(self):
1209
1251
  first_elem = self.parsed_url.path.lstrip("/").split("/")[0]
1210
- if not "." in first_elem:
1252
+ if "." not in first_elem:
1211
1253
  return extract_words(first_elem)
1212
1254
  return set()
1213
1255
 
@@ -1224,7 +1266,6 @@ class URL_UNVERIFIED(BaseEvent):
1224
1266
 
1225
1267
 
1226
1268
  class URL(URL_UNVERIFIED):
1227
-
1228
1269
  def __init__(self, *args, **kwargs):
1229
1270
  super().__init__(*args, **kwargs)
1230
1271
 
@@ -1236,7 +1277,7 @@ class URL(URL_UNVERIFIED):
1236
1277
  @property
1237
1278
  def resolved_hosts(self):
1238
1279
  # TODO: remove this when we rip out httpx
1239
- 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-")}
1240
1281
 
1241
1282
  @property
1242
1283
  def pretty_string(self):
@@ -1266,7 +1307,6 @@ class URL_HINT(URL_UNVERIFIED):
1266
1307
 
1267
1308
 
1268
1309
  class WEB_PARAMETER(DictHostEvent):
1269
-
1270
1310
  def _data_id(self):
1271
1311
  # dedupe by url:name:param_type
1272
1312
  url = self.data.get("url", "")
@@ -1515,7 +1555,25 @@ class WAF(DictHostEvent):
1515
1555
 
1516
1556
 
1517
1557
  class FILESYSTEM(DictPathEvent):
1518
- pass
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
1519
1577
 
1520
1578
 
1521
1579
  class RAW_DNS_RECORD(DictHostEvent, DnsEvent):
@@ -1523,6 +1581,13 @@ class RAW_DNS_RECORD(DictHostEvent, DnsEvent):
1523
1581
  _always_emit_tags = ["target"]
1524
1582
 
1525
1583
 
1584
+ class MOBILE_APP(DictEvent):
1585
+ _always_emit = True
1586
+
1587
+ def _pretty_string(self):
1588
+ return self.data["url"]
1589
+
1590
+
1526
1591
  def make_event(
1527
1592
  data,
1528
1593
  event_type=None,
@@ -1589,23 +1654,23 @@ def make_event(
1589
1654
  tags = set(tags)
1590
1655
 
1591
1656
  if is_event(data):
1592
- data = copy(data)
1593
- if scan is not None and not data.scan:
1594
- data.scan = scan
1595
- if scans is not None and not data.scans:
1596
- 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
1597
1662
  if module is not None:
1598
- data.module = module
1663
+ event.module = module
1599
1664
  if parent is not None:
1600
- data.parent = parent
1665
+ event.parent = parent
1601
1666
  if context is not None:
1602
- data.discovery_context = context
1603
- if internal == True:
1604
- data.internal = True
1667
+ event.discovery_context = context
1668
+ if internal is True:
1669
+ event.internal = True
1605
1670
  if tags:
1606
- data.tags = tags.union(data.tags)
1671
+ event.tags = tags.union(event.tags)
1607
1672
  event_type = data.type
1608
- return data
1673
+ return event
1609
1674
  else:
1610
1675
  if event_type is None:
1611
1676
  event_type, data = get_event_type(data)
@@ -1635,6 +1700,13 @@ def make_event(
1635
1700
  if event_type == "USERNAME" and validators.soft_validate(data, "email"):
1636
1701
  event_type = "EMAIL_ADDRESS"
1637
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
1638
1710
 
1639
1711
  event_class = globals().get(event_type, DefaultEvent)
1640
1712
 
@@ -1691,6 +1763,9 @@ def event_from_json(j, siem_friendly=False):
1691
1763
  data = j["data"]
1692
1764
  kwargs["data"] = data
1693
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])
1694
1769
 
1695
1770
  resolved_hosts = j.get("resolved_hosts", [])
1696
1771
  event._resolved_hosts = set(resolved_hosts)
@@ -1700,6 +1775,10 @@ def event_from_json(j, siem_friendly=False):
1700
1775
  parent_id = j.get("parent", None)
1701
1776
  if parent_id is not None:
1702
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))
1703
1782
  return event
1704
1783
  except KeyError as e:
1705
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')
@@ -14,7 +14,7 @@ from secrets import token_bytes
14
14
  from ansible_runner.interface import run
15
15
  from subprocess import CalledProcessError
16
16
 
17
- from ..misc import can_sudo_without_password, os_platform, rm_at_exit
17
+ from ..misc import can_sudo_without_password, os_platform, rm_at_exit, get_python_constraints
18
18
 
19
19
  log = logging.getLogger("bbot.core.helpers.depsinstaller")
20
20
 
@@ -44,7 +44,13 @@ class DepsInstaller:
44
44
  self.parent_helper.mkdir(self.command_status)
45
45
  self.setup_status = self.read_setup_status()
46
46
 
47
- self.deps_behavior = self.parent_helper.config.get("deps_behavior", "abort_on_failure").lower()
47
+ # make sure we're using a minimal git config
48
+ self.minimal_git_config = self.data_dir / "minimal_git.config"
49
+ self.minimal_git_config.touch()
50
+ os.environ["GIT_CONFIG_GLOBAL"] = str(self.minimal_git_config)
51
+
52
+ self.deps_config = self.parent_helper.config.get("deps", {})
53
+ self.deps_behavior = self.deps_config.get("behavior", "abort_on_failure").lower()
48
54
  self.ansible_debug = self.core.logger.log_level <= logging.DEBUG
49
55
  self.venv = ""
50
56
  if sys.prefix != sys.base_prefix:
@@ -91,11 +97,11 @@ class DepsInstaller:
91
97
  or self.deps_behavior == "force_install"
92
98
  ):
93
99
  if not notified:
94
- log.hugeinfo(f"Installing module dependencies. Please be patient, this may take a while.")
100
+ log.hugeinfo("Installing module dependencies. Please be patient, this may take a while.")
95
101
  notified = True
96
102
  log.verbose(f'Installing dependencies for module "{m}"')
97
103
  # get sudo access if we need it
98
- if preloaded.get("sudo", False) == True:
104
+ if preloaded.get("sudo", False) is True:
99
105
  self.ensure_root(f'Module "{m}" needs root privileges to install its dependencies.')
100
106
  success = await self.install_module(m)
101
107
  self.setup_status[module_hash] = success
@@ -153,7 +159,7 @@ class DepsInstaller:
153
159
  deps_common = preloaded["deps"]["common"]
154
160
  if deps_common:
155
161
  for dep_common in deps_common:
156
- if self.setup_status.get(dep_common, False) == True:
162
+ if self.setup_status.get(dep_common, False) is True:
157
163
  log.debug(
158
164
  f'Skipping installation of dependency "{dep_common}" for module "{module}" since it is already installed'
159
165
  )
@@ -171,10 +177,13 @@ class DepsInstaller:
171
177
 
172
178
  command = [sys.executable, "-m", "pip", "install", "--upgrade"] + packages
173
179
 
174
- if constraints:
175
- constraints_tempfile = self.parent_helper.tempfile(constraints, pipe=False)
176
- command.append("--constraint")
177
- command.append(constraints_tempfile)
180
+ # if no custom constraints are provided, use the constraints of the currently installed version of bbot
181
+ if constraints is not None:
182
+ constraints = get_python_constraints()
183
+
184
+ constraints_tempfile = self.parent_helper.tempfile(constraints, pipe=False)
185
+ command.append("--constraint")
186
+ command.append(constraints_tempfile)
178
187
 
179
188
  process = None
180
189
  try:
@@ -235,7 +244,7 @@ class DepsInstaller:
235
244
  if success:
236
245
  log.info(f"Successfully ran {len(commands):,} shell commands")
237
246
  else:
238
- log.warning(f"Failed to run shell dependencies")
247
+ log.warning("Failed to run shell dependencies")
239
248
  return success
240
249
 
241
250
  def tasks(self, module, tasks):
@@ -248,7 +257,7 @@ class DepsInstaller:
248
257
  return success
249
258
 
250
259
  def ansible_run(self, tasks=None, module=None, args=None, ansible_args=None):
251
- _ansible_args = {"ansible_connection": "local"}
260
+ _ansible_args = {"ansible_connection": "local", "ansible_python_interpreter": sys.executable}
252
261
  if ansible_args is not None:
253
262
  _ansible_args.update(ansible_args)
254
263
  module_args = None
@@ -301,7 +310,7 @@ class DepsInstaller:
301
310
  return success, err
302
311
 
303
312
  def read_setup_status(self):
304
- setup_status = dict()
313
+ setup_status = {}
305
314
  if self.setup_status_cache.is_file():
306
315
  with open(self.setup_status_cache) as f:
307
316
  with suppress(Exception):
@@ -342,7 +351,16 @@ class DepsInstaller:
342
351
  # ensure tldextract data is cached
343
352
  self.parent_helper.tldextract("evilcorp.co.uk")
344
353
  # command: package_name
345
- core_deps = {"unzip": "unzip", "curl": "curl"}
354
+ core_deps = {
355
+ "unzip": "unzip",
356
+ "zipinfo": "unzip",
357
+ "curl": "curl",
358
+ "git": "git",
359
+ "make": "make",
360
+ "gcc": "gcc",
361
+ "bash": "bash",
362
+ "which": "which",
363
+ }
346
364
  for command, package_name in core_deps.items():
347
365
  if not self.parent_helper.which(command):
348
366
  to_install.add(package_name)