bbot 2.6.0.6840rc0__py3-none-any.whl → 2.7.2.7424rc0__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.
Files changed (122) hide show
  1. bbot/__init__.py +1 -1
  2. bbot/cli.py +22 -8
  3. bbot/core/engine.py +1 -1
  4. bbot/core/event/__init__.py +2 -2
  5. bbot/core/event/base.py +138 -110
  6. bbot/core/flags.py +1 -0
  7. bbot/core/helpers/bloom.py +6 -7
  8. bbot/core/helpers/depsinstaller/installer.py +21 -2
  9. bbot/core/helpers/dns/dns.py +0 -1
  10. bbot/core/helpers/dns/engine.py +0 -2
  11. bbot/core/helpers/files.py +2 -2
  12. bbot/core/helpers/git.py +17 -0
  13. bbot/core/helpers/helper.py +6 -5
  14. bbot/core/helpers/misc.py +8 -23
  15. bbot/core/helpers/ntlm.py +0 -2
  16. bbot/core/helpers/regex.py +1 -1
  17. bbot/core/helpers/regexes.py +25 -8
  18. bbot/core/helpers/web/web.py +2 -1
  19. bbot/core/modules.py +22 -60
  20. bbot/defaults.yml +4 -2
  21. bbot/modules/apkpure.py +1 -1
  22. bbot/modules/baddns.py +1 -1
  23. bbot/modules/baddns_direct.py +1 -1
  24. bbot/modules/baddns_zone.py +1 -1
  25. bbot/modules/badsecrets.py +1 -1
  26. bbot/modules/base.py +123 -38
  27. bbot/modules/bucket_amazon.py +1 -1
  28. bbot/modules/bucket_digitalocean.py +1 -1
  29. bbot/modules/bucket_firebase.py +1 -1
  30. bbot/modules/bucket_google.py +1 -1
  31. bbot/modules/{bucket_azure.py → bucket_microsoft.py} +2 -2
  32. bbot/modules/builtwith.py +4 -2
  33. bbot/modules/dnsbimi.py +1 -4
  34. bbot/modules/dnsbrute.py +6 -1
  35. bbot/modules/dnsdumpster.py +35 -52
  36. bbot/modules/dnstlsrpt.py +0 -6
  37. bbot/modules/docker_pull.py +1 -1
  38. bbot/modules/emailformat.py +17 -1
  39. bbot/modules/ffuf.py +4 -1
  40. bbot/modules/ffuf_shortnames.py +6 -3
  41. bbot/modules/filedownload.py +7 -4
  42. bbot/modules/git_clone.py +47 -22
  43. bbot/modules/gitdumper.py +4 -14
  44. bbot/modules/github_workflows.py +6 -5
  45. bbot/modules/gitlab_com.py +31 -0
  46. bbot/modules/gitlab_onprem.py +84 -0
  47. bbot/modules/gowitness.py +0 -6
  48. bbot/modules/graphql_introspection.py +5 -2
  49. bbot/modules/httpx.py +2 -0
  50. bbot/modules/iis_shortnames.py +0 -7
  51. bbot/modules/internal/cloudcheck.py +65 -72
  52. bbot/modules/internal/unarchive.py +9 -3
  53. bbot/modules/lightfuzz/lightfuzz.py +6 -2
  54. bbot/modules/lightfuzz/submodules/esi.py +42 -0
  55. bbot/modules/medusa.py +4 -7
  56. bbot/modules/nuclei.py +1 -1
  57. bbot/modules/otx.py +9 -2
  58. bbot/modules/output/base.py +3 -11
  59. bbot/modules/paramminer_headers.py +10 -7
  60. bbot/modules/portfilter.py +2 -0
  61. bbot/modules/postman_download.py +1 -1
  62. bbot/modules/retirejs.py +232 -0
  63. bbot/modules/securitytxt.py +0 -3
  64. bbot/modules/sslcert.py +2 -2
  65. bbot/modules/subdomaincenter.py +1 -16
  66. bbot/modules/telerik.py +7 -2
  67. bbot/modules/templates/bucket.py +24 -4
  68. bbot/modules/templates/gitlab.py +98 -0
  69. bbot/modules/trufflehog.py +6 -3
  70. bbot/modules/wafw00f.py +2 -2
  71. bbot/presets/web/lightfuzz-heavy.yml +1 -1
  72. bbot/presets/web/lightfuzz-medium.yml +1 -1
  73. bbot/presets/web/lightfuzz-superheavy.yml +1 -1
  74. bbot/scanner/manager.py +44 -37
  75. bbot/scanner/scanner.py +12 -4
  76. bbot/scripts/benchmark_report.py +433 -0
  77. bbot/test/benchmarks/__init__.py +2 -0
  78. bbot/test/benchmarks/test_bloom_filter_benchmarks.py +105 -0
  79. bbot/test/benchmarks/test_closest_match_benchmarks.py +76 -0
  80. bbot/test/benchmarks/test_event_validation_benchmarks.py +438 -0
  81. bbot/test/benchmarks/test_excavate_benchmarks.py +291 -0
  82. bbot/test/benchmarks/test_ipaddress_benchmarks.py +143 -0
  83. bbot/test/benchmarks/test_weighted_shuffle_benchmarks.py +70 -0
  84. bbot/test/test_step_1/test_bbot_fastapi.py +2 -2
  85. bbot/test/test_step_1/test_events.py +22 -21
  86. bbot/test/test_step_1/test_helpers.py +1 -0
  87. bbot/test/test_step_1/test_manager_scope_accuracy.py +45 -0
  88. bbot/test/test_step_1/test_modules_basic.py +40 -15
  89. bbot/test/test_step_1/test_python_api.py +2 -2
  90. bbot/test/test_step_1/test_regexes.py +21 -4
  91. bbot/test/test_step_1/test_scan.py +7 -8
  92. bbot/test/test_step_1/test_web.py +46 -0
  93. bbot/test/test_step_2/module_tests/base.py +6 -1
  94. bbot/test/test_step_2/module_tests/test_module_bucket_amazon.py +52 -18
  95. bbot/test/test_step_2/module_tests/test_module_bucket_google.py +1 -1
  96. bbot/test/test_step_2/module_tests/{test_module_bucket_azure.py → test_module_bucket_microsoft.py} +7 -5
  97. bbot/test/test_step_2/module_tests/test_module_cloudcheck.py +19 -31
  98. bbot/test/test_step_2/module_tests/test_module_dnsbimi.py +2 -1
  99. bbot/test/test_step_2/module_tests/test_module_dnsdumpster.py +3 -5
  100. bbot/test/test_step_2/module_tests/test_module_emailformat.py +1 -1
  101. bbot/test/test_step_2/module_tests/test_module_emails.py +2 -2
  102. bbot/test/test_step_2/module_tests/test_module_excavate.py +57 -4
  103. bbot/test/test_step_2/module_tests/test_module_github_workflows.py +10 -1
  104. bbot/test/test_step_2/module_tests/test_module_gitlab_com.py +66 -0
  105. bbot/test/test_step_2/module_tests/{test_module_gitlab.py → test_module_gitlab_onprem.py} +4 -69
  106. bbot/test/test_step_2/module_tests/test_module_lightfuzz.py +71 -3
  107. bbot/test/test_step_2/module_tests/test_module_nuclei.py +1 -2
  108. bbot/test/test_step_2/module_tests/test_module_otx.py +3 -0
  109. bbot/test/test_step_2/module_tests/test_module_portfilter.py +2 -0
  110. bbot/test/test_step_2/module_tests/test_module_retirejs.py +161 -0
  111. bbot/test/test_step_2/module_tests/test_module_telerik.py +1 -1
  112. bbot/test/test_step_2/module_tests/test_module_trufflehog.py +10 -1
  113. {bbot-2.6.0.6840rc0.dist-info → bbot-2.7.2.7424rc0.dist-info}/METADATA +10 -7
  114. {bbot-2.6.0.6840rc0.dist-info → bbot-2.7.2.7424rc0.dist-info}/RECORD +117 -106
  115. {bbot-2.6.0.6840rc0.dist-info → bbot-2.7.2.7424rc0.dist-info}/WHEEL +1 -1
  116. {bbot-2.6.0.6840rc0.dist-info → bbot-2.7.2.7424rc0.dist-info/licenses}/LICENSE +98 -58
  117. bbot/modules/censys.py +0 -98
  118. bbot/modules/gitlab.py +0 -141
  119. bbot/modules/zoomeye.py +0 -77
  120. bbot/test/test_step_2/module_tests/test_module_censys.py +0 -83
  121. bbot/test/test_step_2/module_tests/test_module_zoomeye.py +0 -35
  122. {bbot-2.6.0.6840rc0.dist-info → bbot-2.7.2.7424rc0.dist-info}/entry_points.txt +0 -0
