diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c694525c..162ee866f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -107,6 +107,11 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Security +- **PyYAML upgraded to >=6.0.3 to address known security vulnerability** (#9055): + Added an explicit `pyyaml>=6.0.3` dependency constraint to `pyproject.toml` to + prevent installation of vulnerable older versions with known YAML parsing security + issues. + - **aiohttp upgraded to >=3.13.4 to remediate CVE-2026-34513 and CVE-2026-34515** (#1549, #1544): Added an explicit `aiohttp>=3.13.4` dependency constraint to `pyproject.toml` to remediate two high-severity open redirect vulnerabilities. Both CVEs affect the CleverAgents platform's diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 358d21b58..3a60bf737 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -35,4 +35,5 @@ Below are some of the specific details of various contributions. * HAL 9000 has contributed the ACMS Index Data Model and File Traversal Engine (PR #9664 / issue #9579): foundational data structures for indexed context entries with hot/warm/cold/archive storage tier classification, tag system, and a timeout-safe chunked file traversal engine for large projects with 10,000+ files. * HAL 9000 has contributed the error-suppression removal fix (PR #9247 / issue #9060): removed both `try...except Exception:` blocks in `register_registry_agents()` that silently suppressed errors from `actor_registry.list_actors()` and the route bridge refresh, enabling exceptions to propagate per CONTRIBUTING.md fail-fast policy. Added three Behave scenarios verifying RuntimeError, AttributeError, and TypeError propagation. * HAL 9000 has contributed the Strategize phase full context snapshot fix (issue #9056): added `_build_strategize_context_snapshot()` helper to `PlanLifecycleService`, updated `_try_record_decision()` to accept and forward a `ContextSnapshot` parameter, and added BDD test coverage verifying all four `ContextSnapshot` fields (`hot_context_hash`, `hot_context_ref`, `actor_state_ref`, `relevant_resources`) are populated during the Strategize phase. -* HAL 9000 has contributed the ACMS context path matching fix (PR #10975 / issue #10972): corrects `_path_matches()` and `_matches_pattern()` to properly match absolute fragment paths against relative glob patterns by auto-prefixing with `**/` before calling `PurePath.full_match()`, preventing silent inefficacy of include/exclude filters for absolute paths in fragment metadata. +* HAL 9000 has contributed the PyYAML security upgrade (PR #9244 / issue #9055): added `pyyaml>=6.0.3` dependency constraint to address known YAML parsing vulnerabilities. + * HAL 9000 has contributed the ACMS context path matching fix (PR #10975 / issue #10972): corrects `_path_matches()` and `_matches_pattern()` to properly match absolute fragment paths against relative glob patterns by auto-prefixing with `**/` before calling `PurePath.full_match()`, preventing silent inefficacy of include/exclude filters for absolute paths in fragment metadata. diff --git a/features/security_pyyaml_dependency.feature b/features/security_pyyaml_dependency.feature new file mode 100644 index 000000000..985f286b3 --- /dev/null +++ b/features/security_pyyaml_dependency.feature @@ -0,0 +1,23 @@ +@tdd_issue @tdd_issue_9055 +Feature: PyYAML is a declared project dependency with minimum secure version + As a CleverAgents developer + I want the PyYAML dependency to be listed in pyproject.toml with a minimum secure version + So that vulnerable older versions of PyYAML cannot be installed + + Background: + Given the pyproject.toml file exists at "pyproject.toml" + + @tdd_issue @tdd_issue_9055 + Scenario: PyYAML is listed in project dependencies with minimum version constraint + When I read the project dependencies from pyproject.toml + Then the dependency list should include a package matching "pyyaml>=" + + @tdd_issue @tdd_issue_9055 + Scenario: PyYAML minimum version is >=6.0.3 + When I read the PyYAML dependency specification from pyproject.toml + Then the minimum version should be at least "6.0.3" + + @tdd_issue @tdd_issue_9055 + Scenario: yaml module is importable as a project dependency + When I attempt to import the "yaml" module + Then the import should succeed without errors diff --git a/features/steps/security_pyyaml_dependency_steps.py b/features/steps/security_pyyaml_dependency_steps.py new file mode 100644 index 000000000..ef9e98579 --- /dev/null +++ b/features/steps/security_pyyaml_dependency_steps.py @@ -0,0 +1,132 @@ +"""Step definitions for PyYAML security dependency verification (#9055).""" + +from __future__ import annotations + +import importlib +import re +from pathlib import Path +from typing import Any + +from behave import given, then, when +from behave.runner import Context + + +@given('the pyproject.toml file exists at "{path}"') +def step_pyproject_exists(context: Context, path: str) -> None: + """Verify pyproject.toml exists at the given path.""" + pyproject_path = Path(path) + assert pyproject_path.exists(), f"pyproject.toml not found at {path}" + context.pyproject_path = pyproject_path + + +@when("I read the project dependencies from pyproject.toml") +def step_read_project_dependencies(context: Context) -> None: + """Read the [project.dependencies] list from pyproject.toml.""" + content = context.pyproject_path.read_bytes() + data: dict[str, Any] = _load_toml(content) + context.project_dependencies = data.get("project", {}).get("dependencies", []) + + +@then('the dependency list should include a package matching "{pattern}"') +def step_dependency_matches_pattern(context: Context, pattern: str) -> None: + """Assert that a dependency in the project dependencies matches the given regex pattern.""" + deps: list[str] = context.project_dependencies + found = any(re.search(pattern, dep) for dep in deps) + assert found, ( + f"No dependency matching '{pattern}' found in " + f"[project.dependencies]. Current deps: {deps}" + ) + + +@when("I read the PyYAML dependency specification from pyproject.toml") +def step_read_pyyaml_dependency(context: Context) -> None: + """Read and parse the PyYAML dependency from pyproject.toml.""" + content = context.pyproject_path.read_bytes() + data: dict[str, Any] = _load_toml(content) + deps: list[str] = data.get("project", {}).get("dependencies", []) + + # Find the pyyaml dependency entry + pyyaml_dep = None + for dep in deps: + if dep.strip().startswith("pyyaml"): + pyyaml_dep = dep.strip() + break + + assert pyyaml_dep is not None, ( + f"PyYAML dependency not found in [project.dependencies]. Current deps: {deps}" + ) + + # Parse the minimum version from the constraint + # Examples: "pyyaml>=6.0.3", "pyyaml>=6.0", "pyyaml==6.0.2" + match = re.search(r"(?:>=|==|~>)([\d]+(?:\.[\d]+)*(?:\.\w+)*)", pyyaml_dep) + assert match is not None, f"Could not parse version from PyYAML dependency: {pyyaml_dep}" + + context.pyyaml_version_spec = pyyaml_dep + context.pyyaml_min_version = match.group(1) + + +@then("the minimum version should be at least \"{expected}\"") +def step_pyyaml_minimum_version(context: Context, expected: str) -> None: + """Assert that the PyYAML minimum version is >= expected version.""" + min_version = context.pyyaml_min_version + assert min_version == expected, ( + f"PyYAML minimum version {min_version} does not match expected {expected}. " + f"Dependency spec: {context.pyyaml_version_spec}" + ) + + +@when('I attempt to import the "{module_name}" module') +def step_attempt_import(context: Context, module_name: str) -> None: + """Attempt to import the given module and record the result.""" + try: + context.imported_module = importlib.import_module(module_name) + context.import_error = None + except ImportError as exc: + context.imported_module = None + context.import_error = exc + + +@then("the import should succeed without errors") +def step_import_succeeded(context: Context) -> None: + """Assert that the previous import attempt succeeded.""" + assert context.import_error is None, f"Import failed with: {context.import_error}" + assert context.imported_module is not None, "Module was not imported" + + +@then('the version constraint in uv.lock should include specifier "{specifier}"') +def step_uv_lock_specifier(context: Context, specifier: str) -> None: + """Assert that pyyaml appears in both package.dependencies and requires-dist in uv.lock.""" + lock_path = context.pyproject_path.parent / "uv.lock" + content = lock_path.read_bytes().decode("utf-8") + + # Check that pyyaml is in the cleveragents package dependencies section + assert ( + '[[package]]\nname = "cleveragents"' in content + ), "cleveragents package section not found in uv.lock" + + # Verify pyyaml entry exists in file after cleveragents section + cleveragents_match = re.search( + r'\[\[package\]\]\s*name = "cleveragents"(.*?)^\[\[package\]\]', + content, + re.DOTALL | re.MULTILINE, + ) + assert cleveragents_match, "Could not find cleveragents package section" + + cleveragents_block = cleveragents_match.group(1) + + assert '{ name = "pyyaml" }' in cleveragents_block, ( + f"pyyaml not found in cleveragents package dependencies in uv.lock" + ) + + +def _load_toml(content: bytes) -> dict[str, Any]: + """Load a TOML file from bytes. Uses tomllib if available, else fallback.""" + try: + import tomllib + + return tomllib.loads(content.decode("utf-8")) + except ImportError: + # Fallback for older Python versions without tomllib + import tomlkit + + return tomlkit.parse(content.decode("utf-8")).unwrap() # type: ignore[no-any-return] diff --git a/pyproject.toml b/pyproject.toml index c1fdf4a71..f711440c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ dependencies = [ "tomlkit>=0.13.0", # TOML writing with comment preservation for config CLI "tenacity>=8.2.0", # Retry framework for service layer resilience "aiohttp>=3.13.4", # CVE-2026-34515 mitigation: open redirect vulnerability + "pyyaml>=6.0.3", # Security: address known YAML parsing vulnerabilities "a2a-sdk>=0.3.0,<1.0.0", # A2A Python SDK — required transport for local (stdio) and server (HTTP) modes (ADR-047); pinned <1.0.0 (removed legacy A2AClient) ] diff --git a/uv.lock b/uv.lock index 31d88e524..c0ed72e3f 100644 --- a/uv.lock +++ b/uv.lock @@ -446,6 +446,7 @@ dependencies = [ { name = "pydantic" }, { name = "pydantic-settings" }, { name = "python-ulid" }, + { name = "pyyaml" }, { name = "restrictedpython" }, { name = "rx" }, { name = "structlog" }, @@ -528,6 +529,7 @@ requires-dist = [ { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" }, + { name = "pyyaml", specifier = ">=6.0.3" }, { name = "python-ulid", specifier = ">=2.7.0" }, { name = "radon", marker = "extra == 'dev'", specifier = ">=6.0.1" }, { name = "restrictedpython", specifier = ">=7.0" },