Skip to content

Commit 34f015e

Browse files
author
github-actions
committed
elastic module
1 parent 2aff5aa commit 34f015e

File tree

4 files changed

+171
-12
lines changed

4 files changed

+171
-12
lines changed

bbot/modules/output/elastic.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from .http import HTTP
2+
3+
4+
class Elastic(HTTP):
5+
watched_events = ["*"]
6+
metadata = {
7+
"description": "Send scan results to Elasticsearch",
8+
"created_date": "2022-11-21",
9+
"author": "@TheTechromancer",
10+
}
11+
options = {
12+
"url": "",
13+
"username": "elastic",
14+
"password": "changeme",
15+
"timeout": 10,
16+
}
17+
options_desc = {
18+
"url": "Elastic URL (e.g. https://localhost:9200/<your_index>/_doc)",
19+
"username": "Elastic username",
20+
"password": "Elastic password",
21+
"timeout": "HTTP timeout",
22+
}

bbot/modules/output/http.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from bbot.models.pydantic import Event
12
from bbot.modules.output.base import BaseOutputModule
23

34

@@ -48,12 +49,15 @@ async def setup(self):
4849

4950
async def handle_event(self, event):
5051
while 1:
52+
event_json = event.json()
53+
event_pydantic = Event(**event_json)
54+
event_json = event_pydantic.model_dump(exclude_none=True)
5155
response = await self.helpers.request(
5256
url=self.url,
5357
method=self.method,
5458
auth=self.auth,
5559
headers=self.headers,
56-
json=event.json(),
60+
json=event_json,
5761
)
5862
is_success = False if response is None else response.is_success
5963
if not is_success:
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import time
2+
import httpx
3+
import asyncio
4+
5+
from .base import ModuleTestBase
6+
7+
8+
class TestElastic(ModuleTestBase):
9+
config_overrides = {
10+
"modules": {
11+
"elastic": {
12+
"url": "https://localhost:9200/bbot_test_events/_doc",
13+
"username": "elastic",
14+
"password": "bbotislife",
15+
}
16+
}
17+
}
18+
skip_distro_tests = True
19+
20+
async def setup_before_prep(self, module_test):
21+
# Start Elasticsearch container
22+
await asyncio.create_subprocess_exec(
23+
"docker",
24+
"run",
25+
"--name",
26+
"bbot-test-elastic",
27+
"--rm",
28+
"-e",
29+
"ELASTIC_PASSWORD=bbotislife",
30+
"-e",
31+
"cluster.routing.allocation.disk.watermark.low=96%",
32+
"-e",
33+
"cluster.routing.allocation.disk.watermark.high=97%",
34+
"-e",
35+
"cluster.routing.allocation.disk.watermark.flood_stage=98%",
36+
"-p",
37+
"9200:9200",
38+
"-d",
39+
"docker.elastic.co/elasticsearch/elasticsearch:8.16.0",
40+
)
41+
42+
# Connect to Elasticsearch with retry logic
43+
async with httpx.AsyncClient(verify=False) as client:
44+
while True:
45+
try:
46+
# Attempt a simple operation to confirm the connection
47+
response = await client.get("https://localhost:9200/_cat/health", auth=("elastic", "bbotislife"))
48+
response.raise_for_status()
49+
break
50+
except Exception as e:
51+
print(f"Connection failed: {e}. Retrying...", flush=True)
52+
time.sleep(0.5)
53+
54+
# Ensure the index is empty
55+
await client.delete(f"https://localhost:9200/bbot_test_events", auth=("elastic", "bbotislife"))
56+
print("Elasticsearch index cleaned up", flush=True)
57+
58+
async def check(self, module_test, events):
59+
try:
60+
from bbot.models.pydantic import Event
61+
62+
events_json = [e.json() for e in events]
63+
events_json.sort(key=lambda x: x["timestamp"])
64+
65+
# Connect to Elasticsearch
66+
async with httpx.AsyncClient(verify=False) as client:
67+
68+
# refresh the index
69+
await client.post(f"https://localhost:9200/bbot_test_events/_refresh", auth=("elastic", "bbotislife"))
70+
71+
# Fetch all events from the index
72+
response = await client.get(
73+
f"https://localhost:9200/bbot_test_events/_search?size=100", auth=("elastic", "bbotislife")
74+
)
75+
response_json = response.json()
76+
import json
77+
78+
print(f"response: {json.dumps(response_json, indent=2)}")
79+
db_events = [hit["_source"] for hit in response_json["hits"]["hits"]]
80+
81+
# make sure we have the same number of events
82+
assert len(events_json) == len(db_events)
83+
84+
for db_event in db_events:
85+
assert isinstance(db_event["timestamp"], float)
86+
assert isinstance(db_event["inserted_at"], float)
87+
88+
# Convert to Pydantic objects and dump them
89+
db_events_pydantic = [Event(**e).model_dump(exclude_none=True) for e in db_events]
90+
db_events_pydantic.sort(key=lambda x: x["timestamp"])
91+
92+
# Find the main event with type DNS_NAME and data blacklanternsecurity.com
93+
main_event = next(
94+
(
95+
e
96+
for e in db_events_pydantic
97+
if e.get("type") == "DNS_NAME" and e.get("data") == "blacklanternsecurity.com"
98+
),
99+
None,
100+
)
101+
assert (
102+
main_event is not None
103+
), "Main event with type DNS_NAME and data blacklanternsecurity.com not found"
104+
105+
# Ensure it has the reverse_host attribute
106+
expected_reverse_host = "blacklanternsecurity.com"[::-1]
107+
assert (
108+
main_event.get("reverse_host") == expected_reverse_host
109+
), f"reverse_host attribute is not correct, expected {expected_reverse_host}"
110+
111+
# Events don't match exactly because the elastic ones have reverse_host and inserted_at
112+
assert events_json != db_events_pydantic
113+
for db_event in db_events_pydantic:
114+
db_event.pop("reverse_host")
115+
db_event.pop("inserted_at")
116+
# They should match after removing reverse_host
117+
assert events_json == db_events_pydantic, "Events do not match"
118+
119+
finally:
120+
# Clean up: Delete all documents in the index
121+
async with httpx.AsyncClient(verify=False) as client:
122+
response = await client.delete(
123+
f"https://localhost:9200/bbot_test_events",
124+
auth=("elastic", "bbotislife"),
125+
params={"ignore": "400,404"},
126+
)
127+
print(f"Deleted documents from index", flush=True)
128+
await asyncio.create_subprocess_exec(
129+
"docker", "stop", "bbot-test-elastic", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
130+
)