bbot/scanner/scanner.py CHANGED
@@ -10,7 +10,7 @@ from datetime import datetime
10
10
  from collections import OrderedDict
11
11
 
12
12
  from bbot import __version__
13
- from bbot.core.event import make_event
13
+ from bbot.core.event import make_event, update_event
14
14
  from .manager import ScanIngress, ScanEgress
15
15
  from bbot.core.helpers.misc import sha1, rand_string
16
16
  from bbot.core.helpers.names_generator import random_name
@@ -99,6 +99,7 @@ class Scanner:
99
99
  def __init__(
100
100
  self,
101
101
  *targets,
102
+ name=None,
102
103
  scan_id=None,
103
104
  dispatcher=None,
104
105
  **kwargs,
@@ -137,6 +138,9 @@ class Scanner:
137
138
 
138
139
  from .preset import Preset
139
140
 
141
+ if name is not None:
142
+ kwargs["scan_name"] = name
143
+
140
144
  base_preset = Preset(*targets, **kwargs)
141
145
 
142
146
  if custom_preset is not None:
@@ -226,8 +230,8 @@ class Scanner:
226
230
  )
227
231
 
228
232
  # url file extensions
233
+ self.url_extension_special = {e.lower() for e in self.config.get("url_extension_special", [])}
229
234
  self.url_extension_blacklist = {e.lower() for e in self.config.get("url_extension_blacklist", [])}
