Skip to content

Commit 6e96b32

Browse files
Merge pull request blacklanternsecurity#1119 from blacklanternsecurity/dev
Dev --> Stable
2 parents 8f358b6 + 1f807f5 commit 6e96b32

File tree

126 files changed

+6216
-2022
lines changed

Some content is hidden

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

126 files changed

+6216
-2022
lines changed

.github/workflows/tests.yml

+26-2
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,7 @@ jobs:
129129
password: ${{ secrets.PYPI_API_TOKEN }}
130130
- name: Get BBOT version
131131
id: version
132-
run: |
133-
echo "::set-output name=BBOT_VERSION::$(poetry version | cut -d' ' -f2 | tr -d v)"
132+
run: echo "BBOT_VERSION=$(poetry version | cut -d' ' -f2)" >> $GITHUB_OUTPUT
134133
- name: Publish to Docker Hub (dev)
135134
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
136135
uses: elgohr/Publish-Docker-Github-Action@v5
@@ -154,3 +153,28 @@ jobs:
154153
username: ${{ secrets.DOCKER_USERNAME }}
155154
password: ${{ secrets.DOCKER_PASSWORD }}
156155
repository: blacklanternsecurity/bbot
156+
outputs:
157+
BBOT_VERSION: ${{ steps.version.outputs.BBOT_VERSION }}
158+
# tag_commit:
159+
# needs: publish_code
160+
# runs-on: ubuntu-latest
161+
# if: github.event_name == 'push' && github.ref == 'refs/heads/stable'
162+
# steps:
163+
# - uses: actions/checkout@v3
164+
# with:
165+
# ref: ${{ github.head_ref }}
166+
# fetch-depth: 0 # Fetch all history for all tags and branches
167+
# - name: Configure git
168+
# run: |
169+
# git config --local user.email "[email protected]"
170+
# git config --local user.name "GitHub Actions"
171+
# - name: Tag commit
172+
# run: |
173+
# VERSION="${{ needs.publish_code.outputs.BBOT_VERSION }}"
174+
# if [[ "${{ github.ref }}" == "refs/heads/dev" ]]; then
175+
# TAG_MESSAGE="Dev Release $VERSION"
176+
# elif [[ "${{ github.ref }}" == "refs/heads/stable" ]]; then
177+
# TAG_MESSAGE="Stable Release $VERSION"
178+
# fi
179+
# git tag -a $VERSION -m "$TAG_MESSAGE"
180+
# git push origin --tags

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
__pycache__/
22
.coverage*
3+
/data/
4+
/neo4j/

README.md

+26-23
Large diffs are not rendered by default.

bbot/cli.py

+14-6
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import traceback
99
from omegaconf import OmegaConf
1010
from contextlib import suppress
11-
from aioconsole import stream
1211

1312
# fix tee buffering
1413
sys.stdout.reconfigure(line_buffering=True)
@@ -20,12 +19,10 @@
2019
from bbot import __version__
2120
from bbot.modules import module_loader
2221
from bbot.core.configurator.args import parser
23-
from bbot.core.helpers.misc import smart_decode
2422
from bbot.core.helpers.logger import log_to_stderr
2523
from bbot.core.configurator import ensure_config_files, check_cli_args, environ
2624

2725
log = logging.getLogger("bbot.cli")
28-
sys.stdout.reconfigure(line_buffering=True)
2926

3027

3128
log_level = get_log_level()
@@ -303,7 +300,12 @@ async def _main():
303300
if not options.dry_run:
304301
log.trace(f"Command: {' '.join(sys.argv)}")
305302

303+
# if we're on the terminal, enable keyboard interaction
306304
if sys.stdin.isatty():
305+
306+
import fcntl
307+
from bbot.core.helpers.misc import smart_decode
308+
307309
if not options.agent_mode and not options.yes:
308310
log.hugesuccess(f"Scan ready. Press enter to execute {scanner.name}")
309311
input()
@@ -324,11 +326,17 @@ def handle_keyboard_input(keyboard_input):
324326
toggle_log_level(logger=log)
325327
scanner.manager.modules_status(_log=True)
326328

