bbot 2.3.0.5546rc0__py3-none-any.whl → 2.3.1__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.
- bbot/__init__.py +1 -1
- bbot/cli.py +1 -1
- bbot/core/engine.py +1 -1
- bbot/core/event/base.py +7 -5
- bbot/core/helpers/async_helpers.py +7 -1
- bbot/core/helpers/depsinstaller/installer.py +7 -2
- bbot/core/helpers/diff.py +13 -4
- bbot/core/helpers/dns/brute.py +8 -2
- bbot/core/helpers/dns/engine.py +3 -2
- bbot/core/helpers/ratelimiter.py +8 -2
- bbot/core/helpers/regexes.py +5 -2
- bbot/core/helpers/web/engine.py +1 -1
- bbot/core/helpers/web/web.py +1 -1
- bbot/core/shared_deps.py +14 -0
- bbot/defaults.yml +44 -0
- bbot/modules/ajaxpro.py +64 -37
- bbot/modules/baddns.py +23 -15
- bbot/modules/baddns_direct.py +2 -2
- bbot/modules/badsecrets.py +2 -2
- bbot/modules/base.py +49 -15
- bbot/modules/censys.py +1 -1
- bbot/modules/deadly/dastardly.py +3 -3
- bbot/modules/deadly/nuclei.py +1 -1
- bbot/modules/dehashed.py +2 -2
- bbot/modules/dnsbrute_mutations.py +3 -1
- bbot/modules/docker_pull.py +1 -1
- bbot/modules/dockerhub.py +2 -2
- bbot/modules/dotnetnuke.py +12 -12
- bbot/modules/extractous.py +1 -1
- bbot/modules/ffuf_shortnames.py +107 -48
- bbot/modules/filedownload.py +6 -0
- bbot/modules/generic_ssrf.py +54 -40
- bbot/modules/github_codesearch.py +2 -2
- bbot/modules/github_org.py +16 -20
- bbot/modules/github_workflows.py +6 -2
- bbot/modules/gowitness.py +6 -0
- bbot/modules/hunt.py +1 -1
- bbot/modules/hunterio.py +1 -1
- bbot/modules/iis_shortnames.py +23 -7
- bbot/modules/internal/excavate.py +5 -3
- bbot/modules/internal/unarchive.py +82 -0
- bbot/modules/jadx.py +2 -2
- bbot/modules/output/asset_inventory.py +1 -1
- bbot/modules/output/base.py +1 -1
- bbot/modules/output/discord.py +2 -1
- bbot/modules/output/slack.py +2 -1
- bbot/modules/output/teams.py +10 -25
- bbot/modules/output/web_parameters.py +55 -0
- bbot/modules/paramminer_headers.py +15 -10
- bbot/modules/portfilter.py +41 -0
- bbot/modules/portscan.py +1 -22
- bbot/modules/postman.py +61 -43
- bbot/modules/postman_download.py +10 -147
- bbot/modules/sitedossier.py +1 -1
- bbot/modules/skymem.py +1 -1
- bbot/modules/templates/postman.py +163 -1
- bbot/modules/templates/subdomain_enum.py +1 -1
- bbot/modules/templates/webhook.py +17 -26
- bbot/modules/trufflehog.py +3 -3
- bbot/modules/wappalyzer.py +1 -1
- bbot/modules/zoomeye.py +1 -1
- bbot/presets/kitchen-sink.yml +1 -1
- bbot/presets/nuclei/nuclei-budget.yml +19 -0
- bbot/presets/nuclei/nuclei-intense.yml +28 -0
- bbot/presets/nuclei/nuclei-technology.yml +23 -0
- bbot/presets/nuclei/nuclei.yml +34 -0
- bbot/presets/spider-intense.yml +13 -0
- bbot/scanner/preset/args.py +29 -3
- bbot/scanner/preset/preset.py +43 -24
- bbot/scanner/scanner.py +17 -7
- bbot/test/bbot_fixtures.py +7 -7
- bbot/test/test_step_1/test_bloom_filter.py +2 -2
- bbot/test/test_step_1/test_cli.py +5 -5
- bbot/test/test_step_1/test_dns.py +33 -0
- bbot/test/test_step_1/test_events.py +15 -5
- bbot/test/test_step_1/test_modules_basic.py +21 -21
- bbot/test/test_step_1/test_presets.py +94 -4
- bbot/test/test_step_1/test_regexes.py +13 -13
- bbot/test/test_step_1/test_scan.py +78 -0
- bbot/test/test_step_1/test_web.py +4 -4
- bbot/test/test_step_2/module_tests/test_module_ajaxpro.py +43 -23
- bbot/test/test_step_2/module_tests/test_module_azure_realm.py +3 -3
- bbot/test/test_step_2/module_tests/test_module_baddns.py +3 -3
- bbot/test/test_step_2/module_tests/test_module_bucket_amazon.py +6 -6
- bbot/test/test_step_2/module_tests/test_module_bufferoverrun.py +3 -3
- bbot/test/test_step_2/module_tests/test_module_cloudcheck.py +3 -3
- bbot/test/test_step_2/module_tests/test_module_dnsbimi.py +3 -3
- bbot/test/test_step_2/module_tests/test_module_dnscaa.py +6 -6
- bbot/test/test_step_2/module_tests/test_module_dnscommonsrv.py +9 -9
- bbot/test/test_step_2/module_tests/test_module_dnstlsrpt.py +12 -12
- bbot/test/test_step_2/module_tests/test_module_excavate.py +15 -15
- bbot/test/test_step_2/module_tests/test_module_extractous.py +3 -3
- bbot/test/test_step_2/module_tests/test_module_ffuf_shortnames.py +8 -8
- bbot/test/test_step_2/module_tests/test_module_generic_ssrf.py +3 -1
- bbot/test/test_step_2/module_tests/test_module_github_codesearch.py +3 -3
- bbot/test/test_step_2/module_tests/test_module_gowitness.py +9 -9
- bbot/test/test_step_2/module_tests/test_module_iis_shortnames.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_paramminer_getparams.py +35 -1
- bbot/test/test_step_2/module_tests/test_module_paramminer_headers.py +3 -3
- bbot/test/test_step_2/module_tests/test_module_portfilter.py +48 -0
- bbot/test/test_step_2/module_tests/test_module_postman.py +338 -3
- bbot/test/test_step_2/module_tests/test_module_postman_download.py +4 -161
- bbot/test/test_step_2/module_tests/test_module_securitytxt.py +12 -12
- bbot/test/test_step_2/module_tests/test_module_teams.py +10 -1
- bbot/test/test_step_2/module_tests/test_module_trufflehog.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_unarchive.py +229 -0
- bbot/test/test_step_2/module_tests/test_module_viewdns.py +3 -3
- bbot/test/test_step_2/module_tests/test_module_web_parameters.py +59 -0
- bbot/test/test_step_2/module_tests/test_module_websocket.py +5 -4
- {bbot-2.3.0.5546rc0.dist-info → bbot-2.3.1.dist-info}/METADATA +7 -7
- {bbot-2.3.0.5546rc0.dist-info → bbot-2.3.1.dist-info}/RECORD +115 -105
- {bbot-2.3.0.5546rc0.dist-info → bbot-2.3.1.dist-info}/WHEEL +1 -1
- bbot/wordlists/ffuf_shortname_candidates.txt +0 -107982
- /bbot/presets/{baddns-thorough.yml → baddns-intense.yml} +0 -0
- {bbot-2.3.0.5546rc0.dist-info → bbot-2.3.1.dist-info}/LICENSE +0 -0
- {bbot-2.3.0.5546rc0.dist-info → bbot-2.3.1.dist-info}/entry_points.txt +0 -0
bbot/modules/output/teams.py
CHANGED
|
@@ -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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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", "
|
|
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", "
|
|
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
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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":
|
|
34
|
+
{"url": repo_url},
|
|
35
35
|
"CODE_REPOSITORY",
|
|
36
36
|
tags="postman",
|
|
37
37
|
parent=event,
|
|
38
|
-
context=
|
|
38
|
+
context=context,
|
|
39
39
|
)
|
|
40
40
|
|
|
41
|
-
async def
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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":
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
bbot/modules/postman_download.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
49
|
-
if
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
bbot/modules/sitedossier.py
CHANGED
|
@@ -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
|
)
|