Skip to content

BBOTCore

This is the first thing that loads when you import BBOT.

Unlike a Preset, BBOTCore holds only the config, not scan-specific stuff like targets, flags, modules, etc.

Its main jobs are:

  • set up logging
  • keep separation between the default and custom config (this allows presets to only display the config options that have changed)
  • allow for easy merging of configs
  • load quickly
Source code in bbot/core/core.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
class BBOTCore:
    """
    This is the first thing that loads when you import BBOT.

    Unlike a Preset, BBOTCore holds only the config, not scan-specific stuff like targets, flags, modules, etc.

    Its main jobs are:

    - set up logging
    - keep separation between the `default` and `custom` config (this allows presets to only display the config options that have changed)
    - allow for easy merging of configs
    - load quickly
    """

    def __init__(self):
        self._logger = None
        self._files_config = None

        self._config: dict | None = None
        self._custom_config: dict | None = None

        # bare minimum == logging
        try:
            self.logger
        except BBOTError as e:
            import sys

            print(f"\n[CRITICAL] {e}\n", file=sys.stderr)
            sys.exit(1)
        self.log = logging.getLogger("bbot.core")

        self._prep_multiprocessing()

    def _prep_multiprocessing(self):
        import multiprocessing
        from .helpers.process import BBOTProcess

        if SHARED_INTERPRETER_STATE.is_main_process:
            # if this is the main bbot process, set the logger and queue for the first time
            from functools import partialmethod

            BBOTProcess.__init__ = partialmethod(
                BBOTProcess.__init__, log_level=self.logger.log_level, log_queue=self.logger.queue
            )

        # this makes our process class the default for process pools, etc.
        mp_context = multiprocessing.get_context("spawn")
        mp_context.Process = BBOTProcess

    @property
    def home(self):
        return Path(self.config["home"]).expanduser().resolve()

    @property
    def cache_dir(self):
        return self.home / "cache"

    @property
    def tools_dir(self):
        return self.home / "tools"

    @property
    def temp_dir(self):
        return self.home / "temp"

    @property
    def lib_dir(self):
        return self.home / "lib"

    @property
    def scans_dir(self):
        return self.home / "scans"

    @property
    def config(self) -> dict:
        """
        .config is just .default_config + .custom_config merged together.

        Any new values should be added to custom_config.
        """
        if self._config is None:
            self._config = deep_merge(self.default_config, self.custom_config)
        return self._config

    @property
    def default_config(self) -> dict:
        """
        The default BBOT config (from `defaults.yml`).
        """
        global DEFAULT_CONFIG
        if DEFAULT_CONFIG is None:
            self.default_config = self.files_config.get_default_config()
            # ensure bbot home dir
            if "home" not in self.default_config:
                self.default_config["home"] = "~/.bbot"
        return DEFAULT_CONFIG

    @default_config.setter
    def default_config(self, value: dict):
        # we temporarily clear out the config so it can be refreshed if/when default_config changes
        global DEFAULT_CONFIG
        self._config = None
        DEFAULT_CONFIG = dict(value) if value else {}

    @property
    def custom_config(self) -> dict:
        """
        Custom BBOT config (from `~/.config/bbot/bbot.yml`)
        """
        # we temporarily clear out the config so it can be refreshed if/when custom_config changes
        self._config = None
        if self._custom_config is None:
            self.custom_config = self.files_config.get_custom_config()
        return self._custom_config

    @custom_config.setter
    def custom_config(self, value: dict):
        self._config = None
        self._custom_config = dict(value) if value else {}

    def no_secrets_config(self, config):
        """Return a copy of `config` with every `sensitive=True` field removed.

        Sensitivity is read from the per-field `json_schema_extra["sensitive"]`
        flag declared on `BBOTConfig` (and each module's `class Config`).
        Module-level redaction uses the composite schema built lazily by
        `MODULE_LOADER.config_schema`; if a key isn't covered by any schema
        (e.g. an unknown module), it passes through unchanged.
        """
        from .config.models import partition_sensitive_config

        return partition_sensitive_config(config, self._config_schema(), keep_sensitive=False)

    def secrets_only_config(self, config):
        """Return a copy of `config` containing only `sensitive=True` fields.

        Inverse of `no_secrets_config()`. Useful for splitting a merged config
        into a public `bbot.yml` and a private `secrets.yml`.
        """
        from .config.models import partition_sensitive_config

        return partition_sensitive_config(config, self._config_schema(), keep_sensitive=True)

    def _config_schema(self):
        """Resolve the runtime BBOTConfig schema (with per-module configs)."""
        try:
            from bbot.core.modules import MODULE_LOADER

            return MODULE_LOADER.config_schema
        except Exception:
            from .config.models import BBOTConfig

            return BBOTConfig

    def merge_custom(self, config):
        """Merge a config dict into the custom config."""
        self.custom_config = deep_merge(self.custom_config, dict(config) if config else {})

    def merge_default(self, config):
        """Merge a config dict into the default config."""
        self.default_config = deep_merge(self.default_config, dict(config) if config else {})

    def copy(self):
        """
        Return a semi-shallow copy of self. (`custom_config` is copied, but `default_config` stays the same)
        """
        core_copy = copy(self)
        core_copy._custom_config = deepcopy(self._custom_config) if self._custom_config else {}
        core_copy._config = None
        return core_copy

    @property
    def files_config(self):
        """
        Get the configs from `bbot.yml` and `defaults.yml`
        """
        if self._files_config is None:
            from .config import files

            self.files = files
            self._files_config = files.BBOTConfigFiles(self)
        return self._files_config

    def create_process(self, *args, **kwargs):
        if os.environ.get("BBOT_TESTING", "") == "True":
            process = self.create_thread(*args, **kwargs)
        else:
            if SHARED_INTERPRETER_STATE.is_scan_process:
                from .helpers.process import BBOTProcess

                process = BBOTProcess(*args, **kwargs)
            else:
                import multiprocessing

                raise BBOTError(f"Tried to start server from process {multiprocessing.current_process().name}")
        process.daemon = True
        return process

    def create_thread(self, *args, **kwargs):
        from .helpers.process import BBOTThread

        return BBOTThread(*args, **kwargs)

    @property
    def logger(self):
        self.config
        if self._logger is None:
            from .config.logger import BBOTLogger

            self._logger = BBOTLogger(self)
        return self._logger

