first commit
This commit is contained in:
606
HYDROGEN_CONFIGURATION.md
Normal file
606
HYDROGEN_CONFIGURATION.md
Normal file
@@ -0,0 +1,606 @@
|
||||
# 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 to `config.yaml` in the current working directory.
|
||||
- `--path` (`-p`) defaults to the `modules/` directory in the current working directory.
|
||||
- `--package-prefix` defaults to `modules`.
|
||||
|
||||
The configuration file is loaded by `config.py`:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```yaml
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```yaml
|
||||
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_mode` is `true`, Hydrogen returns exit code `1`.
|
||||
- Otherwise, Hydrogen returns exit code `0`.
|
||||
|
||||
Relevant code:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
- `low`
|
||||
- `medium`
|
||||
- `high`
|
||||
- `critical`
|
||||
|
||||
This field is also used in `core/evaluation.py`.
|
||||
|
||||
Hydrogen maps severities to numeric weights:
|
||||
|
||||
- `low` -> `1`
|
||||
- `medium` -> `2`
|
||||
- `high` -> `3`
|
||||
- `critical` -> `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: medium` means Hydrogen tolerates only findings below `medium`.
|
||||
- In practice, `medium`, `high`, and `critical` findings can trigger exit code `1` when `strict_mode: true`.
|
||||
- Only `low` findings are below that threshold.
|
||||
|
||||
Examples:
|
||||
|
||||
```yaml
|
||||
allow_failures_below: high
|
||||
```
|
||||
|
||||
Meaning:
|
||||
|
||||
- `low` and `medium` findings are tolerated.
|
||||
- `high` and `critical` findings can fail the run when `strict_mode` is enabled.
|
||||
|
||||
```yaml
|
||||
allow_failures_below: critical
|
||||
```
|
||||
|
||||
Meaning:
|
||||
|
||||
- Only `critical` findings 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`:
|
||||
|
||||
```python
|
||||
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.
|
||||
|
||||
```python
|
||||
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.
|
||||
|
||||
```yaml
|
||||
logging:
|
||||
level: INFO
|
||||
output: stdout
|
||||
```
|
||||
|
||||
Fields:
|
||||
|
||||
- `level`: any standard Python logging level name (`DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`).
|
||||
- `output`: `stdout` or `stderr`.
|
||||
|
||||
### `plugin_packages`
|
||||
|
||||
Type: `PluginPackagesConfig`
|
||||
|
||||
This section controls where Hydrogen discovers renderers, transports, and modules.
|
||||
|
||||
Schema:
|
||||
|
||||
```yaml
|
||||
plugin_packages:
|
||||
renderers:
|
||||
- reporting.exporters
|
||||
transports:
|
||||
- reporting.transports
|
||||
modules:
|
||||
- mycompany.hydrogen_modules
|
||||
```
|
||||
|
||||
Fields:
|
||||
|
||||
- `renderers`: list of Python package names to scan for `BaseRenderer` subclasses.
|
||||
- `transports`: list of Python package names to scan for `BaseTransport` subclasses.
|
||||
- `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:
|
||||
|
||||
```python
|
||||
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.renderers` for renderers.
|
||||
- `hydrogen.transports` for transports.
|
||||
- `hydrogen.modules` for 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:
|
||||
|
||||
```yaml
|
||||
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`: a `PluginConfigRef` with a `type` field and optional extra fields.
|
||||
- `transport`: a `PluginConfigRef` with a `type` field and optional extra fields.
|
||||
|
||||
Hydrogen uses it in `reporting/service.py`:
|
||||
|
||||
```python
|
||||
def publish_many(self, report: AuditReport, outputs: list[ResolvedReportOutput]) -> list[str]:
|
||||
return [self.publish(report, output) for output in outputs]
|
||||
```
|
||||
|
||||
The `PluginConfigRef` model:
|
||||
|
||||
```python
|
||||
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 `type` field selects which renderer/transport is used.
|
||||
- All other fields (the "payload") are passed to the plugin's `config_model` for validation.
|
||||
- Hydrogen validates the payload using each plugin's own `config_model` Pydantic 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`:
|
||||
|
||||
```python
|
||||
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 `PluginRuntimeError` and continues with other modules.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
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
|
||||
|
||||
```yaml
|
||||
outputs:
|
||||
- renderer:
|
||||
type: json
|
||||
```
|
||||
|
||||
Fields:
|
||||
|
||||
- `type`: must match a registered renderer's `content_type` or one of its `aliases`.
|
||||
|
||||
Extra fields are passed to the renderer's `config_model` for validation.
|
||||
|
||||
### Transport Config Ref
|
||||
|
||||
```yaml
|
||||
outputs:
|
||||
- transport:
|
||||
type: file
|
||||
path: reports/security-report
|
||||
append_extension: true
|
||||
```
|
||||
|
||||
Fields:
|
||||
|
||||
- `type`: must match a registered transport's `transport_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
|
||||
|
||||
```yaml
|
||||
outputs:
|
||||
- renderer:
|
||||
type: json
|
||||
transport:
|
||||
type: file
|
||||
path: reports/security-report
|
||||
append_extension: true
|
||||
```
|
||||
|
||||
Fields:
|
||||
|
||||
- `type`: must be `file`
|
||||
- `path`: output file path
|
||||
- `append_extension`: when `true`, Hydrogen appends the renderer's extension if the path has no suffix
|
||||
|
||||
Example result with the JSON renderer:
|
||||
|
||||
- `reports/security-report` becomes `reports/security-report.json`
|
||||
|
||||
### Example: Webhook Transport
|
||||
|
||||
```yaml
|
||||
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 be `webhook`
|
||||
- `url`: validated as an HTTP or HTTPS URL by Pydantic
|
||||
- `method`: `POST`, `PUT`, or `PATCH`
|
||||
- `headers`: arbitrary string-to-string HTTP headers
|
||||
- `timeout_seconds`: positive float
|
||||
- `payload_mode`: `rendered` or `envelope`
|
||||
|
||||
Payload behavior:
|
||||
|
||||
- `rendered`: Hydrogen sends the renderer output directly with the renderer's `media_type` as `Content-Type`.
|
||||
- `envelope`: Hydrogen sends a JSON object containing format metadata and the rendered content with `Content-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 renderers
|
||||
- `hydrogen.transports` — for custom transports
|
||||
- `hydrogen.modules` — for custom security modules
|
||||
|
||||
Example `pyproject.toml` for a third-party renderer:
|
||||
|
||||
```toml
|
||||
[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
|
||||
|
||||
```yaml
|
||||
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
|
||||
|
||||
```yaml
|
||||
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.type` raise an error when `RendererRegistry` cannot find that name.
|
||||
- unknown transport type names raise an error when `TransportRegistry` cannot find that name.
|
||||
- plugin config validation errors (when a plugin's `config_model` rejects the provided payload) raise errors during config resolution in `core/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.
|
||||
|
||||
```yaml
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```python
|
||||
@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`.
|
||||
Reference in New Issue
Block a user