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
@@ -0,0 +1,29 @@
1
+ from pathlib import Path
2
+
3
+ from bbot.modules.templates.sql import SQLTemplate
4
+
5
+
6
+ class SQLite(SQLTemplate):
7
+ watched_events = ["*"]
8
+ meta = {"description": "Output scan data to a SQLite database"}
9
+ options = {
10
+ "database": "",
11
+ }
12
+ options_desc = {
13
+ "database": "The path to the sqlite database file",
14
+ }
15
+ deps_pip = ["sqlmodel", "aiosqlite"]
16
+
17
+ async def setup(self):
18
+ db_file = self.config.get("database", "")
19
+ if not db_file:
20
+ db_file = self.scan.home / "output.sqlite"
21
+ db_file = Path(db_file)
22
+ if not db_file.is_absolute():
23
+ db_file = self.scan.home / db_file
24
+ self.db_file = db_file
25
+ self.db_file.parent.mkdir(parents=True, exist_ok=True)
26
+ return await super().setup()
27
+
28
+ def connection_string(self, mask_password=False):
29
+ return f"sqlite+aiosqlite:///{self.db_file}"
@@ -20,7 +20,7 @@ class Stdout(BaseOutputModule):
20
20
 
21
21
  async def setup(self):
22
22
  self.text_format = self.config.get("format", "text").strip().lower()
23
- if not self.text_format in self.format_choices:
23
+ if self.text_format not in self.format_choices:
24
24
  return (
25
25
  False,
26
26
  f'Invalid text format choice, "{self.text_format}" (choices: {",".join(self.format_choices)})',
@@ -33,7 +33,7 @@ class Stdout(BaseOutputModule):
33
33
 
34
34
  async def filter_event(self, event):
35
35
  if self.accept_event_types:
36
- if not event.type in self.accept_event_types:
36
+ if event.type not in self.accept_event_types:
37
37
  return False, f'Event type "{event.type}" is not in the allowed event_types'
38
38
  return True
39
39
 
@@ -10,14 +10,115 @@ class Teams(WebhookOutputModule):
10
10
  }
11
11
  options = {"webhook_url": "", "event_types": ["VULNERABILITY", "FINDING"], "min_severity": "LOW"}
12
12
  options_desc = {
13
- "webhook_url": "Discord webhook URL",
13
+ "webhook_url": "Teams webhook URL",
14
14
  "event_types": "Types of events to send",
15
15
  "min_severity": "Only allow VULNERABILITY events of this severity or higher",
16
16
  }
17
17
  _module_threads = 5
18
- good_status_code = 200
19
- content_key = "text"
20
18
 
