Skip to content

Commit ade99fe

Browse files
Merge pull request #55 from blacklanternsecurity/tech-improvements
Filtering Improvements
2 parents f5dee34 + 5ee3bbe commit ade99fe

29 files changed

+573
-158
lines changed

README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,71 @@ The SSE server listens at `http://localhost:8807/v1/mcp/`
325325

326326
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.
327327

328+
## As a Python Library
329+
330+
You can interact fully with BBOT Server as a Python library. It supports either local or remote connections, and the interface to both is identical:
331+
332+
### Asynchronous
333+
334+
```python
335+
import asyncio
336+
from bbot_server import BBOTServer
337+
338+
async def main():
339+
# talk directly to local MongoDB + Redis
340+
bbot_server = BBOTServer(interface="python")
341+
342+
# or to a remote BBOT Server instance (config must contain a valid API key)
343+
bbot_server = BBOTServer(interface="http", url="http://bbot:8807/v1/")
344+
345+
# one-time setup
346+
await bbot_server.setup()
347+
348+
hosts = await bbot_server.get_hosts()
349+
print(f"hosts: {hosts}")
350+
351+
if __name__ == "__main__":
352+
asyncio.run(main())
353+
```
354+
355+
### Synchronous
356+
357+
```python
358+
from bbot_server import BBOTServer
359+
360+
if __name__ == "__main__":
361+
# talk directly to local MongoDB + Redis
362+
bbot_server = BBOTServer(interface="python", synchronous=True)
363+
364+
# or to a remote BBOT Server instance (config must contain a valid API key)
365+
bbot_server = BBOTServer(interface="http", url="http://bbot:8807/v1/", synchronous=True)
366+
367+
# one-time setup
368+
bbot_server.setup()
369+
370+
hosts = bbot_server.get_hosts()
371+
print(f"hosts: {hosts}")
372+
```
373+
374+
## Running Tests
375+
376+
When running tests, first start MongoDB and Redis via Docker:
377+
378+
```bash
379+
docker run --rm -p 27017:27017 mongo
380+
docker run --rm -p 6379:6379 redis
381+
```
382+
383+
Then execute `pytest`:
384+
385+
```bash
386+
# run all tests
387+
poetry run pytest -v
388+
389+
# run specific tests
390+
poetry run pytest -v -k test_applet_scans
391+
```
392+
328393
## Screenshots
329394

330395
*Tailing activities in real time*

bbot_server/applets/_root.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
class RootApplet(BaseApplet):
77
name = "Root Applet"
88

9-
attach_to = ""
10-
119
_nested = False
1210

1311
_route_prefix = ""

bbot_server/applets/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ class BaseApplet:
7979
model = None
8080

8181
# which other applet should include this one
82+
# leave blank to attach to the root applet
8283
attach_to = ""
8384

8485
# whether to nest this applet under its parent

bbot_server/db/base.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ def db_config(self):
3333

3434
@property
3535
def uri(self):
36-
return self.db_config.get("uri", "")
36+
uri = self.db_config.get("uri", "")
37+
if not uri:
38+
raise BBOTServerValueError(f"Database URI is missing from config: {self.db_config}")
39+
return uri
3740

3841
@property
3942
def db_name(self):

bbot_server/event_store/_base.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@ class BaseEventStore(BaseDB):
77

88
def __init__(self, *args, **kwargs):
99
super().__init__(*args, **kwargs)
10-
self.event_store_config = self.config.get("event_store", {})
11-
self.archive_after_days = self.event_store_config.get("archive_after", 90)
12-
self.archive_cron = self.event_store_config.get("archive_cron", "0 0 * * *")
10+
self.archive_after_days = self.db_config.get("archive_after", 90)
11+
self.archive_cron = self.db_config.get("archive_cron", "0 0 * * *")
1312

1413
async def insert_event(self, event):
1514
if not isinstance(event, Event):
@@ -20,9 +19,26 @@ async def get_event(self, uuid: str):
2019
event = await self._get_event(uuid)
2120
return Event(**event)
2221

23-
async def get_events(self, host: str = None, type=None, min_timestamp=None, archived=False, active=True):
22+
async def get_events(
23+
self,
24+
host: str = None,
25+
domain: str = None,
26+
type: str = None,
27+
scan: str = None,
28+
min_timestamp: float = None,
29+
max_timestamp: float = None,
30+
archived: bool = False,
31+
active: bool = True,
32+
):
2433
async for event in self._get_events(
25-
host=host, type=type, min_timestamp=min_timestamp, archived=archived, active=active
34+
host=host,
35+
domain=domain,
36+
type=type,
37+
scan=scan,
38+
min_timestamp=min_timestamp,
39+
max_timestamp=max_timestamp,
40+
archived=archived,
41+
active=active,
2642
):
2743
yield Event(**event)
2844

