021d09991a
Three step patterns in pyyaml_security_steps.py clashed with existing step files, causing all Behave features to error at load time: - "I call load_yaml_text with YAML text" clashed with actor_config_coverage_boost_steps.py:103 - "the load_yaml_text result should have key" clashed with actor_config_coverage_boost_steps.py:90 - "a ValueError should be raised" clashed with lsp_registry_steps.py:475 Rename all three to unique patterns and update pyyaml_security.feature to match. Also fix typings/behave/runner.pyi ruff format (.pyi convention: single blank line before class, no blank lines between stub methods) and add missing fastapi>=0.100.0 to pyproject.toml (asgi_app.py imports fastapi but it was absent from declared dependencies, causing typecheck and integration test failures). Refs: #9055
101 lines
3.9 KiB
Python
101 lines
3.9 KiB
Python
"""Step definitions for pyyaml_security.feature.
|
|
|
|
Verifies that PyYAML is pinned to a secure version (>=6.0.3) and that
|
|
all YAML loading in the codebase uses safe_load to prevent arbitrary
|
|
code execution (CVE-2017-18342 mitigation, issue #9055).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from importlib.metadata import version as pkg_version
|
|
from typing import Any
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
|
|
from cleveragents.actor.yaml_loader import load_yaml_text
|
|
|
|
|
|
def _version_tuple(version_str: str) -> tuple[int, ...]:
|
|
"""Convert a version string like '6.0.3' to a comparable tuple."""
|
|
return tuple(int(part) for part in version_str.split(".")[:3])
|
|
|
|
|
|
# ── PyYAML version constraint ────────────────────────────────────────
|
|
|
|
|
|
@given("the PyYAML package is installed")
|
|
def step_pyyaml_installed(context: Context) -> None:
|
|
"""Verify PyYAML is importable and its version is accessible."""
|
|
import yaml # noqa: F401 — presence check
|
|
|
|
context.pyyaml_version = pkg_version("pyyaml")
|
|
|
|
|
|
@when("I check the installed PyYAML version")
|
|
def step_check_pyyaml_version(context: Context) -> None:
|
|
"""Record the installed PyYAML version tuple for assertion."""
|
|
context.pyyaml_version_tuple = _version_tuple(context.pyyaml_version)
|
|
|
|
|
|
@then("the version should be at least 6.0.3")
|
|
def step_assert_pyyaml_version(context: Context) -> None:
|
|
"""Assert that the installed PyYAML version is >= 6.0.3."""
|
|
minimum = (6, 0, 3)
|
|
assert context.pyyaml_version_tuple >= minimum, (
|
|
f"PyYAML version {context.pyyaml_version} is below the required "
|
|
f"minimum 6.0.3. Upgrade PyYAML to address CVE-2017-18342."
|
|
)
|
|
|
|
|
|
# ── yaml_loader safe_load enforcement ───────────────────────────────
|
|
|
|
|
|
@when('I load the YAML text "{yaml_text}" using the secure yaml_loader')
|
|
def step_load_yaml_text(context: Context, yaml_text: str) -> None:
|
|
"""Call load_yaml_text with the given YAML text and store the result.
|
|
|
|
The YAML text is passed as a quoted string in the step, with ``\\n``
|
|
escape sequences representing literal newlines.
|
|
"""
|
|
normalised = yaml_text.replace("\\n", "\n")
|
|
context.load_yaml_result: dict[str, Any] = load_yaml_text(normalised)
|
|
|
|
|
|
@then('the secure yaml_loader result for key "{key}" should be "{value}"')
|
|
def step_assert_yaml_result_key(context: Context, key: str, value: str) -> None:
|
|
"""Assert that the parsed YAML result contains the expected key/value pair."""
|
|
result: dict[str, Any] = context.load_yaml_result
|
|
assert key in result, (
|
|
f"Expected key '{key}' in load_yaml_text result, but it was not found. "
|
|
f"Actual keys: {list(result.keys())}"
|
|
)
|
|
assert result[key] == value, (
|
|
f"Expected result['{key}'] == '{value}', but got '{result[key]}'."
|
|
)
|
|
|
|
|
|
@when("I call load_yaml_text with unsafe YAML containing a Python object tag")
|
|
def step_load_unsafe_yaml(context: Context) -> None:
|
|
"""Attempt to load YAML with a Python object constructor tag.
|
|
|
|
PyYAML's safe_load rejects ``!!python/object`` tags, raising an error.
|
|
This verifies that the yaml_loader does not use the unsafe ``yaml.load``
|
|
with the default Loader (which would execute arbitrary Python code).
|
|
"""
|
|
context.caught_error = None
|
|
unsafe_yaml = "!!python/object/apply:os.system ['echo pwned']"
|
|
try:
|
|
load_yaml_text(unsafe_yaml)
|
|
except Exception as exc:
|
|
context.caught_error = exc
|
|
|
|
|
|
@then("a pyyaml security ValueError should be raised")
|
|
def step_assert_value_error_raised(context: Context) -> None:
|
|
"""Assert that an error was raised when loading unsafe YAML."""
|
|
assert context.caught_error is not None, (
|
|
"Expected an error when loading YAML with Python object tags, "
|
|
"but none was raised. This may indicate unsafe YAML loading is in use."
|
|
)
|