bbot 2.3.0.5546rc0__py3-none-any.whl → 2.3.0.5809rc0__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 (116) hide show
  1. bbot/__init__.py +1 -1
  2. bbot/cli.py +1 -1
  3. bbot/core/engine.py +1 -1
  4. bbot/core/event/base.py +7 -5
  5. bbot/core/helpers/async_helpers.py +7 -1
  6. bbot/core/helpers/depsinstaller/installer.py +7 -2
  7. bbot/core/helpers/diff.py +13 -4
  8. bbot/core/helpers/dns/brute.py +8 -2
  9. bbot/core/helpers/dns/engine.py +3 -2
  10. bbot/core/helpers/ratelimiter.py +8 -2
  11. bbot/core/helpers/regexes.py +5 -2
  12. bbot/core/helpers/web/engine.py +1 -1
  13. bbot/core/helpers/web/web.py +1 -1
  14. bbot/core/shared_deps.py +14 -0
  15. bbot/defaults.yml +44 -0
  16. bbot/modules/ajaxpro.py +64 -37
  17. bbot/modules/baddns.py +23 -15
  18. bbot/modules/baddns_direct.py +2 -2
  19. bbot/modules/badsecrets.py +2 -2
  20. bbot/modules/base.py +49 -15
  21. bbot/modules/censys.py +1 -1
  22. bbot/modules/deadly/dastardly.py +3 -3
  23. bbot/modules/deadly/nuclei.py +1 -1
  24. bbot/modules/dehashed.py +2 -2
  25. bbot/modules/dnsbrute_mutations.py +3 -1
  26. bbot/modules/docker_pull.py +1 -1
  27. bbot/modules/dockerhub.py +2 -2
  28. bbot/modules/dotnetnuke.py +12 -12
  29. bbot/modules/extractous.py +1 -1
  30. bbot/modules/ffuf_shortnames.py +107 -48
  31. bbot/modules/filedownload.py +6 -0
  32. bbot/modules/generic_ssrf.py +54 -40
  33. bbot/modules/github_codesearch.py +2 -2
  34. bbot/modules/github_org.py +16 -20
  35. bbot/modules/github_workflows.py +6 -2
  36. bbot/modules/gowitness.py +6 -0
  37. bbot/modules/hunt.py +1 -1
  38. bbot/modules/hunterio.py +1 -1
  39. bbot/modules/iis_shortnames.py +23 -7
  40. bbot/modules/internal/excavate.py +5 -3
  41. bbot/modules/internal/unarchive.py +82 -0
  42. bbot/modules/jadx.py +2 -2
  43. bbot/modules/output/asset_inventory.py +1 -1
  44. bbot/modules/output/base.py +1 -1
  45. bbot/modules/output/discord.py +2 -1
  46. bbot/modules/output/slack.py +2 -1
  47. bbot/modules/output/teams.py +10 -25
  48. bbot/modules/output/web_parameters.py +55 -0
  49. bbot/modules/paramminer_headers.py +15 -10
  50. bbot/modules/portfilter.py +41 -0
  51. bbot/modules/portscan.py +1 -22
  52. bbot/modules/postman.py +61 -43
  53. bbot/modules/postman_download.py +10 -147
  54. bbot/modules/sitedossier.py +1 -1
  55. bbot/modules/skymem.py +1 -1
  56. bbot/modules/templates/postman.py +163 -1
  57. bbot/modules/templates/subdomain_enum.py +1 -1
  58. bbot/modules/templates/webhook.py +17 -26
  59. bbot/modules/trufflehog.py +3 -3
  60. bbot/modules/wappalyzer.py +1 -1
  61. bbot/modules/zoomeye.py +1 -1
  62. bbot/presets/kitchen-sink.yml +1 -1
  63. bbot/presets/nuclei/nuclei-budget.yml +19 -0
  64. bbot/presets/nuclei/nuclei-intense.yml +28 -0
  65. bbot/presets/nuclei/nuclei-technology.yml +23 -0
  66. bbot/presets/nuclei/nuclei.yml +34 -0
  67. bbot/presets/spider-intense.yml +13 -0
  68. bbot/scanner/preset/args.py +29 -3
  69. bbot/scanner/preset/preset.py +43 -24
  70. bbot/scanner/scanner.py +17 -7
  71. bbot/test/bbot_fixtures.py +7 -7
  72. bbot/test/test_step_1/test_bloom_filter.py +2 -2
  73. bbot/test/test_step_1/test_cli.py +5 -5
  74. bbot/test/test_step_1/test_dns.py +33 -0
  75. bbot/test/test_step_1/test_events.py +15 -5
  76. bbot/test/test_step_1/test_modules_basic.py +21 -21
  77. bbot/test/test_step_1/test_presets.py +94 -4
  78. bbot/test/test_step_1/test_regexes.py +13 -13
  79. bbot/test/test_step_1/test_scan.py +78 -0
  80. bbot/test/test_step_1/test_web.py +4 -4
  81. bbot/test/test_step_2/module_tests/test_module_ajaxpro.py +43 -23
  82. bbot/test/test_step_2/module_tests/test_module_azure_realm.py +3 -3
  83. bbot/test/test_step_2/module_tests/test_module_baddns.py +3 -3
  84. bbot/test/test_step_2/module_tests/test_module_bucket_amazon.py +6 -6
  85. bbot/test/test_step_2/module_tests/test_module_bufferoverrun.py +3 -3
  86. bbot/test/test_step_2/module_tests/test_module_cloudcheck.py +3 -3
  87. bbot/test/test_step_2/module_tests/test_module_dnsbimi.py +3 -3
  88. bbot/test/test_step_2/module_tests/test_module_dnscaa.py +6 -6
  89. bbot/test/test_step_2/module_tests/test_module_dnscommonsrv.py +9 -9
  90. bbot/test/test_step_2/module_tests/test_module_dnstlsrpt.py +12 -12
  91. bbot/test/test_step_2/module_tests/test_module_excavate.py +15 -15
  92. bbot/test/test_step_2/module_tests/test_module_extractous.py +3 -3
  93. bbot/test/test_step_2/module_tests/test_module_ffuf_shortnames.py +8 -8
  94. bbot/test/test_step_2/module_tests/test_module_generic_ssrf.py +3 -1
  95. bbot/test/test_step_2/module_tests/test_module_github_codesearch.py +3 -3
  96. bbot/test/test_step_2/module_tests/test_module_gowitness.py +9 -9
  97. bbot/test/test_step_2/module_tests/test_module_iis_shortnames.py +1 -1
  98. bbot/test/test_step_2/module_tests/test_module_paramminer_getparams.py +35 -1
  99. bbot/test/test_step_2/module_tests/test_module_paramminer_headers.py +3 -3
  100. bbot/test/test_step_2/module_tests/test_module_portfilter.py +48 -0
  101. bbot/test/test_step_2/module_tests/test_module_postman.py +338 -3
  102. bbot/test/test_step_2/module_tests/test_module_postman_download.py +4 -161
  103. bbot/test/test_step_2/module_tests/test_module_securitytxt.py +12 -12
  104. bbot/test/test_step_2/module_tests/test_module_teams.py +10 -1
  105. bbot/test/test_step_2/module_tests/test_module_trufflehog.py +1 -1
  106. bbot/test/test_step_2/module_tests/test_module_unarchive.py +229 -0
  107. bbot/test/test_step_2/module_tests/test_module_viewdns.py +3 -3
  108. bbot/test/test_step_2/module_tests/test_module_web_parameters.py +59 -0
  109. bbot/test/test_step_2/module_tests/test_module_websocket.py +5 -4
  110. {bbot-2.3.0.5546rc0.dist-info → bbot-2.3.0.5809rc0.dist-info}/METADATA +7 -7
  111. {bbot-2.3.0.5546rc0.dist-info → bbot-2.3.0.5809rc0.dist-info}/RECORD +115 -105
  112. {bbot-2.3.0.5546rc0.dist-info → bbot-2.3.0.5809rc0.dist-info}/WHEEL +1 -1
  113. bbot/wordlists/ffuf_shortname_candidates.txt +0 -107982
  114. /bbot/presets/{baddns-thorough.yml → baddns-intense.yml} +0 -0
  115. {bbot-2.3.0.5546rc0.dist-info → bbot-2.3.0.5809rc0.dist-info}/LICENSE +0 -0
  116. {bbot-2.3.0.5546rc0.dist-info → bbot-2.3.0.5809rc0.dist-info}/entry_points.txt +0 -0
