+
Example Domain
+
This domain is for use in illustrative examples in documents. You may use this
+ domain in literature without prior coordination or asking for permission.
+
More information...
+
+ """
+
+ path = "/test_http_helpers_beautifulsoup"
+ url = bbot_httpserver.url_for(path)
+ bbot_httpserver.expect_request(uri=path).respond_with_data(download_content, status=200)
+ webpage = await scan1.helpers.request(url)
+ assert webpage, f"Webpage is False"
+ soup = scan1.helpers.beautifulsoup(webpage, "html.parser")
+ assert soup, f"Soup is False"
+ # pretty_print = soup.prettify()
+ # assert pretty_print, f"PrettyPrint is False"
+ # scan1.helpers.log.info(f"{pretty_print}")
+ html_text = soup.find(text="Example Domain")
+ assert html_text, f"Find HTML Text is False"
+
# 404
path = "/test_http_helpers_download_404"
url = bbot_httpserver.url_for(path)
diff --git a/bbot/test/test_step_2/module_tests/test_module_ffuf_shortnames.py b/bbot/test/test_step_2/module_tests/test_module_ffuf_shortnames.py
index 3d59653eb..cbbec11ea 100644
--- a/bbot/test/test_step_2/module_tests/test_module_ffuf_shortnames.py
+++ b/bbot/test/test_step_2/module_tests/test_module_ffuf_shortnames.py
@@ -177,9 +177,9 @@ def check(self, module_test, events):
basic_detection = False
directory_detection = False
prefix_detection = False
- delimeter_detection = False
- directory_delimeter_detection = False
- prefix_delimeter_detection = False
+ delimiter_detection = False
+ directory_delimiter_detection = False
+ prefix_delimiter_detection = False
short_extensions_detection = False
for e in events:
@@ -191,18 +191,18 @@ def check(self, module_test, events):
if e.data == "http://127.0.0.1:8888/adm_portal.aspx":
prefix_detection = True
if e.data == "http://127.0.0.1:8888/abcconsole.aspx":
- delimeter_detection = True
+ delimiter_detection = True
if e.data == "http://127.0.0.1:8888/abcconsole.aspx":
- directory_delimeter_detection = True
+ directory_delimiter_detection = True
if e.data == "http://127.0.0.1:8888/xyzdirectory/":
- prefix_delimeter_detection = True
+ prefix_delimiter_detection = True
if e.data == "http://127.0.0.1:8888/short.pl":
short_extensions_detection = True
assert basic_detection
assert directory_detection
assert prefix_detection
- assert delimeter_detection
- assert directory_delimeter_detection
- assert prefix_delimeter_detection
+ assert delimiter_detection
+ assert directory_delimiter_detection
+ assert prefix_delimiter_detection
assert short_extensions_detection
diff --git a/bbot/test/test_step_2/module_tests/test_module_http.py b/bbot/test/test_step_2/module_tests/test_module_http.py
index 3b4e819b9..d0afcefb2 100644
--- a/bbot/test/test_step_2/module_tests/test_module_http.py
+++ b/bbot/test/test_step_2/module_tests/test_module_http.py
@@ -1,3 +1,6 @@
+import json
+import httpx
+
from .base import ModuleTestBase
@@ -15,10 +18,46 @@ class TestHTTP(ModuleTestBase):
}
}
+ def verify_data(self, j):
+ return j["data"] == "blacklanternsecurity.com" and j["type"] == "DNS_NAME"
+
async def setup_after_prep(self, module_test):
+ self.got_event = False
+ self.headers_correct = False
+ self.method_correct = False
+ self.url_correct = False
+
+ async def custom_callback(request):
+ j = json.loads(request.content)
+ if request.url == self.downstream_url:
+ self.url_correct = True
+ if request.method == "PUT":
+ self.method_correct = True
+ if "Authorization" in request.headers:
+ self.headers_correct = True
+ if self.verify_data(j):
+ self.got_event = True
+ return httpx.Response(
+ status_code=200,
+ )
+
+ module_test.httpx_mock.add_callback(custom_callback)
+ module_test.httpx_mock.add_callback(custom_callback)
module_test.httpx_mock.add_response(
method="PUT", headers={"Authorization": "bearer auth_token"}, url=self.downstream_url
)
def check(self, module_test, events):
- pass
+ assert self.got_event == True
+ assert self.headers_correct == True
+ assert self.method_correct == True
+ assert self.url_correct == True
+
+
+class TestHTTPSIEMFriendly(TestHTTP):
+ modules_overrides = ["http"]
+ config_overrides = {"output_modules": {"http": dict(TestHTTP.config_overrides["output_modules"]["http"])}}
+ config_overrides["output_modules"]["http"]["siem_friendly"] = True
+
+ def verify_data(self, j):
+ return j["data"] == {"DNS_NAME": "blacklanternsecurity.com"} and j["type"] == "DNS_NAME"
diff --git a/bbot/test/test_step_2/module_tests/test_module_splunk.py b/bbot/test/test_step_2/module_tests/test_module_splunk.py
new file mode 100644
index 000000000..67d67a4ef
--- /dev/null
+++ b/bbot/test/test_step_2/module_tests/test_module_splunk.py
@@ -0,0 +1,58 @@
+import json
+import httpx
+
+from .base import ModuleTestBase
+
+
+class TestSplunk(ModuleTestBase):
+ downstream_url = "https://splunk.blacklanternsecurity.fakedomain:1234/services/collector"
+ config_overrides = {
+ "output_modules": {
+ "splunk": {
+ "url": downstream_url,
+ "hectoken": "HECTOKEN",
+ "index": "bbot_index",
+ "source": "bbot_source",
+ }
+ }
+ }
+
+ def verify_data(self, j):
+ if not j["source"] == "bbot_source":
+ return False
+ if not j["index"] == "bbot_index":
+ return False
+ data = j["event"]
+ if not data["data"] == "blacklanternsecurity.com" and data["type"] == "DNS_NAME":
+ return False
+ return True
+
+ async def setup_after_prep(self, module_test):
+ self.url_correct = False
+ self.method_correct = False
+ self.got_event = False
+ self.headers_correct = False
+
+ async def custom_callback(request):
+ j = json.loads(request.content)
+ if request.url == self.downstream_url:
+ self.url_correct = True
+ if request.method == "POST":
+ self.method_correct = True
+ if "Authorization" in request.headers:
+ self.headers_correct = True
+ if self.verify_data(j):
+ self.got_event = True
+ return httpx.Response(
+ status_code=200,
+ )
+
+ module_test.httpx_mock.add_callback(custom_callback)
+ module_test.httpx_mock.add_callback(custom_callback)
+ module_test.httpx_mock.add_response()
+
+ def check(self, module_test, events):
+ assert self.got_event == True
+ assert self.headers_correct == True
+ assert self.method_correct == True
+ assert self.url_correct == True
diff --git a/docs/contribution.md b/docs/contribution.md
index 2d36cfe44..175c3e7af 100644
--- a/docs/contribution.md
+++ b/docs/contribution.md
@@ -134,7 +134,6 @@ BBOT automates module dependencies with **Ansible**. If your module relies on a
```python
class MyModule(BaseModule):
...
- deps_pip = ["beautifulsoup4"]
deps_apt = ["chromium-browser"]
deps_ansible = [
{
diff --git a/docs/modules/list_of_modules.md b/docs/modules/list_of_modules.md
index a3ffc76c6..ebf4f182f 100644
--- a/docs/modules/list_of_modules.md
+++ b/docs/modules/list_of_modules.md
@@ -107,6 +107,7 @@
| neo4j | output | No | Output to Neo4j | | * | |
| python | output | No | Output via Python API | | * | |
| slack | output | No | Message a Slack channel when certain events are encountered | | * | |
+| splunk | output | No | Send every event to a splunk instance through HTTP Event Collector | | * | |
| subdomains | output | No | Output only resolved, in-scope subdomains | subdomain-enum | DNS_NAME, DNS_NAME_UNRESOLVED | |
| teams | output | No | Message a Teams channel when certain events are encountered | | * | |
| web_report | output | No | Create a markdown report with web assets | | FINDING, TECHNOLOGY, URL, VHOST, VULNERABILITY | |
diff --git a/docs/scanning/advanced.md b/docs/scanning/advanced.md
index 0baaf35c8..8207b7ce7 100644
--- a/docs/scanning/advanced.md
+++ b/docs/scanning/advanced.md
@@ -33,16 +33,10 @@ asyncio.run(main())
```text
-usage: bbot [-h] [--help-all] [-t TARGET [TARGET ...]]
- [-w WHITELIST [WHITELIST ...]] [-b BLACKLIST [BLACKLIST ...]]
- [--strict-scope] [-m MODULE [MODULE ...]] [-l]
- [-em MODULE [MODULE ...]] [-f FLAG [FLAG ...]] [-lf]
- [-rf FLAG [FLAG ...]] [-ef FLAG [FLAG ...]]
- [-om MODULE [MODULE ...]] [--allow-deadly] [-n SCAN_NAME]
- [-o DIR] [-c [CONFIG ...]] [-v] [-d] [-s] [--force] [-y]
- [--dry-run] [--current-config]
- [--no-deps | --force-deps | --retry-deps | --ignore-failed-deps | --install-all-deps]
- [-a] [--version]
+usage: bbot [-h] [--help-all] [-t TARGET [TARGET ...]] [-w WHITELIST [WHITELIST ...]] [-b BLACKLIST [BLACKLIST ...]] [--strict-scope] [-m MODULE [MODULE ...]] [-l]
+ [-em MODULE [MODULE ...]] [-f FLAG [FLAG ...]] [-lf] [-rf FLAG [FLAG ...]] [-ef FLAG [FLAG ...]] [-om MODULE [MODULE ...]] [--allow-deadly] [-n SCAN_NAME] [-o DIR]
+ [-c [CONFIG ...]] [-v] [-d] [-s] [--force] [-y] [--dry-run] [--current-config] [--no-deps | --force-deps | --retry-deps | --ignore-failed-deps | --install-all-deps] [-a]
+ [--version]
Bighuge BLS OSINT Tool
@@ -73,7 +67,7 @@ Modules:
-ef FLAG [FLAG ...], --exclude-flags FLAG [FLAG ...]
Disable modules with these flags. (e.g. -ef aggressive)
-om MODULE [MODULE ...], --output-modules MODULE [MODULE ...]
- Output module(s). Choices: asset_inventory,csv,discord,emails,http,human,json,neo4j,python,slack,subdomains,teams,web_report,websocket
+ Output module(s). Choices: asset_inventory,csv,discord,emails,http,human,json,neo4j,python,slack,splunk,subdomains,teams,web_report,websocket
--allow-deadly Enable the use of highly aggressive modules
Scan:
diff --git a/docs/scanning/configuration.md b/docs/scanning/configuration.md
index 2ed2d61de..d203f3ec4 100644
--- a/docs/scanning/configuration.md
+++ b/docs/scanning/configuration.md
@@ -291,6 +291,7 @@ Many modules accept their own configuration options. These options have the abil
| modules.vhost.lines | int | take only the first N lines from the wordlist when finding directories | 5000 |
| modules.vhost.wordlist | str | Wordlist containing subdomains | https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/DNS/subdomains-top1million-5000.txt |
| modules.wafw00f.generic_detect | bool | When no specific WAF detections are made, try to perform a generic detect | True |
+| modules.anubisdb.limit | int | Limit the number of subdomains returned per query (increasing this may slow the scan due to garbage results from this API) | 1000 |
| modules.bevigil.api_key | str | BeVigil OSINT API Key | |
| modules.bevigil.urls | bool | Emit URLs in addition to DNS_NAMEs | False |
| modules.binaryedge.api_key | str | BinaryEdge API key | |
@@ -308,6 +309,8 @@ Many modules accept their own configuration options. These options have the abil
| modules.credshed.username | str | Credshed username | |
| modules.dehashed.api_key | str | DeHashed API Key | |
| modules.dehashed.username | str | Email Address associated with your API key | |
+| modules.dnscommonsrv.max_event_handlers | int | How many instances of the module to run concurrently | 10 |
+| modules.dnscommonsrv.top | int | How many of the top SRV records to check | 50 |
| modules.fullhunt.api_key | str | FullHunt API Key | |
| modules.github_codesearch.api_key | str | Github token | |
| modules.github_codesearch.limit | int | Limit code search to this many results | 100 |
@@ -320,6 +323,7 @@ Many modules accept their own configuration options. These options have the abil
| modules.ipneighbor.num_bits | int | Netmask size (in CIDR notation) to check. Default is 4 bits (16 hosts) | 4 |
| modules.ipstack.api_key | str | IPStack GeoIP API Key | |
| modules.leakix.api_key | str | LeakIX API Key | |
+| modules.massdns.max_depth | int | How many subdomains deep to brute force, i.e. 5.4.3.2.1.evilcorp.com | 5 |
| modules.massdns.max_mutations | int | Max number of smart mutations per subdomain | 500 |
| modules.massdns.max_resolvers | int | Number of concurrent massdns resolvers | 1000 |
| modules.massdns.wordlist | str | Subdomain wordlist URL | https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/DNS/subdomains-top1million-5000.txt |
@@ -346,6 +350,7 @@ Many modules accept their own configuration options. These options have the abil
| output_modules.http.bearer | str | Authorization Bearer token | |
| output_modules.http.method | str | HTTP method | POST |
| output_modules.http.password | str | Password (basic auth) | |
+| output_modules.http.siem_friendly | bool | Format JSON in a SIEM-friendly way for ingestion into Elastic, Splunk, etc. | False |
| output_modules.http.timeout | int | HTTP timeout | 10 |
| output_modules.http.url | str | Web URL | |
| output_modules.http.username | str | Username (basic auth) | |
@@ -360,6 +365,11 @@ Many modules accept their own configuration options. These options have the abil
| output_modules.slack.event_types | list | Types of events to send | ['VULNERABILITY', 'FINDING'] |
| output_modules.slack.min_severity | str | Only allow VULNERABILITY events of this severity or higher | LOW |
| output_modules.slack.webhook_url | str | Discord webhook URL | |
+| output_modules.splunk.hectoken | str | HEC Token | |
+| output_modules.splunk.index | str | Index to send data to | |
+| output_modules.splunk.source | str | Source path to be added to the metadata | |
+| output_modules.splunk.timeout | int | HTTP timeout | 10 |
+| output_modules.splunk.url | str | Web URL | |
| output_modules.subdomains.include_unresolved | bool | Include unresolved subdomains in output | False |
| output_modules.subdomains.output_file | str | Output to file | |
| output_modules.teams.event_types | list | Types of events to send | ['VULNERABILITY', 'FINDING'] |
diff --git a/docs/scanning/events.md b/docs/scanning/events.md
index 6628fac46..d2aaa4595 100644
--- a/docs/scanning/events.md
+++ b/docs/scanning/events.md
@@ -51,7 +51,7 @@ Below is a full list of event types along with which modules produce/consume the
| Event Type | # Consuming Modules | # Producing Modules | Consuming Modules | Producing Modules |
|---------------------|-----------------------|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| * | 11 | 0 | affiliates, csv, discord, http, human, json, neo4j, python, slack, teams, websocket | |
+| * | 12 | 0 | affiliates, csv, discord, http, human, json, neo4j, python, slack, splunk, teams, websocket | |
| ASN | 0 | 1 | | asn |
| AZURE_TENANT | 1 | 0 | speculate | |
| CODE_REPOSITORY | 0 | 2 | | github_codesearch, github_org |
diff --git a/docs/scanning/output.md b/docs/scanning/output.md
index 81b4b8ede..af1db4737 100644
--- a/docs/scanning/output.md
+++ b/docs/scanning/output.md
@@ -1,6 +1,6 @@
# Output
-By default, BBOT saves its output in TXT, JSON, and CSV formats:
+By default, BBOT saves its output in TXT, JSON, and CSV formats. The filenames are logged at the end of each scan:
![bbot output](https://github.com/blacklanternsecurity/bbot/assets/20261699/bb3da441-2682-408f-b955-19b268823b82)
Every BBOT scan gets a unique and mildly-entertaining name like **`demonic_jimmy`**. Output for that scan, including scan stats and any web screenshots, etc., are saved to a folder by that name in `~/.bbot/scans`. The most recent 20 scans are kept, and older ones are removed. You can change the location of BBOT's output with `--output`, and you can also pick a custom scan name with `--name`.
@@ -135,6 +135,25 @@ output_modules:
password: P@ssw0rd
```
+### Splunk
+
+The `splunk` output module sends [events](events.md) in JSON format to a desired splunk instance via [HEC](https://docs.splunk.com/Documentation/Splunk/9.2.0/Data/UsetheHTTPEventCollector).
+
+You can customize this output with the following config options:
+
+```yaml title="~/.bbot/config/bbot.yml"
+output_modules:
+ splunk:
+ # The full URL with the URI `/services/collector/event`
+ url: https://localhost:8088/services/collector/event
+ # Generated from splunk webui
+ hectoken: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
+ # Defaults to `main` if not set
+ index: my-specific-index
+ # Defaults to `bbot` if not set
+ source: /my/source.json
+```
+
### Asset Inventory
The `asset_inventory` module produces a CSV like this:
diff --git a/poetry.lock b/poetry.lock
index 676f41389..ab85c1785 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -186,6 +186,17 @@ d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
+[[package]]
+name = "cachetools"
+version = "5.3.2"
+description = "Extensible memoizing collections and decorators"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "cachetools-5.3.2-py3-none-any.whl", hash = "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1"},
+ {file = "cachetools-5.3.2.tar.gz", hash = "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2"},
+]
+
[[package]]
name = "certifi"
version = "2024.2.2"
@@ -2027,7 +2038,6 @@ optional = false
python-versions = "*"
files = [
{file = "requests-file-2.0.0.tar.gz", hash = "sha256:20c5931629c558fda566cacc10cfe2cd502433e628f568c34c80d96a0cc95972"},
- {file = "requests_file-2.0.0-py2.py3-none-any.whl", hash = "sha256:3e493d390adb44aa102ebea827a48717336d5268968c370eaf19abaf5cae13bf"},
]
[package.dependencies]
@@ -2088,6 +2098,17 @@ files = [
{file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
]
+[[package]]
+name = "socksio"
+version = "1.0.0"
+description = "Sans-I/O implementation of SOCKS4, SOCKS4A, and SOCKS5."
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3"},
+ {file = "socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac"},
+]
+
[[package]]
name = "soupsieve"
version = "2.5"
@@ -2413,4 +2434,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
-content-hash = "8d9864610f54050aec62bf75415e5b683a851323d054a38ff36e54d9d5c284e3"
+content-hash = "e9c476ba44a5968f7bd6c9759ac4c6f8e679384bd6b0dd4f128af873a68a34da"
diff --git a/pyproject.toml b/pyproject.toml
index 93172c060..f16540fb8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -46,6 +46,8 @@ pydantic = "^2.4.2"
httpx = "^0.26.0"
cloudcheck = "^2.1.0.181"
tldextract = "^5.1.1"
+cachetools = "^5.3.2"
+socksio = "^1.0.0"
[tool.poetry.group.dev.dependencies]
flake8 = "^6.0.0"