15 KiB
Hydrogen Configuration Guide
This document explains how configuration works in Hydrogen, which settings are actually used at runtime, and how configuration connects to module loading, plugin discovery, and report delivery.
Configuration Entry Points
Hydrogen starts in main.py.
--config(-c) defaults toconfig.yamlin the current working directory.--path(-p) defaults to themodules/directory in the current working directory.--package-prefixdefaults tomodules.
The configuration file is loaded by config.py:
def load_config(path: Path) -> config.Config:
with path.open(encoding="utf-8") as f:
content = yaml.safe_load(f)
return config.Config.model_validate(content)
Hydrogen validates the YAML against the Pydantic model core.schemas.config.Config before the audit starts.
Full Configuration Shape
Hydrogen currently expects this top-level structure:
exclude_categories: []
fail_fast: false
dry_run: false
max_concurrency: 4
logging:
level: INFO
output: stdout
plugin_packages:
renderers:
- reporting.exporters
transports:
- reporting.transports
modules: []
strict_mode: true
allow_failures_below: medium
reports:
outputs:
- renderer:
type: json
transport:
type: file
path: reports/latest
append_extension: true
modules:
ssh:
enabled: true
The schema is defined in core/schemas/config.py.
Top-Level Fields
exclude_categories
Type: list[str]
Default: []
This field is actively used in core/module_loader.py.
Hydrogen compares each module manifest's category value against this list:
if module.manifest.category in config.exclude_categories:
logger.info("module %s was skipped due to config exclusion", module.manifest.identifier)
continue
Important details:
- Exclusion is based on
manifest.category, not on the directory name. - Exclusion happens before
build_worker()is called. - If multiple modules share the same category, one category entry disables all of them.
Example:
exclude_categories:
- ssh
- tls
strict_mode
Type: bool
This field is used in core/evaluation.py to determine the final process exit code.
Behavior:
- If a finding reaches the configured severity threshold and
strict_modeistrue, Hydrogen returns exit code1. - Otherwise, Hydrogen returns exit code
0.
Relevant code:
if threshold_crossed and config.strict_mode:
return 1
return 0
This is the main switch controlling whether Hydrogen behaves as a CI gate.
allow_failures_below
Type: AuditSeverity
Allowed values:
lowmediumhighcritical
This field is also used in core/evaluation.py.
Hydrogen maps severities to numeric weights:
low->1medium->2high->3critical->4
Then it fails the run when any finding has a weight greater than or equal to the configured threshold.
How to read the field name correctly:
allow_failures_below: mediummeans Hydrogen tolerates only findings belowmedium.- In practice,
medium,high, andcriticalfindings can trigger exit code1whenstrict_mode: true. - Only
lowfindings are below that threshold.
Examples:
allow_failures_below: high
Meaning:
lowandmediumfindings are tolerated.highandcriticalfindings can fail the run whenstrict_modeis enabled.
allow_failures_below: critical
Meaning:
- Only
criticalfindings can fail the run.
fail_fast
Type: bool
Default: false
When true, Hydrogen stops submitting new worker tasks as soon as any module returns a fail status. Already-running workers are allowed to finish.
This is used in core/runner.py:
if config.fail_fast and result.result.status is AuditStatus.FAIL:
stop_submitting = True
dry_run
Type: bool
Default: false
When true, Hydrogen runs all security modules and evaluates exit codes but skips report publishing. Useful for testing module behavior without side effects.
if resolved_config.dry_run:
logger.info("dry_run enabled, skipping report publish")
max_concurrency
Type: int
Default: 4
Controls the maximum number of worker threads in the ThreadPoolExecutor. Hydrogen runs module workers concurrently up to this limit.
logging
Type: object
Controls logging behavior.
logging:
level: INFO
output: stdout
Fields:
level: any standard Python logging level name (DEBUG,INFO,WARNING,ERROR,CRITICAL).output:stdoutorstderr.
plugin_packages
Type: PluginPackagesConfig
This section controls where Hydrogen discovers renderers, transports, and modules.
Schema:
plugin_packages:
renderers:
- reporting.exporters
transports:
- reporting.transports
modules:
- mycompany.hydrogen_modules
Fields:
renderers: list of Python package names to scan forBaseRenderersubclasses.transports: list of Python package names to scan forBaseTransportsubclasses.modules: list of Python package names to scan for modules (in addition to filesystem discovery).
Hydrogen uses reporting/bootstrap.py and core/plugin_types.py to discover plugins:
def discover_plugins(group_config: PluginGroupConfig, base_class: type[TPlugin]):
for package_name in group_config.package_names:
for plugin_class in _discover_plugin_classes(package_name, base_class):
descriptors.append(PluginDescriptor(plugin=plugin_class(), source=...))
Additionally, Hydrogen discovers plugins via Python entry_points under the following groups:
hydrogen.renderersfor renderers.hydrogen.transportsfor transports.hydrogen.modulesfor modules.
This allows third-party packages installed via pip to register plugins without manual configuration.
reports
Type: ReportsConfig
This section controls report rendering and delivery. Unlike the old single-output schema, Hydrogen now supports multiple report outputs.
Schema:
reports:
outputs:
- renderer:
type: json
transport:
type: file
path: reports/audit
append_extension: true
- renderer:
type: json
transport:
type: webhook
url: https://collector.example.com/hydrogen
method: POST
payload_mode: envelope
Each output contains:
renderer: aPluginConfigRefwith atypefield and optional extra fields.transport: aPluginConfigRefwith atypefield and optional extra fields.
Hydrogen uses it in reporting/service.py:
def publish_many(self, report: AuditReport, outputs: list[ResolvedReportOutput]) -> list[str]:
return [self.publish(report, output) for output in outputs]
The PluginConfigRef model:
class PluginConfigRef(BaseModel):
model_config = ConfigDict(extra="allow")
type: str = Field(min_length=1)
def payload(self) -> dict[str, Any]:
return dict(self.model_extra or {})
This means:
- The
typefield selects which renderer/transport is used. - All other fields (the "payload") are passed to the plugin's
config_modelfor validation. - Hydrogen validates the payload using each plugin's own
config_modelPydantic model.
modules
Type: dict[str, dict[str, Any]]
Default: {}
This section stores per-module configuration.
Hydrogen looks up module settings by manifest identifier and validates them against the module's CONFIG_MODEL:
module_config = config.modules.get(module.manifest.identifier, {})
validated_config = module.config_model.model_validate(module_config)
worker = build_worker(module, validated_config)
Important implications:
- The key must match
MANIFEST.identifier, not the human-readable module name. - If the key is missing, your module receives an empty dict, which is then validated by the module's
config_model. - Module configuration is validated at runtime by the module's own Pydantic config model.
- If validation fails, Hydrogen reports a
PluginRuntimeErrorand continues with other modules.
Example:
modules:
ssh:
enabled: true
test-failure: false
Report Output Configuration
Each output in reports.outputs defines one renderer + transport pair. Hydrogen sends the rendered report through each transport.
Renderer Config Ref
outputs:
- renderer:
type: json
Fields:
type: must match a registered renderer'scontent_typeor one of itsaliases.
Extra fields are passed to the renderer's config_model for validation.
Transport Config Ref
outputs:
- transport:
type: file
path: reports/security-report
append_extension: true
Fields:
type: must match a registered transport'stransport_type.- Additional fields are plugin-specific (e.g.,
path,url,method,headers).
Extra fields are validated against the transport's config_model.
Example: File Transport
outputs:
- renderer:
type: json
transport:
type: file
path: reports/security-report
append_extension: true
Fields:
type: must befilepath: output file pathappend_extension: whentrue, Hydrogen appends the renderer's extension if the path has no suffix
Example result with the JSON renderer:
reports/security-reportbecomesreports/security-report.json
Example: Webhook Transport
outputs:
- renderer:
type: json
transport:
type: webhook
url: https://example.internal/security-ingest
method: POST
headers:
Authorization: Bearer secret-token
timeout_seconds: 10
payload_mode: envelope
Fields:
type: must bewebhookurl: validated as an HTTP or HTTPS URL by Pydanticmethod:POST,PUT, orPATCHheaders: arbitrary string-to-string HTTP headerstimeout_seconds: positive floatpayload_mode:renderedorenvelope
Payload behavior:
rendered: Hydrogen sends the renderer output directly with the renderer'smedia_typeasContent-Type.envelope: Hydrogen sends a JSON object containing format metadata and the rendered content withContent-Type: application/json.
Plugin Discovery via Entry Points
Hydrogen supports discovering plugins via Python package entry points. This is the recommended way to distribute third-party plugins.
Entry point groups:
hydrogen.renderers— for custom renderershydrogen.transports— for custom transportshydrogen.modules— for custom security modules
Example pyproject.toml for a third-party renderer:
[project.entry-points."hydrogen.renderers"]
markdown = "my_package.renderers:MarkdownRenderer"
Entry points can point to either a class (which Hydrogen instantiates) or a pre-built instance. Hydrogen validates that the result is a non-abstract subclass or instance of the expected base class.
Example Production-Oriented Configurations
Save a JSON Report to Disk
fail_fast: false
dry_run: false
exclude_categories: []
logging:
level: INFO
output: stdout
strict_mode: true
allow_failures_below: high
reports:
outputs:
- renderer:
type: json
transport:
type: file
path: reports/hydrogen-audit
append_extension: true
modules:
ssh:
test-failure: false
Send the Report to Multiple Destinations
fail_fast: false
dry_run: false
exclude_categories:
- experimental
logging:
level: DEBUG
output: stdout
strict_mode: true
allow_failures_below: medium
reports:
outputs:
- renderer:
type: json
transport:
type: file
path: reports/hydrogen-audit
append_extension: true
- renderer:
type: json
transport:
type: webhook
url: https://collector.example.com/hydrogen/report
method: POST
headers:
X-Source: hydrogen
timeout_seconds: 5
payload_mode: envelope
modules:
ssh:
test-failure: false
Validation and Failure Modes
Because Hydrogen validates configuration with Pydantic before execution, the following failures happen early:
- invalid enum values such as
allow_failures_below: severe - invalid logging output values
- missing required fields such as
reports.outputs
Other failures happen later at runtime:
- unknown renderer names in
renderer.typeraise an error whenRendererRegistrycannot find that name. - unknown transport type names raise an error when
TransportRegistrycannot find that name. - plugin config validation errors (when a plugin's
config_modelrejects the provided payload) raise errors during config resolution incore/runtime.py. - module import failures are not swallowed and can stop execution.
Multi-Output Architecture
Hydrogen supports multiple report outputs per run. Each output defines an independent renderer + transport pair.
reports:
outputs:
- renderer:
type: json
transport:
type: file
path: reports/audit.json
- renderer:
type: markdown
transport:
type: webhook
url: https://wiki.internal/ingest
method: POST
Hydrogen iterates through all outputs and publishes each one:
def publish_many(self, report: AuditReport, outputs: list[ResolvedReportOutput]) -> list[str]:
return [self.publish(report, output) for output in outputs]
If an output fails, Hydrogen collects the error in plugin_errors and continues with the next output.
Relationship Between --path and --package-prefix
This is not part of YAML, but it matters when you organize Hydrogen modules.
Hydrogen discovers modules from the filesystem path passed to --path, but imports them by Python package name using --package-prefix.
Example:
python main.py --path custom_modules --package-prefix custom_modules
For this to work:
custom_modules/must exist.- Each module must be a package directory with its own
__init__.py. - Python must be able to import
custom_modules.<module_name>.
Additionally, modules can come from plugin_packages.modules in YAML and from hydrogen.modules entry points. All sources are combined in discover_modules() with deduplication.
Internal Runtime Config
After loading and validation, Hydrogen builds a ResolvedConfig dataclass:
@dataclass(frozen=True)
class ResolvedConfig:
exclude_categories: list[str]
allow_failures_below: AuditSeverity
strict_mode: bool
fail_fast: bool
dry_run: bool
max_concurrency: int
logging: LoggingConfig
plugin_packages: PluginPackagesConfig
reports: list[ResolvedReportOutput]
modules: dict[str, BaseModel]
This is the resolved internal config that modules and reporting use at runtime. Each module's config is already validated into its typed config_model.