config property

config: dict

.config is just .default_config + .custom_config merged together.

Any new values should be added to custom_config.

custom_config property writable

custom_config: dict

Custom BBOT config (from ~/.config/bbot/bbot.yml)

default_config property writable

default_config: dict

The default BBOT config (from defaults.yml).

files_config property

files_config

Get the configs from bbot.yml and defaults.yml

copy

copy()

Return a semi-shallow copy of self. (custom_config is copied, but default_config stays the same)

Source code in bbot/core/core.py
176
177
178
179
180
181
182
183
def copy(self):
    """
    Return a semi-shallow copy of self. (`custom_config` is copied, but `default_config` stays the same)
    """
    core_copy = copy(self)
    core_copy._custom_config = deepcopy(self._custom_config) if self._custom_config else {}
    core_copy._config = None
    return core_copy

merge_custom

merge_custom(config)

Merge a config dict into the custom config.

Source code in bbot/core/core.py
168
169
170
def merge_custom(self, config):
    """Merge a config dict into the custom config."""
    self.custom_config = deep_merge(self.custom_config, dict(config) if config else {})

merge_default

merge_default(config)

Merge a config dict into the default config.

Source code in bbot/core/core.py
172
173
174
def merge_default(self, config):
    """Merge a config dict into the default config."""
    self.default_config = deep_merge(self.default_config, dict(config) if config else {})

no_secrets_config

no_secrets_config(config)

Return a copy of config with every sensitive=True field removed.

Sensitivity is read from the per-field json_schema_extra["sensitive"] flag declared on BBOTConfig (and each module's class Config). Module-level redaction uses the composite schema built lazily by MODULE_LOADER.config_schema; if a key isn't covered by any schema (e.g. an unknown module), it passes through unchanged.

Source code in bbot/core/core.py
134
135
136
137
138
139
140
141
142
143
144
145
def no_secrets_config(self, config):
    """Return a copy of `config` with every `sensitive=True` field removed.

    Sensitivity is read from the per-field `json_schema_extra["sensitive"]`
    flag declared on `BBOTConfig` (and each module's `class Config`).
    Module-level redaction uses the composite schema built lazily by
    `MODULE_LOADER.config_schema`; if a key isn't covered by any schema
    (e.g. an unknown module), it passes through unchanged.
    """
    from .config.models import partition_sensitive_config

    return partition_sensitive_config(config, self._config_schema(), keep_sensitive=False)

secrets_only_config

secrets_only_config(config)

Return a copy of config containing only sensitive=True fields.

Inverse of no_secrets_config(). Useful for splitting a merged config into a public bbot.yml and a private secrets.yml.

Source code in bbot/core/core.py
147
148
149
150
151
152
153
154
155
def secrets_only_config(self, config):
    """Return a copy of `config` containing only `sensitive=True` fields.

    Inverse of `no_secrets_config()`. Useful for splitting a merged config
    into a public `bbot.yml` and a private `secrets.yml`.
    """
    from .config.models import partition_sensitive_config

    return partition_sensitive_config(config, self._config_schema(), keep_sensitive=True)