Preset validation - #3058
Conversation
🚀 Performance Benchmark Report
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## dev #3058 +/- ##
=====================================
+ Coverage 90% 90% +1%
=====================================
Files 447 447
Lines 40402 40462 +60
=====================================
+ Hits 36256 36327 +71
+ Misses 4146 4135 -11 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Let's consider replacing the |
Resolved conflicts from blasthttp integration and ffuf/httpx renames: - sslcert.py: take dev's refactored version (cert info from HTTP_RESPONSE) - telerik.py, elastic.py, environ.py: keep preset-validation's Config/docstring - webbrute_shortnames.py: dev's webbrute import + Config (drop ffuf-only fields) - ffuf.py/httpx.py removed; port Config conversion to webbrute.py/http.py - bucket_hetzner.py, generic_ssrf.py: convert to Config(BaseModuleConfig) - uv.lock: regenerated
|
Did some fuzzing against this branch with a working preset of mine plus a pile of mutations (typos, wrong types, malformed YAML, hostile inputs to the Python API). The good stuff first: A few things that I think are worth a look before this merges: YAML preset files bypass the validator entirelyThis is the big one.
The schema exists and works, but the most common entry point doesn't use it. With this, bad values flow through to
|
…onzero on bad config
- validate.py: guard _classify_loc against short loc tuples. config.modules
given as a list (e.g., {"config": {"modules": ["nuclei"]}}) previously
hit IndexError in the loc classifier.
- validate.py: skip module_dirs pre-pass when the value is not a list.
A string module_dirs (e.g., "/tmp/foo") used to iterate characters and
trigger filesystem calls, surfacing as PermissionError.
- validate.py: skip the modules-list post-pass when the value is not a
list, so `modules: "nuclei"` no longer cascades into six bogus
per-character suggestions on top of the real type error.
- validate.py: route len(loc)==1 extra_forbidden suggestions through
PresetSchema field names so `modlues:` suggests `modules` instead of
`module_dirs`, `flgas:` suggests `flags`, etc.
- validate.py: validate_preset_file catches FileNotFoundError, OSError,
and yaml.YAMLError and returns a clean error instead of raising.
- preset.py: call validate_preset() at the top of from_dict so typos in
YAML preset files (loaded via -p or include:) surface up front instead
of flowing through to bake() as raw TypeError or AttributeError.
- cli.py: BBOTArgumentError and BBOTError handlers in _main return False
and log at ERROR level; main() exits 1 on falsy return. Validation
failures now produce a non-zero exit so CI and scripts can detect them.
- webbrute: extensions accepts str or list[str]. Matches the chain_lists
runtime contract and the bundled web/dirbust-heavy.yml preset, which
passes a YAML list.
- tests: eight new cases in test_validate_preset.py covering each fix. Four
existing tests in test_presets.py updated to use real module names
(the prior fake `testpreset1`...`testpreset5` markers are now caught
by the stricter validator).
The prior commit changed _main()'s BBOTArgumentError and BBOTError handlers to `return False` and added a `sys.exit(1)` in main() so bbot would exit non-zero on validation failure. That broke test_cli_customheaders, which asserts `result is None` from `_main()` on invalid `--custom-headers` input, and the failure cascaded across all test-distros jobs and Python 3.14 in CI. Adhere to the existing convention: log the error and return None. Real-CLI exit code stays 0 on validation failures, matching every other CLI-arg failure path in BBOT. Users still see the [ERRR] or [WARN] line; scripts that need to detect failure can grep stderr or check log output.
Replace the schema-walking approach (pure_string_field / resolve_field_annotation) with a flat config type index built from AST annotation strings captured at preload time. The index maps every dotted config path to a frozenset of base type-names, enabling type-directed coercion for both CLI and file/programmatic config without exec-ing any module Config class. - models.py: accepted_types_from_string (AST), accepted_types_from_annotation (live types), coerce_value, coerce_config; remove old pure_string_field / resolve_field_annotation / _acceptable_types - modules.py: capture ast.unparse(annotation) in _extract_pydantic_config, store as options_types, bump PRELOAD_CACHE_VERSION to 3, add config_type_index property on ModuleLoader - args.py: parse_dotted_cli takes index= (flat dict) instead of schema= (pydantic model); _parse_cli_value delegates to coerce_value - preset.py: coerce_config runs over custom_config in bake() before validate() - validate.py: suggestion universe comes from config_type_index (single source of truth, kills the B1 drift class)
Previously bake() coerced config and called validate() itself, and from_dict validated the raw dict up front -- so coercion (a 'bake step') effectively ran before bake(), and the programmatic/bbot.yml entry points were never validated. Invert it: - Preset.validate() is now the precondition: coerces custom_config toward its declared types AND validates it, sets self._validated, returns self. - bake() ENFORCES it: raises if the preset isn't validated (it no longer coerces or validates itself). - merge() / mutations reset _validated, so a changed preset must be revalidated. - Scanner.__init__, the CLI, and from_dict call validate() before baking. Side effects: - config is now validated on every entry point (programmatic Preset(config=)/ Scanner(config=), bbot.yml, presets, CLI), closing the validation-asymmetry gap. - surfaced a missing internal-module toggle: 'unarchive' was absent from BBOTConfig (so 'bbot -c unarchive=false' was rejected); added it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Direct preset.bake() -> preset.validate().bake() (validate() returns self);
bake() now requires a validated preset.
- test_config: the programmatic path is now strictly validated, so the old
sentinel keys ('plumbus', module 'test_option') are correctly rejected as
typos; rewritten to verify config propagation with real keys
(status_frequency, ipneighbor.num_bits, module_timeout).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ion was pushed upstream.
from_dict reads a preset's top-level keys with .get(), so a typo'd or
unknown top-level key (e.g. `modlues:`) was silently dropped instead of
surfaced. And per the validate-before-bake contract, validate() should be
the single comprehensive gate for everything a preset declares.
- validate.py: add prevalidate_preset(), a thorough top-level KEY check
that reports every unknown/typo'd key with a closest-match hint. Keys
only -- it does not coerce or validate config values.
- preset.py from_dict(): auto-run prevalidate_preset() up front, and stop
calling validate() itself -- it now returns an explicitly unvalidated
preset. Coercion + full validation remain the caller's step before
bake() (Scanner and the CLI already do this).
- preset.py validate(): also validate the declared scan/output module
names (reusing _is_valid_module), so an unknown module name fails here
instead of only at bake().
- tests: top-level typo -> from_dict raises; config-value typo and unknown
module name -> validate() raises.
bake() is unchanged and still trusts an already-validated preset.
- Add 'recursive' to scan name adjectives (how was this missing?) - Both easter eggs now require exact name match (recursive_thetechromancer, golden_gus) instead of endswith - Skip easter eggs entirely when NO_COLOR is set
…fore-bake Validate Before Bake
coerce_value now validates each config value against its field's real
pydantic TypeAdapter (a "TryParse"): attempt the value, return it coerced
on success, and leave it unchanged on ValidationError so the schema pass
reports the real error rather than coercion silently swallowing it.
config_type_index is rebuilt by walking the materialized config schema
(global config + every module's exec'd Config), memoizing one TypeAdapter
per annotation. Nested models are recursed into via _field_submodel, so
modules.<name>.<option> keys fall out of the same walk -- replacing the two
divergent build paths (static BBOTConfig walk + preloaded AST strings).
The single materialized schema is now the source of truth for BOTH
validation and coercion, and coercion sees each field's true declared type
(Literal members, Annotated validators, unions) instead of a flattened set
of base type-names.
Deletes the now-dead helpers and tables:
- accepted_types_from_string / accepted_types_from_annotation
- _COLLECTION_NAMES / _TRUE_WORDS / _FALSE_WORDS
- their __all__ entries
args.py: rename the coerce_value arg accepted -> adapter; update docstrings.
All coercion special cases are preserved:
- numeric YAML value -> str field (ConfigDict(coerce_numbers_to_str=True))
- raw CLI strings kept lossless ("1.10", "0755" stay strings, not floats)
- bool words: true/false/yes/no/on/off/1/0
- list/dict literals parsed for collection-typed fields
- os.PathLike -> str (pydantic won't, so we pre-convert)
- date-shaped strings stay strings (_yaml_scalar guard)
- empty string stays ""
- invalid YAML is non-fatal (kept as the raw string)
Behavior change: field validators now run during coercion, so e.g.
SeverityLiteral upcases "low" -> "LOW". Scoped to baddns* min_severity /
min_confidence; strictly more correct; covered by tests.
Tested (clean HOME): 64/64 test_cli/test_validate_preset/test_presets/
test_config, plus 14/14 test_modules_basic + baddns end-to-end. Index =
832 adapters built in ~82ms, cached thereafter.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dapter coerce-value-typeadapter->preset-validation
|
@TheTechromancer thank you mate |
Preset validation rework
Replaces silent acceptance of typos and wrong types in BBOT presets with strict, schema-driven validation. Catches mistakes like
modlues: [...],scope: {strct: true},modules: {nucleii: {tgas: "x"}}, andnuclei.mode: aggressiveat config-load time instead of after a multi-hour scan produces nothing.New public API
Validate any preset dict (e.g. from
yaml.safe_load) without instantiating a Scanner:Returns a list of
PresetValidationErrorobjects. Empty list = valid. All errors across all layers are aggregated in a single pass, so a user with five typos sees five errors at once.The pydantic schemas themselves are also exported, for callers that want type-checking, doc generation, or to validate just one layer (e.g. the global config) on its own:
These are validation schemas only — they have no defaults of their own (defaults live in
bbot/defaults.yml). For full preset validation that also covers per-module Config blocks, usevalidate_preset(); the composite schema is built dynamically from the loaded module set, so it isn't a static class.Sample errors
What changed
Dependency: omegaconf → pydantic + pydantic-settings
omegaconfis gone. Configs are now plain dicts merged with a smalldeep_updatehelper, and validation is done by pydantic.pydantic-settingsis the new dep;pyyamlis now an explicit (was transitive) dep.Module schema:
class Config(BaseModuleConfig)Every module's
options = {...}+options_desc = {...}pair has been migrated to a typed pydantic class:BaseModuleConfigcarries the three universal options (batch_size,module_threads,module_timeout), so every module accepts those without redeclaring them. 114 modules migrated via codemod (bbot/scripts/migrate_options_to_config.py); a handful tightened by hand to use proper types where they matter:nuclei.mode: Literal["manual", "technology", "severe", "budget"]baddns.min_severity: Literal["INFO", "LOW", "MEDIUM", "HIGH", "CRITICAL"]baddns.min_confidence: Literal["UNKNOWN", "LOW", "MEDIUM", "HIGH", "CONFIRMED"]baddns.custom_nameservers: list[str]deps.behavior: Literal["abort_on_failure", "retry_failed", "ignore_failed", "disable", "force_install"]Composite schema, single-pass validation
ModuleLoader.validation_schemabuilds a composite pydantic model on demand:A single
model_validate()call catches typos at every layer. The schema rebuilds when new module dirs are discovered (chicken-and-egg withmodule_dirsis auto-resolved —validate_presetpreloads any custom dirs declared in the preset before validating).Module Config classes captured via AST + exec
Preload still doesn't import modules (so
bbot -lworks on hosts missing module deps). Theclass Configblock is captured viaast.get_source_segment, thenexec'd at schema-build time in a controlled namespace (Field,BaseModuleConfig,typing.*). Pydantic does the rest. No hand-rolled type whitelist, no annotation-string parsing — anything pydantic understands works (Literal,Union,list[str], etc.).Cleanup
BBOTArgs.exclude_from_validationregex anduniversal_module_optionsdict deleted;BaseModuleConfigcovers their job structurally.BBOTArgs.validate()body shrank from ~12 lines of dotted-path lookup to a 4-linevalidate_preset(...)delegation. CLI typos surface through the same code path that handles preset YAML.bbot/scripts/docs.pyreads universal-option descriptions fromBaseModuleConfig.model_fieldsdirectly — no separate constant to keep in sync.omegaconf-specific test helpers and assertions (OmegaConf.merge,omegaconf.errors.ReadonlyConfigError, dot-attribute access) replaced.Breaking changes
nuclei.moderejects unknown values at validation time, not at scan startup with a warning.modlues,flgas, etc.) raiseValidationErrorinstead of being silently ignored.modules:/output_modules:/exclude_modules:raise with a closest-match suggestion.self.optionsis no longer populated for migrated modules. Modules that read user-supplied values must useself.config.get(...). (This caught two pre-existing bugs inbbot/modules/templates/gitlab.pyandbbot/modules/lightfuzz/lightfuzz.pywhere module behavior was relying onself.optionsinstead ofself.config— both fixed in this PR.)${env:FOO}resolver inside YAML values is gone. Pydantic-settings' native env handling (BBOT_*prefix) is available for whole-field overrides.Files of interest
bbot/core/config/models.py—BBOTConfig,PresetSchema,BaseModuleConfig, sub-models. Schema only — defaults live indefaults.yml.bbot/core/config/merge.py—deep_update,dotted_get/dotted_set. ReplacesOmegaConf.merge/select/from_cli.bbot/core/modules.py—_extract_pydantic_config,_exec_config_class,_build_validation_schema,ModuleLoader.validation_schema.bbot/scanner/preset/validate.py—validate_preset,validate_preset_file,PresetValidationError. Single-pass aggregator with closest-match suggestions.bbot/scripts/migrate_options_to_config.py— codemod that performed the module migration. Idempotent; included for reproducibility.Test plan
test_step_1config/preset/cli/validate tests pass (39/39 in the touched set).gitlab_onprem,virustotal,sslcert,robots,crt,httpx, etc.).bbot --help,bbot -l,bbot -lp,bbot --current-presetall work and produce the same shape of output as before.validate_preset({…})round-trips: known-good presets return[]; presets with seeded typos produce specific, labeled errors.module_dirsdeclared in a preset get preloaded before per-module validation, so user modules aren't falsely flagged.