bbot 2.6.1.6913rc0__py3-none-any.whl → 2.7.0__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/core/engine.py +1 -1
- bbot/core/helpers/bloom.py +6 -7
- bbot/core/helpers/dns/dns.py +0 -1
- bbot/core/helpers/dns/engine.py +0 -2
- bbot/core/helpers/files.py +2 -2
- bbot/core/helpers/git.py +17 -0
- bbot/core/helpers/misc.py +1 -0
- bbot/core/helpers/ntlm.py +0 -2
- bbot/core/helpers/regex.py +1 -1
- bbot/core/modules.py +0 -54
- bbot/defaults.yml +4 -2
- bbot/modules/base.py +11 -5
- bbot/modules/dnstlsrpt.py +0 -6
- bbot/modules/git_clone.py +46 -21
- bbot/modules/gitdumper.py +3 -13
- bbot/modules/graphql_introspection.py +5 -2
- bbot/modules/httpx.py +2 -0
- bbot/modules/iis_shortnames.py +0 -7
- bbot/modules/internal/unarchive.py +9 -3
- bbot/modules/lightfuzz/lightfuzz.py +5 -1
- bbot/modules/nuclei.py +1 -1
- bbot/modules/output/base.py +0 -5
- bbot/modules/retirejs.py +232 -0
- bbot/modules/securitytxt.py +0 -3
- bbot/modules/subdomaincenter.py +1 -16
- bbot/modules/telerik.py +6 -1
- bbot/modules/trufflehog.py +1 -1
- bbot/scanner/manager.py +7 -4
- bbot/scanner/scanner.py +1 -1
- bbot/scripts/benchmark_report.py +433 -0
- bbot/test/benchmarks/__init__.py +2 -0
- bbot/test/benchmarks/test_bloom_filter_benchmarks.py +105 -0
- bbot/test/benchmarks/test_closest_match_benchmarks.py +76 -0
- bbot/test/benchmarks/test_event_validation_benchmarks.py +438 -0
- bbot/test/benchmarks/test_excavate_benchmarks.py +291 -0
- bbot/test/benchmarks/test_ipaddress_benchmarks.py +143 -0
- bbot/test/benchmarks/test_weighted_shuffle_benchmarks.py +70 -0
- bbot/test/test_step_1/test_bbot_fastapi.py +2 -2
- bbot/test/test_step_1/test_events.py +0 -1
- bbot/test/test_step_1/test_scan.py +1 -8
- bbot/test/test_step_2/module_tests/base.py +6 -1
- bbot/test/test_step_2/module_tests/test_module_excavate.py +35 -6
- bbot/test/test_step_2/module_tests/test_module_lightfuzz.py +2 -2
- bbot/test/test_step_2/module_tests/test_module_retirejs.py +159 -0
- bbot/test/test_step_2/module_tests/test_module_telerik.py +1 -1
- {bbot-2.6.1.6913rc0.dist-info → bbot-2.7.0.dist-info}/METADATA +3 -2
- {bbot-2.6.1.6913rc0.dist-info → bbot-2.7.0.dist-info}/RECORD +51 -40
- {bbot-2.6.1.6913rc0.dist-info → bbot-2.7.0.dist-info}/LICENSE +0 -0
- {bbot-2.6.1.6913rc0.dist-info → bbot-2.7.0.dist-info}/WHEEL +0 -0
- {bbot-2.6.1.6913rc0.dist-info → bbot-2.7.0.dist-info}/entry_points.txt +0 -0
bbot/modules/retirejs.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from enum import IntEnum
|
|
3
|
+
from bbot.modules.base import BaseModule
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RetireJSSeverity(IntEnum):
|
|
7
|
+
NONE = 0
|
|
8
|
+
LOW = 1
|
|
9
|
+
MEDIUM = 2
|
|
10
|
+
HIGH = 3
|
|
11
|
+
CRITICAL = 4
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def from_string(cls, severity_str):
|
|
15
|
+
try:
|
|
16
|
+
return cls[severity_str.upper()]
|
|
17
|
+
except (KeyError, AttributeError):
|
|
18
|
+
return cls.NONE
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class retirejs(BaseModule):
|
|
22
|
+
watched_events = ["URL_UNVERIFIED"]
|
|
23
|
+
produced_events = ["FINDING"]
|
|
24
|
+
flags = ["active", "safe", "web-thorough"]
|
|
25
|
+
meta = {
|
|
26
|
+
"description": "Detect vulnerable/out-of-date JavaScript libraries",
|
|
27
|
+
"created_date": "2025-08-19",
|
|
28
|
+
"author": "@liquidsec",
|
|
29
|
+
}
|
|
30
|
+
options = {
|
|
31
|
+
"version": "5.3.0",
|
|
32
|
+
"node_version": "18.19.1",
|
|
33
|
+
"severity": "medium",
|
|
34
|
+
}
|
|
35
|
+
options_desc = {
|
|
36
|
+
"version": "retire.js version",
|
|
37
|
+
"node_version": "Node.js version to install locally",
|
|
38
|
+
"severity": "Minimum severity level to report (none, low, medium, high, critical)",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
deps_ansible = [
|
|
42
|
+
# Download Node.js binary (Linux x64)
|
|
43
|
+
{
|
|
44
|
+
"name": "Download Node.js binary (Linux x64)",
|
|
45
|
+
"get_url": {
|
|
46
|
+
"url": "https://nodejs.org/dist/v#{BBOT_MODULES_RETIREJS_NODE_VERSION}/node-v#{BBOT_MODULES_RETIREJS_NODE_VERSION}-linux-x64.tar.xz",
|
|
47
|
+
"dest": "#{BBOT_TEMP}/node-v#{BBOT_MODULES_RETIREJS_NODE_VERSION}-linux-x64.tar.xz",
|
|
48
|
+
"mode": "0644",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
# Extract Node.js binary (x64)
|
|
52
|
+
{
|
|
53
|
+
"name": "Extract Node.js binary (x64)",
|
|
54
|
+
"unarchive": {
|
|
55
|
+
"src": "#{BBOT_TEMP}/node-v#{BBOT_MODULES_RETIREJS_NODE_VERSION}-linux-x64.tar.xz",
|
|
56
|
+
"dest": "#{BBOT_TOOLS}",
|
|
57
|
+
"remote_src": True,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
# Remove existing node directory if it exists
|
|
61
|
+
{
|
|
62
|
+
"name": "Remove existing node directory",
|
|
63
|
+
"file": {"path": "#{BBOT_TOOLS}/node", "state": "absent"},
|
|
64
|
+
},
|
|
65
|
+
# Rename extracted directory to 'node' (x64)
|
|
66
|
+
{
|
|
67
|
+
"name": "Rename Node.js directory (x64)",
|
|
68
|
+
"command": "mv #{BBOT_TOOLS}/node-v#{BBOT_MODULES_RETIREJS_NODE_VERSION}-linux-x64 #{BBOT_TOOLS}/node",
|
|
69
|
+
},
|
|
70
|
+
# Set permissions on entire Node.js bin directory
|
|
71
|
+
{
|
|
72
|
+
"name": "Set permissions on Node.js bin directory",
|
|
73
|
+
"file": {"path": "#{BBOT_TOOLS}/node/bin", "mode": "0755", "recurse": "yes"},
|
|
74
|
+
},
|
|
75
|
+
# Make Node.js binary executable
|
|
76
|
+
{
|
|
77
|
+
"name": "Make Node.js binary executable",
|
|
78
|
+
"file": {"path": "#{BBOT_TOOLS}/node/bin/node", "mode": "0755"},
|
|
79
|
+
},
|
|
80
|
+
# Remove existing retirejs directory if it exists
|
|
81
|
+
{
|
|
82
|
+
"name": "Remove existing retirejs directory",
|
|
83
|
+
"file": {"path": "#{BBOT_TOOLS}/retirejs", "state": "absent"},
|
|
84
|
+
},
|
|
85
|
+
# Create retire.js local directory
|
|
86
|
+
{
|
|
87
|
+
"name": "Create retire.js directory in BBOT_TOOLS",
|
|
88
|
+
"file": {"path": "#{BBOT_TOOLS}/retirejs", "state": "directory", "mode": "0755"},
|
|
89
|
+
},
|
|
90
|
+
# Install retire.js locally using local Node.js
|
|
91
|
+
{
|
|
92
|
+
"name": "Install retire.js locally",
|
|
93
|
+
"shell": "cd #{BBOT_TOOLS}/retirejs && #{BBOT_TOOLS}/node/bin/node #{BBOT_TOOLS}/node/lib/node_modules/npm/bin/npm-cli.js install --prefix . retire@#{BBOT_MODULES_RETIREJS_VERSION} --no-fund --no-audit --silent --no-optional",
|
|
94
|
+
"args": {"creates": "#{BBOT_TOOLS}/retirejs/node_modules/.bin/retire"},
|
|
95
|
+
"timeout": 600,
|
|
96
|
+
"ignore_errors": False,
|
|
97
|
+
},
|
|
98
|
+
# Make retire script executable
|
|
99
|
+
{
|
|
100
|
+
"name": "Make retire script executable",
|
|
101
|
+
"file": {"path": "#{BBOT_TOOLS}/retirejs/node_modules/.bin/retire", "mode": "0755"},
|
|
102
|
+
},
|
|
103
|
+
# Create retire cache directory
|
|
104
|
+
{
|
|
105
|
+
"name": "Create retire cache directory",
|
|
106
|
+
"file": {"path": "#{BBOT_CACHE}/retire_cache", "state": "directory", "mode": "0755"},
|
|
107
|
+
},
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
accept_url_special = True
|
|
111
|
+
scope_distance_modifier = 1
|
|
112
|
+
_module_threads = 4
|
|
113
|
+
|
|
114
|
+
async def setup(self):
|
|
115
|
+
excavate_enabled = self.scan.config.get("excavate")
|
|
116
|
+
if not excavate_enabled:
|
|
117
|
+
return None, "retirejs will not function without excavate enabled"
|
|
118
|
+
|
|
119
|
+
# Validate severity level
|
|
120
|
+
valid_severities = ["none", "low", "medium", "high", "critical"]
|
|
121
|
+
configured_severity = self.config.get("severity", "medium").lower()
|
|
122
|
+
if configured_severity not in valid_severities:
|
|
123
|
+
return (
|
|
124
|
+
False,
|
|
125
|
+
f"Invalid severity level '{configured_severity}'. Valid options are: {', '.join(valid_severities)}",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
self.repofile = await self.helpers.download(
|
|
129
|
+
"https://raw.githubusercontent.com/RetireJS/retire.js/master/repository/jsrepository-v4.json", cache_hrs=24
|
|
130
|
+
)
|
|
131
|
+
if not self.repofile:
|
|
132
|
+
return False, "failed to download retire.js repository file"
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
async def handle_event(self, event):
|
|
136
|
+
js_file = await self.helpers.request(event.data)
|
|
137
|
+
if js_file:
|
|
138
|
+
js_file_body = js_file.text
|
|
139
|
+
if js_file_body:
|
|
140
|
+
js_file_body_saved = self.helpers.tempfile(js_file_body, pipe=False, extension="js")
|
|
141
|
+
results = await self.execute_retirejs(js_file_body_saved)
|
|
142
|
+
if not results:
|
|
143
|
+
self.warning("no output from retire.js")
|
|
144
|
+
return
|
|
145
|
+
results_json = json.loads(results)
|
|
146
|
+
if results_json.get("data"):
|
|
147
|
+
for file_result in results_json["data"]:
|
|
148
|
+
for component_result in file_result.get("results", []):
|
|
149
|
+
component = component_result.get("component", "unknown")
|
|
150
|
+
version = component_result.get("version", "unknown")
|
|
151
|
+
vulnerabilities = component_result.get("vulnerabilities", [])
|
|
152
|
+
for vuln in vulnerabilities:
|
|
153
|
+
severity = vuln.get("severity", "unknown")
|
|
154
|
+
|
|
155
|
+
# Filter by minimum severity level
|
|
156
|
+
min_severity = RetireJSSeverity.from_string(self.config.get("severity", "medium"))
|
|
157
|
+
vuln_severity = RetireJSSeverity.from_string(severity)
|
|
158
|
+
if vuln_severity < min_severity:
|
|
159
|
+
self.debug(
|
|
160
|
+
f"Skipping vulnerability with severity '{severity}' (below minimum '{min_severity.name.lower()}')"
|
|
161
|
+
)
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
identifiers = vuln.get("identifiers", {})
|
|
165
|
+
summary = identifiers.get("summary", "Unknown vulnerability")
|
|
166
|
+
cves = identifiers.get("CVE", [])
|
|
167
|
+
description_parts = [
|
|
168
|
+
f"Vulnerable JavaScript library detected: {component} v{version}",
|
|
169
|
+
f"Severity: {severity.upper()}",
|
|
170
|
+
f"Summary: {summary}",
|
|
171
|
+
f"JavaScript URL: {event.data}",
|
|
172
|
+
]
|
|
173
|
+
if cves:
|
|
174
|
+
description_parts.append(f"CVE(s): {', '.join(cves)}")
|
|
175
|
+
|
|
176
|
+
below_version = vuln.get("below", "")
|
|
177
|
+
at_or_above = vuln.get("atOrAbove", "")
|
|
178
|
+
if at_or_above and below_version:
|
|
179
|
+
description_parts.append(f"Affected versions: [{at_or_above} to {below_version})")
|
|
180
|
+
elif below_version:
|
|
181
|
+
description_parts.append(f"Affected versions: [< {below_version}]")
|
|
182
|
+
elif at_or_above:
|
|
183
|
+
description_parts.append(f"Affected versions: [>= {at_or_above}]")
|
|
184
|
+
description = " ".join(description_parts)
|
|
185
|
+
data = {
|
|
186
|
+
"description": description,
|
|
187
|
+
"severity": severity,
|
|
188
|
+
"component": component,
|
|
189
|
+
"url": event.parent.data["url"],
|
|
190
|
+
}
|
|
191
|
+
await self.emit_event(
|
|
192
|
+
data,
|
|
193
|
+
"FINDING",
|
|
194
|
+
parent=event,
|
|
195
|
+
context=f"{{module}} identified vulnerable JavaScript library {component} v{version} ({severity} severity)",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
async def filter_event(self, event):
|
|
199
|
+
url_extension = getattr(event, "url_extension", "")
|
|
200
|
+
if url_extension != "js":
|
|
201
|
+
return False, f"it is a {url_extension} URL but retirejs only accepts js URLs"
|
|
202
|
+
return True
|
|
203
|
+
|
|
204
|
+
async def execute_retirejs(self, js_file):
|
|
205
|
+
cache_dir = self.helpers.cache_dir / "retire_cache"
|
|
206
|
+
retire_dir = self.scan.helpers.tools_dir / "retirejs"
|
|
207
|
+
local_node_dir = self.scan.helpers.tools_dir / "node"
|
|
208
|
+
|
|
209
|
+
# Use the retire binary directly with our local Node.js
|
|
210
|
+
retire_binary_path = retire_dir / "node_modules" / ".bin" / "retire"
|
|
211
|
+
command = [
|
|
212
|
+
str(local_node_dir / "bin" / "node"),
|
|
213
|
+
str(retire_binary_path),
|
|
214
|
+
"--outputformat",
|
|
215
|
+
"json",
|
|
216
|
+
"--cachedir",
|
|
217
|
+
str(cache_dir),
|
|
218
|
+
"--path",
|
|
219
|
+
js_file,
|
|
220
|
+
"--jsrepo",
|
|
221
|
+
str(self.repofile),
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
proxy = self.scan.web_config.get("http_proxy")
|
|
225
|
+
if proxy:
|
|
226
|
+
command.extend(["--proxy", proxy])
|
|
227
|
+
|
|
228
|
+
self.verbose(f"Running retire.js on {js_file}")
|
|
229
|
+
self.verbose(f"retire.js command: {command}")
|
|
230
|
+
|
|
231
|
+
result = await self.run_process(command)
|
|
232
|
+
return result.stdout
|
bbot/modules/securitytxt.py
CHANGED
bbot/modules/subdomaincenter.py
CHANGED
|
@@ -12,25 +12,10 @@ class subdomaincenter(subdomain_enum):
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
base_url = "https://api.subdomain.center"
|
|
15
|
-
retries = 2
|
|
16
|
-
|
|
17
|
-
async def sleep(self, time_to_wait):
|
|
18
|
-
self.info(f"Sleeping for {time_to_wait} seconds to avoid rate limit")
|
|
19
|
-
await self.helpers.sleep(time_to_wait)
|
|
20
15
|
|
|
21
16
|
async def request_url(self, query):
|
|
22
17
|
url = f"{self.base_url}/?domain={self.helpers.quote(query)}"
|
|
23
|
-
response =
|
|
24
|
-
status_code = 0
|
|
25
|
-
for i, _ in enumerate(range(self.retries + 1)):
|
|
26
|
-
if i > 0:
|
|
27
|
-
self.verbose(f"Retry #{i} for {query} after response code {status_code}")
|
|
28
|
-
response = await self.helpers.request(url, timeout=self.http_timeout + 30)
|
|
29
|
-
status_code = getattr(response, "status_code", 0)
|
|
30
|
-
if status_code == 429:
|
|
31
|
-
await self.sleep(20)
|
|
32
|
-
else:
|
|
33
|
-
break
|
|
18
|
+
response = await self.api_request(url)
|
|
34
19
|
return response
|
|
35
20
|
|
|
36
21
|
async def parse_results(self, r, query):
|
bbot/modules/telerik.py
CHANGED
|
@@ -204,7 +204,7 @@ class telerik(BaseModule):
|
|
|
204
204
|
webresource = "Telerik.Web.UI.WebResource.axd?type=rau"
|
|
205
205
|
result, _ = await self.test_detector(base_url, webresource)
|
|
206
206
|
if result:
|
|
207
|
-
if "RadAsyncUpload handler is registered
|
|
207
|
+
if "RadAsyncUpload handler is registered succesfully" in result.text:
|
|
208
208
|
self.verbose("Detected Telerik instance (Telerik.Web.UI.WebResource.axd?type=rau)")
|
|
209
209
|
|
|
210
210
|
probe_data = {
|
|
@@ -263,6 +263,11 @@ class telerik(BaseModule):
|
|
|
263
263
|
str(root_tool_path / "testfile.txt"),
|
|
264
264
|
result.url,
|
|
265
265
|
]
|
|
266
|
+
|
|
267
|
+
# Add proxy if set in the scan config
|
|
268
|
+
if self.scan.http_proxy:
|
|
269
|
+
command.append(self.scan.http_proxy)
|
|
270
|
+
|
|
266
271
|
output = await self.run_process(command)
|
|
267
272
|
description = f"[CVE-2017-11317] [{str(version)}] {webresource}"
|
|
268
273
|
if "fileInfo" in output.stdout:
|
bbot/modules/trufflehog.py
CHANGED
bbot/scanner/manager.py
CHANGED
|
@@ -94,10 +94,6 @@ class ScanIngress(BaseInterceptModule):
|
|
|
94
94
|
# special handling of URL extensions
|
|
95
95
|
url_extension = getattr(event, "url_extension", None)
|
|
96
96
|
if url_extension is not None:
|
|
97
|
-
if url_extension in self.scan.url_extension_httpx_only:
|
|
98
|
-
event.add_tag("httpx-only")
|
|
99
|
-
event._omit = True
|
|
100
|
-
|
|
101
97
|
# blacklist by extension
|
|
102
98
|
if url_extension in self.scan.url_extension_blacklist:
|
|
103
99
|
self.debug(
|
|
@@ -209,6 +205,13 @@ class ScanEgress(BaseInterceptModule):
|
|
|
209
205
|
)
|
|
210
206
|
event.internal = True
|
|
211
207
|
|
|
208
|
+
# mark special URLs (e.g. Javascript) as internal so they don't get output except when they're critical to the graph
|
|
209
|
+
if event.type.startswith("URL"):
|
|
210
|
+
extension = getattr(event, "url_extension", "")
|
|
211
|
+
if extension in self.scan.url_extension_special:
|
|
212
|
+
event.internal = True
|
|
213
|
+
self.debug(f"Making {event} internal because it is a special URL (extension {extension})")
|
|
214
|
+
|
|
212
215
|
if event.type in self.scan.omitted_event_types:
|
|
213
216
|
self.debug(f"Omitting {event} because its type is omitted in the config")
|
|
214
217
|
event._omit = True
|
bbot/scanner/scanner.py
CHANGED
|
@@ -230,8 +230,8 @@ class Scanner:
|
|
|
230
230
|
)
|
|
231
231
|
|
|
232
232
|
# url file extensions
|
|
233
|
+
self.url_extension_special = {e.lower() for e in self.config.get("url_extension_special", [])}
|
|
233
234
|
self.url_extension_blacklist = {e.lower() for e in self.config.get("url_extension_blacklist", [])}
|
|
234
|
-
self.url_extension_httpx_only = {e.lower() for e in self.config.get("url_extension_httpx_only", [])}
|
|
235
235
|
|
|
236
236
|
# url querystring behavior
|
|
237
237
|
self.url_querystring_remove = self.config.get("url_querystring_remove", True)
|