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
23 changes: 23 additions & 0 deletions bbot/scanner/preset/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,29 @@ def add_path(self, path):
self.paths = [p for p in self.paths if not p.is_relative_to(path)]
self.paths.insert(0, path)

def find_file(self, filename):
"""Search known preset paths for a file of any type (e.g. target lists).

For absolute paths, checks directly. For relative paths, searches each
known preset path and its subdirectories (consistent with how ``find()``
uses rglob for preset YAML files), then falls back to CWD.

Returns the resolved Path if found, otherwise None.
"""
filename_path = Path(filename).expanduser()
if filename_path.is_absolute():
resolved = filename_path.resolve()
return resolved if resolved.is_file() else None
for path in self.paths:
for match in path.rglob(str(filename_path)):
if match.is_file():
return match.resolve()
# fall back to CWD
candidate = filename_path.resolve()
if candidate.is_file():
return candidate
return None

def __iter__(self):
yield from self.paths

Expand Down
46 changes: 43 additions & 3 deletions bbot/scanner/preset/preset.py
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,23 @@ def blacklisted(self, host):
def in_target(self, host):
return self.target.in_target(host)

@staticmethod
def _resolve_file_entries(entries):
"""Resolve relative file paths in target/seeds/blacklist entries via PresetPath.

Replaces entries that match a file in PresetPath's known directories with
their absolute path, so that chain_lists' existing try_files logic can find them.
Entries that don't match a file are left as-is.
"""
resolved = []
for entry in entries:
found = PRESET_PATH.find_file(entry)
if found is not None:
resolved.append(str(found))
else:
resolved.append(entry)
return resolved

@classmethod
def from_dict(cls, preset_dict, name=None, _exclude=None, _log=False):
"""
Expand All @@ -646,15 +663,35 @@ def from_dict(cls, preset_dict, name=None, _exclude=None, _log=False):
Examples:
>>> preset = Preset.from_dict({"target": ["evilcorp.com"], "modules": ["portscan"]})
"""
from bbot.core.helpers.misc import chain_lists