21
- def evaluate_response(self, response):
22
- text = getattr(response, "text", "")
23
- return text == "1"
19
+ async def handle_event(self, event):
20
+ while 1:
21
+ data = self.format_message(event)
22
+
23
+ response = await self.helpers.request(
24
+ url=self.webhook_url,
25
+ method="POST",
26
+ json=data,
27
+ )
28
+ status_code = getattr(response, "status_code", 0)
29
+ if self.evaluate_response(response):
30
+ break
31
+ else:
32
+ response_data = getattr(response, "text", "")
33
+ try:
34
+ retry_after = response.json().get("retry_after", 1)
35
+ except Exception:
36
+ retry_after = 1
37
+ self.verbose(
38
+ f"Error sending {event}: status code {status_code}, response: {response_data}, retrying in {retry_after} seconds"
39
+ )
40
+ await self.helpers.sleep(retry_after)
41
+
42
+ def trim_message(self, message):
43
+ if len(message) > self.message_size_limit:
44
+ message = message[: self.message_size_limit - 3] + "..."
45
+ return message
46
+
47
+ def format_message_str(self, event):
48
+ items = []
49
+ msg = self.trim_message(event.data)
50
+ items.append({"type": "TextBlock", "text": f"{msg}", "wrap": True})
51
+ items.append({"type": "FactSet", "facts": [{"title": "Tags:", "value": ", ".join(event.tags)}]})
52
+ return items
53
+
54
+ def format_message_other(self, event):
55
+ items = [{"type": "FactSet", "facts": []}]
56
+ for key, value in event.data.items():
57
+ if key != "severity":
58
+ msg = self.trim_message(str(value))
59
+ items[0]["facts"].append({"title": f"{key}:", "value": msg})
60
+ return items
61
+
62
+ def get_severity_color(self, event):
63
+ color = "Accent"
64
+ if event.type == "VULNERABILITY":
65
+ severity = event.data.get("severity", "UNKNOWN")
66
+ if severity == "CRITICAL":
67
+ color = "Attention"
68
+ elif severity == "HIGH":
69
+ color = "Attention"
70
+ elif severity == "MEDIUM":
71
+ color = "Warning"
72
+ elif severity == "LOW":
73
+ color = "Good"
74
+ return color
75
+
76
+ def format_message(self, event):
77
+ adaptive_card = {
78
+ "type": "message",
79
+ "attachments": [
80
+ {
81
+ "contentType": "application/vnd.microsoft.card.adaptive",
82
+ "contentUrl": None,
83
+ "content": {
84
+ "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
85
+ "type": "AdaptiveCard",
86
+ "version": "1.2",
87
+ "msteams": {"width": "full"},
88
+ "body": [],
89
+ },
90
+ }
91
+ ],
92
+ }
93
+ heading = {"type": "TextBlock", "text": f"{event.type}", "wrap": True, "size": "Large", "style": "heading"}
94
+ body = adaptive_card["attachments"][0]["content"]["body"]
95
+ body.append(heading)
96
+ if event.type in ("VULNERABILITY", "FINDING"):
97
+ subheading = {
98
+ "type": "TextBlock",
99
+ "text": event.data.get("severity", "UNKNOWN"),
100
+ "spacing": "None",
101
+ "size": "Large",
102
+ "wrap": True,
103
+ }
104
+ subheading["color"] = self.get_severity_color(event)
105
+ body.append(subheading)
106
+ main_text = {
107
+ "type": "ColumnSet",
108
+ "separator": True,
109
+ "spacing": "Medium",
110
+ "columns": [
111
+ {
112
+ "type": "Column",
113
+ "width": "stretch",
114
+ "items": [],
115
+ }
116
+ ],
117
+ }
118
+ if isinstance(event.data, str):
119
+ items = self.format_message_str(event)
120
+ else:
121
+ items = self.format_message_other(event)
122
+ main_text["columns"][0]["items"] = items
123
+ body.append(main_text)
124
+ return adaptive_card
@@ -15,7 +15,7 @@ class paramminer_headers(BaseModule):
15
15
  meta = {
16
16
  "description": "Use smart brute-force to check for common HTTP header parameters",
17
17
  "created_date": "2022-04-15",
18
- "author": "@pmueller",
18
+ "author": "@liquidsec",
19
19
  }
20
20
  options = {
21
21
  "wordlist": "", # default is defined within setup function
@@ -82,7 +82,6 @@ class paramminer_headers(BaseModule):
82
82
  header_regex = re.compile(r"^[!#$%&\'*+\-.^_`|~0-9a-zA-Z]+: [^\r\n]+$")
83
83
 
84
84
  async def setup(self):
85
-
86
85
  self.recycle_words = self.config.get("recycle_words", True)
87
86
  self.event_dict = {}
88
87
  self.already_checked = set()
@@ -90,11 +89,11 @@ class paramminer_headers(BaseModule):
90
89
  if not wordlist:
91
90
  wordlist = f"{self.helpers.wordlist_dir}/{self.default_wordlist}"
92
91
  self.debug(f"Using wordlist: [{wordlist}]")
93
- self.wl = set(
92
+ self.wl = {
94
93
  h.strip().lower()
95
94
  for h in self.helpers.read_file(await self.helpers.wordlist(wordlist))
96
95
  if len(h) > 0 and "%" not in h
97
- )
96
+ }
98
97
 
99
98
  # check against the boring list (if the option is set)
100
99
  if self.config.get("skip_boring_words", True):
@@ -157,7 +156,6 @@ class paramminer_headers(BaseModule):
157
156
  )