@@ -8,36 +8,21 @@ class Teams(WebhookOutputModule):
8
8
  "created_date": "2023-08-14",
9
9
  "author": "@TheTechromancer",
10
10
  }
11
- options = {"webhook_url": "", "event_types": ["VULNERABILITY", "FINDING"], "min_severity": "LOW"}
11
+ options = {"webhook_url": "", "event_types": ["VULNERABILITY", "FINDING"], "min_severity": "LOW", "retries": 10}
12
12
  options_desc = {
13
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
+ "retries": "Number of times to retry sending the message before skipping the event",
16
17
  }
17
- _module_threads = 5
18
18
 
19
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)
20
+ data = self.format_message(event)
21
+ await self.api_request(
22
+ url=self.webhook_url,
23
+ method="POST",
24
+ json=data,
25
+ )
41
26
 
42
27
  def trim_message(self, message):
43
28
  if len(message) > self.message_size_limit:
@@ -62,7 +47,7 @@ class Teams(WebhookOutputModule):
62
47
  def get_severity_color(self, event):
63
48
  color = "Accent"
64
49
  if event.type == "VULNERABILITY":
65
- severity = event.data.get("severity", "UNKNOWN")
50
+ severity = event.data.get("severity", "INFO")
66
51
  if severity == "CRITICAL":
