Skip to content

Commit ec3b927

Browse files
Merge pull request #27 from blacklanternsecurity/authentication
Add Authentication
2 parents 468e0f2 + 4ec470c commit ec3b927

Some content is hidden

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

43 files changed

+1190
-247
lines changed

.github/workflows/docker-tests.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: docker tests
2+
on:
3+
push:
4+
branches:
5+
- stable
6+
- dev
7+
pull_request:
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
11+
cancel-in-progress: true
12+
13+
jobs:
14+
test:
15+
runs-on: self-hosted
16+
steps:
17+
- uses: actions/checkout@v4
18+
- name: Set up Python
19+
uses: actions/setup-python@v4
20+
with:
21+
python-version: "3.11"
22+
- name: Install dependencies
23+
run: |
24+
pip install poetry
25+
poetry install
26+
- name: Run tests
27+
run: |
28+
BBOT_SERVER_TEST_DOCKER_COMPOSE=true poetry run pytest --disable-warnings --log-cli-level=INFO -k test_docker_compose

README.md

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,53 @@ Note: this requires Docker and Docker Compose to be installed.
4646
bbctl server start
4747
```
4848

49-
## Start a scan (direct from BBOT CLI)
49+
## Interacting with BBOT Server Remotely (Multiplayer)
50+
51+
By default, BBOT Server listens on localhost. Use `--listen` to expose it to the network:
52+
53+
```bash
54+
bbctl server start --listen 0.0.0.0
55+
```
56+
57+
### Authentication
58+
59+
The first time you start BBOT Server, an API key will be auto generated and put into `~/.config/bbot_server/config.yml`:
60+
61+
```yaml
62+
# ~/.config/bbot_server/config.yml
63+
64+
# list of API keys to be considered valid
65+
api_keys:
66+
- 4aa8b3c2-9b4d-4208-890c-4ce9ad3b4710
67+
```
68+
69+
The `api_keys` value in `config.yml` is used by both the server (as a database of valid API keys), and by the client (it will pick one from the list and use it). Normally it just works and you don't have to mess with it. But to access BBOT Server remotely, you'll need to copy the API key from the server onto your local system, along with its URL:
70+
71+
```yaml
72+
# ~/.config/bbot_server/config.yml
73+
url: http://1.2.3.4:8807/v1/
74+
api_keys:
75+
- deadbeef-9b4d-4208-890c-4ce9ad3b4710
76+
```
77+
78+
This tells `bbctl` (the client) where the server is, and gives it the means to authenticate.
79+
80+
### Adding and Revoking API Keys
81+
82+
API keys can be added and removed if you are on the server machine:
83+
84+
```bash
85+
# add an API key
86+
bbctl server apikey add
87+
88+
# list API keys
89+
bbctl server apikey list
90+
91+
# revoke an API key
92+
bbctl server apikey delete deadbeef-9b4d-4208-890c-4ce9ad3b4710
93+
```
94+
95+
## Send a BBOT Scan to the Server
5096

5197
You can output a BBOT scan directly to BBOT server:
5298

@@ -146,6 +192,23 @@ bbctl scan list
146192
bbctl scan cancel "demonic_jimmy"
147193
```
148194

195+
## Targets
196+
197+
BBOT server categorizes its assets by target.
198+
199+
You can list targets like so:
200+
201+
```bash
202+
# List targets
203+
bbctl target list
204+
205+
# Create a new target
206+
bbctl target create --seeds seeds.txt --blacklist blacklist.txt --name custom_target
207+
208+
# List only the assets that match your new target
209+
bbctl asset list --target custom_target
210+
```
211+
149212
## Custom triggers
150213

151214
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.

bbot_server/agent/agent.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@
88
from typing import Callable, Any
99
from urllib.parse import urlparse, urlunparse, urljoin
1010

11+
import bbot_server.config as bbcfg
1112
from bbot.scanner import Scanner, Preset
1213
from bbot.scanner.dispatcher import Dispatcher
1314
from bbot.constants import get_scan_status_code, get_scan_status_name, SCAN_STATUS_NOT_STARTED, SCAN_STATUS_FAILED
1415

15-
from bbot_server.config import BBOT_SERVER_CONFIG
1616
from bbot_server.errors import BBOTServerValueError
1717
from bbot_server.utils.async_utils import async_to_sync_class
1818
from bbot_server.models.agent_models import AgentResponse
1919