158
157
 
159
158
  async def handle_event(self, event):
160
-
161
159
  # If recycle words is enabled, we will collect WEB_PARAMETERS we find to build our list in finish()
162
160
  # We also collect any parameters of type "SPECULATIVE"
163
161
  if event.type == "WEB_PARAMETER":
@@ -174,7 +172,7 @@ class paramminer_headers(BaseModule):
174
172
  self.debug(f"Error initializing compare helper: {e}")
175
173
  return
176
174
  batch_size = await self.count_test(url)
177
- if batch_size == None or batch_size <= 0:
175
+ if batch_size is None or batch_size <= 0:
178
176
  self.debug(f"Failed to get baseline max {self.compare_mode} count, aborting")
179
177
  return
180
178
  self.debug(f"Resolved batch_size at {str(batch_size)}")
@@ -197,11 +195,11 @@ class paramminer_headers(BaseModule):
197
195
  baseline = await self.helpers.request(url)
198
196
  if baseline is None:
199
197
  return
200
- if str(baseline.status_code)[0] in ("4", "5"):
198
+ if str(baseline.status_code)[0] in {"4", "5"}:
201
199
  return
202
200
  for count, args, kwargs in self.gen_count_args(url):
203
201
  r = await self.helpers.request(*args, **kwargs)
204
- if r is not None and not ((str(r.status_code)[0] in ("4", "5"))):
202
+ if r is not None and str(r.status_code)[0] not in {"4", "5"}:
205
203
  return count
206
204
 
207
205
  def gen_count_args(self, url):
@@ -224,7 +222,7 @@ class paramminer_headers(BaseModule):
224
222
  elif len(group) > 1 or (len(group) == 1 and len(reasons) == 0):
225
223
  for group_slice in self.helpers.split_list(group):
226
224
  match, reasons, reflection, subject_response = await self.check_batch(compare_helper, url, group_slice)
227
- if match == False:
225
+ if match is False:
228
226
  async for r in self.binary_search(compare_helper, url, group_slice, reasons, reflection):
229
227
  yield r
230
228
  else:
@@ -240,8 +238,7 @@ class paramminer_headers(BaseModule):
240
238
  return await compare_helper.compare(url, headers=test_headers, check_reflection=(len(header_list) == 1))
241
239
 
242
240
  async def finish(self):
243
-
244
- untested_matches = sorted(list(self.extracted_words_master.copy()))
241
+ untested_matches = sorted(self.extracted_words_master.copy())
245
242
  for url, (event, batch_size) in list(self.event_dict.items()):
246
243
  try:
247
244
  compare_helper = self.helpers.http_compare(url)
@@ -11,36 +11,36 @@ class passivetotal(subdomain_enum_apikey):
11
11
  "author": "@TheTechromancer",
12
12
  "auth_required": True,
13
13
  }
14
- options = {"username": "", "api_key": ""}
15
- options_desc = {"username": "RiskIQ Username", "api_key": "RiskIQ API Key"}
14
+ options = {"api_key": ""}
15
+ options_desc = {"api_key": "PassiveTotal API Key in the format of 'username:api_key'"}
16
16
 
17
17
  base_url = "https://api.passivetotal.org/v2"
18
18
 
19
19
  async def setup(self):
20
- self.username = self.config.get("username", "")
21
- self.api_key = self.config.get("api_key", "")
22
- self.auth = (self.username, self.api_key)
23
20
  return await super().setup()
24
21
 
25
22
  async def ping(self):
26
23
  url = f"{self.base_url}/account/quota"
27
- j = (await self.request_with_fail_count(url, auth=self.auth)).json()
24
+ j = (await self.api_request(url)).json()
28
25
  limit = j["user"]["limits"]["search_api"]