# Handle seeds and targets from dict
# for user-friendliness, we allow both "target" and "targets" to be used. we merge them into a single list.
target_vals = (preset_dict.get("target") or []) + (preset_dict.get("targets") or [])
targets = list(dict.fromkeys(target_vals))
# resolve relative file paths via PresetPath (which knows the preset's directory)
targets = chain_lists(
cls._resolve_file_entries(target_vals),
try_files=True,
msg="Reading targets from preset file: {filename}",
)
seeds = preset_dict.get("seeds")
if seeds is not None:
seeds = chain_lists(
cls._resolve_file_entries(seeds),
try_files=True,
msg="Reading seeds from preset file: {filename}",
)
blacklist = preset_dict.get("blacklist")
if blacklist is not None:
blacklist = chain_lists(
cls._resolve_file_entries(blacklist),
try_files=True,
msg="Reading blacklist from preset file: {filename}",
)
new_preset = cls(
*targets,
seeds=seeds,
blacklist=preset_dict.get("blacklist"),
blacklist=blacklist,
modules=preset_dict.get("modules"),
output_modules=preset_dict.get("output_modules"),
exclude_modules=preset_dict.get("exclude_modules"),
Expand Down Expand Up @@ -724,7 +761,10 @@ def from_yaml_file(cls, filename, _exclude=None, _log=False):
except FileNotFoundError:
raise PresetNotFoundError(f'Could not find preset at "{filename}" - file does not exist')
preset = cls.from_dict(
omegaconf.OmegaConf.create(yaml_str), name=filename.stem, _exclude=_exclude, _log=_log
omegaconf.OmegaConf.create(yaml_str),
name=filename.stem,
_exclude=_exclude,
_log=_log,
)
preset._yaml_str = yaml_str
preset.filename = filename
Expand Down
79 changes: 79 additions & 0 deletions bbot/test/test_step_1/test_presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1195,3 +1195,82 @@ async def test_preset_serialization(clean_default_config):
assert preset_dict_round_tripped == preset_dict
assert preset_dict["target"] == ["192.168.1.1"]
assert "seeds" not in preset_dict


def test_preset_file_targets(tmp_path):
"""Test that file paths in preset target/seeds/blacklist are resolved via PresetPath.

The preset and its target files live in tmp_path (NOT CWD), so relative paths
like "targets.txt" can only be found if PresetPath adds the preset's directory
to its search paths. This is the core behavior being tested.
"""
import os

# sanity check: tmp_path is not CWD (otherwise relative resolution is ambiguous)
assert os.getcwd() != str(tmp_path)

# create target files next to where the preset will live
targets_file = tmp_path / "targets.txt"
targets_file.write_text("evilcorp.com\n1.2.3.4\n")
seeds_file = tmp_path / "seeds.txt"
seeds_file.write_text("seed1.evilcorp.com\nseed2.evilcorp.com\n")
blacklist_file = tmp_path / "blacklist.txt"
blacklist_file.write_text("internal.evilcorp.com\n10.0.0.0/8\n")

# relative paths: resolved from the preset's directory via PresetPath
preset_file = tmp_path / "my_preset.yml"
preset_file.write_text("target:\n - targets.txt\nseeds:\n - seeds.txt\nblacklist:\n - blacklist.txt\n")
preset = Preset.from_yaml_file(str(preset_file))
target_inputs = set(preset._target_list)
assert "evilcorp.com" in target_inputs
assert "1.2.3.4" in target_inputs
assert "targets.txt" not in target_inputs
seed_inputs = set(preset._seeds)
assert "seed1.evilcorp.com" in seed_inputs
assert "seed2.evilcorp.com" in seed_inputs
blacklist_inputs = set(preset._blacklist)
assert "internal.evilcorp.com" in blacklist_inputs
assert "10.0.0.0/8" in blacklist_inputs

# absolute paths for targets, seeds, and blacklist
preset_file2 = tmp_path / "my_preset2.yml"
preset_file2.write_text(
f"target:\n - {targets_file}\nseeds:\n - {seeds_file}\nblacklist:\n - {blacklist_file}\n"
)
preset2 = Preset.from_yaml_file(str(preset_file2))
target_inputs2 = set(preset2._target_list)
assert "evilcorp.com" in target_inputs2
assert "1.2.3.4" in target_inputs2
seed_inputs2 = set(preset2._seeds)
assert "seed1.evilcorp.com" in seed_inputs2
assert "seed2.evilcorp.com" in seed_inputs2
blacklist_inputs2 = set(preset2._blacklist)
assert "internal.evilcorp.com" in blacklist_inputs2
assert "10.0.0.0/8" in blacklist_inputs2

# mixed: file paths + literal targets
preset_file3 = tmp_path / "my_preset3.yml"
preset_file3.write_text("target:\n - targets.txt\n - extra.evilcorp.com\n")
preset3 = Preset.from_yaml_file(str(preset_file3))
target_inputs3 = set(preset3._target_list)
assert "evilcorp.com" in target_inputs3
assert "1.2.3.4" in target_inputs3
assert "extra.evilcorp.com" in target_inputs3

# non-existent file strings are kept as literal targets
preset4 = Preset.from_dict({"target": ["not_a_file.txt", "192.168.1.1"]})
target_inputs4 = set(preset4._target_list)
assert "not_a_file.txt" in target_inputs4
assert "192.168.1.1" in target_inputs4

# subdirectory: preset in a nested dir references a file in the same nested dir
subdir = tmp_path / "nested" / "presets"
subdir.mkdir(parents=True)
nested_targets = subdir / "my_targets.txt"
nested_targets.write_text("nested.evilcorp.com\n")
nested_preset = subdir / "nested_preset.yml"
nested_preset.write_text("target:\n - my_targets.txt\n")
preset5 = Preset.from_yaml_file(str(nested_preset))
target_inputs5 = set(preset5._target_list)
assert "nested.evilcorp.com" in target_inputs5
assert "my_targets.txt" not in target_inputs5
22 changes: 21 additions & 1 deletion docs/scanning/presets.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,27 @@ bbot -p ./mypreset.yml --current-preset

## Advanced Usage

BBOT Presets support advanced features like environment variable substitution and custom conditions.
BBOT Presets support advanced features like file-based targets, environment variable substitution, and custom conditions.

### Files as Targets

You can specify file paths in your preset's `target`, `seeds`, or `blacklist` fields. BBOT will read each file and expand its lines as individual entries:

```yaml title="my_preset.yml"
target:
- targets.txt
- extra.evilcorp.com

seeds:
- seeds.txt

blacklist:
- /home/user/blacklist.txt
```

Relative paths (like `targets.txt`) are resolved relative to the preset file's directory first, then the current working directory. Absolute paths are used as-is.

You can mix file paths and literal targets in the same list. If an entry doesn't point to an existing file, it is treated as a literal target.

### Custom Modules

Expand Down
Loading