Skip to content

Commit b0ea387

Browse files
committed
Merge dev, resolve import conflict in http.py
2 parents 2427d58 + f256ec3 commit b0ea387

199 files changed

Lines changed: 9756 additions & 2153 deletions

File tree

Some content is hidden

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

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ jobs:
4646
name: pytest-debug-logs-${{ env.PYTHON_VERSION }}
4747
path: pytest_debug.log
4848
- name: Upload Code Coverage
49-
uses: codecov/codecov-action@v6
49+
uses: codecov/codecov-action@v7
5050
with:
5151
token: ${{ secrets.CODECOV_TOKEN }}
5252
files: ./cov.xml

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ For details, see [Configuration](https://www.blacklanternsecurity.com/bbot/Stabl
387387
- [List of Modules](https://www.blacklanternsecurity.com/bbot/Stable/modules/list_of_modules)
388388
- [Nuclei](https://www.blacklanternsecurity.com/bbot/Stable/modules/nuclei)
389389
- [Custom YARA Rules](https://www.blacklanternsecurity.com/bbot/Stable/modules/custom_yara_rules)
390-
- [Lightfuzz](https://www.blacklanternsecurity.com/bbot/Stable/modules/lightfuzz)
390+
- [Lightfuzz (DAST)](https://www.blacklanternsecurity.com/bbot/Stable/modules/lightfuzz)
391391
- **Misc**
392392
- [Contribution](https://www.blacklanternsecurity.com/bbot/Stable/contribution)
393393
- [Release History](https://www.blacklanternsecurity.com/bbot/Stable/release_history)
@@ -397,6 +397,7 @@ For details, see [Configuration](https://www.blacklanternsecurity.com/bbot/Stabl
397397
- [Setting Up a Dev Environment](https://www.blacklanternsecurity.com/bbot/Stable/dev/dev_environment)
398398
- [BBOT Internal Architecture](https://www.blacklanternsecurity.com/bbot/Stable/dev/architecture)
399399
- [How to Write a BBOT Module](https://www.blacklanternsecurity.com/bbot/Stable/dev/module_howto)
400+
- [Validating & Inspecting Presets](https://www.blacklanternsecurity.com/bbot/Stable/dev/preset_validation)
400401
- [Unit Tests](https://www.blacklanternsecurity.com/bbot/Stable/dev/tests)
401402
- [Discord Bot Example](https://www.blacklanternsecurity.com/bbot/Stable/dev/discord_bot)
402403
- **Code Reference**

bbot/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ async def _main():
102102
preset._default_internal_modules = []
103103

104104
# Bake a temporary copy of the preset so that flags correctly enable their associated modules before listing them
105+
preset.validate()
105106
preset = preset.bake()
106107

107108
# --list-modules
@@ -156,6 +157,7 @@ async def _main():
156157
print(row)
157158
return
158159

160+
preset.validate()
159161
baked_preset = preset.bake()
160162

161163
# --current-preset / --current-preset-full

bbot/core/config/files.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import sys
2+
import yaml
23
from pathlib import Path
3-
from omegaconf import OmegaConf
44

5+
from .merge import deep_merge
56
from ...logger import log_to_stderr
67
from ...errors import ConfigLoadError
78

@@ -18,24 +19,36 @@ class BBOTConfigFiles:
1819
def __init__(self, core):
1920
self.core = core
2021

21-
def _get_config(self, filename, name="config"):
22+
def _get_config(self, filename, name="config") -> dict:
2223
filename = Path(filename).resolve()
24+
if not filename.exists():
25+
return {}
2326
try:
24-
conf = OmegaConf.load(str(filename))
27+
with open(filename) as f:
28+
conf = yaml.safe_load(f) or {}
29+
if not isinstance(conf, dict):
30+
raise ConfigLoadError(
31+
f"Error parsing config at {filename}: expected a YAML mapping at the top level, "
32+
f"got {type(conf).__name__}"
33+
)
2534
cli_silent = any(x in sys.argv for x in ("-s", "--silent"))
2635
if __name__ == "__main__" and not cli_silent:
2736
log_to_stderr(f"Loaded {name} from {filename}")
2837
return conf
38+
except ConfigLoadError:
39+
raise
40+
except yaml.YAMLError as e:
41+
raise ConfigLoadError(
42+
f"YAML syntax error in {filename}:\n\n{e}\n\nPlease check the file for indentation or formatting errors."
43+
)
2944
except Exception as e:
30-
if filename.exists():
31-
raise ConfigLoadError(f"Error parsing config at {filename}:\n\n{e}")
32-
return OmegaConf.create()
45+
raise ConfigLoadError(f"Error parsing config at {filename}:\n\n{e}")
3346

34-
def get_custom_config(self):
35-
return OmegaConf.merge(
47+
def get_custom_config(self) -> dict:
48+
return deep_merge(
3649
self._get_config(self.config_filename, name="config"),
3750
self._get_config(self.secrets_filename, name="secrets"),
3851
)
3952

40-
def get_default_config(self):
53+
def get_default_config(self) -> dict:
4154
return self._get_config(self.defaults_filename, name="defaults")

bbot/core/config/merge.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""
2+
Deep-merge helpers replacing omegaconf's merge semantics.
3+
4+
`deep_merge(a, b)` returns a new dict that is `a` with `b` merged in: nested
5+
dicts are merged recursively, leaf values (and lists) from `b` replace those in
6+
`a`. This matches `OmegaConf.merge(a, b)` for BBOT's preset layering use case.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from copy import deepcopy
12+
from typing import Any
13+
14+
15+
def deep_merge(base: dict[str, Any] | None, *updates: dict[str, Any] | None) -> dict[str, Any]:
16+
"""
17+
Deep-merge one or more update dicts into a copy of `base`. Last wins on
18+
leaf conflicts; lists are replaced wholesale (not concatenated).
19+
20+
The returned dict shares no mutable state with the inputs — nested dicts,
21+
lists, and other mutable values are deep-copied as they're carried over.
22+
"""
23+
result: dict[str, Any] = deepcopy(base) if base else {}
24+
for update in updates:
25+
if not update:
26+
continue
27+
for k, v in update.items():
28+
if k in result and isinstance(result[k], dict) and isinstance(v, dict):
29+
result[k] = deep_merge(result[k], v)
30+
else:
31+
result[k] = deepcopy(v)
32+
return result
33+
34+
35+
def dotted_get(data: dict[str, Any], path: str, default: Any = None) -> Any:
36+
"""
37+
Look up a dotted path in a nested dict.
38+
39+
Note: keys containing literal `.` are not addressable (no escape syntax).
40+
41+
>>> dotted_get({"a": {"b": {"c": 1}}}, "a.b.c")
42+
1
43+
>>> dotted_get({"a": 1}, "a.b.c", default="x")
44+
'x'
45+
"""
46+
cursor: Any = data
47+
for part in path.split("."):
48+
if not isinstance(cursor, dict) or part not in cursor:
49+
return default
50+
cursor = cursor[part]
51+
return cursor
52+
53+
54+
def dotted_set(data: dict[str, Any], path: str, value: Any) -> None:
55+
"""
56+
Set a dotted path in a nested dict, creating intermediate dicts as needed.
57+
58+
Non-dict intermediates are silently replaced. This is intentional —
59+
callers (CLI parsing) feed the result through pydantic validation, which
60+
surfaces any resulting type mismatch.
61+
62+
>>> d = {}
63+
>>> dotted_set(d, "a.b.c", 1)
64+
>>> d
65+
{'a': {'b': {'c': 1}}}
66+
"""
67+
parts = path.split(".")
68+
cursor = data
69+
for part in parts[:-1]:
70+
if part not in cursor or not isinstance(cursor[part], dict):
71+
cursor[part] = {}
72+
cursor = cursor[part]
73+
cursor[parts[-1]] = value
74+
75+
76+
def iter_dotted_paths(data: dict[str, Any], prefix: str = "") -> list[str]:
77+
"""
78+
Return every dotted leaf path in a nested dict. Empty dicts are treated
79+
as leaves (so they round-trip through dotted_get/dotted_set).
80+
81+
>>> iter_dotted_paths({"a": 1, "b": {"c": 2}})
82+
['a', 'b.c']
83+
"""
84+
paths: list[str] = []
85+
for k, v in data.items():
86+
path = f"{prefix}.{k}" if prefix else k
87+
if isinstance(v, dict) and v:
88+
paths.extend(iter_dotted_paths(v, path))
89+
else:
90+
paths.append(path)
91+
return paths
92+
93+
94+
__all__ = ["deep_merge", "dotted_get", "dotted_set", "iter_dotted_paths"]

0 commit comments

Comments
 (0)