29
26
  used = j["user"]["counts"]["search_api"]
30
27
  assert used < limit, "No quota remaining"
31
28
 
29
+ def prepare_api_request(self, url, kwargs):
30
+ api_username, api_key = self.api_key.split(":", 1)
31
+ kwargs["auth"] = (api_username, api_key)
32
+ return url, kwargs
33
+
32
34
  async def abort_if(self, event):
33
35
  # RiskIQ is famous for their junk data
34
36
  return await super().abort_if(event) or "unresolved" in event.tags
35
37
 
36
38
  async def request_url(self, query):
37
39
  url = f"{self.base_url}/enrichment/subdomains?query={self.helpers.quote(query)}"
38
- return await self.request_with_fail_count(url, auth=self.auth)
40
+ return await self.api_request(url)
39
41
 
40
- def parse_results(self, r, query):
42
+ async def parse_results(self, r, query):
43
+ results = set()
41
44
  for subdomain in r.json().get("subdomains", []):
42
- yield f"{subdomain}.{query}"
43
-
44
- @property
45
- def auth_secret(self):
46
- return self.username and self.api_key
45
+ results.add(f"{subdomain}.{query}")
46
+ return results
bbot/modules/portscan.py CHANGED
@@ -6,6 +6,9 @@ from radixtarget import RadixTarget
6
6
  from bbot.modules.base import BaseModule
7
7
 
8
8
 
9
+ # TODO: this module is getting big. It should probably be two modules: one for ping and one for SYN.
10
+
11
+
9
12
  class portscan(BaseModule):
10
13
  flags = ["active", "portscan", "safe"]
11
14
  watched_events = ["IP_ADDRESS", "IP_RANGE", "DNS_NAME"]
@@ -27,6 +30,8 @@ class portscan(BaseModule):
27
30
  "adapter_ip": "",
28
31
  "adapter_mac": "",
29
32
  "router_mac": "",
33
+ "cdn_tags": "cdn-",
34
+ "allowed_cdn_ports": None,
30
35
  }
31
36
  options_desc = {
32
37
  "top_ports": "Top ports to scan (default 100) (to override, specify 'ports')",
@@ -39,6 +44,8 @@ class portscan(BaseModule):
39
44
  "adapter_ip": "Send packets using this IP address. Not needed unless masscan's autodetection fails",
40
45
  "adapter_mac": "Send packets using this as the source MAC address. Not needed unless masscan's autodetection fails",
41
46
  "router_mac": "Send packets to this MAC address as the destination. Not needed unless masscan's autodetection fails",
47
+ "cdn_tags": "Comma-separated list of tags to skip, e.g. 'cdn,cloud'",
48
+ "allowed_cdn_ports": "Comma-separated list of ports that are allowed to be scanned for CDNs",
42
49
  }
43
50
  deps_common = ["masscan"]
44
51
  batch_size = 1000000
@@ -60,7 +67,15 @@ class portscan(BaseModule):
60
67
  try:
61
68
  self.helpers.parse_port_string(self.ports)
62
69
  except ValueError as e:
63
- return False, f"Error parsing ports: {e}"
70
+ return False, f"Error parsing ports '{self.ports}': {e}"
71
+ self.cdn_tags = [t.strip() for t in self.config.get("cdn_tags", "").split(",")]
72
+ self.allowed_cdn_ports = self.config.get("allowed_cdn_ports", None)
73
+ if self.allowed_cdn_ports is not None:
74
+ try:
75
+ self.allowed_cdn_ports = [int(p.strip()) for p in self.allowed_cdn_ports.split(",")]
76
+ except Exception as e:
77
+ return False, f"Error parsing allowed CDN ports '{self.allowed_cdn_ports}': {e}"
78
+
64
79
  # whether we've finished scanning our original scan targets
65
80
  self.scanned_initial_targets = False
66
81
  # keeps track of individual scanned IPs and their open ports
