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.

Files changed (51) hide show
  1. bbot/__init__.py +1 -1
  2. bbot/core/engine.py +1 -1
  3. bbot/core/helpers/bloom.py +6 -7
  4. bbot/core/helpers/dns/dns.py +0 -1
  5. bbot/core/helpers/dns/engine.py +0 -2
  6. bbot/core/helpers/files.py +2 -2
  7. bbot/core/helpers/git.py +17 -0
  8. bbot/core/helpers/misc.py +1 -0
  9. bbot/core/helpers/ntlm.py +0 -2
  10. bbot/core/helpers/regex.py +1 -1
  11. bbot/core/modules.py +0 -54
  12. bbot/defaults.yml +4 -2
  13. bbot/modules/base.py +11 -5
  14. bbot/modules/dnstlsrpt.py +0 -6
  15. bbot/modules/git_clone.py +46 -21
  16. bbot/modules/gitdumper.py +3 -13
  17. bbot/modules/graphql_introspection.py +5 -2
  18. bbot/modules/httpx.py +2 -0
  19. bbot/modules/iis_shortnames.py +0 -7
  20. bbot/modules/internal/unarchive.py +9 -3
  21. bbot/modules/lightfuzz/lightfuzz.py +5 -1
  22. bbot/modules/nuclei.py +1 -1
  23. bbot/modules/output/base.py +0 -5
  24. bbot/modules/retirejs.py +232 -0
  25. bbot/modules/securitytxt.py +0 -3
  26. bbot/modules/subdomaincenter.py +1 -16
  27. bbot/modules/telerik.py +6 -1
  28. bbot/modules/trufflehog.py +1 -1
  29. bbot/scanner/manager.py +7 -4
  30. bbot/scanner/scanner.py +1 -1
  31. bbot/scripts/benchmark_report.py +433 -0
  32. bbot/test/benchmarks/__init__.py +2 -0
  33. bbot/test/benchmarks/test_bloom_filter_benchmarks.py +105 -0
  34. bbot/test/benchmarks/test_closest_match_benchmarks.py +76 -0
  35. bbot/test/benchmarks/test_event_validation_benchmarks.py +438 -0
  36. bbot/test/benchmarks/test_excavate_benchmarks.py +291 -0
  37. bbot/test/benchmarks/test_ipaddress_benchmarks.py +143 -0
  38. bbot/test/benchmarks/test_weighted_shuffle_benchmarks.py +70 -0
  39. bbot/test/test_step_1/test_bbot_fastapi.py +2 -2
  40. bbot/test/test_step_1/test_events.py +0 -1
  41. bbot/test/test_step_1/test_scan.py +1 -8
  42. bbot/test/test_step_2/module_tests/base.py +6 -1
  43. bbot/test/test_step_2/module_tests/test_module_excavate.py +35 -6
  44. bbot/test/test_step_2/module_tests/test_module_lightfuzz.py +2 -2
  45. bbot/test/test_step_2/module_tests/test_module_retirejs.py +159 -0
  46. bbot/test/test_step_2/module_tests/test_module_telerik.py +1 -1
  47. {bbot-2.6.1.6913rc0.dist-info → bbot-2.7.0.dist-info}/METADATA +3 -2
  48. {bbot-2.6.1.6913rc0.dist-info → bbot-2.7.0.dist-info}/RECORD +51 -40
  49. {bbot-2.6.1.6913rc0.dist-info → bbot-2.7.0.dist-info}/LICENSE +0 -0
  50. {bbot-2.6.1.6913rc0.dist-info → bbot-2.7.0.dist-info}/WHEEL +0 -0
  51. {bbot-2.6.1.6913rc0.dist-info → bbot-2.7.0.dist-info}/entry_points.txt +0 -0
@@ -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
@@ -123,6 +123,3 @@ class securitytxt(BaseModule):
123
123
 
124
124
  if found_url != url and self._urls is True:
125
125
  await self.emit_event(found_url, "URL_UNVERIFIED", parent=event, tags=tags)
126
-
127
-
128
- # EOF
@@ -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 = None
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 successfully" in result.text:
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:
@@ -14,7 +14,7 @@ class trufflehog(BaseModule):
14
14
  }
15
15
 
16
16
  options = {
17
- "version": "3.90.5",
17
+ "version": "3.90.6",
18
18
  "config": "",
19
19
  "only_verified": True,
20
20
  "concurrency": 8,
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)