bbot_server/event_store/mongo.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
from pymongo import WriteConcern, AsyncMongoClient
22

33

4-
from bbot_server.errors import BBOTServerNotFoundError
4+
from bbot_server.errors import BBOTServerNotFoundError, BBOTServerValueError
55
from bbot_server.event_store._base import BaseEventStore
66

77

88
class MongoEventStore(BaseEventStore):
99
"""
10-
docker run --rm -p 27017:27017 mongo
10+
docker run --rm -p 127.0.0.1:27017:27017 mongo
1111
"""
1212

1313
async def _setup(self):
@@ -28,21 +28,43 @@ async def _insert_event(self, event):
2828
event_json = event.model_dump()
2929
await self.collection.insert_one(event_json)
3030

31-
async def _get_events(self, host: str, type: str, min_timestamp: float, archived: bool, active: bool):
31+
async def _get_events(
32+
self,
33+
host: str,
34+
domain: str,
35+
type: str,
36+
scan: str,
37+
min_timestamp: float,
38+
max_timestamp: float,
39+
active: bool,
40+
archived: bool,
41+
):
3242
"""
33-
Get all events from the database, or if min_timestamp is provided, get the newest events up to that timestamp
43+
Get all events from the database based on the provided filters
3444
"""
3545
query = {}
3646
if type is not None:
37-
query["type"] = {"$eq": type}
47+
query["type"] = type
3848
if min_timestamp is not None:
3949
query["timestamp"] = {"$gte": min_timestamp}
50+
# if both active and archived are true, we don't need to filter anything
4051
if not (active and archived):
52+
# if both are false, we need to raise an error
4153
if not (active or archived):
42-
raise ValueError("Must query at least one of active or archived")
54+
raise BBOTServerValueError("Must query at least one of active or archived")
55+
# otherwise if only one is true, we need to filter by the other
4356
query["archived"] = {"$eq": archived}
57+
if max_timestamp is not None:
58+
query["timestamp"] = {"$lte": max_timestamp}
59+
if scan is not None:
60+
query["scan"] = scan
4461
if host is not None:
4562
query["host"] = host
63+
if domain is not None:
64+
# match reverse_host with regex
65+
reversed_host = domain[::-1]
66+
query["reverse_host"] = {"$regex": f"^{reversed_host}(\\.|$)"}
67+
self.log.debug(f"Querying events: {query}")
4668
async for event in self.collection.find(query):
4769
yield event
4870

bbot_server/message_queue/redis.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class RedisMessageQueue(BaseMessageQueue):
2929
- bbot:stream:{subject}: for persistent, tailable streams - e.g. events, activities
3030
- bbot:work:{subject}: for one-time messages, e.g. tasks
3131
32-
docker run --rm -p 6379:6379 redis
32+
docker run --rm -p 127.0.0.1:6379:6379 redis
3333
"""
3434

3535
def __init__(self, *args, **kwargs):

bbot_server/modules/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def load_python_file(file, namespace, module_dict, base_class_name, module_key_a
133133
log.error(f"Module {value.__name__} does not define required attribute{module_key_attr}")
134134
parent_name = getattr(value, "attach_to", "")
135135
if not parent_name:
136-
log.error(f"Module {value.__name__} does not define required attribute 'attach_to'")
136+
parent_name = "root_applet"
137137
module_family = module_dict.get(parent_name, {})
138138
# if we get a duplicate module name, raise an error
139139
if module_name in module_family:

bbot_server/modules/activity/activity_api.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88
class ActivityApplet(BaseApplet):
99
name = "Activity"
1010
watched_activities = ["*"]
11-
description = "Query and tail BBOT server activity"
12-
attach_to = "root_applet"
11+
description = "Query BBOT server activities"
1312
model = Activity
1413

1514
async def handle_activity(self, activity: Activity, asset: Asset = None):

bbot_server/modules/agents/agents_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ async def _kickoff_queued_scans(self):
252252
return 0
253253

254254
async def _kickoff_queued_scans_loop(self):
255-
for i in range(1000):
255+
while True:
256256
online_agents = await self.get_online_agents()
257257
online_agents = [str(agent.id) for agent in online_agents]
258258
if not online_agents:

0 commit comments

Comments
 (0)