Skip to content
Open
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
12 changes: 12 additions & 0 deletions .pyrit_conf_example
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,18 @@ max_concurrent_scenario_runs: 3
# Default: false
allow_custom_initializers: false

# Local Backend Server
# --------------------
# Client settings used by pyrit_scan when connecting to or launching a backend.
# - url: Backend URL used when --server-url is omitted.
# - startup_timeout: Seconds to wait for --start-server before cleaning up the
# spawned process and returning an error.
#
# Both settings can be overridden with --server-url and --startup-timeout.
server:
url: http://localhost:8000
startup_timeout: 120

# Silent Mode
# -----------
# If true, suppresses print statements during initialization.
Expand Down
22 changes: 22 additions & 0 deletions doc/getting_started/pyrit_conf.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,23 @@ env_files:

If `true`, suppresses print statements during initialization. Useful for non-interactive environments or when embedding PyRIT in other tools. Defaults to `false`.

### `server`

Client settings for connecting to or launching a PyRIT backend.

| Field | Description | Default |
|---|---|---|
| `url` | Backend URL used when `--server-url` is omitted | `http://localhost:8000` |
| `startup_timeout` | Seconds `pyrit_scan --start-server` waits for a healthy backend before terminating the spawned process | `120` |

`startup_timeout` must be a finite number greater than zero. The `--startup-timeout` CLI option overrides the configured value for an individual scanner invocation.

```yaml
server:
url: http://localhost:8000
startup_timeout: 120
```

## Configuration Precedence

PyRIT uses a 3-layer configuration precedence model. **Later layers override earlier ones:**
Expand Down Expand Up @@ -282,6 +299,11 @@ initializers:

# Suppress initialization messages
silent: false

# Backend connection and local startup settings
server:
url: http://localhost:8000
startup_timeout: 120
```

## What's Next?
Expand Down
89 changes: 62 additions & 27 deletions pyrit/cli/_config_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
"""
Lightweight config reader for the PyRIT CLI thin client.

Reads only the ``server.url`` field from ``~/.pyrit/.pyrit_conf`` (and an
optional overlay file) using ``yaml.safe_load``. No heavy pyrit imports.
Reads the ``server`` settings used by the client from ``~/.pyrit/.pyrit_conf``
(and an optional overlay file) using ``yaml.safe_load``. No heavy pyrit imports.
"""

from __future__ import annotations

import math
from dataclasses import dataclass
from pathlib import Path
from typing import Any

Expand All @@ -18,13 +20,22 @@
_DEFAULT_CONFIG_FILE = _DEFAULT_CONFIG_DIR / ".pyrit_conf"

DEFAULT_SERVER_URL = "http://localhost:8000"
DEFAULT_SERVER_STARTUP_TIMEOUT = 120.0


@dataclass(frozen=True)
class ServerSettings:
"""Client-side settings for connecting to or launching a backend server."""

url: str | None = None
startup_timeout: float = DEFAULT_SERVER_STARTUP_TIMEOUT


class ConfigError(Exception):
"""
Raised when a CLI config file exists but cannot be parsed or is structurally
invalid (e.g. malformed YAML, a non-mapping root, or a wrong-typed
``server.url``).
``server`` setting).

A *missing* config file or a *missing* field is not an error -- the CLI just
falls back to its defaults. This is reserved for configs the user clearly
Expand Down Expand Up @@ -78,6 +89,25 @@ def read_server_url(*, config_file: Path | None = None) -> str | None:
Returns:
str | None: The server URL, or ``None`` if not configured.

Raises:
ConfigError: If a config file exists but is malformed.
"""
return read_server_settings(config_file=config_file).url


def read_server_settings(*, config_file: Path | None = None) -> ServerSettings:
"""
Read client-side server settings from the default config and an optional overlay.

A later ``server`` block replaces the earlier block. Config files that omit
``server`` leave the prior settings unchanged.

Args:
config_file: Optional explicit config path.

Returns:
ServerSettings: The resolved URL and startup timeout.

Raises:
ConfigError: If a config file exists but is malformed.
"""
Expand All @@ -89,10 +119,13 @@ def read_server_url(*, config_file: Path | None = None) -> str | None:
if config_file is not None and config_file.exists():
paths.append(config_file)

url: str | None = None
settings = ServerSettings()
for p in paths:
url = _extract_server_url(path=p, yaml_module=yaml) or url
return url
data = _load_config_mapping(path=p, yaml_module=yaml)
if data is None or "server" not in data:
continue
settings = _extract_server_settings(data=data, path=p)
return settings


def validate_client_config(*, config_file: Path | None = None) -> None:
Expand Down Expand Up @@ -125,38 +158,40 @@ def validate_client_config(*, config_file: Path | None = None) -> None:
)


def _extract_server_url(*, path: Path, yaml_module: Any) -> str | None:
def _extract_server_settings(*, data: dict[str, Any], path: Path) -> ServerSettings:
"""
Extract ``server.url`` from a single YAML file.
Extract client settings from one parsed ``server`` block.

Args:
path (Path): YAML config file path.
yaml_module (Any): The imported ``yaml`` module (passed to avoid
top-level import).
data: Parsed top-level config mapping.
path: YAML config file path used in error messages.

Returns:
str | None: The URL string, or ``None`` if absent.
ServerSettings: Settings represented by this config layer.

Raises:
ConfigError: If the file is malformed, or ``server`` / ``server.url``
are present but have the wrong type.
ConfigError: If ``server`` or one of its supported fields has the wrong type.
"""
data = _load_config_mapping(path=path, yaml_module=yaml_module)
if data is None:
return None

server_block = data.get("server")
if server_block is None:
return None
return ServerSettings()
if not isinstance(server_block, dict):
raise ConfigError(
f"Config file {path}: 'server' must be a mapping with a 'url' field, got {type(server_block).__name__}."
)
raise ConfigError(f"Config file {path}: 'server' must be a mapping, got {type(server_block).__name__}.")

raw_url = server_block.get("url")
if raw_url is None:
return None
if not isinstance(raw_url, str):
if raw_url is not None and not isinstance(raw_url, str):
raise ConfigError(f"Config file {path}: 'server.url' must be a string, got {type(raw_url).__name__}.")

return raw_url.strip() or None
url: str | None = None
if isinstance(raw_url, str):
url = raw_url.strip() or None

startup_timeout = server_block.get("startup_timeout", DEFAULT_SERVER_STARTUP_TIMEOUT)
if (
isinstance(startup_timeout, bool)
or not isinstance(startup_timeout, int | float)
or not math.isfinite(startup_timeout)
or startup_timeout <= 0
):
raise ConfigError(f"Config file {path}: 'server.startup_timeout' must be a finite number greater than 0.")

return ServerSettings(url=url, startup_timeout=float(startup_timeout))
Loading
Loading