67
52
  color = "Attention"
68
53
  elif severity == "HIGH":
@@ -96,7 +81,7 @@ class Teams(WebhookOutputModule):
96
81
  if event.type in ("VULNERABILITY", "FINDING"):
97
82
  subheading = {
98
83
  "type": "TextBlock",
99
- "text": event.data.get("severity", "UNKNOWN"),
84
+ "text": event.data.get("severity", "INFO"),
100
85
  "spacing": "None",
101
86
  "size": "Large",
102
87
  "wrap": True,
@@ -0,0 +1,55 @@
1
+ from contextlib import suppress
2
+ from collections import defaultdict
3
+
4
+ from bbot.modules.output.base import BaseOutputModule
5
+
6
+
7
+ class Web_parameters(BaseOutputModule):
8
+ watched_events = ["WEB_PARAMETER"]
9
+ meta = {
10
+ "description": "Output WEB_PARAMETER names to a file",
11
+ "created_date": "2025-01-25",
12
+ "author": "@liquidsec",
13
+ }
14
+ options = {"output_file": "", "include_count": False}
15
+ options_desc = {
16
+ "output_file": "Output to file",
17
+ "include_count": "Include the count of each parameter in the output",
18
+ }
19
+
20
+ output_filename = "web_parameters.txt"
21
+
22
+ async def setup(self):
23
+ self._prep_output_dir(self.output_filename)
24
+ self.parameter_counts = defaultdict(int)
25
+ return True
26
+
27
+ async def handle_event(self, event):
28
+ parameter_name = event.data.get("name", "")
29
+ if parameter_name:
30
+ self.parameter_counts[parameter_name] += 1
31
+
32
+ async def cleanup(self):
33
+ if getattr(self, "_file", None) is not None:
34
+ with suppress(Exception):
35
+ self.file.close()
36
+
37
+ async def report(self):
38
+ include_count = self.config.get("include_count", False)
39
+
40
+ # Sort behavior:
41
+ # - If include_count is True, sort by count (descending) and then alphabetically by name
42
+ # - If include_count is False, sort alphabetically by name only
43
+ sorted_parameters = sorted(
44
+ self.parameter_counts.items(), key=lambda x: (-x[1], x[0]) if include_count else x[0]
45
+ )
46
+ for param, count in sorted_parameters:
47
+ if include_count:
48
+ # Include the count of each parameter in the output
49
+ self.file.write(f"{count}\t{param}\n")
50
+ else:
51
+ # Only include the parameter name, effectively deduplicating by name
52
+ self.file.write(f"{param}\n")
53
+ self.file.flush()
54
+ if getattr(self, "_file", None) is not None:
55
+ self.info(f"Saved web parameters to {self.output_file}")
@@ -140,7 +140,7 @@ class paramminer_headers(BaseModule):
140
140
  tags = ["http_reflection"]
141
141
  description = f"[Paramminer] {self.compare_mode.capitalize()}: [{result}] Reasons: [{reasons}] Reflection: [{str(reflection)}]"
142
142
  reflected = "reflected " if reflection else ""
143
- self.extracted_words_master.add(result)
143
+
144
144
  await self.emit_event(
145
145
  {
146
146
  "host": str(event.host),
@@ -159,9 +159,12 @@ class paramminer_headers(BaseModule):
159
159
  # If recycle words is enabled, we will collect WEB_PARAMETERS we find to build our list in finish()
160
160
  # We also collect any parameters of type "SPECULATIVE"
161
161
  if event.type == "WEB_PARAMETER":
162
+ parameter_name = event.data.get("name")
162
163
  if self.recycle_words or (event.data.get("type") == "SPECULATIVE"):
163
- parameter_name = event.data.get("name")
164
- if self.config.get("skip_boring_words", True) and parameter_name not in self.boring_words:
164
+ if self.config.get("skip_boring_words", True) and parameter_name in self.boring_words:
165
+ return
166
+ if parameter_name not in self.wl: # Ensure it's not already in the wordlist
167
+ self.debug(f"Adding {parameter_name} to wordlist")
165
168
  self.extracted_words_master.add(parameter_name)
166
169
 
167
170
  elif event.type == "HTTP_RESPONSE":
@@ -238,27 +241,29 @@ class paramminer_headers(BaseModule):
238
241
  return await compare_helper.compare(url, headers=test_headers, check_reflection=(len(header_list) == 1))
239
242
 
240
243
  async def finish(self):
241
- untested_matches = sorted(self.extracted_words_master.copy())
242
244
  for url, (event, batch_size) in list(self.event_dict.items()):
243
245
  try:
244
246
  compare_helper = self.helpers.http_compare(url)
245
247
  except HttpCompareError as e:
246
248
  self.debug(f"Error initializing compare helper: {e}")
247
249
  continue
248
- untested_matches_copy = untested_matches.copy()
249
- for i in untested_matches:
250
- h = hash(i + url)
251
- if h in self.already_checked:
252
- untested_matches_copy.remove(i)
250
+ words_to_process = {
251
+ i for i in self.extracted_words_master.copy() if hash(i + url) not in self.already_checked
252
+ }
253
253
  try:
254
- results = await self.do_mining(untested_matches_copy, url, batch_size, compare_helper)
254
+ results = await self.do_mining(words_to_process, url, batch_size, compare_helper)
255
255
  except HttpCompareError as e:
256
256
  self.debug(f"Encountered HttpCompareError: [{e}] for URL [{url}]")
257
257
  continue
258
258
  await self.process_results(event, results)
259
259
 
260
260
  async def filter_event(self, event):
261
+ # Filter out static endpoints
262
+ if event.data.get("url").endswith(tuple(f".{ext}" for ext in self.config.get("url_extension_static", []))):
263
+ return False
264
+
261
265
  # We don't need to look at WEB_PARAMETERS that we produced
262
266
  if str(event.module).startswith("paramminer"):
263
267
  return False
268
+
264
269
  return True
@@ -0,0 +1,41 @@
1
+ from bbot.modules.base import BaseInterceptModule
2
+
3
+
4
+ class portfilter(BaseInterceptModule):
5
+ watched_events = ["OPEN_TCP_PORT"]
6
+ flags = ["passive", "safe"]
7
+ meta = {
8
+ "description": "Filter out unwanted open ports from cloud/CDN targets",
9
+ "created_date": "2025-01-06",
10
+ "author": "@TheTechromancer",
11
+ }
12
+ options = {
13
+ "cdn_tags": "cdn-",
14
+ "allowed_cdn_ports": "80,443",
15
+ }
16
+ options_desc = {
17
+ "cdn_tags": "Comma-separated list of tags to skip, e.g. 'cdn,cloud'",
18
+ "allowed_cdn_ports": "Comma-separated list of ports that are allowed to be scanned for CDNs",
19
+ }
20
+
21
+ async def setup(self):
22
+ self.cdn_tags = [t.strip() for t in self.config.get("cdn_tags", "").split(",")]
23
+ self.allowed_cdn_ports = self.config.get("allowed_cdn_ports", "").strip()
24
+ if self.allowed_cdn_ports:
25
+ try:
26
+ self.allowed_cdn_ports = [int(p.strip()) for p in self.allowed_cdn_ports.split(",")]
27
+ except Exception as e:
28
+ return False, f"Error parsing allowed CDN ports '{self.allowed_cdn_ports}': {e}"
29
+ return True
30
+
31
+ async def handle_event(self, event):
32
+ # if the port isn't in our list of allowed CDN ports
33
+ if event.port not in self.allowed_cdn_ports:
34
+ for cdn_tag in self.cdn_tags:
35
+ # and if any of the event's tags match our CDN filter
36
+ if any(t.startswith(str(cdn_tag)) for t in event.tags):
37
+ return (
38
+ False,
39
+ f"one of the event's tags matches the tag '{cdn_tag}' and the port is not in the allowed list",
40
+ )
41
+ return True
bbot/modules/portscan.py CHANGED
@@ -30,8 +30,6 @@ class portscan(BaseModule):
30
30
  "adapter_ip": "",
31
31
  "adapter_mac": "",
32
32
  "router_mac": "",
33
- "cdn_tags": "cdn-",
34
- "allowed_cdn_ports": None,
35
33
  }
36
34
  options_desc = {
37
35
  "top_ports": "Top ports to scan (default 100) (to override, specify 'ports')",
@@ -44,8 +42,6 @@ class portscan(BaseModule):
44
42
  "adapter_ip": "Send packets using this IP address. Not needed unless masscan's autodetection fails",
45
43
  "adapter_mac": "Send packets using this as the source MAC address. Not needed unless masscan's autodetection fails",
46
44
  "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",
49
45
  }
50
46
  deps_common = ["masscan"]
51
47
  batch_size = 1000000
@@ -68,13 +64,6 @@ class portscan(BaseModule):
68
64
  self.helpers.parse_port_string(self.ports)
69
65
  except ValueError as e:
70
66
  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
67
 
79
68
  # whether we've finished scanning our original scan targets
80
69
  self.scanned_initial_targets = False
@@ -243,19 +232,9 @@ class portscan(BaseModule):
243
232
  context=f"{{module}} executed a {scan_type} scan against {parent_event.data} and found: {{event.type}}: {{event.data}}",
244
233
  )
245
234
 
246
- await self.emit_event(event, abort_if=self.abort_if)
235
+ await self.emit_event(event)
247
236
  return event
248
237
 
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
-
259
238
  def parse_json_line(self, line):
260
239
  try:
261
240
  j = json.loads(line)
bbot/modules/postman.py CHANGED
@@ -10,52 +10,64 @@ class postman(postman):
10
10
  "created_date": "2024-09-07",
11
11
  "author": "@domwhewell-sage",
12
12
  }