20-
default_server_url = BBOT_SERVER_CONFIG.get("url", "http://localhost:8807/v1/")
21-
default_bbot_preset = BBOT_SERVER_CONFIG.get("agent", {}).get("base_preset", {})
20+
api_key = bbcfg.get_api_key()
21+
default_bbot_preset = bbcfg.BBOT_SERVER_CONFIG.get("agent", {}).get("base_preset", {})
2222

2323
log = logging.getLogger("bbot_server.agent")
2424

@@ -219,9 +219,7 @@ def make_agent_preset(self):
219219
output_modules=["http"],
220220
config={
221221
"modules": {
222-
"http": {
223-
"url": self.scan_output_url,
224-
}
222+
"http": {"url": self.scan_output_url, "headers": {bbcfg.API_KEY_NAME: bbcfg.get_api_key()}}
225223
}
226224
},
227225
)
@@ -234,7 +232,9 @@ async def loop(self):
234232
self.log.info(f"Agent {self.name} connecting to {self.websocket_dock_url}...")
235233
# "async for" will use websocket's builtin retry/reconnect mechanism, with exponential backoff
236234
# https://websockets.readthedocs.io/en/stable/reference/asyncio/client.html
237-
async for websocket in websockets.connect(self.websocket_dock_url):
235+
async for websocket in websockets.connect(
236+
self.websocket_dock_url, additional_headers={bbcfg.API_KEY_NAME: api_key}
237+
):
238238
self.log.info(f"Agent {self.name} successfully connected to {self.websocket_dock_url}")
239239
self.websocket = websocket
240240
try:

bbot_server/api/_fastapi.py

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
from fastapi import FastAPI
1+
import logging
22
from contextlib import asynccontextmanager
3+
from fastapi.openapi.utils import get_openapi
4+
from fastapi import FastAPI, HTTPException, Depends, Request, WebSocket
35
from fastapi.responses import RedirectResponse, ORJSONResponse
46

7+
import bbot_server.config as bbcfg
58
from bbot_server.errors import BBOTServerError, handle_bbot_server_error
69

10+
log = logging.getLogger("bbot_server.api.fastapi")
11+
712
# from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html
813

914
app_kwargs = {
@@ -12,12 +17,44 @@
1217
"debug": True,
1318
}
1419

20+
# API key header
21+
# api_key_header = APIKeyHeader(name=bbcfg.API_KEY_NAME, auto_error=False)
22+
23+
24+
def api_key_dependency_http(request: Request = None):
25+
if request is not None:
26+
api_key = request.headers.get(bbcfg.API_KEY_NAME, "")
27+
if not api_key:
28+
raise HTTPException(status_code=401, detail="API key is required")
29+
valid, reason = bbcfg.check_api_key(api_key)
30+
if not valid:
31+
raise HTTPException(status_code=401, detail=reason)
32+
return api_key
33+
34+
35+
async def api_key_dependency_websocket(websocket: WebSocket = None):
36+
if websocket is not None:
37+
api_key = websocket.headers.get(bbcfg.API_KEY_NAME, "")
38+
if not api_key:
39+
await websocket.close(code=3000, reason="API key is required")
40+
raise HTTPException(status_code=401, detail="API key is required")
41+
valid, reason = bbcfg.check_api_key(api_key)
42+
if not valid:
43+
await websocket.close(code=3000, reason=reason)
44+
raise HTTPException(status_code=401, detail=reason)
45+
return api_key
1546

16-
def make_app(config=None):
47+
48+
# Dependency to verify the API key
49+
# async def api_key_dependency(api_key: str = Security(api_key_header)):
50+
# await verify_api_key(api_key)
51+
52+
53+
def make_app():
1754
from bbot_server.api.mcp import make_mcp_server
1855
from bbot_server.applets import BBOTServerRootApplet
1956

20-
app_root = BBOTServerRootApplet(config=config)
57+
app_root = BBOTServerRootApplet()
2158
app_root._is_main_server = True
2259

2360
@asynccontextmanager
@@ -31,9 +68,52 @@ async def lifespan(app: FastAPI):
3168
root_path="/v1",
3269
openapi_tags=app_root.tags_metadata,
3370
default_response_class=ORJSONResponse,
71+
dependencies=[Depends(api_key_dependency_http), Depends(api_key_dependency_websocket)],
3472
**app_kwargs,
3573
)
3674

