Skip to content

Commit 5ce0e18

Browse files
committed
Replace httpx/curl HTTP infrastructure with blasthttp
- Add blasthttp (>=0.1.3) as HTTP engine, remove httpx subprocess dependency - Remove HTTPEngine subprocess, all HTTP now in-process via shared blasthttp client - Remove curl helper, use request() with resolve_ip and request_target - Remove obsolete ffuf module (replaced by web_brute) - Remove obsolete httpx module (replaced by http) - Add native http module using blasthttp batch API - Add native web_brute module using blasthttp batch API - Add web_brute_shortnames module - Add generic_ssrf module - Rewrite sslcert to use blasthttp cert_info - Add blasthttp mock infrastructure for tests - Add resolve_ip passthrough in test conftest for localhost - Add rate limit tests - Add 5-minute default timeout for downloads - Rename output http module to webhook - Fix elastic output module import - Update all module tests for blasthttp mock API
1 parent 007424f commit 5ce0e18

191 files changed

Lines changed: 3492 additions & 2471 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ __pycache__/
33
/data/
44
/neo4j/
55
.DS_Store
6+
pytest_debug.log

AGENTS.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ Key helpers:
146146
| Helper | What it does |
147147
|--------|-------------|
148148
| `self.helpers.request(url)` | Make an HTTP request (with retries, SSL handling, etc.) |
149+
| `self.helpers.blasthttp` | Shared blasthttp client (rate-limited via `web.http_rate_limit` config) |
149150
| `self.helpers.resolve(host)` | DNS resolution |
150151
| `self.helpers.is_ip(s)` | Check if string is an IP |
151152
| `self.helpers.is_dns_name(s)` | Check if string is a hostname |
@@ -219,7 +220,7 @@ from .base import ModuleTestBase
219220