@@ -84,17 +99,17 @@ class portscan(BaseModule):
84
99
  return False, "Masscan failed to run"
85
100
  returncode = getattr(ipv6_result, "returncode", 0)
86
101
  if returncode and "failed to detect IPv6 address" in ipv6_result.stderr:
87
- self.warning(f"It looks like you are not set up for IPv6. IPv6 targets will not be scanned.")
102
+ self.warning("It looks like you are not set up for IPv6. IPv6 targets will not be scanned.")
88
103
  self.ipv6_support = False
89
104
  return True
90
105
 
91
106
  async def handle_batch(self, *events):
92
- # on our first run, we automatically include all our intial scan targets
107
+ # on our first run, we automatically include all our initial scan targets
93
108
  if not self.scanned_initial_targets:
94
109
  self.scanned_initial_targets = True
95
110
  events = set(events)
96
111
  events.update(
97
- set([e for e in self.scan.target.seeds.events if e.type in ("DNS_NAME", "IP_ADDRESS", "IP_RANGE")])
112
+ {e for e in self.scan.target.seeds.events if e.type in ("DNS_NAME", "IP_ADDRESS", "IP_RANGE")}
98
113
  )
99
114
 
100
115
  # ping scan
@@ -227,9 +242,20 @@ class portscan(BaseModule):
227
242
  parent=parent_event,
228
243
  context=f"{{module}} executed a {scan_type} scan against {parent_event.data} and found: {{event.type}}: {{event.data}}",
229
244
  )
230
- await self.emit_event(event)
245
+
246
+ await self.emit_event(event, abort_if=self.abort_if)
231
247
  return event
232
248
 
249
+ def abort_if(self, event):
250
+ if self.allowed_cdn_ports is not None:
251
+ # if the host is a CDN
252
+ for cdn_tag in self.cdn_tags:
253
+ if any(t.startswith(str(cdn_tag)) for t in event.tags):
254
+ # and if its port isn't in the list of allowed CDN ports
255
+ if event.port not in self.allowed_cdn_ports:
256
+ return True, "event is a CDN and port is not in the allowed list"
257
+ return False
258
+
233
259
  def parse_json_line(self, line):
234
260
  try:
235
261
  j = json.loads(line)
@@ -308,7 +334,7 @@ class portscan(BaseModule):
308
334
  if "FAIL" in s:
309
335
  self.warning(s)
310
336
  self.warning(
311
- f'Masscan failed to detect interface. Recommend passing "adapter_ip", "adapter_mac", and "router_mac" config options to portscan module.'
337
+ 'Masscan failed to detect interface. Recommend passing "adapter_ip", "adapter_mac", and "router_mac" config options to portscan module.'
312
338
  )
313
339
  else:
314
340
  self.verbose(s)
bbot/modules/postman.py CHANGED
@@ -1,36 +1,62 @@
1
- from bbot.modules.templates.subdomain_enum import subdomain_enum
1
+ from bbot.modules.templates.postman import postman
2
2
 
3
3
 
4
- class postman(subdomain_enum):
5
- watched_events = ["DNS_NAME"]
6
- produced_events = ["URL_UNVERIFIED"]
4
+ class postman(postman):
5
+ watched_events = ["ORG_STUB", "SOCIAL"]
6
+ produced_events = ["CODE_REPOSITORY"]
7
7
  flags = ["passive", "subdomain-enum", "safe", "code-enum"]
8
8
  meta = {
9
- "description": "Query Postman's API for related workspaces, collections, requests",
10
- "created_date": "2023-12-23",
9
+ "description": "Query Postman's API for related workspaces, collections, requests and download them",
10
+ "created_date": "2024-09-07",
11
11
  "author": "@domwhewell-sage",
12
12
  }
13
13
 
