Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion bbot/scanner/preset/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class PresetPath:

def __init__(self):
self.paths = [DEFAULT_PRESET_PATH]
self._listable = {DEFAULT_PRESET_PATH}

def find(self, filename):
filename_path = Path(filename).expanduser()
Expand Down Expand Up @@ -49,13 +50,17 @@ def find(self, filename):
def __str__(self):
return ":".join([str(s) for s in self.paths])

def add_path(self, path):
def add_path(self, path, listable=False):
path = Path(path).expanduser().resolve()
# skip if already in paths
if path in self.paths:
if listable:
self._listable.add(path)
return
# skip if path is a subdirectory of any path in paths
if any(path.is_relative_to(p) for p in self.paths):
if listable:
self._listable.add(path)
return
# skip if path is not a directory
if not path.is_dir():
Expand All @@ -65,6 +70,12 @@ def add_path(self, path):
# but never remove the default preset path
self.paths = [p for p in self.paths if p == DEFAULT_PRESET_PATH or not p.is_relative_to(path)]
self.paths.insert(0, path)
if listable:
self._listable.add(path)

@property
def listable_paths(self):
return [p for p in self.paths if p in self._listable]

def find_file(self, filename):
"""Search known preset paths for a file of any type (e.g. target lists).
Expand Down
6 changes: 3 additions & 3 deletions bbot/scanner/preset/preset.py
Original file line number Diff line number Diff line change
Expand Up @@ -1053,16 +1053,16 @@ def all_presets(self):
"""
Recursively find all the presets and return them as a dictionary
"""
# first, add local preset dir to PRESET_PATH
PRESET_PATH.add_path(self.preset_dir)
# first, add local preset dir to PRESET_PATH (listable so -lp enumerates it)
PRESET_PATH.add_path(self.preset_dir, listable=True)

# ensure local preset directory exists
mkdir(self.preset_dir)

global DEFAULT_PRESETS
if DEFAULT_PRESETS is None:
presets = {}
for preset_path in PRESET_PATH:
for preset_path in PRESET_PATH.listable_paths:
for ext in ("yml", "yaml"):
# for every yaml file
for original_filename in preset_path.rglob(f"**/*.{ext}"):
Expand Down
68 changes: 68 additions & 0 deletions bbot/test/test_step_1/test_presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1374,3 +1374,71 @@ def test_malformed_yaml_config_file(tmp_path):
malformed.write_text("web:\n http_rate_limit: 100\n bad_key: value\n")
with pytest.raises(ConfigLoadError, match="YAML syntax error"):
BBOTConfigFiles._get_config(None, str(malformed))


def test_all_presets_ignores_non_preset_yaml(tmp_path):
"""Regression test for https://github.com/blacklanternsecurity/bbot/issues/3189

When -p loads a preset from an arbitrary directory (e.g. $HOME), that
directory must NOT be searched by all_presets / -lp. Otherwise every
.yml file under it (Ansible collections, CI configs, etc.) is parsed
and produces warning spam.
"""
import bbot.scanner.preset.preset as preset_mod
from bbot.scanner.preset.path import PresetPath

# create a valid preset in tmp_path (simulates ~/my_preset.yml)
preset_file = tmp_path / "my_preset.yml"
preset_file.write_text("description: test preset\nmodules:\n - sslcert\n")

# create non-preset yaml files nearby (simulates Ansible, CI, etc.)
junk_dir = tmp_path / "ansible"
junk_dir.mkdir()
(junk_dir / "playbook.yml").write_text("hosts: all\ntasks: []\n")
(tmp_path / "ci.yml").write_text("on: push\njobs: {}\n")

# save and replace global state so we get a clean PRESET_PATH
orig_preset_path = preset_mod.PRESET_PATH
orig_default_presets = preset_mod.DEFAULT_PRESETS
try:
fresh_path = PresetPath()
preset_mod.PRESET_PATH = fresh_path
# also patch the path module's reference
import bbot.scanner.preset.path as path_mod

orig_path_singleton = path_mod.PRESET_PATH
path_mod.PRESET_PATH = fresh_path

# simulate -p /tmp/xxx/my_preset.yml: find() adds tmp_path to search paths
found = fresh_path.find(str(preset_file))
assert found == preset_file.resolve()
# tmp_path is now in search paths (needed for include resolution)
assert tmp_path.resolve() in fresh_path.paths

# reset the cached presets so all_presets re-enumerates
preset_mod.DEFAULT_PRESETS = None

preset = Preset()

# collect warnings emitted during all_presets enumeration
import logging

warnings = []
handler = logging.Handler()
handler.emit = lambda record: (
warnings.append(record.getMessage()) if record.levelno >= logging.WARNING else None
)
preset_logger = logging.getLogger("bbot.presets")
preset_logger.addHandler(handler)
try:
preset.all_presets
finally:
preset_logger.removeHandler(handler)

# no warnings should reference the junk files from tmp_path
junk_warnings = [w for w in warnings if "playbook.yml" in w or "ci.yml" in w]
assert not junk_warnings, f"all_presets tried to parse non-preset YAML files: {junk_warnings}"
finally:
preset_mod.DEFAULT_PRESETS = orig_default_presets
preset_mod.PRESET_PATH = orig_preset_path
path_mod.PRESET_PATH = orig_path_singleton
Loading