230
- self.url_extension_httpx_only = {e.lower() for e in self.config.get("url_extension_httpx_only", [])}
231
235
 
232
236
  # url querystring behavior
233
237
  self.url_querystring_remove = self.config.get("url_querystring_remove", True)
@@ -480,7 +484,7 @@ class Scanner:
480
484
  for module in self.modules.values():
481
485
  module.start()
482
486
 
483
- async def setup_modules(self, remove_failed=True):
487
+ async def setup_modules(self, remove_failed=True, deps_only=False):
484
488
  """Asynchronously initializes all loaded modules by invoking their `setup()` methods.
485
489
 
486
490
  Args:
@@ -505,7 +509,7 @@ class Scanner:
505
509
  hard_failed = []
506
510
  soft_failed = []
507
511
 
508
- async for task in self.helpers.as_completed([m._setup() for m in self.modules.values()]):
512
+ async for task in self.helpers.as_completed([m._setup(deps_only=deps_only) for m in self.modules.values()]):
509
513
  module, status, msg = await task
510
514
  if status is True:
511
515
  self.debug(f"Setup succeeded for {module.name} ({msg})")
@@ -991,6 +995,10 @@ class Scanner:
991
995
  event = make_event(*args, **kwargs)
992
996
  return event
993
997
 
998
+ def update_event(self, event, **kwargs):
999
+ kwargs["scan"] = self
1000
+ return update_event(event, **kwargs)
1001
+
994
1002
  @property
995
1003
  def root_event(self):
996
1004
  """