docs/scanning/output.md

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -155,27 +155,30 @@ config:
155155

156156
### Elasticsearch
157157

158-
When outputting to Elastic, use the `http` output module with the following settings (replace `<your_index>` with your desired index, e.g. `bbot`):
158+
- Step 1: Spin up a quick Elasticsearch docker image
159+
160+
```bash
161+
docker run -d -p 9200:9200 --name=bbot-elastic --v "$(pwd)/elastic_data:/usr/share/elasticsearch/data" -e ELASTIC_PASSWORD=bbotislife -m 1GB docker.elastic.co/elasticsearch/elasticsearch:8.16.0
162+
```
163+
164+
- Step 2: Execute a scan with `elastic` output module
159165

160166
```bash
161167
# send scan results directly to elasticsearch
162-
bbot -t evilcorp.com -om http -c \
163-
modules.http.url=http://localhost:8000/<your_index>/_doc \
164-
modules.http.siem_friendly=true \
165-
modules.http.username=elastic \
166-
modules.http.password=changeme
168+
# note: you can replace "bbot_events" with your own index name
169+
bbot -t evilcorp.com -om elastic -c \
170+
modules.elastic.url=https://localhost:9200/bbot_events/_doc \
171+
modules.elastic.password=bbotislife
167172
```
168173

169174
Alternatively, via a preset:
170175

171176
```yaml title="elastic_preset.yml"
172177
config:
173178
modules:
174-
http:
175-
url: http://localhost:8000/<your_index>/_doc
176-
siem_friendly: true
177-
username: elastic
178-
password: changeme
179+
elastic:
180+
url: http://localhost:9200/bbot_events/_doc
181+
password: bbotislife
179182
```
180183

181184
### Splunk

0 commit comments

Comments
 (0)