14
- base_url = "https://www.postman.com/_api"
15
-
16
- headers = {
17
- "Content-Type": "application/json",
18
- "X-App-Version": "10.18.8-230926-0808",
19
- "X-Entity-Team-Id": "0",
20
- "Origin": "https://www.postman.com",
21
- "Referer": "https://www.postman.com/search?q=&scope=public&type=all",
22
- }
23
-
24
14
  reject_wildcards = False
25
15
 
26
16
  async def handle_event(self, event):
27
- query = self.make_query(event)
28
- self.verbose(f"Searching for any postman workspaces, collections, requests belonging to {query}")
29
- for url, context in await self.query(query):
30
- await self.emit_event(url, "URL_UNVERIFIED", parent=event, tags="httpx-safe", context=context)
17
+ # Handle postman profile
18
+ if event.type == "SOCIAL":
19
+ await self.handle_profile(event)
20
+ elif event.type == "ORG_STUB":
21
+ await self.handle_org_stub(event)
22
+
23
+ async def handle_profile(self, event):
24
+ profile_name = event.data.get("profile_name", "")
25
+ self.verbose(f"Searching for postman workspaces, collections, requests belonging to {profile_name}")
26
+ for item in await self.query(profile_name):
27
+ workspace = item["document"]
28
+ name = workspace["slug"]
29
+ profile = workspace["publisherHandle"]
30
+ if profile_name.lower() == profile.lower():
31
+ self.verbose(f"Got {name}")
32
+ workspace_url = f"{self.html_url}/{profile}/{name}"
33
+ await self.emit_event(
34
+ {"url": workspace_url},
35
+ "CODE_REPOSITORY",
36
+ tags="postman",
37
+ parent=event,
38
+ context=f'{{module}} searched postman.com for workspaces belonging to "{profile_name}" and found "{name}" at {{event.type}}: {workspace_url}',
39
+ )
40
+
41
+ async def handle_org_stub(self, event):
42
+ org_name = event.data
43
+ self.verbose(f"Searching for any postman workspaces, collections, requests for {org_name}")
44
+ for item in await self.query(org_name):
45
+ workspace = item["document"]
46
+ name = workspace["slug"]
47
+ profile = workspace["publisherHandle"]
48
+ self.verbose(f"Got {name}")
49
+ workspace_url = f"{self.html_url}/{profile}/{name}"
50
+ await self.emit_event(
51
+ {"url": workspace_url},
52
+ "CODE_REPOSITORY",
53
+ tags="postman",
54
+ parent=event,
55
+ context=f'{{module}} searched postman.com for "{org_name}" and found matching workspace "{name}" at {{event.type}}: {workspace_url}',
56
+ )
31
57
 
32
58
  async def query(self, query):
33
- interesting_urls = []
59
+ data = []
34
60
  url = f"{self.base_url}/ws/proxy"