@@ -0,0 +1,433 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Branch-based benchmark comparison tool for BBOT performance tests.
4
+
5
+ This script takes two git branches, runs benchmarks on each, and generates
6
+ a comparison report showing performance differences between them.
7
+ """
8
+
9
+ import json
10
+ import argparse
11
+ import subprocess
12
+ import tempfile
13
+ from pathlib import Path
14
+ from typing import Dict, List, Any, Tuple
15
+
16
+
17
+ def run_command(cmd: List[str], cwd: Path = None, capture_output: bool = True) -> subprocess.CompletedProcess:
18
+ """Run a shell command and return the result."""
19
+ try:
20
+ result = subprocess.run(cmd, cwd=cwd, capture_output=capture_output, text=True, check=True)
21
+ return result
22
+ except subprocess.CalledProcessError as e:
23
+ print(f"Command failed: {' '.join(cmd)}")
24
+ print(f"Exit code: {e.returncode}")
25
+ print(f"Error output: {e.stderr}")
26
+ raise
27
+
28
+
29
+ def get_current_branch() -> str:
30
+ """Get the current git branch name."""
31
+ result = run_command(["git", "branch", "--show-current"])
32
+ return result.stdout.strip()
33
+
34
+
35
+ def checkout_branch(branch: str, repo_path: Path = None):
36
+ """Checkout a git branch."""
37
+ print(f"Checking out branch: {branch}")
38
+ run_command(["git", "checkout", branch], cwd=repo_path)
39
+
40
+
41
+ def run_benchmarks(output_file: Path, repo_path: Path = None) -> bool:
42
+ """Run benchmarks and save results to JSON file."""
43
+ print(f"Running benchmarks, saving to {output_file}")
44
+
45
+ # Check if benchmarks directory exists
46
+ benchmarks_dir = repo_path / "bbot/test/benchmarks" if repo_path else Path("bbot/test/benchmarks")
47
+ if not benchmarks_dir.exists():
48
+ print(f"Benchmarks directory not found: {benchmarks_dir}")
49
+ print("This branch likely doesn't have benchmark tests yet.")
50
+ return False
51
+
52
+ try:
53
+ cmd = [
54
+ "poetry",
55
+ "run",
56
+ "python",
57
+ "-m",
58
+ "pytest",
59
+ "bbot/test/benchmarks/",
60
+ "--benchmark-only",
61
+ f"--benchmark-json={output_file}",
62
+ "-q",
63
+ ]
64
+ run_command(cmd, cwd=repo_path, capture_output=False)
65
+ return True
66
+ except subprocess.CalledProcessError:
67
+ print("Benchmarks failed for current state")
68
+ return False
69
+
70
+
71
+ def load_benchmark_data(filepath: Path) -> Dict[str, Any]:
72
+ """Load benchmark data from JSON file."""
73
+ try:
74
+ with open(filepath, "r") as f:
75
+ return json.load(f)
76
+ except FileNotFoundError:
77
+ print(f"Warning: Benchmark file not found: {filepath}")
78
+ return {}
79
+ except json.JSONDecodeError:
80
+ print(f"Warning: Could not parse JSON from {filepath}")
81
+ return {}
82
+
83
+
84
+ def format_time(seconds: float) -> str:
85
+ """Format time in human-readable format."""
86
+ if seconds < 0.000001: # Less than 1 microsecond
87
+ return f"{seconds * 1000000000:.0f}ns" # Show as nanoseconds with no decimal
88
+ elif seconds < 0.001: # Less than 1 millisecond
89
+ return f"{seconds * 1000000:.2f}µs" # Show as microseconds with 2 decimal places
90
+ elif seconds < 1: # Less than 1 second
91
+ return f"{seconds * 1000:.2f}ms" # Show as milliseconds with 2 decimal places
92
+ else:
93
+ return f"{seconds:.3f}s" # Show as seconds with 3 decimal places
94
+
95
+
96
+ def format_ops(ops: float) -> str:
97
+ """Format operations per second."""
98
+ if ops > 1000:
99
+ return f"{ops / 1000:.1f}K ops/sec"
100
+ else:
101
+ return f"{ops:.1f} ops/sec"
102
+
103
+
104
+ def calculate_change_percentage(old_value: float, new_value: float) -> Tuple[float, str]:
105
+ """Calculate percentage change and return emoji indicator."""
106
+ if old_value == 0:
107
+ return 0, "🆕"
108
+
109
+ change = ((new_value - old_value) / old_value) * 100
110
+
111
+ if change > 10:
112
+ return change, "⚠️" # Regression (slower)
113
+ elif change < -10:
114
+ return change, "🚀" # Improvement (faster)
115
+ else:
116
+ return change, "✅" # No significant change
117
+
118
+
119
+ def generate_benchmark_table(benchmarks: List[Dict[str, Any]], title: str = "Results") -> str:
120
+ """Generate markdown table for benchmark results."""
121
+ if not benchmarks:
122
+ return f"### {title}\nNo benchmark data available.\n"
123
+
124
+ table = f"""### {title}
125
+
126
+ | Test Name | Mean Time | Ops/sec | Min | Max |
127
+ |-----------|-----------|---------|-----|-----|
128
+ """
129
+
130
+ for bench in benchmarks:
131
+ stats = bench.get("stats", {})
132
+ name = bench.get("name", "Unknown")
133
+ # Generic test name cleanup - just remove 'test_' prefix and format nicely
134
+ test_name = name.replace("test_", "").replace("_", " ").title()
135
+
136
+ mean = format_time(stats.get("mean", 0))
137
+ ops = format_ops(stats.get("ops", 0))
138
+ min_time = format_time(stats.get("min", 0))
139
+ max_time = format_time(stats.get("max", 0))
140
+
141
+ table += f"| {test_name} | {mean} | {ops} | {min_time} | {max_time} |\n"
142
+
143
+ return table + "\n"
144
+
145
+
146
+ def generate_comparison_table(current_data: Dict, base_data: Dict, current_branch: str, base_branch: str) -> str:
147
+ """Generate comparison table between current and base benchmark results."""
148
+ if not current_data or not base_data:
149
+ return ""
150
+
151
+ current_benchmarks = current_data.get("benchmarks", [])
152
+ base_benchmarks = base_data.get("benchmarks", [])
153
+
154
+ # Create lookup for base benchmarks
155
+ base_lookup = {bench["name"]: bench for bench in base_benchmarks}
156
+
157
+ if not current_benchmarks:
158
+ return ""
159
+
160
+ # Count changes for summary
161
+ improvements = 0
162
+ regressions = 0
163
+ no_change = 0
164
+
165
+ table = f"""## 📊 Performance Benchmark Report
166
+
167
+ > Comparing **`{base_branch}`** (baseline) vs **`{current_branch}`** (current)
168
+
169
+ <details>
170
+ <summary>📈 <strong>Detailed Results</strong> (All Benchmarks)</summary>
171
+
172
+ > 📋 **Complete results for all benchmarks** - includes both significant and insignificant changes
173
+
174
+ | 🧪 Test Name | 📏 Base | 📏 Current | 📈 Change | 🎯 Status |
175
+ |--------------|---------|------------|-----------|-----------|"""
176
+
177
+ significant_changes = []
178
+ performance_summary = []
179
+
180
+ for current_bench in current_benchmarks:
181
+ name = current_bench.get("name", "Unknown")
182
+ # Generic test name cleanup - just remove 'test_' prefix and format nicely
183
+ test_name = name.replace("test_", "").replace("_", " ").title()
184
+
185
+ current_stats = current_bench.get("stats", {})
186
+ current_mean = current_stats.get("mean", 0)
187
+ # For multi-item benchmarks, calculate correct ops/sec
188
+ if "excavate" in name:
189
+ current_ops = 100 / current_mean # 100 segments per test
190
+ elif "event_validation" in name and "small" in name:
191
+ current_ops = 100 / current_mean # 100 targets per test
192
+ elif "event_validation" in name and "large" in name:
193
+ current_ops = 1000 / current_mean # 1000 targets per test
194
+ elif "make_event" in name and "small" in name:
195
+ current_ops = 100 / current_mean # 100 items per test
196
+ elif "make_event" in name and "large" in name:
197
+ current_ops = 1000 / current_mean # 1000 items per test
198
+ elif "ip" in name:
199
+ current_ops = 1000 / current_mean # 1000 IPs per test
200
+ elif "bloom_filter" in name:
201
+ if "dns_mutation" in name:
202
+ current_ops = 2500 / current_mean # 2500 operations per test
203
+ else:
204
+ current_ops = 13000 / current_mean # 13000 operations per test
205
+ else:
206
+ current_ops = 1 / current_mean # Default: single operation
207
+
208
+ base_bench = base_lookup.get(name)
209
+ if base_bench:
210
+ base_stats = base_bench.get("stats", {})
211
+ base_mean = base_stats.get("mean", 0)
212
+ # For multi-item benchmarks, calculate correct ops/sec
213
+ if "excavate" in name:
214
+ base_ops = 100 / base_mean # 100 segments per test
215
+ elif "event_validation" in name and "small" in name:
216
+ base_ops = 100 / base_mean # 100 targets per test
217
+ elif "event_validation" in name and "large" in name:
218
+ base_ops = 1000 / base_mean # 1000 targets per test
219
+ elif "make_event" in name and "small" in name:
220
+ base_ops = 100 / base_mean # 100 items per test
221
+ elif "make_event" in name and "large" in name:
222
+ base_ops = 1000 / base_mean # 1000 items per test
223
+ elif "ip" in name:
224
+ base_ops = 1000 / base_mean # 1000 IPs per test
225
+ elif "bloom_filter" in name:
226
+ if "dns_mutation" in name:
227
+ base_ops = 2500 / base_mean # 2500 operations per test
228
+ else:
229
+ base_ops = 13000 / base_mean # 13000 operations per test
230
+ else:
231
+ base_ops = 1 / base_mean # Default: single operation
232
+
233
+ change_percent, emoji = calculate_change_percentage(base_mean, current_mean)
234
+
235
+ # Create visual change indicator
236
+ if abs(change_percent) > 20:
237
+ change_bar = "🔴🔴🔴" if change_percent > 0 else "🟢🟢🟢"
238
+ elif abs(change_percent) > 10:
239
+ change_bar = "🟡🟡" if change_percent > 0 else "🟢🟢"
240
+ else:
241
+ change_bar = "⚪"
242
+
243
+ table += f"\n| **{test_name}** | `{format_time(base_mean)}` | `{format_time(current_mean)}` | **{change_percent:+.1f}%** {change_bar} | {emoji} |"
244
+
245
+ # Track significant changes
246
+ if abs(change_percent) > 10:
247
+ direction = "🐌 slower" if change_percent > 0 else "🚀 faster"
248
+ significant_changes.append(f"- **{test_name}**: {abs(change_percent):.1f}% {direction}")
249
+ if change_percent > 0:
250
+ regressions += 1
251
+ else:
252
+ improvements += 1
253
+ else:
254
+ no_change += 1
255
+
256
+ # Add to performance summary
257
+ ops_change = ((current_ops - base_ops) / base_ops) * 100 if base_ops > 0 else 0
258
+ performance_summary.append(
259
+ {
260
+ "name": test_name,
261
+ "time_change": change_percent,
262
+ "ops_change": ops_change,
263
+ "current_ops": current_ops,
264
+ }
265
+ )
266
+ else:
267
+ table += f"\n| **{test_name}** | `-` | `{format_time(current_mean)}` | **New** 🆕 | 🆕 |"
268
+ significant_changes.append(
269
+ f"- **{test_name}**: New test 🆕 ({format_time(current_mean)}, {format_ops(current_ops)})"
270
+ )
271
+
272
+ table += "\n\n</details>\n\n"
273
+
274
+ # Add performance summary
275
+ table += "## 🎯 Performance Summary\n\n"
276
+
277
+ if improvements > 0 or regressions > 0:
278
+ table += "```diff\n"
279
+ if improvements > 0:
280
+ table += f"+ {improvements} improvement{'s' if improvements != 1 else ''} 🚀\n"
281
+ if regressions > 0:
282
+ table += f"! {regressions} regression{'s' if regressions != 1 else ''} ⚠️\n"
283
+ if no_change > 0:
284
+ table += f" {no_change} unchanged ✅\n"
285
+ table += "```\n\n"
286
+ else:
287
+ table += "✅ **No significant performance changes detected** (all changes <10%)\n\n"
288
+
289
+ # Add significant changes section
290
+ if significant_changes:
291
+ table += "### 🔍 Significant Changes (>10%)\n\n"
292
+ for change in significant_changes:
293
+ table += f"{change}\n"
294
+ table += "\n"
295
+
296
+ return table
297
+
298
+
299
+ def generate_report(current_data: Dict, base_data: Dict, current_branch: str, base_branch: str) -> str:
300
+ """Generate complete benchmark comparison report."""
301
+
302
+ if not current_data:
303
+ report = """## 🚀 Performance Benchmark Report
304
+
305
+ > ⚠️ **No current benchmark data available**
306
+ >
307
+ > This might be because:
308
+ > - Benchmarks failed to run
309
+ > - No benchmark tests found
310
+ > - Dependencies missing
311
+
312
+ """
313
+ return report
314
+
315
+ if not base_data:
316
+ report = f"""## 🚀 Performance Benchmark Report
317
+
318
+ > ℹ️ **No baseline benchmark data available**
319
+ >
320
+ > Showing current results for **{current_branch}** only.
321
+
322
+ """
323
+ current_benchmarks = current_data.get("benchmarks", [])
324
+ if current_benchmarks:
325
+ report += f"""<details>
326
+ <summary>📊 Current Results ({current_branch}) - Click to expand</summary>
327
+
328
+ {generate_benchmark_table(current_benchmarks, "Results")}
329
+ </details>"""
330
+ else:
331
+ # Add comparison
332
+ comparison = generate_comparison_table(current_data, base_data, current_branch, base_branch)
333
+ if comparison:
334
+ report = comparison
335
+ else:
336
+ # Fallback if no comparison data
337
+ report = f"""## 🚀 Performance Benchmark Report
338
+
339
+ > ℹ️ **No baseline benchmark data available**
340
+ >
341
+ > Showing current results for **{current_branch}** only.
342
+
343
+ """
344
+
345
+ # Get Python version info
346
+ machine_info = current_data.get("machine_info", {})
347
+ python_version = machine_info.get("python_version", "Unknown")
348
+
349
+ report += f"\n\n---\n\n🐍 Python Version {python_version}"
350
+
351
+ return report
352
+
353
+
354
+ def main():
355
+ parser = argparse.ArgumentParser(description="Compare benchmark performance between git branches")
356
+ parser.add_argument("--base", required=True, help="Base branch name (e.g., 'main', 'dev')")
357
+ parser.add_argument("--current", required=True, help="Current branch name (e.g., 'feature-branch', 'HEAD')")
358
+ parser.add_argument("--output", type=Path, help="Output markdown file (default: stdout)")
359
+ parser.add_argument("--keep-results", action="store_true", help="Keep intermediate JSON files")
360
+
361
+ args = parser.parse_args()
362
+
363
+ # Get current working directory
364
+ repo_path = Path.cwd()
365
+
366
+ # Save original branch to restore later
367
+ try:
368
+ original_branch = get_current_branch()
369
+ print(f"Current branch: {original_branch}")
370
+ except subprocess.CalledProcessError:
371
+ print("Warning: Could not determine current branch")
372
+ original_branch = None
373
+
374
+ # Create temporary files for benchmark results
375
+ with tempfile.TemporaryDirectory() as temp_dir:
376
+ temp_path = Path(temp_dir)
377
+ base_results_file = temp_path / "base_results.json"
378
+ current_results_file = temp_path / "current_results.json"
379
+
380
+ base_data = {}
381
+ current_data = {}
382
+
383
+ base_data = {}
384
+ current_data = {}
385
+
386
+ try:
387
+ # Run benchmarks on base branch
388
+ print(f"\n=== Running benchmarks on base branch: {args.base} ===")
389
+ checkout_branch(args.base, repo_path)
390
+ if run_benchmarks(base_results_file, repo_path):
391
+ base_data = load_benchmark_data(base_results_file)
392
+
393
+ # Run benchmarks on current branch
394
+ print(f"\n=== Running benchmarks on current branch: {args.current} ===")
395
+ checkout_branch(args.current, repo_path)
396
+ if run_benchmarks(current_results_file, repo_path):
397
+ current_data = load_benchmark_data(current_results_file)
398
+
399
+ # Generate report
400
+ print("\n=== Generating comparison report ===")
401
+ report = generate_report(current_data, base_data, args.current, args.base)
402
+
403
+ # Output report
404
+ if args.output:
405
+ with open(args.output, "w") as f:
406
+ f.write(report)
407
+ print(f"Report written to {args.output}")
408
+ else:
409
+ print("\n" + "=" * 80)
410
+ print(report)
411
+
412
+ # Keep results if requested
413
+ if args.keep_results:
414
+ if base_data:
415
+ with open("base_benchmark_results.json", "w") as f:
416
+ json.dump(base_data, f, indent=2)
417
+ if current_data:
418
+ with open("current_benchmark_results.json", "w") as f:
419
+ json.dump(current_data, f, indent=2)
420
+ print("Benchmark result files saved.")
421
+
422
+ finally:
423
+ # Restore original branch
424
+ if original_branch:
425
+ print(f"\nRestoring original branch: {original_branch}")
426
+ try:
427
+ checkout_branch(original_branch, repo_path)
428
+ except subprocess.CalledProcessError:
429
+ print(f"Warning: Could not restore original branch {original_branch}")
430
+
431
+
432
+ if __name__ == "__main__":
433
+ main()
@@ -0,0 +1,2 @@
1
+ # Benchmark tests for BBOT performance monitoring
2
+ # These tests measure performance of critical code paths
@@ -0,0 +1,105 @@
1
+ import pytest
2
+ import string
3
+ import random
4
+ from bbot.scanner import Scanner
5
+
6
+
7
+ class TestBloomFilterBenchmarks:
8
+ """
9
+ Benchmark tests for Bloom Filter operations.
10
+
11
+ These tests measure the performance of bloom filter operations which are
12
+ critical for DNS brute-forcing efficiency in BBOT.
13
+ """
14
+
15
+ def setup_method(self):
16
+ """Setup common test data"""
17
+ self.scan = Scanner()
18
+
19
+ # Generate test data of different sizes
20
+ self.items_small = self._generate_random_strings(1000) # 1K items
21
+ self.items_medium = self._generate_random_strings(10000) # 10K items
22
+
23
+ def _generate_random_strings(self, n, length=10):
24
+ """Generate a list of n random strings."""
25
+ # Slightly longer strings for testing performance difference
26
+ length = length + 2 # Make strings 2 chars longer
27
+ return ["".join(random.choices(string.ascii_letters + string.digits, k=length)) for _ in range(n)]
28
+
29
+ @pytest.mark.benchmark(group="bloom_filter_operations")
30
+ def test_bloom_filter_dns_mutation_tracking_performance(self, benchmark):
31
+ """Benchmark comprehensive bloom filter operations (add, check, mixed) for DNS brute-forcing"""
32
+
33
+ def comprehensive_bloom_operations():
34
+ bloom_filter = self.scan.helpers.bloom_filter(size=8000000) # 8M bits
35
+
36
+ # Phase 1: Add operations (simulating storing tried DNS mutations)
37
+ for item in self.items_small:
38
+ bloom_filter.add(item)
39
+
40
+ # Phase 2: Check operations (simulating lookup of existing mutations)
41
+ found_count = 0
42
+ for item in self.items_small:
43
+ if item in bloom_filter:
44
+ found_count += 1
45
+
46
+ # Phase 3: Mixed operations (realistic DNS brute-force simulation)
47
+ # Add new items while checking existing ones
48
+ for i, item in enumerate(self.items_medium[:500]): # Smaller subset for mixed ops
49
+ bloom_filter.add(item)
50
+ # Every few additions, check some existing items
51
+ if i % 10 == 0:
52
+ for check_item in self.items_small[i : i + 5]:
53
+ if check_item in bloom_filter:
54
+ found_count += 1
55
+
56
+ return {
57
+ "items_added": len(self.items_small) + 500,
58
+ "items_checked": found_count,
59
+ "bloom_size": bloom_filter.size,
60
+ }
61
+
62
+ result = benchmark(comprehensive_bloom_operations)
63
+ assert result["items_added"] > 1000
64
+ assert result["items_checked"] > 0
65
+
66
+ @pytest.mark.benchmark(group="bloom_filter_scalability")
67
+ def test_bloom_filter_large_scale_dns_brute_force(self, benchmark):
68
+ """Benchmark bloom filter performance with large-scale DNS brute-force simulation"""
69
+
70
+ def large_scale_simulation():
71
+ bloom_filter = self.scan.helpers.bloom_filter(size=8000000) # 8M bits
72
+
73
+ # Simulate a large DNS brute-force session
74
+ mutations_tried = 0
75
+ duplicate_attempts = 0
76
+
77
+ # Add all medium dataset (simulating 10K DNS mutations)
78
+ for item in self.items_medium:
79
+ bloom_filter.add(item)
80
+ mutations_tried += 1
81
+
82
+ # Simulate checking for duplicates during brute-force
83
+ for item in self.items_medium[:2000]: # Check subset for duplicates
84
+ if item in bloom_filter:
85
+ duplicate_attempts += 1
86
+
87
+ # Simulate adding more mutations with duplicate checking
88
+ for item in self.items_small:
89
+ if item not in bloom_filter: # Only add if not already tried
90
+ bloom_filter.add(item)
91
+ mutations_tried += 1
92
+ else:
93
+ duplicate_attempts += 1
94
+
95
+ return {
96
+ "total_mutations_tried": mutations_tried,
97
+ "duplicates_avoided": duplicate_attempts,
98
+ "efficiency_ratio": mutations_tried / (mutations_tried + duplicate_attempts)
99
+ if duplicate_attempts > 0
100
+ else 1.0,
101
+ }
102
+
103
+ result = benchmark(large_scale_simulation)
104
+ assert result["total_mutations_tried"] > 10000
105
+ assert result["efficiency_ratio"] > 0
@@ -0,0 +1,76 @@
1
+ import pytest
2
+ import random
3
+ from bbot.core.helpers.misc import closest_match
4
+
5
+
6
+ class TestClosestMatchBenchmarks:
7
+ """
8
+ Benchmark tests for closest_match operations.
9
+
10
+ This function is critical for BBOT's DNS brute forcing, where it finds the best
11
+ matching parent event among thousands of choices. Performance here directly impacts
12
+ scan throughput and DNS mutation efficiency.
13
+ """
14
+
15
+ def setup_method(self):
16
+ """Setup common test data"""
17
+ # Set deterministic seed for consistent benchmark results
18
+ random.seed(42) # Fixed seed for reproducible results
19
+
20
+ # Generate test data for benchmarks
21
+ self.large_closest_match_choices = self._generate_large_closest_match_choices()
22
+ self.realistic_closest_match_choices = self._generate_realistic_closest_match_choices()
23
+
24
+ def _generate_large_closest_match_choices(self):
25
+ """Generate large closest match dataset (stress test with many parent events)"""
26
+ choices = []
27
+ for i in range(10000):
28
+ # Generate realistic domain names with more variety
29
+ tld = random.choice(["com", "net", "org", "io", "co", "dev"])
30
+ domain = f"subdomain{i}.example{i % 100}.{tld}"
31
+ choices.append(domain)
32
+ return choices
33
+
34
+ def _generate_realistic_closest_match_choices(self):
35
+ """Generate realistic closest match parent event choices (like actual BBOT usage)"""
36
+ choices = []
37
+
38
+ # Common TLDs
39
+ tlds = ["com", "net", "org", "io", "co", "dev", "test", "local"]
40
+
41
+ # Generate parent domains with realistic patterns
42
+ for i in range(5000):
43
+ # Base domain patterns
44
+ if i % 10 == 0:
45
+ # Simple domains
46
+ domain = f"example{i}.{random.choice(tlds)}"
47
+ elif i % 5 == 0:
48
+ # Multi-level domains
49
+ domain = f"sub{i}.example{i}.{random.choice(tlds)}"
50
+ else:
51
+ # Complex domains
52
+ domain = f"level1{i}.level2{i}.example{i}.{random.choice(tlds)}"
53
+
54
+ choices.append(domain)
55
+
56
+ return choices
57
+
58
+ @pytest.mark.benchmark(group="closest_match")
59
+ def test_large_closest_match_lookup(self, benchmark):
60
+ """Benchmark closest_match with large closest match workload (many parent events)"""
61
+
62
+ def find_large_closest_match():
63
+ return closest_match("subdomain5678.example50.com", self.large_closest_match_choices)
64
+
65
+ result = benchmark.pedantic(find_large_closest_match, iterations=50, rounds=10)
66
+ assert result is not None
67
+
68
+ @pytest.mark.benchmark(group="closest_match")
69
+ def test_realistic_closest_match_workload(self, benchmark):
70
+ """Benchmark closest_match with realistic BBOT closest match parent event choices"""
71
+
72
+ def find_realistic_closest_match():
73
+ return closest_match("subdomain123.example5.com", self.realistic_closest_match_choices)
74
+
75
+ result = benchmark.pedantic(find_realistic_closest_match, iterations=50, rounds=10)
76
+ assert result is not None