220221
class TestMyModule(ModuleTestBase):
221222
async def setup_after_prep(self, module_test):
222-
module_test.httpx_mock.add_response(
223+
module_test.blasthttp_mock.add_response(
223224
url="https://api.example.com/lookup?domain=blacklanternsecurity.com",
224225
json={"emails": ["info@blacklanternsecurity.com"]},
225226
)
@@ -370,7 +371,7 @@ Whether to process seed events (the initial targets provided to the scan).
370371
Whether to accept "special" URLs (e.g. JavaScript files) that are not normally distributed to web modules.
371372

372373
```python
373-
# httpx.py - needs to process all URLs including special ones
374+
# http.py - needs to process all URLs including special ones
374375
accept_url_special = True
375376
```
376377

@@ -535,7 +536,7 @@ _preserve_graph = True
535536
Exclude this module from scan statistics. Used by output and report modules.
536537

537538
##### `_disable_auto_module_deps` (bool) -- default: `False`
538-
Prevent BBOT from automatically enabling dependency modules. For example, if your module watches `URL` events, BBOT normally auto-enables `httpx`. Set this to `True` to prevent that.
539+
Prevent BBOT from automatically enabling dependency modules. For example, if your module watches `URL` events, BBOT normally auto-enables `http`. Set this to `True` to prevent that.
539540

540541
---
541542

@@ -875,7 +876,7 @@ class TestMyModule(ModuleTestBase):
875876
targets = ["http://127.0.0.1:8888"]
876877

877878
# Optional: override which modules are enabled
878-
modules_overrides = ["httpx", "my_module"]
879+
modules_overrides = ["http", "my_module"]
879880

880881
# Optional: override config
881882
config_overrides = {"modules": {"my_module": {"some_option": True}}}
@@ -887,7 +888,7 @@ class TestMyModule(ModuleTestBase):
887888
async def setup_after_prep(self, module_test):
888889
"""Called AFTER the scan is prepared. Modify modules, add mocks here."""
889890
# Mock an HTTP response
890-
module_test.httpx_mock.add_response(
891+
module_test.blasthttp_mock.add_response(
891892
url="https://api.example.com/lookup?domain=blacklanternsecurity.com",
892893
json={"results": ["sub.blacklanternsecurity.com"]},
893894
)
@@ -920,7 +921,7 @@ The test lifecycle runs:
920921

921922
### Test Utilities
922923

923-
- **`module_test.httpx_mock`** - mock HTTP responses (from pytest-httpx)
924+
- **`module_test.blasthttp_mock`** - mock HTTP responses
924925
- **`module_test.httpserver`** - real HTTP server on port 8888
925926
- **`module_test.httpserver_ssl`** - real HTTPS server on port 9999
926927
- **`module_test.mock_dns(data)`** - mock DNS responses
@@ -933,7 +934,7 @@ Real example -- `test_module_robots.py`:
933934
```python
934935
class TestRobots(ModuleTestBase):
935936
targets = ["http://127.0.0.1:8888"]
936-
modules_overrides = ["httpx", "robots"]
937+
modules_overrides = ["http", "robots"]
937938
config_overrides = {"modules": {"robots": {"include_sitemap": True}}}
938939

939940
async def setup_after_prep(self, module_test):

bbot/core/event/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ class BaseEvent:
101101
"parent": "OPEN_TCP_PORT:cf7e6a937b161217eaed99f0c566eae045d094c7",
102102
"tags": ["in-scope", "distance-0", "dir", "status-301"],
103103
"http_title": "301 Moved Permanently",
104-
"module": "httpx",
105-
"module_sequence": "httpx"
104+
"module": "http",
105+
"module_sequence": "http"
106106
}
107107
```
108108
"""

bbot/core/helpers/command.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
import asyncio
33
import logging
4+
import contextlib
45
import traceback
56
from signal import SIGINT
67
from subprocess import CompletedProcess, CalledProcessError, SubprocessError
@@ -157,7 +158,18 @@ async def run_live(self, *command, check=False, text=True, idle_timeout=None, **
157158
command_str = " ".join(command)
158159
log.warning(f"Stderr for run_live({command_str}):\n\t{stderr}")
159160
finally:
160-
proc_tracker.remove(proc)
161+
proc_tracker.discard(proc)
162+
# Kill the subprocess if it's still running (e.g. generator was cancelled/closed)
163+
if proc.returncode is None:
164+
with contextlib.suppress(Exception):
165+
proc.terminate()
166+
try:
167+
await asyncio.wait_for(proc.wait(), timeout=5)
168+
except (asyncio.TimeoutError, Exception):
169+
with contextlib.suppress(Exception):
170+
proc.kill()
171+
if input_task is not None:
172+
input_task.cancel()
161173

162174

163175
async def _spawn_proc(self, *command, **kwargs):
@@ -270,7 +282,7 @@ def _prepare_command_kwargs(self, command, kwargs):
270282
>>> _prepare_command_kwargs(['ls', '-l'], {'sudo': True})
271283
(['sudo', '-E', '-A', 'LD_LIBRARY_PATH=...', 'PATH=...', 'ls', '-l'], {'limit': 104857600, 'stdout': -1, 'stderr': -1, 'env': environ(...)})
272284
"""
273-
# limit = 100MB (this is needed for cases like httpx that are sending large JSON blobs over stdout)
285+
# limit = 100MB (this is needed for cases that are sending large JSON blobs over stdout)
274286
if "limit" not in kwargs:
275287
kwargs["limit"] = 1024 * 1024 * 100
276288
if "stdout" not in kwargs:

bbot/core/helpers/diff.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ def compare_headers(self, headers_1, headers_2):
148148
for x in list(ddiff[k]):
149149
try:
150150
header_value = str(x).split("'")[1]
151-
except KeyError:
151+
except (KeyError, IndexError):
152152
continue
153153
differing_headers.append(header_value)
154154
return differing_headers

bbot/core/helpers/helper.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,14 @@ def __init__(self, preset):
8686
self.process_pool = ProcessPoolExecutor(max_workers=num_processes)
8787

8888
self._cloud = None
89+
self._blasthttp_client = None
8990

9091
self.re = RegexHelper(self)
9192
self.yara = YaraHelper(self)
9293
self.simhash = SimHashHelper()
9394
self._dns = None
9495
self._web = None
96+
self._asn = None
9597
self._cloudcheck = None
9698
self._asn = None
9799
self.config_aware_validators = self.validators.Validators(self)
@@ -117,6 +119,17 @@ def asn(self):
117119
self._asn = ASNHelper(self)
118120
return self._asn
119121

122+
@property
123+
def blasthttp(self):
124+
if self._blasthttp_client is None:
125+
import blasthttp as _blasthttp
126+
127+
self._blasthttp_client = _blasthttp.BlastHTTP()
128+
rate_limit = self.web_config.get("http_rate_limit", 0)
129+
if rate_limit:
130+
self._blasthttp_client.set_rate_limit(rate_limit)
131+
return self._blasthttp_client
132+
120133
@property
121134
def cloudcheck(self):
122135
if self._cloudcheck is None:

bbot/core/helpers/misc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1661,7 +1661,7 @@ def rm_rf(f, ignore_errors=False):
16611661
f (str or Path): The directory path to delete.
16621662
16631663
Examples:
1664-
>>> rm_rf("/tmp/httpx98323849")
1664+
>>> rm_rf("/tmp/bbot98323849")
16651665
"""
16661666
import shutil
16671667

bbot/core/helpers/names_generator.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"blazed",
2626
"bloodshot",
2727
"brown",
28+
"cantankerous",
2829
"cheeky",
2930
"childish",
3031
"chiseled",
@@ -52,6 +53,7 @@
5253
"demented",
5354
"demonic",
5455
"demonstrative",
56+
"derpy",
5557
"depraved",
5658
"depressed",
5759
"deranged",
@@ -103,6 +105,7 @@
103105
"glutinous",
104106
"golden",
105107
"gothic",
108+
"greasy",
106109
"grievous",
107110
"gummy",
108111
"hallucinogenic",
@@ -137,11 +140,14 @@
137140
"intoxicated",
138141
"inventive",
139142
"irritable",
143+
"janky",
144+
"lackadaisical",
140145
"large",
141146
"liquid",
142147
"loveable",
143148
"lovely",
144149
"lucid",
150+
"lumpy",
145151
"malevolent",
146152
"malfunctioning",
147153
"malicious",
@@ -221,6 +227,7 @@
221227
"sinful",
222228
"sinister",
223229
"slippery",
230+
"sloppy",
224231
"sly",
225232
"sneaky",
226233
"soft",
@@ -311,6 +318,7 @@
311318
"amir",
312319
"amy",
313320
"andrea",
321+
"andres",
314322
"andrew",
315323
"angela",
316324
"ann",
@@ -344,6 +352,7 @@
344352
"bradley",
345353
"brandon",
346354
"brandybuck",
355+
"brendan",
347356
"brenda",
348357
"brian",
349358
"brianna",
@@ -399,6 +408,7 @@
399408
"diana",
400409
"diane",
401410
"dobby",
411+
"dominic",
402412
"donald",
403413
"donna",
404414
"dooku",

0 commit comments

Comments
 (0)