13
-
13
+ options = {"api_key": ""}
14
+ options_desc = {"api_key": "Postman API Key"}
14
15
  reject_wildcards = False
15
16
 
16
17
  async def handle_event(self, event):
17
18
  # Handle postman profile
18
19
  if event.type == "SOCIAL":
19
- await self.handle_profile(event)
20
+ owner = event.data.get("profile_name", "")
21
+ in_scope_workspaces = await self.process_workspaces(user=owner)
20
22
  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}"
23
+ owner = event.data
24
+ in_scope_workspaces = await self.process_workspaces(org=owner)
25
+ if in_scope_workspaces:
26
+ for workspace in in_scope_workspaces:
27
+ repo_url = workspace["url"]
28
+ repo_name = workspace["repo_name"]
29
+ if event.type == "SOCIAL":
30
+ context = f'{{module}} searched postman.com for workspaces belonging to "{owner}" and found "{repo_name}" at {{event.type}}: {repo_url}'
31
+ elif event.type == "ORG_STUB":
32
+ context = f'{{module}} searched postman.com for "{owner}" and found matching workspace "{repo_name}" at {{event.type}}: {repo_url}'
33
33
  await self.emit_event(
34
- {"url": workspace_url},
34
+ {"url": repo_url},
35
35
  "CODE_REPOSITORY",
36
36
  tags="postman",
37
37
  parent=event,
38
- context=f'{{module}} searched postman.com for workspaces belonging to "{profile_name}" and found "{name}" at {{event.type}}: {workspace_url}',
38
+ context=context,
39
39
  )