327-
# Reader
328-
reader = stream.StandardStreamReader()
329-
protocol = stream.StandardStreamReaderProtocol(reader)
329+
reader = asyncio.StreamReader()
330+
protocol = asyncio.StreamReaderProtocol(reader)
330331
await asyncio.get_event_loop().connect_read_pipe(lambda: protocol, sys.stdin)
331332

333+
# set stdout and stderr to blocking mode
334+
# this is needed to prevent BlockingIOErrors in logging etc.
335+
fds = [sys.stdout.fileno(), sys.stderr.fileno()]
336+
for fd in fds:
337+
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
338+
fcntl.fcntl(fd, fcntl.F_SETFL, flags & ~os.O_NONBLOCK)
339+
332340
async def akeyboard_listen():
333341
try:
334342
allowed_errors = 10

bbot/core/event/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
from .helpers import make_event_id, is_event_id
1+
from .helpers import make_event_id
22
from .base import make_event, is_event, event_from_json

bbot/core/event/base.py

+27-16
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import logging
55
import ipaddress
66
import traceback
7+
from copy import copy
78
from typing import Optional
89
from datetime import datetime
910
from contextlib import suppress
@@ -29,7 +30,6 @@
2930
split_host_port,
3031
tagify,
3132
validators,
32-
truncate_string,
3333
)
3434

3535

@@ -106,6 +106,8 @@ class BaseEvent:
106106
_dummy = False
107107
# Data validation, if data is a dictionary
108108
_data_validator = None
109+
# Whether to increment scope distance if the child and parent hosts are the same
110+
_scope_distance_increment_same_host = False
109111