75+
# Customize OpenAPI to better document the authentication
76+
def custom_openapi():
77+
if app.openapi_schema:
78+
return app.openapi_schema
79+
80+
openapi_schema = get_openapi(
81+
title=app.title,
82+
version=app.version,
83+
description=app.description,
84+
routes=app.routes,
85+
)
86+
87+
# Prepend root_path to all paths in the OpenAPI schema
88+
root_path = app.root_path or ""
89+
if root_path and root_path != "/":
90+
new_paths = {}
91+
for path, path_item in openapi_schema["paths"].items():
92+
if not path.startswith(root_path):
93+
new_path = root_path + path
94+
else:
95+
new_path = path
96+
new_paths[new_path] = path_item
97+
openapi_schema["paths"] = new_paths
98+
99+
# Add security scheme to OpenAPI schema
100+
openapi_schema["components"]["securitySchemes"] = {
101+
"APIKeyHeader": {
102+
"type": "apiKey",
103+
"in": "header",
104+
"name": bbcfg.API_KEY_NAME,
105+
"description": "API key authentication",
106+
}
107+
}
108+
109+
# Add global security requirement
110+
openapi_schema["security"] = [{"APIKeyHeader": []}]
111+
112+
app.openapi_schema = openapi_schema
113+
return app.openapi_schema
114+
115+
app.openapi = custom_openapi
116+
37117
app.include_router(app_root.router)
38118

39119
# add MCP server to the fastapi app
@@ -67,8 +147,8 @@ async def docs_redirect():
67147
return app, lifespan
68148

69149

70-
def make_server_app(config=None):
71-
app, lifespan = make_app(config=config)
150+
def make_server_app():
151+
app, lifespan = make_app()
72152

73153
# includes the /v1 prefix
74154
server_app = FastAPI(

bbot_server/applets/_base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@ def _add_custom_routes(self):
449449
# see if the value has an "_endpoint" attribute
450450
endpoint = getattr(function, "_endpoint", None)
451451
# if it's a callable function and it has _endpoint, it's an @api_endpoint
452+
452453
if endpoint is not None:
453454
fastapi_kwargs = dict(getattr(function, "_kwargs", {}))
454455
endpoint_type = fastapi_kwargs.pop("type", "http")

bbot_server/applets/_root.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
from omegaconf import OmegaConf
1+
import bbot_server.config as bbcfg
22

33
from bbot_server.applets._base import BaseApplet
4-
from bbot_server.config import BBOT_SERVER_CONFIG
54

6-
# assets imports
5+
# applet imports
6+
from bbot_server.applets.stats import StatsApplet
77
from bbot_server.applets.assets import AssetsApplet
88
from bbot_server.applets.events import EventsApplet
99
from bbot_server.applets.scans import ScansApplet
1010
from bbot_server.applets.activity import ActivityApplet
11-
from bbot_server.applets.stats import StatsApplet
1211

1312

1413
class RootApplet(BaseApplet):
@@ -24,11 +23,9 @@ def __init__(self, config=None, **kwargs):
2423
"""
2524
"config" can be either a dictionary or an omegaconf object
2625
"""
27-
super().__init__(**kwargs)
2826
if config is not None:
29-
self._config = OmegaConf.merge(BBOT_SERVER_CONFIG, config)
30-
else:
31-
self._config = BBOT_SERVER_CONFIG
27+
bbcfg.refresh_config(config)
28+
super().__init__(**kwargs)
3229
self._interface_type = "python"
3330
self._mcp = None
3431

@@ -41,26 +38,26 @@ async def setup(self):
4138
from bbot_server.store.user_store import UserStore
4239
from bbot_server.store.asset_store import AssetStore
4340

44-
self.asset_store = AssetStore(self.global_config)
41+
self.asset_store = AssetStore()
4542
await self.asset_store.setup()
4643
self.asset_db = self.asset_store.db
4744
self.asset_fs = self.asset_store.fs
4845

49-
self.user_store = UserStore(self.global_config)
46+
self.user_store = UserStore()
5047
await self.user_store.setup()
5148
self.user_db = self.user_store.db
5249
self.user_fs = self.user_store.fs
5350

5451
# set up event store
5552
from bbot_server.event_store import EventStore
5653

57-
self.event_store = EventStore(self.global_config)
54+
self.event_store = EventStore()
5855
await self.event_store.setup()
5956

6057
# set up NATS client
6158
from bbot_server.message_queue import MessageQueue
6259

63-
self.message_queue = MessageQueue(self.global_config)
60+
self.message_queue = MessageQueue()
6461
await self.message_queue.setup()
6562

6663
await self._setup()
@@ -70,6 +67,10 @@ async def setup(self):
7067
def config(self):
7168
return self._config
7269

70+
@property
71+
def _config(self):
72+
return bbcfg.BBOT_SERVER_CONFIG
73+
7374
async def cleanup(self):
7475
if self.is_native:
7576
await self.asset_store.cleanup()

0 commit comments

Comments
 (0)