Skip to content

Commit 76b8bf0

Browse files
Merge pull request #26 from blacklanternsecurity/hackspace-dev
Hackspace Dev
2 parents bc357d9 + a3ef31e commit 76b8bf0

Some content is hidden

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

56 files changed

+1461
-378
lines changed

.github/workflows/tests.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ concurrency:
1111

1212
jobs:
1313
test:
14-
runs-on: ubuntu-latest
14+
runs-on: self-hosted
1515
strategy:
1616
fail-fast: false
1717
matrix:
@@ -20,11 +20,11 @@ jobs:
2020
mongo:
2121
image: mongo
2222
ports:
23-
- 27017:27017
23+
- 127.0.0.1:27017:27017
2424
redis:
2525
image: redis
2626
ports:
27-
- 6379:6379
27+
- 127.0.0.1:6379:6379
2828
steps:
2929
- uses: actions/checkout@v4
3030
- name: Set up Python

README.md

Lines changed: 85 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ BBOT Server is a database and multiplayer hub for all your [BBOT](https://github
2121
- [x] REST API
2222
- [x] Python SDK
2323
- [x] Export to JSON/CSV
24+
- [x] [AI interaction via MCP](#MCP)
2425

2526
## Installation
2627

@@ -112,7 +113,7 @@ bbctl scan start "evilcorp_subdomains"
112113

113114
You can monitor the scan's progress in several ways:
114115

115-
**Tail asset activity**:
116+
### Tail asset activity:
116117

117118
This will output an activity whenever a change is detected to an asset, e.g. a change in DNS, new open port, vulnerability, or technology.
118119

@@ -121,7 +122,7 @@ This will output an activity whenever a change is detected to an asset, e.g. a c
121122
bbctl activity tail
122123
```
123124

124-
**Tail raw events**:
125+
### Tail raw events:
125126

126127
If you'd like, you can also tail the raw events as they stream in from the BBOT scan.
127128

@@ -130,7 +131,7 @@ If you'd like, you can also tail the raw events as they stream in from the BBOT
130131
bbctl event tail
131132
```
132133

133-
**Check scan status**:
134+
### Check scan status:
134135

135136
You can monitor or stop an in-progress scan:
136137

@@ -151,23 +152,33 @@ You can list targets like so:
151152
```bash
152153
# List targets
153154
bbctl target list
154-
```
155155

156-
You can create a target manually:
156+
# Create a new target
157+
bbctl target create --seeds seeds.txt --blacklist blacklist.txt
158+
```
157159

158160
## Custom triggers
159161

160-
TODO
162+
You can kick off a custom command or bash script whenever a certain activity happens, such as when a new technology or open port is discovered.
163+
164+
```bash
165+
# Trigger a custom command whenever a new open port is discovered
166+
bbctl activity tail --json | jq -r 'select(.type == "PORT_OPENED") | .netloc' | while read netloc
167+
do
168+
echo "New open port at $netloc"
169+
./custom_script.sh "$netloc"
170+
done
171+
```
161172

162173
## Alerting
163174

164175
TODO
165176

166-
## View/export the data
177+
## Query and Export Data
167178

168179
You can query and export the data even while a scan is running.
169180

170-
**List assets**:
181+
### Assets
171182

172183
```bash
173184
# List assets
@@ -180,7 +191,7 @@ bbctl asset export --csv > assets.csv
180191
bbctl asset export --json | jq
181192
```
182193

183-
**List events**:
194+
### Events
184195

185196
```bash
186197
# List events
@@ -193,19 +204,78 @@ bbctl event export --csv > events.csv
193204
bbctl event export --json | jq
194205
```
195206

207+
### Technologies
208+
209+
```bash
210+
# List technologies
211+
bbctl technology list
212+
213+
# List technologies by specific domain
214+
bbctl technology list --domain evilcorp.com
215+
```
216+
217+
### Findings
218+
219+
```bash
220+
# List findings
221+
bbctl finding list
222+
223+
# Search findings for a certain string
224+
bbctl finding list --search "IIS"
225+
```
226+
227+
### Statistics
228+
229+
Overarching statistics are stored for all assets, and can be queried by target or domain.
230+
231+
```bash
232+
# List stats for all assets
233+
bbctl asset stats | jq
234+
235+
# List stats for specific domain
236+
bbctl asset stats --domain evilcorp.com | jq
237+
```
238+
239+
### MCP
240+
241+
BBOT Server supports chat-based AI interaction via MCP (Model Context Protocol).
242+
243+
The SSE server listens at `http://localhost:8807/v1/mcp/`
244+
245+
`mcp.json` (cursor / vs code):
246+
```json
247+
{
248+
"mcpServers": {
249+
"bbot": {
250+
"url": "http://localhost:8807/v1/mcp/"
251+
}
252+
}
253+
}
254+
```
255+
256+
After connecting your AI client to BBOT Server, you can ask it sensible questions like, "Use MCP to get all the bbot findings", "what are the top open ports?", "what else can you do with BBOT MCP?", etc.
257+
196258
## Screenshots
197259

198-
*Scan editor (terminal UI)*
260+
*Tailing activities in real time*
261+
262+
![activity-tail](https://github.com/user-attachments/assets/8188f32c-45bc-4f81-bf98-c59adfbdc5df)
263+
264+
*AI Chat interaction via MCP*
265+
266+
![mcp](https://github.com/user-attachments/assets/3997b534-2ed8-4e04-b8c3-a7b42daf4106)
267+
268+
*Scans*
199269

200-
![scan-editor](https://github.com/user-attachments/assets/9c31d2ef-f4f0-4d65-bd45-263a8d16bd7f)
270+
![scan-run-list](https://github.com/user-attachments/assets/d6ffb6e5-06d7-4439-936a-3d2b1a6306ee)
201271

202-
*Launch and monitor concurrent scans*
272+
*Technologies*
203273

204-
![scans](https://github.com/user-attachments/assets/7644809f-e111-49f8-b627-c0c77a65110a)
274+
![technology-list](https://github.com/user-attachments/assets/7b662858-8c08-4bb9-a520-6381d2964dde)
205275

206-
*Realtime asset monitoring*
276+
*Findings*
207277

208-
![monitor-assets](https://github.com/user-attachments/assets/ed7ac9f2-34e8-4770-a971-49fdf7f77bea)
278+
![finding-list](https://github.com/user-attachments/assets/3fcbb977-6d47-4dc1-81b7-a26e8e3bc292)
209279

210280
*REST API*
211281

bbot_server/agent/agent.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from bbot.scanner import Scanner, Preset
1212
from bbot.scanner.dispatcher import Dispatcher
13+
from bbot.constants import get_scan_status_name, SCAN_STATUS_NOT_STARTED
1314

1415
from bbot_server.config import BBOT_SERVER_CONFIG
1516
from bbot_server.errors import BBOTServerValueError
@@ -20,6 +21,7 @@
2021
default_server_url = BBOT_SERVER_CONFIG.get("url", "http://localhost:8807/v1/")
2122
default_bbot_preset = BBOT_SERVER_CONFIG.get("agent", {}).get("base_preset", {})
2223

24+
log = logging.getLogger("bbot_server.agent")
2325

2426
VALID_AGENT_COMMANDS = {}
2527

@@ -120,7 +122,12 @@ async def start_scan(self, scan_run: dict[str, Any]):
120122
scan = Scanner(preset=preset, scan_id=scan_run.id, dispatcher=self.dispatcher)
121123
self._patch_scan(scan)
122124
self.scan_task = asyncio.create_task(self._start_scan_task(scan))
123-
return {"scan_id": scan.id, "scan_status": scan.status, "status": "success"}
125+
return {
126+
"scan_id": scan.id,
127+
"scan_status": scan.status,
128+
"scan_status_code": scan._status_code,
129+
"status": "success",
130+
}
124131

125132
@command
126133
async def cancel_scan(self, force: bool = False):
@@ -159,7 +166,13 @@ async def _start_scan_task(self, scan):
159166

160167
@command
161168
async def get_agent_status(self, detailed: bool = False):
162-
ret = {"agent_status": self.status, "scan_status": getattr(self.scan, "status", "NOT_RUNNING")}
169+
scan_status_code = getattr(self.scan, "_status_code", SCAN_STATUS_NOT_STARTED)
170+
scan_status = get_scan_status_name(scan_status_code)
171+
ret = {
172+
"agent_status": self.status,
173+
"scan_status": scan_status,
174+
"scan_status_code": scan_status_code,
175+
}
163176
if detailed and self.scan is not None:
164177
ret["scan_status_detail"] = self.scan.modules_status(detailed=detailed)
165178
return ret
@@ -262,14 +275,16 @@ async def loop(self):
262275
async def gratuitous_status_update(self, scan=None, scan_status=None):
263276
scan_id = getattr(scan, "id", None)
264277
scan_name = getattr(scan, "name", None)
265-
scan_status = scan_status or getattr(scan, "status", "NOT_RUNNING")
278+
scan_status_code = getattr(scan, "_status_code", SCAN_STATUS_NOT_STARTED)
279+
scan_status = get_scan_status_name(scan_status_code)
266280
status = {
267281
"agent_status": self.status,
268282
"scan_status": scan_status,
283+
"scan_status_code": scan_status_code,
269284
"scan_id": scan_id,
270285
"scan_name": scan_name,
271286
}
272-
if scan_id:
287+
if scan_id and scan is not None:
273288
try:
274289
status["scan_status_detail"] = scan.modules_status(detailed=True)
275290
except Exception as e:

bbot_server/api/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from ._fastapi import make_server_app
2+
3+
__all__ = ["make_server_app"]

bbot_server/applets/_base.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,13 @@ async def build_indexes(self, model):
235235
if "indexed" in field.metadata:
236236
index = [(fieldname, ASCENDING)]
237237
self.log.debug(f"Creating index: {index}")
238-
await self.collection.create_index(index, unique=unique)
238+
try:
239+
await self.collection.create_index(index, unique=unique)
240+
except OperationFailure as e:
241+
if "existing index has the same name" in str(e):
242+
self.log.debug(f"Index {index} already exists, skipping")
243+
else:
244+
raise
239245
# text indexes
240246
if "indexed-text" in field.metadata:
241247
index = [(fieldname, "text")]
@@ -281,7 +287,7 @@ async def build_indexes(self, model):
281287
fields = [fieldname] + fields
282288
index = [(fieldname, ASCENDING) for fieldname in fields]
283289
self.log.debug(f"Creating compound index: {index}")
284-
await self.collection.create_index(index, unique=unique)
290+
await self.collection.create_index(index, unique=True)
285291

286292
async def register_watchdog_tasks(self, broker):
287293
# register watchdog tasks
@@ -339,6 +345,7 @@ async def emit_activity(self, *args, **kwargs):
339345
await self._emit_activity(activity)
340346

341347
async def _emit_activity(self, activity: Activity):
348+
self.log.info(f"Emitting activity: {activity.type} - {activity.description}")
342349
await self.root.message_queue.publish_asset(activity)
343350

344351
def include_app(self, app_class):

bbot_server/applets/agents/agents.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
from fastapi import WebSocket
66
from contextlib import suppress
77
from datetime import datetime, timezone
8+
9+
from bbot.constants import get_scan_status_code, get_scan_status_name, SCAN_STATUS_QUEUED
10+
811
from bbot_server.models.agent_models import Agent
912
from bbot_server.applets._base import BaseApplet, api_endpoint
1013

@@ -138,12 +141,22 @@ async def dock(self, websocket: WebSocket, agent_id: UUID4):
138141
self.log.debug(f"Server received gratuitous message from agent {agent.name}: {message}")
139142
if "agent_status" in message.response:
140143
agent_status = message.response["agent_status"]
141-
scan_status = message.response["scan_status"]
144+
scan_status_code = message.response.get("scan_status_code", SCAN_STATUS_QUEUED)
145+
scan_status_code = get_scan_status_code(scan_status_code)
146+
scan_status = get_scan_status_name(scan_status_code)
142147
scan_run_id = message.response.get("scan_id", None)
143148
scan_name = message.response.get("scan_name", None)
144149
if scan_name and scan_run_id:
145-
existing_scan = await self.root.get_scan_run(scan_run_id=scan_run_id)
146-
if existing_scan and existing_scan.status != scan_status:
150+
try:
151+
existing_scan = await self.root.get_scan_run(scan_run_id=scan_run_id)
152+
except self.BBOTServerNotFoundError as e:
153+
self.log.error(f"Error getting scan run {scan_run_id}: {e}")
154+
existing_scan = None
155+
existing_scan_status_code = getattr(existing_scan, "status_code", SCAN_STATUS_QUEUED)
156+
if scan_status_code > existing_scan_status_code:
157+
await self.root.update_scan_run_status(
158+
scan_run_id=scan_run_id, status_code=scan_status_code
159+
)
147160
await self.emit_activity(
148161
type="SCAN_STATUS",
149162
detail={
@@ -166,7 +179,7 @@ async def dock(self, websocket: WebSocket, agent_id: UUID4):
166179
self.log.error(traceback.format_exc())
167180

168181
except Exception as e:
169-
self.log.error(f"Error in server-side websocket loop for agent {agent.id}: {e}")
182+
self.log.error(f"Error in server-side websocket for agent {agent.id}: {e}")
170183
self.log.error(traceback.format_exc())
171184

172185
finally:

bbot_server/applets/assets.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# applets imports
2-
from bbot_server.applets.risk import Risk
32
from bbot_server.applets.emails import EmailsApplet
43
from bbot_server.applets.export import ExportApplet
54
from bbot_server.applets.findings import FindingsApplet
65
from bbot_server.applets.dns_links import DNSLinksApplet
76
from bbot_server.applets.open_ports import OpenPortsApplet
87
from bbot_server.applets.web_screenshots import WebScreenshotsApplet
98
from bbot_server.applets.technologies import TechnologiesApplet
9+
from bbot_server.applets.cloud import CloudApplet
1010

1111
from bbot_server.assets import Asset
1212
from bbot_server.utils.misc import utc_now
@@ -17,14 +17,14 @@ class AssetsApplet(BaseApplet):
1717
name = "Assets"
1818
description = "hostnames and IP addresses discovered during scans"
1919
include_apps = [
20-
FindingsApplet,
2120
OpenPortsApplet,
2221
DNSLinksApplet,
2322
EmailsApplet,
2423
WebScreenshotsApplet,
2524
ExportApplet,
26-
Risk,
2725
TechnologiesApplet,
26+
CloudApplet,
27+
FindingsApplet,
2828
]
2929

3030
model = Asset
@@ -153,7 +153,7 @@ async def _get_asset(
153153
fields: list[str] = None,
154154
):
155155
query = dict(query or {})
156-
if type is not None:
156+
if type is not None and "type" not in query:
157157
query["type"] = type
158158
if host is not None:
159159
query["host"] = host
@@ -175,3 +175,6 @@ async def _query_assets(self, query: dict, fields: list[str] = None, sort: list[
175175
cursor = cursor.sort(sort)
176176
async for asset in cursor:
177177
yield asset
178+
179+
async def _update_asset(self, host: str, update: dict):
180+
await self.collection.update_one({"host": host}, {"$set": update})

bbot_server/applets/buckets.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from bbot_server.applets._base import BaseApplet
2+
3+
4+
class BucketsApplet(BaseApplet):
5+
name = "Buckets"
6+
description = "storage buckets discovered during scans"

0 commit comments

Comments
 (0)