40
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
- )
41
+ async def process_workspaces(self, user=None, org=None):
42
+ in_scope_workspaces = []
43
+ owner = user or org
44
+ if owner:
45
+ self.verbose(f"Searching for postman workspaces, collections, requests for {owner}")
46
+ for item in await self.query(owner):
47
+ workspace = item["document"]
48
+ slug = workspace["slug"]
49
+ profile = workspace["publisherHandle"]
50
+ repo_url = f"{self.html_url}/{profile}/{slug}"
51
+ workspace_id = await self.get_workspace_id(repo_url)
52
+ if (org and workspace_id) or (user and owner.lower() == profile.lower()):
53
+ self.verbose(f"Found workspace ID {workspace_id} for {repo_url}")
54
+ data = await self.request_workspace(workspace_id)
55
+ in_scope = await self.validate_workspace(
56
+ data["workspace"], data["environments"], data["collections"]
57
+ )
58
+ if in_scope:
59
+ in_scope_workspaces.append({"url": repo_url, "repo_name": slug})
60
+ else:
61
+ self.verbose(
62
+ f"Failed to validate {repo_url} is in our scope as it does not contain any in-scope dns_names / emails"
63
+ )
64
+ return in_scope_workspaces
57
65
 
58
66
  async def query(self, query):