110112
def __init__(
111113
self,
@@ -211,6 +213,9 @@ def __init__(
211213
# an event indicating whether the event has undergone DNS resolution
212214
self._resolved = asyncio.Event()
213215

216+
# inherit web spider distance from parent
217+
self.web_spider_distance = getattr(self.source, "web_spider_distance", 0)
218+
214219
@property
215220
def data(self):
216221
return self._data
@@ -413,7 +418,7 @@ def source(self, source):
413418
if source.scope_distance >= 0:
414419
new_scope_distance = int(source.scope_distance)
415420
# only increment the scope distance if the host changes
416-
if not hosts_are_same:
421+
if self._scope_distance_increment_same_host or not hosts_are_same:
417422
new_scope_distance += 1
418423
self.scope_distance = new_scope_distance
419424
# inherit certain tags
@@ -490,7 +495,10 @@ def data_human(self):
490495
return self._data_human()
491496

492497
def _data_human(self):
493-
return truncate_string(str(self.data), n=2000)
498+
if isinstance(self.data, (dict, list)):
499+
with suppress(Exception):
500+
return json.dumps(self.data, sort_keys=True)
501+
return smart_decode(self.data)
494502

495503
def _data_load(self, data):
496504
"""
@@ -524,10 +532,7 @@ def pretty_string(self):
524532
return self._pretty_string()
525533

526534
def _pretty_string(self):
527-
if isinstance(self.data, dict):
528-
with suppress(Exception):
529-
return json.dumps(self.data, sort_keys=True)
530-
return smart_decode(self.data)
535+
return self._data_human()
531536

532537
@property
533538
def data_graph(self):
@@ -753,9 +758,6 @@ def sanitize_data(self, data):
753758
self.parsed = validators.validate_url_parsed(url)
754759
return data
755760

756-
def _data_human(self):
757-
return json.dumps(self.data, sort_keys=True)
758-
759761
def _data_load(self, data):
760762
if isinstance(data, str):
761763
return json.loads(data)
@@ -880,7 +882,6 @@ class URL_UNVERIFIED(BaseEvent):
880882

881883
def __init__(self, *args, **kwargs):
882884
super().__init__(*args, **kwargs)
883-
self.web_spider_distance = getattr(self.source, "web_spider_distance", 0)
884885
# increment the web spider distance
885886
if self.type == "URL_UNVERIFIED" and getattr(self.module, "name", "") != "TARGET":
886887
self.web_spider_distance += 1
@@ -967,6 +968,11 @@ class _data_validator(BaseModel):
967968
url: str
968969
_validate_url = field_validator("url")(validators.validate_url)
969970

971+
def sanitize_data(self, data):
972+
data = super().sanitize_data(data)
973+
data["name"] = data["name"].lower()
974+
return data
975+
970976
def _words(self):
971977
return self.data["name"]
972978

@@ -999,6 +1005,7 @@ def __init__(self, *args, **kwargs):
9991005
def sanitize_data(self, data):
10001006
url = data.get("url", "")
10011007
self.parsed = validators.validate_url_parsed(url)
1008+
data["url"] = self.parsed.geturl()
10021009

10031010
header_dict = {}
10041011
for i in data.get("raw_header", "").splitlines():
@@ -1164,9 +1171,10 @@ class USERNAME(BaseEvent):
11641171
_quick_emit = True
11651172

11661173

1167-
class SOCIAL(DictEvent):
1174+
class SOCIAL(DictHostEvent):
11681175
_always_emit = True
11691176
_quick_emit = True
1177+
_scope_distance_increment_same_host = True
11701178

11711179

11721180
class WEBSCREENSHOT(DictHostEvent):
@@ -1186,13 +1194,13 @@ class WAF(DictHostEvent):
11861194
class _data_validator(BaseModel):
11871195
url: str
11881196
host: str
1189-
WAF: str
1197+
waf: str
11901198
info: Optional[str] = None
11911199
_validate_url = field_validator("url")(validators.validate_url)
11921200
_validate_host = field_validator("host")(validators.validate_host)
11931201

11941202
def _pretty_string(self):
1195-
return self.data["WAF"]
1203+
return self.data["waf"]
11961204

11971205

11981206
def make_event(
@@ -1256,9 +1264,10 @@ def make_event(
12561264
tags = []
12571265
elif isinstance(tags, str):
12581266
tags = [tags]
1259-
tags = list(tags)
1267+
tags = set(tags)
12601268

12611269
if is_event(data):
1270+
data = copy(data)
12621271
if scan is not None and not data.scan:
12631272
data.scan = scan
12641273
if scans is not None and not data.scans:
@@ -1269,6 +1278,8 @@ def make_event(
12691278
data.source = source
12701279
if internal == True:
12711280
data.internal = True
1281+
if tags:
1282+
data.tags = tags.union(data.tags)
12721283
event_type = data.type
12731284
return data
12741285
else:
@@ -1299,7 +1310,7 @@ def make_event(
12991310
# USERNAME <--> EMAIL_ADDRESS confusion
13001311
if event_type == "USERNAME" and validators.soft_validate(data, "email"):
13011312
event_type = "EMAIL_ADDRESS"
1302-
tags.append("affiliate")
1313+
tags.add("affiliate")
13031314

13041315
event_class = globals().get(event_type, DefaultEvent)
13051316

bbot/core/event/helpers.py

+1-7
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from contextlib import suppress
44

55
from bbot.core.errors import ValidationError
6+
from bbot.core.helpers.regexes import event_type_regexes
67
from bbot.core.helpers import sha1, smart_decode, smart_encode_punycode
7-
from bbot.core.helpers.regexes import event_type_regexes, event_id_regex
88

99

1010
log = logging.getLogger("bbot.core.event.helpers")
@@ -52,11 +52,5 @@ def get_event_type(data):
5252
raise ValidationError(f'Unable to autodetect event type from "{data}"')
5353

5454

55-
def is_event_id(s):
56-
if event_id_regex.match(str(s)):
57-
return True
58-
return False
59-
60-
6155
def make_event_id(data, event_type):
6256
return f"{event_type}:{sha1(data).hexdigest()}"

bbot/core/flags.py

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"active": "Makes active connections to target systems",
33
"affiliates": "Discovers affiliated hostnames/domains",
44
"aggressive": "Generates a large amount of network traffic",
5+
"baddns": "Runs all modules from the DNS auditing tool BadDNS",
56
"cloud-enum": "Enumerates cloud resources",
67
"deadly": "Highly aggressive",
78
"email-enum": "Enumerates email addresses",
@@ -13,6 +14,7 @@
1314
"service-enum": "Identifies protocols running on open ports",
1415
"slow": "May take a long time to complete",
1516
"social-enum": "Enumerates social media",
17+
"repo-enum": "Enumerates code repositories",
1618
"subdomain-enum": "Enumerates subdomains",
1719
"subdomain-hijack": "Detects hijackable subdomains",
1820
"web-basic": "Basic, non-intrusive web scan functionality",

0 commit comments

Comments
 (0)