35
61
  json = {
36
62
  "service": "search",
@@ -39,11 +65,6 @@ class postman(subdomain_enum):
39
65
  "body": {
40
66
  "queryIndices": [
41
67
  "collaboration.workspace",
42
- "runtime.collection",
43
- "runtime.request",
44
- "adp.api",
45
- "flow.flow",
46
- "apinetwork.team",
47
68
  ],
48
69
  "queryText": self.helpers.quote(query),
49
70
  "size": 100,
@@ -57,108 +78,11 @@ class postman(subdomain_enum):
57
78
  }
58
79
  r = await self.helpers.request(url, method="POST", json=json, headers=self.headers)
59
80
  if r is None:
60
- return interesting_urls
81
+ return data
61
82
  status_code = getattr(r, "status_code", 0)
62
83
  try:
63
84
  json = r.json()
64
85
  except Exception as e:
65
86
  self.warning(f"Failed to decode JSON for {r.url} (HTTP status: {status_code}): {e}")
66
- return interesting_urls
67
- workspaces = []
68
- for item in json.get("data", {}):
69
- for workspace in item.get("document", {}).get("workspaces", []):
70
- if workspace not in workspaces:
71
- workspaces.append(workspace)
72
- for item in workspaces:
73
- id = item.get("id", "")
74
- name = item.get("name", "")
75
- tldextract = self.helpers.tldextract(query)
76
- if tldextract.domain.lower() in name.lower():
77
- self.verbose(f"Discovered workspace {name} ({id})")
78
- workspace_url = f"{self.base_url}/workspace/{id}"
79
- interesting_urls.append(
80
- (
81
- workspace_url,
82
- f'{{module}} searched postman.com for "{query}" and found matching workspace "{name}" at {{event.type}}: {workspace_url}',
83
- )
84
- )
85
- environments, collections = await self.search_workspace(id)
86
- globals_url = f"{self.base_url}/workspace/{id}/globals"
87
- interesting_urls.append(
88
- (
89
- globals_url,
90
- f'{{module}} searched postman.com for "{query}", found matching workspace "{name}" at {workspace_url}, and found globals at {{event.type}}: {globals_url}',
91
- )
92
- )
93
- for e_id in environments:
94
- env_url = f"{self.base_url}/environment/{e_id}"
95
- interesting_urls.append(
96
- (
97
- env_url,
98
- f'{{module}} searched postman.com for "{query}", found matching workspace "{name}" at {workspace_url}, enumerated environments, and found {{event.type}}: {env_url}',
99
- )
100
- )
101
- for c_id in collections:
102
- collection_url = f"{self.base_url}/collection/{c_id}"
103
- interesting_urls.append(
104
- (
105
- collection_url,
106
- f'{{module}} searched postman.com for "{query}", found matching workspace "{name}" at {workspace_url}, enumerated collections, and found {{event.type}}: {collection_url}',
107
- )
108
- )
109
- requests = await self.search_collections(id)
110
- for r_id in requests:
111
- request_url = f"{self.base_url}/request/{r_id}"
112
- interesting_urls.append(
113
- (
114
- request_url,
115
- f'{{module}} searched postman.com for "{query}", found matching workspace "{name}" at {workspace_url}, enumerated requests, and found {{event.type}}: {request_url}',
116
- )
117
- )
118
- else:
119
- self.verbose(f"Skipping workspace {name} ({id}) as it does not appear to be in scope")
120
- return interesting_urls
121
-
122
- async def search_workspace(self, id):
123
- url = f"{self.base_url}/workspace/{id}"
124
- r = await self.helpers.request(url)
125
- if r is None:
126
- return [], []
127
- status_code = getattr(r, "status_code", 0)
128
- try:
129
- json = r.json()
130
- if not isinstance(json, dict):
131
- raise ValueError(f"Got unexpected value for JSON: {json}")
132
- except Exception as e:
133
- self.warning(f"Failed to decode JSON for {r.url} (HTTP status: {status_code}): {e}")
134
- return [], []
135
- environments = json.get("data", {}).get("dependencies", {}).get("environments", [])
136
- collections = json.get("data", {}).get("dependencies", {}).get("collections", [])
137
- return environments, collections
138
-
139
- async def search_collections(self, id):
140
- request_ids = []
141
- url = f"{self.base_url}/list/collection?workspace={id}"
142
- r = await self.helpers.request(url, method="POST")
143
- if r is None:
144
- return request_ids
145
- status_code = getattr(r, "status_code", 0)
146
- try:
147
- json = r.json()
148
- except Exception as e:
149
- self.warning(f"Failed to decode JSON for {r.url} (HTTP status: {status_code}): {e}")
150
- return request_ids
151
- for item in json.get("data", {}):
152
- request_ids.extend(await self.parse_collection(item))
153
- return request_ids
154
-
155
- async def parse_collection(self, json):
156
- request_ids = []
157
- folders = json.get("folders", [])
158
- requests = json.get("requests", [])
159
- for folder in folders:
160
- request_ids.extend(await self.parse_collection(folder))
161
- for request in requests:
162
- r_id = request.get("id", "")
163
- request_ids.append(r_id)
164
- return request_ids
87
+ return None
88
+ return json.get("data", [])