67
+ def api_page_iter(url, page, page_size, offset, **kwargs):
68
+ kwargs["json"]["body"]["from"] = offset
69
+ return url, kwargs
70
+
59
71
  data = []
60
72
  url = f"{self.base_url}/ws/proxy"
61
73
  json = {
@@ -67,7 +79,7 @@ class postman(postman):
67
79
  "collaboration.workspace",
68
80
  ],
69
81
  "queryText": self.helpers.quote(query),
70
- "size": 100,
82
+ "size": 25,
71
83
  "from": 0,
72
84
  "clientTraceId": "",
73
85
  "requestOrigin": "srp",
@@ -76,13 +88,19 @@ class postman(postman):
76
88
  "domain": "public",
77
89
  },
78
90
  }
79
- r = await self.helpers.request(url, method="POST", json=json, headers=self.headers)
80
- if r is None:
81
- return data
82
- status_code = getattr(r, "status_code", 0)
83
- try:
84
- json = r.json()
85
- except Exception as e:
86
- self.warning(f"Failed to decode JSON for {r.url} (HTTP status: {status_code}): {e}")
87
- return None
88
- return json.get("data", [])
91
+
92
+ agen = self.api_page_iter(
93
+ url, page_size=25, method="POST", iter_key=api_page_iter, json=json, _json=False, headers=self.headers
94
+ )
95
+ async for r in agen:
96
+ status_code = getattr(r, "status_code", 0)
97
+ if status_code != 200:
98
+ self.debug(f"Reached end of postman search results (url: {r.url}) with status code {status_code}")
99
+ break
100
+ try:
101
+ data.extend(r.json().get("data", []))
102
+ except Exception as e:
103
+ self.warning(f"Failed to decode JSON for {r.url} (HTTP status: {status_code}): {e}")
104
+ return None
105
+
106
+ return data
@@ -24,11 +24,7 @@ class postman_download(postman):
24
24
  else:
25
25
  self.output_dir = self.scan.home / "postman_workspaces"
26
26
  self.helpers.mkdir(self.output_dir)
27
- return await self.require_api_key()
28
-
29
- def prepare_api_request(self, url, kwargs):
30
- kwargs["headers"]["X-Api-Key"] = self.api_key
31
- return url, kwargs
27
+ return await super().setup()
32
28
 
33
29
  async def filter_event(self, event):
34
30
  if event.type == "CODE_REPOSITORY":
@@ -45,149 +41,16 @@ class postman_download(postman):
45
41
  workspace = data["workspace"]
46
42
  environments = data["environments"]
47
43
  collections = data["collections"]
48
- in_scope = await self.validate_workspace(workspace, environments, collections)
49
- if in_scope:
50
- workspace_path = self.save_workspace(workspace, environments, collections)
51
- if workspace_path:
52
- self.verbose(f"Downloaded workspace from {repo_url} to {workspace_path}")
53
- codebase_event = self.make_event(
54
- {"path": str(workspace_path)}, "FILESYSTEM", tags=["postman", "workspace"], parent=event
55
- )
56
- await self.emit_event(
57
- codebase_event,
58
- context=f"{{module}} downloaded postman workspace at {repo_url} to {{event.type}}: {workspace_path}",
59
- )
60
- else:
61
- self.verbose(
62
- f"Failed to validate {repo_url} is in our scope as it does not contain any in-scope dns_names / emails, skipping download"
44
+ workspace_path = self.save_workspace(workspace, environments, collections)
45
+ if workspace_path:
46
+ self.verbose(f"Downloaded workspace from {repo_url} to {workspace_path}")
47
+ codebase_event = self.make_event(
48
+ {"path": str(workspace_path)}, "FILESYSTEM", tags=["postman", "workspace"], parent=event
49
+ )
50
+ await self.emit_event(
51
+ codebase_event,
52
+ context=f"{{module}} downloaded postman workspace at {repo_url} to {{event.type}}: {workspace_path}",
63
53
  )
64
-
65
- async def get_workspace_id(self, repo_url):
66
- workspace_id = ""
67
- profile = repo_url.split("/")[-2]
68
- name = repo_url.split("/")[-1]
69
- url = f"{self.base_url}/ws/proxy"
70
- json = {
71
- "service": "workspaces",
72
- "method": "GET",
73
- "path": f"/workspaces?handle={profile}&slug={name}",
74
- }
75
- r = await self.helpers.request(url, method="POST", json=json, headers=self.headers)
76
- if r is None:
77
- return workspace_id
78
- status_code = getattr(r, "status_code", 0)
79
- try:
80
- json = r.json()
81
- except Exception as e:
82
- self.warning(f"Failed to decode JSON for {r.url} (HTTP status: {status_code}): {e}")
83
- return workspace_id
84
- data = json.get("data", [])
85
- if len(data) == 1:
86
- workspace_id = data[0]["id"]
87
- return workspace_id
88
-
89
- async def request_workspace(self, id):
90
- data = {"workspace": {}, "environments": [], "collections": []}
91
- workspace = await self.get_workspace(id)
92
- if workspace:
93
- # Main Workspace
94
- name = workspace["name"]
95
- data["workspace"] = workspace
96
-
97
- # Workspace global variables
98
- self.verbose(f"Downloading globals for workspace {name}")
99
- globals = await self.get_globals(id)
100
- data["environments"].append(globals)
101
-
102
- # Workspace Environments
103
- workspace_environments = workspace.get("environments", [])
104
- if workspace_environments:
105
- self.verbose(f"Downloading environments for workspace {name}")
106
- for _ in workspace_environments:
107
- environment_id = _["uid"]
108
- environment = await self.get_environment(environment_id)
109
- data["environments"].append(environment)
110
-
111
- # Workspace Collections
112
- workspace_collections = workspace.get("collections", [])
113
- if workspace_collections:
114
- self.verbose(f"Downloading collections for workspace {name}")
115
- for _ in workspace_collections:
116
- collection_id = _["uid"]
117
- collection = await self.get_collection(collection_id)
118
- data["collections"].append(collection)
119
- return data
120
-
121
- async def get_workspace(self, workspace_id):
122
- workspace = {}
123
- workspace_url = f"{self.api_url}/workspaces/{workspace_id}"
124
- r = await self.api_request(workspace_url)
125
- if r is None:
126
- return workspace
127
- status_code = getattr(r, "status_code", 0)
128
- try:
129
- json = r.json()
130
- except Exception as e:
131
- self.warning(f"Failed to decode JSON for {r.url} (HTTP status: {status_code}): {e}")
132
- return workspace
133
- workspace = json.get("workspace", {})
134
- return workspace
135
-
136
- async def get_globals(self, workspace_id):
137
- globals = {}
138
- globals_url = f"{self.base_url}/workspace/{workspace_id}/globals"
139
- r = await self.helpers.request(globals_url, headers=self.headers)
140
- if r is None:
141
- return globals
142
- status_code = getattr(r, "status_code", 0)
143
- try:
144
- json = r.json()
145
- except Exception as e:
146
- self.warning(f"Failed to decode JSON for {r.url} (HTTP status: {status_code}): {e}")
147
- return globals
148
- globals = json.get("data", {})
149
- return globals
150
-
151
- async def get_environment(self, environment_id):
152
- environment = {}
153
- environment_url = f"{self.api_url}/environments/{environment_id}"
154
- r = await self.api_request(environment_url)
155
- if r is None:
156
- return environment
157
- status_code = getattr(r, "status_code", 0)
158
- try:
159
- json = r.json()
160
- except Exception as e:
161
- self.warning(f"Failed to decode JSON for {r.url} (HTTP status: {status_code}): {e}")
162
- return environment
163
- environment = json.get("environment", {})
164
- return environment
165
-
166
- async def get_collection(self, collection_id):
167
- collection = {}
168
- collection_url = f"{self.api_url}/collections/{collection_id}"
169
- r = await self.api_request(collection_url)
170
- if r is None:
171
- return collection
172
- status_code = getattr(r, "status_code", 0)
173
- try:
174
- json = r.json()
175
- except Exception as e:
176
- self.warning(f"Failed to decode JSON for {r.url} (HTTP status: {status_code}): {e}")
177
- return collection
178
- collection = json.get("collection", {})
179
- return collection
180
-
181
- async def validate_workspace(self, workspace, environments, collections):
182
- name = workspace.get("name", "")
183
- full_wks = str([workspace, environments, collections])
184
- in_scope_hosts = await self.scan.extract_in_scope_hostnames(full_wks)
185
- if in_scope_hosts:
186
- self.verbose(
187
- f'Found in-scope hostname(s): "{in_scope_hosts}" in workspace {name}, it appears to be in-scope'
188
- )
189
- return True
190
- return False
191
54
 
192
55
  def save_workspace(self, workspace, environments, collections):
193
56
  zip_path = None
@@ -36,7 +36,7 @@ class sitedossier(subdomain_enum):
36
36
  base_url = f"{self.base_url}/{self.helpers.quote(query)}"
37
37
  url = str(base_url)
38
38
  for i, page in enumerate(range(1, 100 * self.max_pages + 2, 100)):
39
- self.verbose(f"Fetching page #{i+1} for {query}")
39
+ self.verbose(f"Fetching page #{i + 1} for {query}")
40
40
  if page > 1:
41
41
  url = f"{base_url}/{page}"
42
42
  response = await self.helpers.request(url)
bbot/modules/skymem.py CHANGED
@@ -51,5 +51,5 @@ class skymem(emailformat):
51
51
  email,
52
52
  "EMAIL_ADDRESS",
53
53
  parent=event,
54
- context=f'{{module}} searched skymem.info for "{query}" and found {{event.type}} on page {i+1}: {{event.data}}',
54
+ context=f'{{module}} searched skymem.info for "{query}" and found {{event.type}} on page {i + 1}: {{event.data}}',
55
55
  )