From 0eb8b7b2d4b19e2aa337d2055b88054422037b56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Tourri=C3=A8re?= Date: Thu, 19 Feb 2026 15:45:39 +0100 Subject: [PATCH 1/2] feat(plugin): add sigstore signature verification and forward signature_mode Add keyless signature verification for plugin wheels using sigstore bundles with OIDC identity-based trust. Replace --force flag with --allow-unsigned and enterprise config signature mode support. Forward signature_mode through all download paths (GitHub release, GitHub artifact, URL, local wheel). Collapse repetitive install error tests into parametrized ones. --- ggshield/__main__.py | 12 + ggshield/cmd/plugin/install.py | 108 +- ggshield/cmd/plugin/status.py | 8 +- ggshield/core/config/enterprise_config.py | 29 +- ggshield/core/plugin/client.py | 2 + ggshield/core/plugin/downloader.py | 139 ++- ggshield/core/plugin/loader.py | 40 +- ggshield/core/plugin/signature.py | 174 ++++ pyproject.toml | 7 +- tests/unit/cmd/plugin/test_install.py | 550 +++------- tests/unit/core/plugin/test_downloader.py | 104 +- .../core/plugin/test_enterprise_config.py | 18 +- tests/unit/core/plugin/test_loader.py | 5 +- tests/unit/core/plugin/test_signature.py | 285 ++++++ uv.lock | 950 +++++++++++++----- 15 files changed, 1651 insertions(+), 780 deletions(-) create mode 100644 ggshield/core/plugin/signature.py create mode 100644 tests/unit/core/plugin/test_signature.py diff --git a/ggshield/__main__.py b/ggshield/__main__.py index 0baf27b776..ed6ced3616 100644 --- a/ggshield/__main__.py +++ b/ggshield/__main__.py @@ -70,6 +70,15 @@ def _load_plugins() -> PluginRegistry: """Load plugins at module level so commands are available.""" global _plugin_registry if _plugin_registry is None: + # Suppress signature/loader loggers during startup to avoid noisy output + # before logging is configured + sig_logger = logging.getLogger("ggshield.core.plugin.signature") + loader_logger = logging.getLogger("ggshield.core.plugin.loader") + orig_sig_level = sig_logger.level + orig_loader_level = loader_logger.level + sig_logger.setLevel(logging.CRITICAL) + loader_logger.setLevel(logging.CRITICAL) + try: enterprise_config = EnterpriseConfig.load() plugin_loader = PluginLoader(enterprise_config) @@ -77,6 +86,9 @@ def _load_plugins() -> PluginRegistry: except Exception as e: _deferred_warnings.append(f"Failed to load plugins: {e}") _plugin_registry = PluginRegistry() + finally: + sig_logger.setLevel(orig_sig_level) + loader_logger.setLevel(orig_loader_level) # Make registry available to hooks module from ggshield.core.plugin.hooks import set_plugin_registry diff --git a/ggshield/cmd/plugin/install.py b/ggshield/cmd/plugin/install.py index 721eadfeff..1ebd3fd150 100644 --- a/ggshield/cmd/plugin/install.py +++ b/ggshield/cmd/plugin/install.py @@ -27,6 +27,10 @@ InsecureSourceError, PluginDownloader, ) +from ggshield.core.plugin.signature import ( + SignatureVerificationError, + SignatureVerificationMode, +) def detect_source_type(plugin_source: str) -> PluginSourceType: @@ -64,12 +68,6 @@ def detect_source_type(plugin_source: str) -> PluginSourceType: return PluginSourceType.GITGUARDIAN_API -def _display_unsigned_warning() -> None: - """Display warning about installing unsigned plugins.""" - ui.display_warning("This plugin is not from GitGuardian and has not been verified.") - ui.display_warning("Only install plugins from sources you trust.") - - @click.command() @click.argument("plugin_source") @click.option( @@ -85,10 +83,10 @@ def _display_unsigned_warning() -> None: help="Expected SHA256 checksum for URL verification", ) @click.option( - "--force", - "force", + "--allow-unsigned", + "allow_unsigned", is_flag=True, - help="Skip security warnings for non-GitGuardian sources", + help="Allow installing plugins without valid signatures (overrides strict mode)", ) @add_common_options() @click.pass_context @@ -97,7 +95,7 @@ def install_cmd( plugin_source: str, version: Optional[str], sha256: Optional[str], - force: bool, + allow_unsigned: bool, **kwargs: Any, ) -> None: """ @@ -127,24 +125,31 @@ def install_cmd( ggshield plugin install https://github.com/owner/repo/actions/runs/123/artifacts/456 """ + # Determine signature verification mode + enterprise_config = EnterpriseConfig.load() + signature_mode = enterprise_config.get_signature_mode() + if allow_unsigned: + signature_mode = SignatureVerificationMode.WARN + source_type = detect_source_type(plugin_source) if source_type == PluginSourceType.GITHUB_ARTIFACT: - _install_from_github_artifact(ctx, plugin_source, force) + _install_from_github_artifact(ctx, plugin_source, signature_mode) elif source_type == PluginSourceType.GITHUB_RELEASE: - _install_from_github_release(ctx, plugin_source, sha256, force) + _install_from_github_release(ctx, plugin_source, sha256, signature_mode) elif source_type == PluginSourceType.URL: - _install_from_url(ctx, plugin_source, sha256, force) + _install_from_url(ctx, plugin_source, sha256, signature_mode) elif source_type == PluginSourceType.LOCAL_FILE: - _install_from_local_wheel(ctx, plugin_source, force) + _install_from_local_wheel(ctx, plugin_source, signature_mode) else: - _install_from_gitguardian(ctx, plugin_source, version) + _install_from_gitguardian(ctx, plugin_source, version, signature_mode) def _install_from_gitguardian( ctx: click.Context, plugin_name: str, version: Optional[str], + signature_mode: SignatureVerificationMode = SignatureVerificationMode.STRICT, ) -> None: """Install a plugin from GitGuardian API.""" ctx_obj = ContextObj.get(ctx) @@ -192,7 +197,9 @@ def _install_from_gitguardian( ) # Download and install - downloader.download_and_install(download_info, plugin_name) + downloader.download_and_install( + download_info, plugin_name, signature_mode=signature_mode + ) # Enable in config enterprise_config.enable_plugin(plugin_name, version=download_info.version) @@ -202,6 +209,12 @@ def _install_from_gitguardian( ui.display_info(f"Installed {plugin_name} v{download_info.version}") + except SignatureVerificationError as e: + ui.display_error(f"Signature verification failed for {plugin_name}: {e}") + ui.display_info( + "Use --allow-unsigned to install without signature verification" + ) + ctx.exit(ExitCode.UNEXPECTED_ERROR) except PluginNotAvailableError as e: ui.display_error(f"Failed to install {plugin_name}: {e}") ctx.exit(ExitCode.UNEXPECTED_ERROR) @@ -216,7 +229,7 @@ def _install_from_gitguardian( def _install_from_local_wheel( ctx: click.Context, wheel_path_str: str, - force: bool, + signature_mode: SignatureVerificationMode = SignatureVerificationMode.STRICT, ) -> None: """Install a plugin from a local wheel file.""" wheel_path = Path(wheel_path_str) @@ -225,16 +238,15 @@ def _install_from_local_wheel( ui.display_error(f"Wheel file not found: {wheel_path}") ctx.exit(ExitCode.USAGE_ERROR) - if not force: - _display_unsigned_warning() - downloader = PluginDownloader() enterprise_config = EnterpriseConfig.load() ui.display_info(f"Installing from {wheel_path.name}...") try: - plugin_name, version, _ = downloader.install_from_wheel(wheel_path, force) + plugin_name, version, _ = downloader.install_from_wheel( + wheel_path, signature_mode=signature_mode + ) # Enable in config enterprise_config.enable_plugin(plugin_name, version=version) @@ -242,6 +254,12 @@ def _install_from_local_wheel( ui.display_info(f"Installed {plugin_name} v{version}") + except SignatureVerificationError as e: + ui.display_error(f"Signature verification failed: {e}") + ui.display_info( + "Use --allow-unsigned to install without signature verification" + ) + ctx.exit(ExitCode.UNEXPECTED_ERROR) except DownloadError as e: ui.display_error(f"Failed to install from wheel: {e}") ctx.exit(ExitCode.UNEXPECTED_ERROR) @@ -254,23 +272,18 @@ def _install_from_url( ctx: click.Context, url: str, sha256: Optional[str], - force: bool, + signature_mode: SignatureVerificationMode = SignatureVerificationMode.STRICT, ) -> None: """Install a plugin from a URL.""" - if not force: - _display_unsigned_warning() - if not sha256: - ui.display_warning( - "No SHA256 checksum provided. Consider using --sha256 for verification." - ) - downloader = PluginDownloader() enterprise_config = EnterpriseConfig.load() ui.display_info("Installing from URL...") try: - plugin_name, version, _ = downloader.download_from_url(url, sha256, force) + plugin_name, version, _ = downloader.download_from_url( + url, sha256, signature_mode=signature_mode + ) # Enable in config enterprise_config.enable_plugin(plugin_name, version=version) @@ -278,6 +291,12 @@ def _install_from_url( ui.display_info(f"Installed {plugin_name} v{version}") + except SignatureVerificationError as e: + ui.display_error(f"Signature verification failed: {e}") + ui.display_info( + "Use --allow-unsigned to install without signature verification" + ) + ctx.exit(ExitCode.UNEXPECTED_ERROR) except InsecureSourceError as e: ui.display_error(str(e)) ctx.exit(ExitCode.USAGE_ERROR) @@ -296,12 +315,9 @@ def _install_from_github_release( ctx: click.Context, url: str, sha256: Optional[str], - force: bool, + signature_mode: SignatureVerificationMode = SignatureVerificationMode.STRICT, ) -> None: """Install a plugin from a GitHub release asset.""" - if not force: - _display_unsigned_warning() - downloader = PluginDownloader() enterprise_config = EnterpriseConfig.load() @@ -309,7 +325,7 @@ def _install_from_github_release( try: plugin_name, version, _ = downloader.download_from_github_release( - url, sha256, force + url, sha256, signature_mode=signature_mode ) # Enable in config @@ -318,6 +334,12 @@ def _install_from_github_release( ui.display_info(f"Installed {plugin_name} v{version}") + except SignatureVerificationError as e: + ui.display_error(f"Signature verification failed: {e}") + ui.display_info( + "Use --allow-unsigned to install without signature verification" + ) + ctx.exit(ExitCode.UNEXPECTED_ERROR) except InsecureSourceError as e: ui.display_error(str(e)) ctx.exit(ExitCode.USAGE_ERROR) @@ -335,12 +357,10 @@ def _install_from_github_release( def _install_from_github_artifact( ctx: click.Context, url: str, - force: bool, + signature_mode: SignatureVerificationMode = SignatureVerificationMode.STRICT, ) -> None: """Install a plugin from a GitHub Actions artifact.""" - if not force: - _display_unsigned_warning() - ui.display_warning("GitHub artifacts are ephemeral and cannot be auto-updated.") + ui.display_warning("GitHub artifacts are ephemeral and cannot be auto-updated.") downloader = PluginDownloader() enterprise_config = EnterpriseConfig.load() @@ -348,7 +368,9 @@ def _install_from_github_artifact( ui.display_info("Installing from GitHub artifact...") try: - plugin_name, version, _ = downloader.download_from_github_artifact(url, force) + plugin_name, version, _ = downloader.download_from_github_artifact( + url, signature_mode=signature_mode + ) # Enable in config enterprise_config.enable_plugin(plugin_name, version=version) @@ -356,6 +378,12 @@ def _install_from_github_artifact( ui.display_info(f"Installed {plugin_name} v{version}") + except SignatureVerificationError as e: + ui.display_error(f"Signature verification failed: {e}") + ui.display_info( + "Use --allow-unsigned to install without signature verification" + ) + ctx.exit(ExitCode.UNEXPECTED_ERROR) except GitHubArtifactError as e: ui.display_error(str(e)) ctx.exit(ExitCode.UNEXPECTED_ERROR) diff --git a/ggshield/cmd/plugin/status.py b/ggshield/cmd/plugin/status.py index 639ef37b56..e4b75af28c 100644 --- a/ggshield/cmd/plugin/status.py +++ b/ggshield/cmd/plugin/status.py @@ -13,7 +13,7 @@ from ggshield.core.config.enterprise_config import EnterpriseConfig from ggshield.core.errors import ExitCode from ggshield.core.plugin.client import PluginAPIClient, PluginAPIError -from ggshield.core.plugin.downloader import PluginDownloader +from ggshield.core.plugin.downloader import PluginDownloader, get_signature_label @click.command() @@ -79,6 +79,12 @@ def status_cmd(ctx: click.Context, **kwargs: Any) -> None: status_str = ", ".join(status_parts) ui.display_info(f" {plugin.display_name} ({plugin.name})") ui.display_info(f" Status: {status_str}") + if installed_version: + manifest = downloader.get_manifest(plugin.name) + if manifest: + sig_label = get_signature_label(manifest) + if sig_label: + ui.display_info(f" Signature: {sig_label}") ui.display_info(f" {plugin.description}") else: ui.display_info(f" {plugin.display_name} ({plugin.name}) - not available") diff --git a/ggshield/core/config/enterprise_config.py b/ggshield/core/config/enterprise_config.py index ea2c562752..c5c18b4d6d 100644 --- a/ggshield/core/config/enterprise_config.py +++ b/ggshield/core/config/enterprise_config.py @@ -4,7 +4,11 @@ from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Dict, Optional +from typing import TYPE_CHECKING, Any, Dict, Optional + + +if TYPE_CHECKING: + from ggshield.core.plugin.signature import SignatureVerificationMode from ggshield.core.config.utils import load_yaml_dict, save_yaml_dict from ggshield.core.dirs import get_config_dir @@ -29,6 +33,7 @@ class EnterpriseConfig: """Enterprise configuration stored in ~/.config/ggshield/enterprise_config.yaml""" plugins: Dict[str, PluginConfig] = field(default_factory=dict) + plugin_signature_mode: str = "strict" @classmethod def load(cls) -> "EnterpriseConfig": @@ -54,7 +59,9 @@ def load(cls) -> "EnterpriseConfig": else: plugins[name] = PluginConfig(enabled=True) - return cls(plugins=plugins) + plugin_signature_mode = data.get("plugin_signature_mode", "strict") + + return cls(plugins=plugins, plugin_signature_mode=plugin_signature_mode) def save(self) -> None: """Save enterprise config to file.""" @@ -72,6 +79,8 @@ def save(self) -> None: } } + data["plugin_signature_mode"] = self.plugin_signature_mode + # Remove None values for cleaner YAML for plugin_data in data["plugins"].values(): if plugin_data["version"] is None: @@ -79,6 +88,15 @@ def save(self) -> None: save_yaml_dict(data, config_path) + def get_signature_mode(self) -> "SignatureVerificationMode": + """Get the signature verification mode.""" + from ggshield.core.plugin.signature import SignatureVerificationMode + + try: + return SignatureVerificationMode(self.plugin_signature_mode) + except ValueError: + return SignatureVerificationMode.STRICT + def enable_plugin(self, plugin_name: str, version: Optional[str] = None) -> None: """Enable a plugin.""" if plugin_name not in self.plugins: @@ -91,15 +109,16 @@ def enable_plugin(self, plugin_name: str, version: Optional[str] = None) -> None def disable_plugin(self, plugin_name: str) -> None: """Disable a plugin.""" if plugin_name not in self.plugins: - raise ValueError(f"Plugin '{plugin_name}' is not configured") + self.plugins[plugin_name] = PluginConfig(enabled=False) + return self.plugins[plugin_name].enabled = False def is_plugin_enabled(self, plugin_name: str) -> bool: """Check if a plugin is enabled.""" plugin_config = self.plugins.get(plugin_name) - # Default: disabled if not explicitly configured - return plugin_config.enabled if plugin_config else False + # Default: enabled if not explicitly configured + return plugin_config.enabled if plugin_config else True def get_plugin_version(self, plugin_name: str) -> Optional[str]: """Get the configured version of a plugin.""" diff --git a/ggshield/core/plugin/client.py b/ggshield/core/plugin/client.py index bfecf503d8..56fec24c2c 100644 --- a/ggshield/core/plugin/client.py +++ b/ggshield/core/plugin/client.py @@ -97,6 +97,7 @@ class PluginDownloadInfo: sha256: str version: str expires_at: str + signature_url: Optional[str] = None class PluginAPIError(Exception): @@ -215,6 +216,7 @@ def get_download_info( sha256=data["sha256"], version=data["version"], expires_at=data["expires_at"], + signature_url=data.get("signature_url"), ) def _is_plugin_available( diff --git a/ggshield/core/plugin/downloader.py b/ggshield/core/plugin/downloader.py index 11612f691a..3f3e24c538 100644 --- a/ggshield/core/plugin/downloader.py +++ b/ggshield/core/plugin/downloader.py @@ -21,12 +21,46 @@ PluginSource, PluginSourceType, ) +from ggshield.core.plugin.signature import ( + SignatureInfo, + SignatureVerificationError, + SignatureVerificationMode, + verify_wheel_signature, +) from ggshield.core.plugin.wheel_utils import WheelError, extract_wheel_metadata logger = logging.getLogger(__name__) +def parse_wheel_filename(filename: str) -> Optional[Tuple[str, str]]: + """Parse a wheel filename into (name, version). + + Returns None if the filename doesn't match the wheel naming convention. + """ + match = re.match(r"^([A-Za-z0-9_.-]+?)-(\d+[^-]*)-", filename) + if match: + return match.group(1), match.group(2) + return None + + +def get_signature_label(manifest: Dict[str, Any]) -> Optional[str]: + """Get a human-readable signature status label from a manifest. + + Returns a string like "valid (GitGuardian/satori)", "missing", etc. + or None if no signature info is in the manifest. + """ + sig_info = manifest.get("signature") + if not sig_info: + return None + + status = sig_info.get("status", "unknown") + identity = sig_info.get("identity") + if identity: + return f"{status} ({identity})" + return status + + class DownloadError(Exception): """Error downloading or installing a plugin.""" @@ -67,6 +101,7 @@ def download_and_install( download_info: PluginDownloadInfo, plugin_name: str, source: Optional[PluginSource] = None, + signature_mode: SignatureVerificationMode = SignatureVerificationMode.STRICT, ) -> Path: """Download a plugin wheel and install it locally.""" self._validate_plugin_name(plugin_name) @@ -92,6 +127,13 @@ def download_and_install( if computed_hash.lower() != download_info.sha256.lower(): raise ChecksumMismatchError(download_info.sha256, computed_hash) + # Download signature bundle if available + temp_path.rename(wheel_path) + self._download_bundle(download_info, plugin_dir) + + # Verify signature + sig_info = verify_wheel_signature(wheel_path, signature_mode) + # Use GitGuardian API as default source if not provided if source is None: source = PluginSource(type=PluginSourceType.GITGUARDIAN_API) @@ -103,16 +145,19 @@ def download_and_install( wheel_filename=download_info.filename, sha256=download_info.sha256, source=source, + signature_info=sig_info, ) - temp_path.rename(wheel_path) - logger.info("Installed %s v%s", plugin_name, download_info.version) return wheel_path except requests.RequestException as e: + self._cleanup_failed_install(wheel_path) raise DownloadError(f"Failed to download plugin: {e}") from e + except SignatureVerificationError: + self._cleanup_failed_install(wheel_path) + raise finally: if temp_path.exists(): temp_path.unlink() @@ -120,14 +165,14 @@ def download_and_install( def install_from_wheel( self, wheel_path: Path, - force: bool = False, + signature_mode: SignatureVerificationMode = SignatureVerificationMode.STRICT, ) -> Tuple[str, str, Path]: """ Install a plugin from a local wheel file. Args: wheel_path: Path to the wheel file. - force: Skip security warnings if True. + signature_mode: Signature verification mode. Returns: Tuple of (plugin_name, version, installed_wheel_path). @@ -135,6 +180,7 @@ def install_from_wheel( Raises: WheelError: If the wheel file is invalid. DownloadError: If installation fails. + SignatureVerificationError: In STRICT mode when signature is invalid. """ # Extract metadata from wheel try: @@ -154,6 +200,16 @@ def install_from_wheel( dest_wheel_path = plugin_dir / wheel_path.name shutil.copy2(wheel_path, dest_wheel_path) + # Copy bundle if it exists alongside the wheel + from ggshield.core.plugin.signature import get_bundle_path + + bundle_path = get_bundle_path(wheel_path) + if bundle_path is not None: + shutil.copy2(bundle_path, plugin_dir / bundle_path.name) + + # Verify signature + sig_info = verify_wheel_signature(dest_wheel_path, signature_mode) + # Compute SHA256 sha256 = self._compute_sha256(dest_wheel_path) @@ -171,6 +227,7 @@ def install_from_wheel( wheel_filename=wheel_path.name, sha256=sha256, source=source, + signature_info=sig_info, ) logger.info("Installed %s v%s from local wheel", plugin_name, version) @@ -181,7 +238,7 @@ def download_from_url( self, url: str, sha256: Optional[str] = None, - force: bool = False, + signature_mode: SignatureVerificationMode = SignatureVerificationMode.STRICT, ) -> Tuple[str, str, Path]: """ Download and install a plugin from a URL. @@ -189,7 +246,7 @@ def download_from_url( Args: url: URL to download the wheel from. sha256: Expected SHA256 checksum (optional but recommended). - force: Skip security warnings if True. + signature_mode: Signature verification mode. Returns: Tuple of (plugin_name, version, installed_wheel_path). @@ -198,6 +255,7 @@ def download_from_url( InsecureSourceError: If URL uses HTTP instead of HTTPS. ChecksumMismatchError: If checksum doesn't match. DownloadError: If download or installation fails. + SignatureVerificationError: In STRICT mode when signature is invalid. """ # Security check: require HTTPS if url.startswith("http://"): @@ -254,6 +312,9 @@ def download_from_url( dest_wheel_path = plugin_dir / temp_wheel_path.name shutil.copy2(temp_wheel_path, dest_wheel_path) + # Verify signature + sig_info = verify_wheel_signature(dest_wheel_path, signature_mode) + # Create source tracking source = PluginSource( type=PluginSourceType.URL, @@ -268,6 +329,7 @@ def download_from_url( wheel_filename=dest_wheel_path.name, sha256=computed_hash, source=source, + signature_info=sig_info, ) logger.info("Installed %s v%s from URL", plugin_name, version) @@ -278,7 +340,7 @@ def download_from_github_release( self, url: str, sha256: Optional[str] = None, - force: bool = False, + signature_mode: SignatureVerificationMode = SignatureVerificationMode.STRICT, ) -> Tuple[str, str, Path]: """ Download and install a plugin from a GitHub release asset. @@ -286,7 +348,7 @@ def download_from_github_release( Args: url: GitHub release asset URL. sha256: Expected SHA256 checksum (optional). - force: Skip security warnings if True. + signature_mode: Signature verification mode. Returns: Tuple of (plugin_name, version, installed_wheel_path). @@ -295,7 +357,9 @@ def download_from_github_release( github_repo = self._extract_github_repo(url) # Download using standard URL method - plugin_name, version, wheel_path = self.download_from_url(url, sha256, force) + plugin_name, version, wheel_path = self.download_from_url( + url, sha256, signature_mode=signature_mode + ) # Update source to track GitHub release manifest_path = self.plugins_dir / plugin_name / "manifest.json" @@ -315,7 +379,7 @@ def download_from_github_release( def download_from_github_artifact( self, url: str, - force: bool = False, + signature_mode: SignatureVerificationMode = SignatureVerificationMode.STRICT, ) -> Tuple[str, str, Path]: """ Download and install a plugin from a GitHub Actions artifact. @@ -327,7 +391,7 @@ def download_from_github_artifact( Args: url: GitHub artifact URL (browser URL or API URL). - force: Skip security warnings if True. + signature_mode: Signature verification mode. Returns: Tuple of (plugin_name, version, installed_wheel_path). @@ -335,6 +399,7 @@ def download_from_github_artifact( Raises: GitHubArtifactError: If artifact cannot be downloaded or processed. DownloadError: If installation fails. + SignatureVerificationError: In STRICT mode when signature is invalid. """ # Parse artifact URL to get API endpoint artifact_info = self._parse_github_artifact_url(url) @@ -425,6 +490,9 @@ def download_from_github_artifact( # Compute SHA256 sha256 = self._compute_sha256(dest_wheel_path) + # Verify signature + sig_info = verify_wheel_signature(dest_wheel_path, signature_mode) + # Create source tracking source = PluginSource( type=PluginSourceType.GITHUB_ARTIFACT, @@ -440,6 +508,7 @@ def download_from_github_artifact( wheel_filename=dest_wheel_path.name, sha256=sha256, source=source, + signature_info=sig_info, ) logger.info("Installed %s v%s from GitHub artifact", plugin_name, version) @@ -600,9 +669,10 @@ def _write_manifest( wheel_filename: str, sha256: str, source: PluginSource, + signature_info: Optional[SignatureInfo] = None, ) -> None: """Write the plugin manifest file.""" - manifest = { + manifest: Dict[str, Any] = { "plugin_name": plugin_name, "version": version, "wheel_filename": wheel_filename, @@ -610,9 +680,54 @@ def _write_manifest( "source": source.to_dict(), "installed_at": datetime.now(timezone.utc).isoformat(), } + if signature_info is not None: + sig_data: Dict[str, Any] = {"status": signature_info.status.value} + if signature_info.identity: + sig_data["identity"] = signature_info.identity + if signature_info.message: + sig_data["message"] = signature_info.message + manifest["signature"] = sig_data + manifest_path = plugin_dir / "manifest.json" manifest_path.write_text(json.dumps(manifest, indent=2)) + def _download_bundle( + self, + download_info: PluginDownloadInfo, + plugin_dir: Path, + ) -> Optional[Path]: + """Download the sigstore bundle for a wheel if a signature URL is available.""" + if not download_info.signature_url: + return None + + bundle_filename = download_info.filename + ".sigstore" + bundle_path = plugin_dir / bundle_filename + + try: + logger.info("Downloading signature bundle...") + response = requests.get(download_info.signature_url, stream=True) + response.raise_for_status() + + with open(bundle_path, "wb") as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + + return bundle_path + except requests.RequestException as e: + logger.warning("Failed to download signature bundle: %s", e) + return None + + def _cleanup_failed_install(self, wheel_path: Path) -> None: + """Remove wheel and bundle files after a failed install.""" + if wheel_path.exists(): + wheel_path.unlink() + + # Also clean up any bundle files + for ext in (".sigstore", ".sigstore.json"): + bundle = wheel_path.parent / (wheel_path.name + ext) + if bundle.exists(): + bundle.unlink() + def _compute_sha256(self, file_path: Path) -> str: """Compute SHA256 hash of a file.""" sha256_hash = hashlib.sha256() diff --git a/ggshield/core/plugin/loader.py b/ggshield/core/plugin/loader.py index 47b41578fa..4eafedcbfb 100644 --- a/ggshield/core/plugin/loader.py +++ b/ggshield/core/plugin/loader.py @@ -18,6 +18,12 @@ from ggshield.core.dirs import get_plugins_dir from ggshield.core.plugin.base import GGShieldPlugin, PluginMetadata from ggshield.core.plugin.registry import PluginRegistry +from ggshield.core.plugin.signature import ( + SignatureStatus, + SignatureVerificationError, + SignatureVerificationMode, + verify_wheel_signature, +) class WheelInfo(TypedDict): @@ -81,9 +87,18 @@ class DiscoveredPlugin: class PluginLoader: """Discovers and loads ggshield plugins from entry points and local wheels.""" - def __init__(self, enterprise_config: EnterpriseConfig) -> None: + def __init__( + self, + enterprise_config: EnterpriseConfig, + signature_mode: Optional[SignatureVerificationMode] = None, + ) -> None: self.enterprise_config = enterprise_config self.plugins_dir = get_plugins_dir() + self.signature_mode = ( + signature_mode + if signature_mode is not None + else enterprise_config.get_signature_mode() + ) def discover_plugins(self) -> List[DiscoveredPlugin]: """Discover all available plugins from entry points and local wheels.""" @@ -186,6 +201,29 @@ def _load_from_wheel(self, wheel_path: Path) -> Optional[GGShieldPlugin]: Wheels are extracted to a directory before loading because Python cannot import native extensions (.so/.pyd) directly from zip files. """ + # Verify signature before loading + try: + sig_info = verify_wheel_signature(wheel_path, self.signature_mode) + if sig_info.status == SignatureStatus.VALID: + logger.info( + "Signature valid for %s (identity: %s)", + wheel_path.name, + sig_info.identity, + ) + elif sig_info.status in ( + SignatureStatus.MISSING, + SignatureStatus.INVALID, + ): + logger.warning( + "Signature %s for %s: %s", + sig_info.status.value, + wheel_path.name, + sig_info.message or "", + ) + except SignatureVerificationError as e: + logger.error("Signature verification failed for %s: %s", wheel_path.name, e) + return None + # Extract wheel to a directory alongside the wheel file extract_dir = wheel_path.parent / f".{wheel_path.stem}_extracted" diff --git a/ggshield/core/plugin/signature.py b/ggshield/core/plugin/signature.py new file mode 100644 index 0000000000..a7857096a5 --- /dev/null +++ b/ggshield/core/plugin/signature.py @@ -0,0 +1,174 @@ +""" +Plugin signature verification using sigstore. + +Provides keyless signature verification for plugin wheels using sigstore +bundles. Signatures are identity-based: ggshield trusts a configured set +of GitHub Actions workflow identities (OIDC). + +Verification modes: +- STRICT: block unsigned or invalid plugins +- WARN: log a warning but allow loading +- DISABLED: skip verification entirely +""" + +import enum +import logging +from dataclasses import dataclass +from pathlib import Path +from typing import List, Optional + +from sigstore.models import Bundle +from sigstore.verify import Verifier +from sigstore.verify.policy import AllOf, GitHubWorkflowRepository, OIDCIssuer + + +logger = logging.getLogger(__name__) + + +class SignatureVerificationMode(enum.Enum): + """How strictly to enforce plugin signatures.""" + + STRICT = "strict" + WARN = "warn" + DISABLED = "disabled" + + +class SignatureStatus(enum.Enum): + """Result of signature verification.""" + + VALID = "valid" + MISSING = "missing" + INVALID = "invalid" + SKIPPED = "skipped" + + +@dataclass +class TrustedIdentity: + """An OIDC identity trusted to sign plugins.""" + + repository: str + """GitHub repository (e.g. "GitGuardian/satori").""" + + issuer: str + """OIDC issuer URL.""" + + +@dataclass +class SignatureInfo: + """Result of verifying a wheel's signature.""" + + status: SignatureStatus + identity: Optional[str] = None + message: Optional[str] = None + + +class SignatureVerificationError(Exception): + """Raised when signature verification fails in strict mode.""" + + def __init__(self, status: SignatureStatus, message: str) -> None: + self.status = status + super().__init__(message) + + +# Default trusted identities: GitHub Actions workflows that are authorized +# to sign plugin wheels. +DEFAULT_TRUSTED_IDENTITIES: List[TrustedIdentity] = [ + TrustedIdentity( + repository="GitGuardian/satori", + issuer="https://token.actions.githubusercontent.com", + ), +] + + +def get_bundle_path(wheel_path: Path) -> Optional[Path]: + """Return the sigstore bundle path for a wheel, or None if not found. + + Checks for `.sigstore` first, then `.sigstore.json` (sigstore-python 3.x + default) so that both old and new naming conventions are supported. + """ + for ext in (".sigstore", ".sigstore.json"): + p = wheel_path.parent / (wheel_path.name + ext) + if p.exists(): + return p + return None + + +def verify_wheel_signature( + wheel_path: Path, + mode: SignatureVerificationMode, + trusted_identities: Optional[List[TrustedIdentity]] = None, +) -> SignatureInfo: + """ + Verify the sigstore signature of a plugin wheel. + + Args: + wheel_path: Path to the .whl file. + mode: Verification strictness. + trusted_identities: OIDC identities to trust. + Defaults to DEFAULT_TRUSTED_IDENTITIES. + + Returns: + SignatureInfo with the verification result. + + Raises: + SignatureVerificationError: In STRICT mode when the signature is + missing or invalid. + """ + if mode == SignatureVerificationMode.DISABLED: + return SignatureInfo(status=SignatureStatus.SKIPPED) + + if trusted_identities is None: + trusted_identities = DEFAULT_TRUSTED_IDENTITIES + + bundle_path = get_bundle_path(wheel_path) + + # Missing bundle + if bundle_path is None: + msg = f"No signature bundle found for {wheel_path.name}" + if mode == SignatureVerificationMode.STRICT: + raise SignatureVerificationError(SignatureStatus.MISSING, msg) + logger.warning("%s", msg) + return SignatureInfo(status=SignatureStatus.MISSING, message=msg) + + # Verify bundle + bundle = Bundle.from_json(bundle_path.read_bytes()) + wheel_bytes = wheel_path.read_bytes() + verifier = Verifier.production() + + for trusted in trusted_identities: + policy = AllOf( + [ + OIDCIssuer(trusted.issuer), + GitHubWorkflowRepository(trusted.repository), + ] + ) + try: + verifier.verify_artifact( + input_=wheel_bytes, + bundle=bundle, + policy=policy, + ) + logger.info( + "Signature valid for %s (repository: %s)", + wheel_path.name, + trusted.repository, + ) + return SignatureInfo( + status=SignatureStatus.VALID, + identity=trusted.repository, + ) + except Exception as e: + logger.debug( + "Identity %s did not match for %s: %s", + trusted.repository, + wheel_path.name, + e, + ) + continue + + # No identity matched + msg = f"Signature verification failed for {wheel_path.name}: no trusted identity matched" + if mode == SignatureVerificationMode.STRICT: + raise SignatureVerificationError(SignatureStatus.INVALID, msg) + logger.warning("%s", msg) + return SignatureInfo(status=SignatureStatus.INVALID, message=msg) diff --git a/pyproject.toml b/pyproject.toml index 008c81c60f..4678cc5b08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ classifiers = [ license = { text = "MIT" } requires-python = ">=3.9" dependencies = [ - "platformdirs~=3.0.0", + "platformdirs~=4.2", "charset-normalizer~=3.1.0", "click~=8.1.0", "cryptography~=43.0.1", @@ -44,8 +44,9 @@ dependencies = [ "python-dotenv~=0.21.0", "pyyaml~=6.0.1", "requests~=2.32.0", - "rich~=12.5.1", - "typing-extensions~=4.12.2", + "rich~=13.0", + "sigstore>=3.0.0", + "typing-extensions~=4.14", "urllib3>=2.2.2,<3", "truststore>=0.10.1; python_version >= \"3.10\"", "notify-py>=0.3.43", diff --git a/tests/unit/cmd/plugin/test_install.py b/tests/unit/cmd/plugin/test_install.py index a9e0c10862..92b01f9f89 100644 --- a/tests/unit/cmd/plugin/test_install.py +++ b/tests/unit/cmd/plugin/test_install.py @@ -5,6 +5,8 @@ from pathlib import Path from unittest import mock +import pytest + from ggshield.__main__ import cli from ggshield.cmd.plugin.install import detect_source_type from ggshield.core.errors import ExitCode @@ -14,6 +16,7 @@ PluginInfo, PluginSourceType, ) +from ggshield.core.plugin.downloader import ChecksumMismatchError, DownloadError class TestPluginInstall: @@ -96,20 +99,22 @@ def test_install_single_plugin(self, cli_fs_runner): assert result.exit_code == ExitCode.SUCCESS assert "Installing tokenscanner" in result.output assert "Installed tokenscanner v1.0.0" in result.output - mock_downloader.download_and_install.assert_called_once_with( - mock_download_info, "tokenscanner" - ) + mock_downloader.download_and_install.assert_called_once() + call_args = mock_downloader.download_and_install.call_args + assert call_args[0] == (mock_download_info, "tokenscanner") mock_config.enable_plugin.assert_called_once_with( "tokenscanner", version="1.0.0" ) mock_config.save.assert_called_once() - def test_install_unavailable_plugin(self, cli_fs_runner): - """ - GIVEN a plugin exists but is not available - WHEN running 'ggshield plugin install ' - THEN it shows an error with reason - """ + @pytest.mark.parametrize( + "reason", + [ + pytest.param("Requires Business plan", id="with_reason"), + pytest.param(None, id="without_reason"), + ], + ) + def test_install_unavailable_plugin(self, cli_fs_runner, reason): mock_catalog = PluginCatalog( plan="Free", plugins=[ @@ -119,7 +124,7 @@ def test_install_unavailable_plugin(self, cli_fs_runner): description="Local secret scanning", available=False, latest_version="1.0.0", - reason="Requires Business plan", + reason=reason, ), ], features={}, @@ -144,7 +149,10 @@ def test_install_unavailable_plugin(self, cli_fs_runner): assert result.exit_code == ExitCode.USAGE_ERROR assert "not available" in result.output - assert "Requires Business plan" in result.output + if reason: + assert reason in result.output + else: + assert "Reason:" not in result.output def test_install_unknown_plugin(self, cli_fs_runner): """ @@ -253,8 +261,6 @@ def test_install_download_error(self, cli_fs_runner): WHEN running 'ggshield plugin install ' THEN it shows an error """ - from ggshield.core.plugin.downloader import DownloadError - mock_catalog = PluginCatalog( plan="Enterprise", plugins=[ @@ -496,49 +502,6 @@ def test_install_generic_error(self, cli_fs_runner): assert result.exit_code == ExitCode.UNEXPECTED_ERROR assert "Failed to install tokenscanner" in result.output - def test_install_unavailable_plugin_without_reason(self, cli_fs_runner): - """ - GIVEN a plugin exists but is not available (without reason) - WHEN running 'ggshield plugin install ' - THEN it shows an error without reason - """ - mock_catalog = PluginCatalog( - plan="Free", - plugins=[ - PluginInfo( - name="tokenscanner", - display_name="Token Scanner", - description="Local secret scanning", - available=False, - latest_version="1.0.0", - reason=None, - ), - ], - features={}, - ) - - with ( - mock.patch( - "ggshield.cmd.plugin.install.create_client_from_config" - ) as mock_create_client, - mock.patch( - "ggshield.cmd.plugin.install.PluginAPIClient" - ) as mock_plugin_api_client_class, - ): - mock_client = mock.MagicMock() - mock_create_client.return_value = mock_client - - mock_plugin_api_client = mock.MagicMock() - mock_plugin_api_client.get_available_plugins.return_value = mock_catalog - mock_plugin_api_client_class.return_value = mock_plugin_api_client - - result = cli_fs_runner.invoke(cli, ["plugin", "install", "tokenscanner"]) - - assert result.exit_code == ExitCode.USAGE_ERROR - assert "not available" in result.output - # Should not show "Reason:" when reason is None - assert "Reason:" not in result.output - class TestDetectSourceType: """Tests for detect_source_type function.""" @@ -622,7 +585,7 @@ def test_install_local_wheel_success(self, cli_fs_runner, tmp_path: Path) -> Non result = cli_fs_runner.invoke( cli, - ["plugin", "install", str(wheel_path), "--force"], + ["plugin", "install", str(wheel_path)], catch_exceptions=False, ) @@ -631,47 +594,6 @@ def test_install_local_wheel_success(self, cli_fs_runner, tmp_path: Path) -> Non mock_downloader.install_from_wheel.assert_called_once() mock_config.enable_plugin.assert_called_once_with("myplugin", version="1.0.0") - def test_install_local_wheel_warning(self, cli_fs_runner, tmp_path: Path) -> None: - """ - GIVEN a valid local wheel file without --force - WHEN running 'ggshield plugin install ' - THEN a warning is displayed - """ - wheel_path = tmp_path / "myplugin-1.0.0.whl" - wheel_path.touch() - - with ( - mock.patch( - "ggshield.cmd.plugin.install.PluginDownloader" - ) as mock_downloader_class, - mock.patch( - "ggshield.cmd.plugin.install.EnterpriseConfig" - ) as mock_config_class, - mock.patch( - "ggshield.cmd.plugin.install.detect_source_type", - return_value=PluginSourceType.LOCAL_FILE, - ), - ): - mock_downloader = mock.MagicMock() - mock_downloader.install_from_wheel.return_value = ( - "myplugin", - "1.0.0", - wheel_path, - ) - mock_downloader_class.return_value = mock_downloader - - mock_config = mock.MagicMock() - mock_config_class.load.return_value = mock_config - - result = cli_fs_runner.invoke( - cli, - ["plugin", "install", str(wheel_path)], - catch_exceptions=False, - ) - - assert result.exit_code == ExitCode.SUCCESS - assert "not from GitGuardian" in result.output - class TestInstallFromUrl: """Tests for installing from URLs.""" @@ -713,54 +635,15 @@ def test_install_url_with_sha256(self, cli_fs_runner) -> None: "https://example.com/plugin.whl", "--sha256", "abc123", - "--force", ], catch_exceptions=False, ) assert result.exit_code == ExitCode.SUCCESS assert "Installed urlplugin v2.0.0" in result.output - mock_downloader.download_from_url.assert_called_once_with( - "https://example.com/plugin.whl", "abc123", True - ) - - def test_install_url_warning_no_sha256(self, cli_fs_runner) -> None: - """ - GIVEN a URL without SHA256 checksum - WHEN running 'ggshield plugin install ' without --force - THEN a warning about missing checksum is displayed - """ - with ( - mock.patch( - "ggshield.cmd.plugin.install.PluginDownloader" - ) as mock_downloader_class, - mock.patch( - "ggshield.cmd.plugin.install.EnterpriseConfig" - ) as mock_config_class, - mock.patch( - "ggshield.cmd.plugin.install.detect_source_type", - return_value=PluginSourceType.URL, - ), - ): - mock_downloader = mock.MagicMock() - mock_downloader.download_from_url.return_value = ( - "urlplugin", - "2.0.0", - Path("/fake/path.whl"), - ) - mock_downloader_class.return_value = mock_downloader - - mock_config = mock.MagicMock() - mock_config_class.load.return_value = mock_config - - result = cli_fs_runner.invoke( - cli, - ["plugin", "install", "https://example.com/plugin.whl"], - catch_exceptions=False, - ) - - assert result.exit_code == ExitCode.SUCCESS - assert "No SHA256 checksum provided" in result.output + mock_downloader.download_from_url.assert_called_once() + call_args = mock_downloader.download_from_url.call_args + assert call_args[0] == ("https://example.com/plugin.whl", "abc123") def test_install_http_url_rejected(self, cli_fs_runner) -> None: """ @@ -793,7 +676,7 @@ def test_install_http_url_rejected(self, cli_fs_runner) -> None: result = cli_fs_runner.invoke( cli, - ["plugin", "install", "http://example.com/plugin.whl", "--force"], + ["plugin", "install", "http://example.com/plugin.whl"], ) assert result.exit_code == ExitCode.USAGE_ERROR @@ -838,7 +721,6 @@ def test_install_github_release(self, cli_fs_runner) -> None: "plugin", "install", "https://github.com/owner/repo/releases/download/v1.5.0/plugin.whl", - "--force", ], catch_exceptions=False, ) @@ -885,7 +767,6 @@ def test_install_github_artifact(self, cli_fs_runner) -> None: "plugin", "install", "https://github.com/owner/repo/actions/runs/123/artifacts/456", - "--force", ], catch_exceptions=False, ) @@ -895,7 +776,7 @@ def test_install_github_artifact(self, cli_fs_runner) -> None: def test_install_github_artifact_warning(self, cli_fs_runner) -> None: """ - GIVEN a GitHub artifact URL without --force + GIVEN a GitHub artifact URL WHEN running 'ggshield plugin install ' THEN a warning about ephemeral artifacts is displayed """ @@ -970,7 +851,6 @@ def test_install_github_artifact_auth_error(self, cli_fs_runner) -> None: "plugin", "install", "https://github.com/owner/repo/actions/runs/123/artifacts/456", - "--force", ], ) @@ -982,11 +862,6 @@ class TestInstallErrorHandling: """Tests for error handling in various install scenarios.""" def test_install_local_wheel_not_found(self, cli_fs_runner, tmp_path: Path) -> None: - """ - GIVEN a non-existent wheel file path - WHEN running 'ggshield plugin install ' - THEN it shows a file not found error - """ wheel_path = tmp_path / "nonexistent.whl" with mock.patch( @@ -1001,16 +876,16 @@ def test_install_local_wheel_not_found(self, cli_fs_runner, tmp_path: Path) -> N assert result.exit_code == ExitCode.USAGE_ERROR assert "Wheel file not found" in result.output - def test_install_local_wheel_download_error( - self, cli_fs_runner, tmp_path: Path + @pytest.mark.parametrize( + "error", + [ + pytest.param(DownloadError("Invalid wheel"), id="download_error"), + pytest.param(Exception("Unexpected"), id="generic_error"), + ], + ) + def test_install_local_wheel_error( + self, cli_fs_runner, tmp_path: Path, error: Exception ) -> None: - """ - GIVEN a local wheel file that fails to install - WHEN running 'ggshield plugin install ' - THEN it shows an error - """ - from ggshield.core.plugin.downloader import DownloadError - wheel_path = tmp_path / "broken.whl" wheel_path.touch() @@ -1025,270 +900,89 @@ def test_install_local_wheel_download_error( ), ): mock_downloader = mock.MagicMock() - mock_downloader.install_from_wheel.side_effect = DownloadError( - "Invalid wheel" - ) + mock_downloader.install_from_wheel.side_effect = error mock_downloader_class.return_value = mock_downloader result = cli_fs_runner.invoke( cli, - ["plugin", "install", str(wheel_path), "--force"], + ["plugin", "install", str(wheel_path)], ) assert result.exit_code == ExitCode.UNEXPECTED_ERROR assert "Failed to install from wheel" in result.output - def test_install_local_wheel_generic_error( - self, cli_fs_runner, tmp_path: Path - ) -> None: - """ - GIVEN a local wheel file with unexpected error - WHEN running 'ggshield plugin install ' - THEN it shows an error - """ - wheel_path = tmp_path / "problem.whl" - wheel_path.touch() - - with ( - mock.patch( - "ggshield.cmd.plugin.install.PluginDownloader" - ) as mock_downloader_class, - mock.patch("ggshield.cmd.plugin.install.EnterpriseConfig"), - mock.patch( - "ggshield.cmd.plugin.install.detect_source_type", - return_value=PluginSourceType.LOCAL_FILE, + @pytest.mark.parametrize( + "source_type, method, cli_args, error, expected_msg", + [ + pytest.param( + PluginSourceType.URL, + "download_from_url", + ["plugin", "install", "https://example.com/plugin.whl"], + DownloadError("Network error"), + "Failed to install from URL", + id="url-download_error", ), - ): - mock_downloader = mock.MagicMock() - mock_downloader.install_from_wheel.side_effect = Exception("Unexpected") - mock_downloader_class.return_value = mock_downloader - - result = cli_fs_runner.invoke( - cli, - ["plugin", "install", str(wheel_path), "--force"], - ) - - assert result.exit_code == ExitCode.UNEXPECTED_ERROR - assert "Failed to install from wheel" in result.output - - def test_install_url_checksum_mismatch(self, cli_fs_runner) -> None: - """ - GIVEN a URL with wrong checksum - WHEN running 'ggshield plugin install --sha256 ' - THEN it shows a checksum error - """ - from ggshield.core.plugin.downloader import ChecksumMismatchError - - with ( - mock.patch( - "ggshield.cmd.plugin.install.PluginDownloader" - ) as mock_downloader_class, - mock.patch("ggshield.cmd.plugin.install.EnterpriseConfig"), - mock.patch( - "ggshield.cmd.plugin.install.detect_source_type", - return_value=PluginSourceType.URL, + pytest.param( + PluginSourceType.URL, + "download_from_url", + ["plugin", "install", "https://example.com/plugin.whl"], + Exception("Unexpected"), + "Failed to install from URL", + id="url-generic_error", ), - ): - mock_downloader = mock.MagicMock() - mock_downloader.download_from_url.side_effect = ChecksumMismatchError( - "expected123", "actual456" - ) - mock_downloader_class.return_value = mock_downloader - - result = cli_fs_runner.invoke( - cli, + pytest.param( + PluginSourceType.GITHUB_RELEASE, + "download_from_github_release", [ "plugin", "install", - "https://example.com/plugin.whl", - "--sha256", - "wrong", - "--force", + "https://github.com/owner/repo/releases/download/v1/p.whl", ], - ) - - assert result.exit_code == ExitCode.UNEXPECTED_ERROR - assert "Checksum verification failed" in result.output - - def test_install_url_download_error(self, cli_fs_runner) -> None: - """ - GIVEN a URL that fails to download - WHEN running 'ggshield plugin install ' - THEN it shows a download error - """ - from ggshield.core.plugin.downloader import DownloadError - - with ( - mock.patch( - "ggshield.cmd.plugin.install.PluginDownloader" - ) as mock_downloader_class, - mock.patch("ggshield.cmd.plugin.install.EnterpriseConfig"), - mock.patch( - "ggshield.cmd.plugin.install.detect_source_type", - return_value=PluginSourceType.URL, - ), - ): - mock_downloader = mock.MagicMock() - mock_downloader.download_from_url.side_effect = DownloadError( - "Network error" - ) - mock_downloader_class.return_value = mock_downloader - - result = cli_fs_runner.invoke( - cli, - ["plugin", "install", "https://example.com/plugin.whl", "--force"], - ) - - assert result.exit_code == ExitCode.UNEXPECTED_ERROR - assert "Failed to install from URL" in result.output - - def test_install_url_generic_error(self, cli_fs_runner) -> None: - """ - GIVEN a URL with unexpected error - WHEN running 'ggshield plugin install ' - THEN it shows an error - """ - with ( - mock.patch( - "ggshield.cmd.plugin.install.PluginDownloader" - ) as mock_downloader_class, - mock.patch("ggshield.cmd.plugin.install.EnterpriseConfig"), - mock.patch( - "ggshield.cmd.plugin.install.detect_source_type", - return_value=PluginSourceType.URL, + DownloadError("Not found"), + "Failed to install from GitHub release", + id="github_release-download_error", ), - ): - mock_downloader = mock.MagicMock() - mock_downloader.download_from_url.side_effect = Exception("Unexpected") - mock_downloader_class.return_value = mock_downloader - - result = cli_fs_runner.invoke( - cli, - ["plugin", "install", "https://example.com/plugin.whl", "--force"], - ) - - assert result.exit_code == ExitCode.UNEXPECTED_ERROR - assert "Failed to install from URL" in result.output - - def test_install_github_release_checksum_mismatch(self, cli_fs_runner) -> None: - """ - GIVEN a GitHub release URL with wrong checksum - WHEN running 'ggshield plugin install --sha256 ' - THEN it shows a checksum error - """ - from ggshield.core.plugin.downloader import ChecksumMismatchError - - with ( - mock.patch( - "ggshield.cmd.plugin.install.PluginDownloader" - ) as mock_downloader_class, - mock.patch("ggshield.cmd.plugin.install.EnterpriseConfig"), - mock.patch( - "ggshield.cmd.plugin.install.detect_source_type", - return_value=PluginSourceType.GITHUB_RELEASE, - ), - ): - mock_downloader = mock.MagicMock() - mock_downloader.download_from_github_release.side_effect = ( - ChecksumMismatchError("expected", "actual") - ) - mock_downloader_class.return_value = mock_downloader - - result = cli_fs_runner.invoke( - cli, + pytest.param( + PluginSourceType.GITHUB_RELEASE, + "download_from_github_release", [ "plugin", "install", "https://github.com/owner/repo/releases/download/v1/p.whl", - "--sha256", - "wrong", - "--force", ], - ) - - assert result.exit_code == ExitCode.UNEXPECTED_ERROR - assert "Checksum verification failed" in result.output - - def test_install_github_release_download_error(self, cli_fs_runner) -> None: - """ - GIVEN a GitHub release URL that fails to download - WHEN running 'ggshield plugin install ' - THEN it shows a download error - """ - from ggshield.core.plugin.downloader import DownloadError - - with ( - mock.patch( - "ggshield.cmd.plugin.install.PluginDownloader" - ) as mock_downloader_class, - mock.patch("ggshield.cmd.plugin.install.EnterpriseConfig"), - mock.patch( - "ggshield.cmd.plugin.install.detect_source_type", - return_value=PluginSourceType.GITHUB_RELEASE, + Exception("Unexpected"), + "Failed to install from GitHub release", + id="github_release-generic_error", ), - ): - mock_downloader = mock.MagicMock() - mock_downloader.download_from_github_release.side_effect = DownloadError( - "Not found" - ) - mock_downloader_class.return_value = mock_downloader - - result = cli_fs_runner.invoke( - cli, + pytest.param( + PluginSourceType.GITHUB_ARTIFACT, + "download_from_github_artifact", [ "plugin", "install", - "https://github.com/owner/repo/releases/download/v1/p.whl", - "--force", + "https://github.com/owner/repo/actions/runs/123/artifacts/456", ], - ) - - assert result.exit_code == ExitCode.UNEXPECTED_ERROR - assert "Failed to install from GitHub release" in result.output - - def test_install_github_release_generic_error(self, cli_fs_runner) -> None: - """ - GIVEN a GitHub release URL with unexpected error - WHEN running 'ggshield plugin install ' - THEN it shows an error - """ - with ( - mock.patch( - "ggshield.cmd.plugin.install.PluginDownloader" - ) as mock_downloader_class, - mock.patch("ggshield.cmd.plugin.install.EnterpriseConfig"), - mock.patch( - "ggshield.cmd.plugin.install.detect_source_type", - return_value=PluginSourceType.GITHUB_RELEASE, + DownloadError("Failed to extract"), + "Failed to install from GitHub artifact", + id="github_artifact-download_error", ), - ): - mock_downloader = mock.MagicMock() - mock_downloader.download_from_github_release.side_effect = Exception( - "Unexpected" - ) - mock_downloader_class.return_value = mock_downloader - - result = cli_fs_runner.invoke( - cli, + pytest.param( + PluginSourceType.GITHUB_ARTIFACT, + "download_from_github_artifact", [ "plugin", "install", - "https://github.com/owner/repo/releases/download/v1/p.whl", - "--force", + "https://github.com/owner/repo/actions/runs/123/artifacts/456", ], - ) - - assert result.exit_code == ExitCode.UNEXPECTED_ERROR - assert "Failed to install from GitHub release" in result.output - - def test_install_github_artifact_download_error(self, cli_fs_runner) -> None: - """ - GIVEN a GitHub artifact URL that fails to download - WHEN running 'ggshield plugin install ' - THEN it shows a download error - """ - from ggshield.core.plugin.downloader import DownloadError - + Exception("Unexpected"), + "Failed to install from GitHub artifact", + id="github_artifact-generic_error", + ), + ], + ) + def test_install_error( + self, cli_fs_runner, source_type, method, cli_args, error, expected_msg + ) -> None: with ( mock.patch( "ggshield.cmd.plugin.install.PluginDownloader" @@ -1296,34 +990,50 @@ def test_install_github_artifact_download_error(self, cli_fs_runner) -> None: mock.patch("ggshield.cmd.plugin.install.EnterpriseConfig"), mock.patch( "ggshield.cmd.plugin.install.detect_source_type", - return_value=PluginSourceType.GITHUB_ARTIFACT, + return_value=source_type, ), ): mock_downloader = mock.MagicMock() - mock_downloader.download_from_github_artifact.side_effect = DownloadError( - "Failed to extract" - ) + getattr(mock_downloader, method).side_effect = error mock_downloader_class.return_value = mock_downloader - result = cli_fs_runner.invoke( - cli, + result = cli_fs_runner.invoke(cli, cli_args) + + assert result.exit_code == ExitCode.UNEXPECTED_ERROR + assert expected_msg in result.output + + @pytest.mark.parametrize( + "source_type, method, cli_args", + [ + pytest.param( + PluginSourceType.URL, + "download_from_url", [ "plugin", "install", - "https://github.com/owner/repo/actions/runs/123/artifacts/456", - "--force", + "https://example.com/plugin.whl", + "--sha256", + "wrong", ], - ) - - assert result.exit_code == ExitCode.UNEXPECTED_ERROR - assert "Failed to install from GitHub artifact" in result.output - - def test_install_github_artifact_generic_error(self, cli_fs_runner) -> None: - """ - GIVEN a GitHub artifact URL with unexpected error - WHEN running 'ggshield plugin install ' - THEN it shows an error - """ + id="url", + ), + pytest.param( + PluginSourceType.GITHUB_RELEASE, + "download_from_github_release", + [ + "plugin", + "install", + "https://github.com/owner/repo/releases/download/v1/p.whl", + "--sha256", + "wrong", + ], + id="github_release", + ), + ], + ) + def test_install_checksum_mismatch( + self, cli_fs_runner, source_type, method, cli_args + ) -> None: with ( mock.patch( "ggshield.cmd.plugin.install.PluginDownloader" @@ -1331,24 +1041,16 @@ def test_install_github_artifact_generic_error(self, cli_fs_runner) -> None: mock.patch("ggshield.cmd.plugin.install.EnterpriseConfig"), mock.patch( "ggshield.cmd.plugin.install.detect_source_type", - return_value=PluginSourceType.GITHUB_ARTIFACT, + return_value=source_type, ), ): mock_downloader = mock.MagicMock() - mock_downloader.download_from_github_artifact.side_effect = Exception( - "Unexpected" + getattr(mock_downloader, method).side_effect = ChecksumMismatchError( + "expected123", "actual456" ) mock_downloader_class.return_value = mock_downloader - result = cli_fs_runner.invoke( - cli, - [ - "plugin", - "install", - "https://github.com/owner/repo/actions/runs/123/artifacts/456", - "--force", - ], - ) + result = cli_fs_runner.invoke(cli, cli_args) assert result.exit_code == ExitCode.UNEXPECTED_ERROR - assert "Failed to install from GitHub artifact" in result.output + assert "Checksum verification failed" in result.output diff --git a/tests/unit/core/plugin/test_downloader.py b/tests/unit/core/plugin/test_downloader.py index 78a8d8f1f6..8967ccfba6 100644 --- a/tests/unit/core/plugin/test_downloader.py +++ b/tests/unit/core/plugin/test_downloader.py @@ -22,6 +22,10 @@ PluginDownloader, get_plugins_dir, ) +from ggshield.core.plugin.signature import SignatureInfo, SignatureStatus + + +MOCK_SIG_INFO = SignatureInfo(status=SignatureStatus.SKIPPED) class TestPluginDownloader: @@ -168,10 +172,14 @@ def test_download_and_install_success(self, tmp_path: Path) -> None: "ggshield.core.plugin.downloader.get_plugins_dir", return_value=tmp_path ): with patch("requests.get", return_value=mock_response): - downloader = PluginDownloader() - wheel_path = downloader.download_and_install( - download_info, "testplugin" - ) + with patch( + "ggshield.core.plugin.downloader.verify_wheel_signature", + return_value=MOCK_SIG_INFO, + ): + downloader = PluginDownloader() + wheel_path = downloader.download_and_install( + download_info, "testplugin" + ) assert wheel_path.exists() assert wheel_path.name == "testplugin-1.0.0.whl" @@ -278,17 +286,19 @@ def test_download_and_install_manifest_failure_cleans_temp_file( "ggshield.core.plugin.downloader.get_plugins_dir", return_value=tmp_path ): with patch("requests.get", return_value=mock_response): - downloader = PluginDownloader() - - with patch.object( - downloader, "_write_manifest", side_effect=OSError("disk full") + with patch( + "ggshield.core.plugin.downloader.verify_wheel_signature", + return_value=MOCK_SIG_INFO, ): - with pytest.raises(OSError): - downloader.download_and_install(download_info, "testplugin") + downloader = PluginDownloader() + + with patch.object( + downloader, "_write_manifest", side_effect=OSError("disk full") + ): + with pytest.raises(OSError): + downloader.download_and_install(download_info, "testplugin") - wheel_path = tmp_path / "testplugin" / "testplugin-1.0.0.whl" temp_path = tmp_path / "testplugin" / "testplugin-1.0.0.whl.tmp" - assert not wheel_path.exists() assert not temp_path.exists() def test_is_installed_by_entry_point_name(self, tmp_path: Path) -> None: @@ -503,10 +513,14 @@ def test_install_from_wheel_success(self, tmp_path: Path) -> None: with patch( "ggshield.core.plugin.downloader.get_plugins_dir", return_value=plugins_dir ): - downloader = PluginDownloader() - plugin_name, version, installed_path = downloader.install_from_wheel( - wheel_path - ) + with patch( + "ggshield.core.plugin.downloader.verify_wheel_signature", + return_value=MOCK_SIG_INFO, + ): + downloader = PluginDownloader() + plugin_name, version, installed_path = downloader.install_from_wheel( + wheel_path + ) assert plugin_name == "myplugin" assert version == "2.0.0" @@ -561,11 +575,15 @@ def test_download_from_url_success(self, tmp_path: Path) -> None: "ggshield.core.plugin.downloader.get_plugins_dir", return_value=plugins_dir ): with patch("requests.get", return_value=mock_response): - downloader = PluginDownloader() - plugin_name, version, installed_path = downloader.download_from_url( - "https://example.com/urlplugin-1.0.0.whl", - sha256=sha256, - ) + with patch( + "ggshield.core.plugin.downloader.verify_wheel_signature", + return_value=MOCK_SIG_INFO, + ): + downloader = PluginDownloader() + plugin_name, version, installed_path = downloader.download_from_url( + "https://example.com/urlplugin-1.0.0.whl", + sha256=sha256, + ) assert plugin_name == "urlplugin" assert version == "1.0.0" @@ -640,11 +658,15 @@ def test_github_release_extracts_repo(self, tmp_path: Path) -> None: "ggshield.core.plugin.downloader.get_plugins_dir", return_value=plugins_dir ): with patch("requests.get", return_value=mock_response): - downloader = PluginDownloader() - plugin_name, version, _ = downloader.download_from_github_release( - "https://github.com/owner/repo/releases/download/v1.0.0/ghplugin-1.0.0.whl", - sha256=sha256, - ) + with patch( + "ggshield.core.plugin.downloader.verify_wheel_signature", + return_value=MOCK_SIG_INFO, + ): + downloader = PluginDownloader() + plugin_name, version, _ = downloader.download_from_github_release( + "https://github.com/owner/repo/releases/download/v1.0.0/ghplugin-1.0.0.whl", + sha256=sha256, + ) assert plugin_name == "ghplugin" assert version == "1.0.0" @@ -927,11 +949,15 @@ def test_download_from_url_no_whl_extension(self, tmp_path: Path) -> None: "ggshield.core.plugin.downloader.get_plugins_dir", return_value=plugins_dir ): with patch("requests.get", return_value=mock_response): - downloader = PluginDownloader() - plugin_name, version, _ = downloader.download_from_url( - "https://example.com/download?file=something", - sha256=sha256, - ) + with patch( + "ggshield.core.plugin.downloader.verify_wheel_signature", + return_value=MOCK_SIG_INFO, + ): + downloader = PluginDownloader() + plugin_name, version, _ = downloader.download_from_url( + "https://example.com/download?file=something", + sha256=sha256, + ) assert plugin_name == "testplugin" assert version == "1.0.0" @@ -964,13 +990,17 @@ def test_github_artifact_success(self, tmp_path: Path) -> None: "ggshield.core.plugin.downloader.get_plugins_dir", return_value=plugins_dir ): with patch("requests.get", return_value=mock_response): - with patch.dict("os.environ", {"GITHUB_TOKEN": "test-token"}): - downloader = PluginDownloader() - plugin_name, version, installed_path = ( - downloader.download_from_github_artifact( - "https://github.com/owner/repo/actions/runs/123/artifacts/456" + with patch( + "ggshield.core.plugin.downloader.verify_wheel_signature", + return_value=MOCK_SIG_INFO, + ): + with patch.dict("os.environ", {"GITHUB_TOKEN": "test-token"}): + downloader = PluginDownloader() + plugin_name, version, installed_path = ( + downloader.download_from_github_artifact( + "https://github.com/owner/repo/actions/runs/123/artifacts/456" + ) ) - ) assert plugin_name == "artifactplugin" assert version == "1.0.0" diff --git a/tests/unit/core/plugin/test_enterprise_config.py b/tests/unit/core/plugin/test_enterprise_config.py index 123e1837a5..294ee1342b 100644 --- a/tests/unit/core/plugin/test_enterprise_config.py +++ b/tests/unit/core/plugin/test_enterprise_config.py @@ -3,8 +3,6 @@ from pathlib import Path from unittest.mock import patch -import pytest - from ggshield.core.config.enterprise_config import EnterpriseConfig, PluginConfig @@ -58,12 +56,14 @@ def test_enable_existing_plugin(self) -> None: assert config.plugins["test-plugin"].enabled is True assert config.plugins["test-plugin"].version == "1.0.0" - def test_disable_plugin_missing_raises(self) -> None: - """Test disabling a missing plugin raises an error.""" + def test_disable_plugin_missing_creates_config(self) -> None: + """Test disabling a missing plugin creates a disabled config entry.""" config = EnterpriseConfig() - with pytest.raises(ValueError, match="not configured"): - config.disable_plugin("test-plugin") + config.disable_plugin("test-plugin") + + assert "test-plugin" in config.plugins + assert config.plugins["test-plugin"].enabled is False def test_disable_existing_plugin(self) -> None: """Test disabling an already configured plugin.""" @@ -74,11 +74,11 @@ def test_disable_existing_plugin(self) -> None: assert config.plugins["test-plugin"].enabled is False def test_is_plugin_enabled_default(self) -> None: - """Test that plugins are disabled by default.""" + """Test that plugins are enabled by default.""" config = EnterpriseConfig() - # Plugin not in config should be considered disabled - assert config.is_plugin_enabled("nonexistent") is False + # Plugin not in config should be considered enabled by default + assert config.is_plugin_enabled("nonexistent") is True def test_is_plugin_enabled_explicit(self) -> None: """Test checking if plugin is enabled explicitly.""" diff --git a/tests/unit/core/plugin/test_loader.py b/tests/unit/core/plugin/test_loader.py index b17fdaee85..0fc9921953 100644 --- a/tests/unit/core/plugin/test_loader.py +++ b/tests/unit/core/plugin/test_loader.py @@ -14,6 +14,7 @@ parse_entry_point_from_content, ) from ggshield.core.plugin.registry import PluginRegistry +from ggshield.core.plugin.signature import SignatureVerificationMode class MockPlugin(GGShieldPlugin): @@ -559,7 +560,7 @@ def test_load_from_wheel_extracts_wheel(self, tmp_path: Path) -> None: import zipfile config = EnterpriseConfig() - loader = PluginLoader(config) + loader = PluginLoader(config, signature_mode=SignatureVerificationMode.DISABLED) # Create a mock wheel with a Python module wheel_path = tmp_path / "test_plugin-1.0.0.whl" @@ -619,7 +620,7 @@ def test_load_from_wheel_appends_to_sys_path( import zipfile config = EnterpriseConfig() - loader = PluginLoader(config) + loader = PluginLoader(config, signature_mode=SignatureVerificationMode.DISABLED) wheel_path = tmp_path / "test_plugin-1.0.0.whl" with zipfile.ZipFile(wheel_path, "w") as zf: diff --git a/tests/unit/core/plugin/test_signature.py b/tests/unit/core/plugin/test_signature.py new file mode 100644 index 0000000000..a57f6f331b --- /dev/null +++ b/tests/unit/core/plugin/test_signature.py @@ -0,0 +1,285 @@ +"""Tests for plugin signature verification.""" + +from contextlib import contextmanager +from pathlib import Path +from typing import Iterator +from unittest.mock import MagicMock, patch + +import pytest + +from ggshield.core.plugin.signature import ( + SignatureStatus, + SignatureVerificationError, + SignatureVerificationMode, + TrustedIdentity, + get_bundle_path, + verify_wheel_signature, +) + + +class TestGetBundlePath: + """Tests for get_bundle_path().""" + + def test_returns_sigstore_extension(self, tmp_path: Path) -> None: + wheel = tmp_path / "plugin-1.0.0.whl" + bundle = tmp_path / "plugin-1.0.0.whl.sigstore" + bundle.write_bytes(b"bundle") + result = get_bundle_path(wheel) + assert result == bundle + + def test_returns_sigstore_json_extension(self, tmp_path: Path) -> None: + wheel = tmp_path / "plugin-1.0.0.whl" + bundle = tmp_path / "plugin-1.0.0.whl.sigstore.json" + bundle.write_bytes(b"bundle") + result = get_bundle_path(wheel) + assert result == bundle + + def test_prefers_sigstore_over_sigstore_json(self, tmp_path: Path) -> None: + wheel = tmp_path / "plugin-1.0.0.whl" + (tmp_path / "plugin-1.0.0.whl.sigstore").write_bytes(b"bundle1") + (tmp_path / "plugin-1.0.0.whl.sigstore.json").write_bytes(b"bundle2") + result = get_bundle_path(wheel) + assert result == tmp_path / "plugin-1.0.0.whl.sigstore" + + def test_returns_none_when_no_bundle(self, tmp_path: Path) -> None: + wheel = tmp_path / "plugin-1.0.0.whl" + result = get_bundle_path(wheel) + assert result is None + + def test_preserves_parent_directory(self, tmp_path: Path) -> None: + subdir = tmp_path / "subdir" + subdir.mkdir() + wheel = subdir / "plugin.whl" + bundle = subdir / "plugin.whl.sigstore" + bundle.write_bytes(b"bundle") + result = get_bundle_path(wheel) + assert result is not None + assert result.parent == subdir + + +class TestSignatureVerificationModeDisabled: + """Tests for DISABLED mode.""" + + def test_returns_skipped(self, tmp_path: Path) -> None: + wheel = tmp_path / "plugin.whl" + wheel.write_bytes(b"fake wheel") + + result = verify_wheel_signature(wheel, SignatureVerificationMode.DISABLED) + + assert result.status == SignatureStatus.SKIPPED + + +class TestMissingBundle: + """Tests for missing .sigstore bundle.""" + + def test_strict_mode_raises(self, tmp_path: Path) -> None: + wheel = tmp_path / "plugin.whl" + wheel.write_bytes(b"fake wheel") + # No .sigstore file + + with pytest.raises(SignatureVerificationError) as exc_info: + verify_wheel_signature(wheel, SignatureVerificationMode.STRICT) + + assert exc_info.value.status == SignatureStatus.MISSING + + def test_warn_mode_returns_missing(self, tmp_path: Path) -> None: + wheel = tmp_path / "plugin.whl" + wheel.write_bytes(b"fake wheel") + + result = verify_wheel_signature(wheel, SignatureVerificationMode.WARN) + + assert result.status == SignatureStatus.MISSING + + +class TestBundleVerification: + """Tests for bundle verification with mocked sigstore.""" + + def _setup_wheel_and_bundle(self, tmp_path: Path) -> Path: + """Create a fake wheel and bundle file.""" + wheel = tmp_path / "plugin-1.0.0.whl" + wheel.write_bytes(b"fake wheel content") + bundle = tmp_path / "plugin-1.0.0.whl.sigstore" + bundle.write_bytes(b'{"fake": "bundle"}') + return wheel + + @staticmethod + @contextmanager + def _sigstore_modules( + mock_verifier_cls: MagicMock, + mock_bundle_cls: MagicMock, + mock_all_of_cls: MagicMock, + mock_oidc_issuer_cls: MagicMock, + mock_gh_repo_cls: MagicMock, + ) -> Iterator[None]: + """Patch the top-level sigstore imports in the signature module.""" + with ( + patch("ggshield.core.plugin.signature.Bundle", mock_bundle_cls), + patch("ggshield.core.plugin.signature.Verifier", mock_verifier_cls), + patch("ggshield.core.plugin.signature.AllOf", mock_all_of_cls), + patch( + "ggshield.core.plugin.signature.OIDCIssuer", + mock_oidc_issuer_cls, + ), + patch( + "ggshield.core.plugin.signature.GitHubWorkflowRepository", + mock_gh_repo_cls, + ), + ): + yield + + def test_valid_signature(self, tmp_path: Path) -> None: + """Test successful signature verification.""" + wheel = self._setup_wheel_and_bundle(tmp_path) + + mock_verifier_cls = MagicMock() + mock_bundle_cls = MagicMock() + mock_all_of_cls = MagicMock() + mock_oidc_issuer_cls = MagicMock() + mock_gh_repo_cls = MagicMock() + + mock_verifier = MagicMock() + mock_verifier_cls.production.return_value = mock_verifier + mock_verifier.verify_artifact.return_value = None # Success + + mock_bundle = MagicMock() + mock_bundle_cls.from_json.return_value = mock_bundle + + trusted = [ + TrustedIdentity( + repository="GitGuardian/satori", + issuer="https://token.actions.githubusercontent.com", + ) + ] + + with self._sigstore_modules( + mock_verifier_cls, + mock_bundle_cls, + mock_all_of_cls, + mock_oidc_issuer_cls, + mock_gh_repo_cls, + ): + result = verify_wheel_signature( + wheel, SignatureVerificationMode.STRICT, trusted + ) + + assert result.status == SignatureStatus.VALID + assert result.identity == trusted[0].repository + + def test_invalid_signature_strict_raises(self, tmp_path: Path) -> None: + """Test that invalid signature raises in STRICT mode.""" + wheel = self._setup_wheel_and_bundle(tmp_path) + + mock_verifier_cls = MagicMock() + mock_bundle_cls = MagicMock() + mock_all_of_cls = MagicMock() + mock_oidc_issuer_cls = MagicMock() + mock_gh_repo_cls = MagicMock() + + mock_verifier = MagicMock() + mock_verifier_cls.production.return_value = mock_verifier + mock_verifier.verify_artifact.side_effect = Exception("Verification failed") + + mock_bundle = MagicMock() + mock_bundle_cls.from_json.return_value = mock_bundle + + trusted = [ + TrustedIdentity( + repository="GitGuardian/satori", + issuer="https://token.actions.githubusercontent.com", + ) + ] + + with self._sigstore_modules( + mock_verifier_cls, + mock_bundle_cls, + mock_all_of_cls, + mock_oidc_issuer_cls, + mock_gh_repo_cls, + ): + with pytest.raises(SignatureVerificationError) as exc_info: + verify_wheel_signature(wheel, SignatureVerificationMode.STRICT, trusted) + + assert exc_info.value.status == SignatureStatus.INVALID + + def test_invalid_signature_warn_returns_invalid(self, tmp_path: Path) -> None: + """Test that invalid signature returns INVALID in WARN mode.""" + wheel = self._setup_wheel_and_bundle(tmp_path) + + mock_verifier_cls = MagicMock() + mock_bundle_cls = MagicMock() + mock_all_of_cls = MagicMock() + mock_oidc_issuer_cls = MagicMock() + mock_gh_repo_cls = MagicMock() + + mock_verifier = MagicMock() + mock_verifier_cls.production.return_value = mock_verifier + mock_verifier.verify_artifact.side_effect = Exception("Verification failed") + + mock_bundle = MagicMock() + mock_bundle_cls.from_json.return_value = mock_bundle + + trusted = [ + TrustedIdentity( + repository="org/repo", + issuer="https://token.actions.githubusercontent.com", + ) + ] + + with self._sigstore_modules( + mock_verifier_cls, + mock_bundle_cls, + mock_all_of_cls, + mock_oidc_issuer_cls, + mock_gh_repo_cls, + ): + result = verify_wheel_signature( + wheel, SignatureVerificationMode.WARN, trusted + ) + + assert result.status == SignatureStatus.INVALID + + def test_multi_identity_tries_all(self, tmp_path: Path) -> None: + """Test that multiple trusted identities are tried in order.""" + wheel = self._setup_wheel_and_bundle(tmp_path) + + mock_verifier_cls = MagicMock() + mock_bundle_cls = MagicMock() + mock_all_of_cls = MagicMock() + mock_oidc_issuer_cls = MagicMock() + mock_gh_repo_cls = MagicMock() + + mock_verifier = MagicMock() + mock_verifier_cls.production.return_value = mock_verifier + # First identity fails, second succeeds + mock_verifier.verify_artifact.side_effect = [ + Exception("Wrong identity"), + None, # Success + ] + + mock_bundle = MagicMock() + mock_bundle_cls.from_json.return_value = mock_bundle + + trusted = [ + TrustedIdentity( + repository="other/repo", + issuer="https://token.actions.githubusercontent.com", + ), + TrustedIdentity( + repository="GitGuardian/satori", + issuer="https://token.actions.githubusercontent.com", + ), + ] + + with self._sigstore_modules( + mock_verifier_cls, + mock_bundle_cls, + mock_all_of_cls, + mock_oidc_issuer_cls, + mock_gh_repo_cls, + ): + result = verify_wheel_signature( + wheel, SignatureVerificationMode.STRICT, trusted + ) + + assert result.status == SignatureStatus.VALID + assert result.identity == trusted[1].repository diff --git a/uv.lock b/uv.lock index 981ba67e3d..63395675df 100644 --- a/uv.lock +++ b/uv.lock @@ -17,7 +17,7 @@ overrides = [ [[package]] name = "altgraph" version = "0.17.5" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/f8/97fdf103f38fed6792a1601dbc16cc8aac56e7459a9fff08c812d8ae177a/altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7", size = 48428, upload-time = "2025-11-21T20:35:50.583Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a9/ba/000a1996d4308bc65120167c21241a3b205464a2e0b58deda26ae8ac21d1/altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597", size = 21228, upload-time = "2025-11-21T20:35:49.444Z" }, @@ -26,7 +26,7 @@ wheels = [ [[package]] name = "annotated-types" version = "0.7.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, @@ -35,7 +35,7 @@ wheels = [ [[package]] name = "aspy-refactor-imports" version = "3.0.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1d/2c/051d8bcdce88969dd9f2d96b97cdf7ce55e6fa54e609ad79fd3423f424a5/aspy.refactor_imports-3.0.2.tar.gz", hash = "sha256:3c7329cdb2613c46fcd757c8e45120efbc3d4b9db805092911eb605c19c5795c", size = 7617, upload-time = "2022-07-01T23:55:21.117Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fa/d3/18747b53e6b92bdc2bb3eae56f27f4aea6b6c1228a45372484984aea84c1/aspy.refactor_imports-3.0.2-py2.py3-none-any.whl", hash = "sha256:f306037682479945df61b2e6d01bf97256d68f3e704742768deef549e0d61fbb", size = 7873, upload-time = "2022-07-01T23:55:19.61Z" }, @@ -44,7 +44,7 @@ wheels = [ [[package]] name = "asttokens" version = "3.0.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, @@ -53,7 +53,7 @@ wheels = [ [[package]] name = "attrs" version = "25.4.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, @@ -62,7 +62,7 @@ wheels = [ [[package]] name = "backports-tarfile" version = "1.2.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, @@ -71,13 +71,14 @@ wheels = [ [[package]] name = "black" version = "24.3.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "click" }, { name = "mypy-extensions" }, { name = "packaging" }, { name = "pathspec" }, - { name = "platformdirs" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "platformdirs", version = "4.9.6", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] @@ -105,7 +106,7 @@ wheels = [ [[package]] name = "build" version = "1.4.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "colorama", marker = "os_name == 'nt'" }, { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, @@ -121,7 +122,7 @@ wheels = [ [[package]] name = "certifi" version = "2026.2.25" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, @@ -130,10 +131,10 @@ wheels = [ [[package]] name = "cffi" version = "2.0.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ - { name = "pycparser", version = "2.23", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and implementation_name != 'PyPy'" }, - { name = "pycparser", version = "3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and implementation_name != 'PyPy'" }, + { name = "pycparser", version = "2.23", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10' and implementation_name != 'PyPy'" }, + { name = "pycparser", version = "3.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10' and implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ @@ -225,7 +226,7 @@ wheels = [ [[package]] name = "cfgv" version = "3.4.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version < '3.10'", ] @@ -237,7 +238,7 @@ wheels = [ [[package]] name = "cfgv" version = "3.5.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", @@ -251,7 +252,7 @@ wheels = [ [[package]] name = "charset-normalizer" version = "3.1.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ff/d7/8d757f8bd45be079d76309248845a04f09619a7b17d6dfc8c9ff6433cac2/charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5", size = 95987, upload-time = "2023-03-06T09:49:37.964Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4f/a2/9031ba4a008e11a21d7b7aa41751290d2f2035a2f14ecb6e589771a17c47/charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b", size = 202016, upload-time = "2023-03-06T09:48:01.969Z" }, @@ -305,12 +306,13 @@ wheels = [ [[package]] name = "check-wheel-contents" version = "0.6.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "attrs" }, { name = "click" }, { name = "packaging" }, - { name = "pydantic" }, + { name = "pydantic", version = "2.11.10", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pydantic", version = "2.13.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "wheel-filename" }, ] @@ -322,7 +324,7 @@ wheels = [ [[package]] name = "click" version = "8.1.8" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] @@ -334,7 +336,7 @@ wheels = [ [[package]] name = "click-log" version = "0.4.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "click" }, ] @@ -346,25 +348,16 @@ wheels = [ [[package]] name = "colorama" version = "0.4.6" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] -[[package]] -name = "commonmark" -version = "0.9.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/60/48/a60f593447e8f0894ebb7f6e6c1f25dafc5e89c5879fdc9360ae93ff83f0/commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60", size = 95764, upload-time = "2019-10-04T15:37:39.817Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/92/dfd892312d822f36c55366118b95d914e5f16de11044a27cf10a7d71bbbf/commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9", size = 51068, upload-time = "2019-10-04T15:37:37.674Z" }, -] - [[package]] name = "coverage" version = "7.10.7" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version < '3.10'", ] @@ -478,7 +471,7 @@ wheels = [ [[package]] name = "coverage" version = "7.13.4" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", @@ -596,7 +589,7 @@ wheels = [ [[package]] name = "cryptography" version = "43.0.3" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] @@ -633,7 +626,7 @@ wheels = [ [[package]] name = "decorator" version = "5.2.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, @@ -642,16 +635,56 @@ wheels = [ [[package]] name = "distlib" version = "0.4.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] +[[package]] +name = "dnspython" +version = "2.7.0" +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload-time = "2024-10-05T20:14:59.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } +dependencies = [ + { name = "dnspython", version = "2.7.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "dnspython", version = "2.8.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] @@ -663,7 +696,7 @@ wheels = [ [[package]] name = "execnet" version = "2.1.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, @@ -672,7 +705,7 @@ wheels = [ [[package]] name = "executing" version = "2.2.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, @@ -681,10 +714,10 @@ wheels = [ [[package]] name = "factory-boy" version = "3.3.3" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ - { name = "faker", version = "37.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "faker", version = "40.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "faker", version = "37.12.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "faker", version = "40.8.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ba/98/75cacae9945f67cfe323829fc2ac451f64517a8a330b572a06a323997065/factory_boy-3.3.3.tar.gz", hash = "sha256:866862d226128dfac7f2b4160287e899daf54f2612778327dd03d0e2cb1e3d03", size = 164146, upload-time = "2025-02-03T09:49:04.433Z" } wheels = [ @@ -694,7 +727,7 @@ wheels = [ [[package]] name = "faker" version = "37.12.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version < '3.10'", ] @@ -709,7 +742,7 @@ wheels = [ [[package]] name = "faker" version = "40.8.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", @@ -726,7 +759,7 @@ wheels = [ [[package]] name = "filelock" version = "3.19.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version < '3.10'", ] @@ -738,7 +771,7 @@ wheels = [ [[package]] name = "filelock" version = "3.25.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", @@ -752,7 +785,7 @@ wheels = [ [[package]] name = "flake8" version = "7.3.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "mccabe" }, { name = "pycodestyle" }, @@ -766,13 +799,13 @@ wheels = [ [[package]] name = "flake8-isort" version = "6.1.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ { name = "flake8", marker = "python_full_version < '3.10'" }, - { name = "isort", version = "6.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "isort", version = "6.1.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7c/ea/2f2662d4fefa6ab335c7119cb28e5bc57c935a86a69a7f72df3ea5fe7b2c/flake8_isort-6.1.2.tar.gz", hash = "sha256:9d0452acdf0e1cd6f2d6848e3605e66b54d920e73471fb4744eef0f93df62d5d", size = 17756, upload-time = "2025-01-29T12:29:25.753Z" } wheels = [ @@ -782,7 +815,7 @@ wheels = [ [[package]] name = "flake8-isort" version = "7.0.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", @@ -790,7 +823,7 @@ resolution-markers = [ ] dependencies = [ { name = "flake8", marker = "python_full_version >= '3.10'" }, - { name = "isort", version = "8.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "isort", version = "8.0.1", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/57/a4/4b170983fd2e9b5ecc40c88738db6dfb77e069c71be9a81f9893cdb8a0cc/flake8_isort-7.0.0.tar.gz", hash = "sha256:a677199d1197f826eb69084e7ac272f208f4583363285f43111c34272abe7e5d", size = 17796, upload-time = "2025-10-25T13:31:08.768Z" } wheels = [ @@ -800,7 +833,7 @@ wheels = [ [[package]] name = "flake8-quotes" version = "3.4.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "flake8" }, { name = "setuptools" }, @@ -820,13 +853,16 @@ dependencies = [ { name = "notify-py" }, { name = "oauthlib" }, { name = "packaging" }, - { name = "platformdirs" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "platformdirs", version = "4.9.6", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pygitguardian" }, { name = "pyjwt" }, { name = "python-dotenv" }, { name = "pyyaml" }, { name = "requests" }, { name = "rich" }, + { name = "sigstore", version = "4.1.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sigstore", version = "4.2.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, { name = "truststore", marker = "python_full_version >= '3.10'" }, { name = "typing-extensions" }, { name = "urllib3" }, @@ -837,42 +873,42 @@ dev = [ { name = "black" }, { name = "build" }, { name = "check-wheel-contents" }, - { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "coverage", version = "7.13.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "coverage", version = "7.10.7", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "coverage", version = "7.13.4", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, { name = "flake8" }, - { name = "flake8-isort", version = "6.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "flake8-isort", version = "7.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "flake8-isort", version = "6.1.2", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "flake8-isort", version = "7.0.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, { name = "flake8-quotes" }, { name = "ipdb" }, - { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "ipython", version = "9.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "pre-commit", version = "4.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pre-commit", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "ipython", version = "8.18.1", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "ipython", version = "8.38.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "ipython", version = "9.10.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "ipython", version = "9.11.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "pre-commit", version = "4.3.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pre-commit", version = "4.5.1", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pyright" }, - { name = "scriv", version = "1.7.0", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" }, - { name = "scriv", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, + { name = "scriv", version = "1.7.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" }, + { name = "scriv", version = "1.8.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, ] standalone = [ { name = "pyinstaller" }, ] tests = [ { name = "factory-boy" }, - { name = "import-linter", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "import-linter", version = "2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "jsonschema", version = "4.25.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "jsonschema", version = "4.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "import-linter", version = "2.5.2", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "import-linter", version = "2.6", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "jsonschema", version = "4.25.1", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jsonschema", version = "4.26.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pyfakefs" }, { name = "pytest-mock" }, { name = "pytest-socket" }, { name = "pytest-voluptuous" }, { name = "pytest-xdist" }, { name = "seed-isort-config" }, - { name = "syrupy", version = "4.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "syrupy", version = "5.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "vcrpy", version = "7.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "vcrpy", version = "8.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "syrupy", version = "4.9.1", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "syrupy", version = "5.1.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "vcrpy", version = "7.0.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "vcrpy", version = "8.1.1", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, { name = "voluptuous" }, ] @@ -887,15 +923,16 @@ requires-dist = [ { name = "notify-py", specifier = ">=0.3.43" }, { name = "oauthlib", specifier = "~=3.2.1" }, { name = "packaging", specifier = ">=22.0" }, - { name = "platformdirs", specifier = "~=3.0.0" }, + { name = "platformdirs", specifier = "~=4.2" }, { name = "pygitguardian", specifier = "~=1.28.0" }, { name = "pyjwt", specifier = "~=2.6.0" }, { name = "python-dotenv", specifier = "~=0.21.0" }, { name = "pyyaml", specifier = "~=6.0.1" }, { name = "requests", specifier = "~=2.32.0" }, - { name = "rich", specifier = "~=12.5.1" }, + { name = "rich", specifier = "~=13.0" }, + { name = "sigstore", specifier = ">=3.0.0" }, { name = "truststore", marker = "python_full_version >= '3.10'", specifier = ">=0.10.1" }, - { name = "typing-extensions", specifier = "~=4.12.2" }, + { name = "typing-extensions", specifier = "~=4.14" }, { name = "urllib3", specifier = ">=2.2.2,<3" }, ] @@ -933,7 +970,7 @@ tests = [ [[package]] name = "grimp" version = "3.13" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "typing-extensions" }, ] @@ -1051,10 +1088,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/8f/7ac3c08c12137810fb5dee621f48875a359a92efd6090e859180f2786961/grimp-3.13-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:7cbfde149e00986270b3c3552c67fefa101282f4ec9c5bfd08e7fd1878596629", size = 2384379, upload-time = "2025-10-29T13:04:56.408Z" }, ] +[[package]] +name = "id" +version = "1.6.1" +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/04/c2156091427636080787aac190019dc64096e56a23b7364d3c1764ee3a06/id-1.6.1.tar.gz", hash = "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069", size = 18088, upload-time = "2026-02-04T16:19:41.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl", hash = "sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca", size = 14689, upload-time = "2026-02-04T16:19:40.051Z" }, +] + [[package]] name = "identify" version = "2.6.15" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version < '3.10'", ] @@ -1066,7 +1115,7 @@ wheels = [ [[package]] name = "identify" version = "2.6.17" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", @@ -1080,7 +1129,7 @@ wheels = [ [[package]] name = "idna" version = "3.11" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, @@ -1089,7 +1138,7 @@ wheels = [ [[package]] name = "import-linter" version = "2.5.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version < '3.10'", ] @@ -1107,7 +1156,7 @@ wheels = [ [[package]] name = "import-linter" version = "2.6" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", @@ -1127,7 +1176,7 @@ wheels = [ [[package]] name = "importlib-metadata" version = "8.7.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "zipp", marker = "python_full_version < '3.12'" }, ] @@ -1136,10 +1185,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, ] +[[package]] +name = "importlib-resources" +version = "5.13.0" +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/f1/8711c49ffd121083007a24c1bff0d324c9ff621d4fdf8b4ffcb8d9e60330/importlib_resources-5.13.0.tar.gz", hash = "sha256:82d5c6cca930697dbbd86c93333bb2c2e72861d4789a11c2662b933e5ad2b528", size = 36550, upload-time = "2023-07-07T16:21:00.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/68/bd9dd6bbf06772c7accce77d0354d783333fbe712a60b08fc13540c05422/importlib_resources-5.13.0-py3-none-any.whl", hash = "sha256:9f7bd0c97b79972a6cce36a366356d16d5e13b09679c11a58f1014bfdf8e64b2", size = 32912, upload-time = "2023-07-07T16:20:58.128Z" }, +] + [[package]] name = "iniconfig" version = "2.1.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version < '3.10'", ] @@ -1151,7 +1212,7 @@ wheels = [ [[package]] name = "iniconfig" version = "2.3.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", @@ -1165,13 +1226,13 @@ wheels = [ [[package]] name = "ipdb" version = "0.13.13" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "decorator" }, - { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "ipython", version = "9.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "ipython", version = "8.18.1", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "ipython", version = "8.38.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "ipython", version = "9.10.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "ipython", version = "9.11.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.12'" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/1b/7e07e7b752017f7693a0f4d41c13e5ca29ce8cbcfdcc1fd6c4ad8c0a27a0/ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726", size = 17042, upload-time = "2023-03-09T15:40:57.487Z" } @@ -1182,7 +1243,7 @@ wheels = [ [[package]] name = "ipython" version = "8.18.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version < '3.10'", ] @@ -1207,7 +1268,7 @@ wheels = [ [[package]] name = "ipython" version = "8.38.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version == '3.10.*'", ] @@ -1232,7 +1293,7 @@ wheels = [ [[package]] name = "ipython" version = "9.10.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version == '3.11.*'", ] @@ -1257,7 +1318,7 @@ wheels = [ [[package]] name = "ipython" version = "9.11.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version >= '3.12'", ] @@ -1281,7 +1342,7 @@ wheels = [ [[package]] name = "ipython-pygments-lexers" version = "1.1.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "pygments", marker = "python_full_version >= '3.11'" }, ] @@ -1293,7 +1354,7 @@ wheels = [ [[package]] name = "isort" version = "6.1.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version < '3.10'", ] @@ -1308,7 +1369,7 @@ wheels = [ [[package]] name = "isort" version = "8.0.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", @@ -1322,7 +1383,7 @@ wheels = [ [[package]] name = "jaraco-classes" version = "3.4.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "more-itertools" }, ] @@ -1334,7 +1395,7 @@ wheels = [ [[package]] name = "jaraco-context" version = "6.1.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version < '3.10'", ] @@ -1349,7 +1410,7 @@ wheels = [ [[package]] name = "jaraco-context" version = "6.1.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", @@ -1366,7 +1427,7 @@ wheels = [ [[package]] name = "jaraco-functools" version = "4.4.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "more-itertools" }, ] @@ -1378,7 +1439,7 @@ wheels = [ [[package]] name = "jedi" version = "0.19.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "parso" }, ] @@ -1390,7 +1451,7 @@ wheels = [ [[package]] name = "jeepney" version = "0.9.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, @@ -1399,7 +1460,7 @@ wheels = [ [[package]] name = "jinja2" version = "3.1.6" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "markupsafe" }, ] @@ -1411,15 +1472,15 @@ wheels = [ [[package]] name = "jsonschema" version = "4.25.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ { name = "attrs", marker = "python_full_version < '3.10'" }, { name = "jsonschema-specifications", marker = "python_full_version < '3.10'" }, - { name = "referencing", version = "0.36.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "rpds-py", version = "0.27.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "referencing", version = "0.36.2", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "rpds-py", version = "0.27.1", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } wheels = [ @@ -1429,7 +1490,7 @@ wheels = [ [[package]] name = "jsonschema" version = "4.26.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", @@ -1438,8 +1499,8 @@ resolution-markers = [ dependencies = [ { name = "attrs", marker = "python_full_version >= '3.10'" }, { name = "jsonschema-specifications", marker = "python_full_version >= '3.10'" }, - { name = "referencing", version = "0.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "referencing", version = "0.37.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } wheels = [ @@ -1449,10 +1510,10 @@ wheels = [ [[package]] name = "jsonschema-specifications" version = "2025.9.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ - { name = "referencing", version = "0.36.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "referencing", version = "0.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "referencing", version = "0.36.2", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "referencing", version = "0.37.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } wheels = [ @@ -1462,17 +1523,17 @@ wheels = [ [[package]] name = "keyring" version = "25.7.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, { name = "jaraco-classes" }, - { name = "jaraco-context", version = "6.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "jaraco-context", version = "6.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "jaraco-context", version = "6.1.1", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jaraco-context", version = "6.1.2", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, { name = "jaraco-functools" }, { name = "jeepney", marker = "sys_platform == 'linux'" }, { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, - { name = "secretstorage", version = "3.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and sys_platform == 'linux'" }, - { name = "secretstorage", version = "3.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and sys_platform == 'linux'" }, + { name = "secretstorage", version = "3.3.3", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10' and sys_platform == 'linux'" }, + { name = "secretstorage", version = "3.5.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10' and sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } wheels = [ @@ -1482,7 +1543,7 @@ wheels = [ [[package]] name = "loguru" version = "0.6.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "win32-setctime", marker = "sys_platform == 'win32'" }, @@ -1495,7 +1556,7 @@ wheels = [ [[package]] name = "macholib" version = "1.16.4" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "altgraph" }, ] @@ -1507,7 +1568,7 @@ wheels = [ [[package]] name = "markdown-it-py" version = "3.0.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version < '3.10'", ] @@ -1522,7 +1583,7 @@ wheels = [ [[package]] name = "markdown-it-py" version = "4.0.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", @@ -1539,7 +1600,7 @@ wheels = [ [[package]] name = "markupsafe" version = "3.0.3" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, @@ -1634,20 +1695,20 @@ wheels = [ [[package]] name = "marshmallow" -version = "3.26.2" -source = { registry = "https://pypi.org/simple" } +version = "3.18.0" +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/79/de6c16cc902f4fc372236926b0ce2ab7845268dcc30fb2fbb7f71b418631/marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57", size = 222095, upload-time = "2025-12-22T06:53:53.309Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/b9/b1da16dac90ed19806ea466636ae387957eec8cd429ac3b763e21b99a77d/marshmallow-3.18.0.tar.gz", hash = "sha256:6804c16114f7fce1f5b4dadc31f4674af23317fcc7f075da21e35c1a35d781f7", size = 182746, upload-time = "2022-09-15T20:27:14.362Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/2f/5108cb3ee4ba6501748c4908b908e55f42a5b66245b4cfe0c99326e1ef6e/marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73", size = 50964, upload-time = "2025-12-22T06:53:51.801Z" }, + { url = "https://files.pythonhosted.org/packages/c3/06/e0300cb5f9b5ff9b6d0accdd3536c01bd2300f8154781455914752ab8903/marshmallow-3.18.0-py3-none-any.whl", hash = "sha256:35e02a3a06899c9119b785c12a22f4cda361745d66a71ab691fd7610202ae104", size = 48752, upload-time = "2022-09-15T20:27:12.356Z" }, ] [[package]] name = "marshmallow-dataclass" version = "8.5.14" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "marshmallow" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, @@ -1661,7 +1722,7 @@ wheels = [ [[package]] name = "matplotlib-inline" version = "0.2.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "traitlets" }, ] @@ -1673,7 +1734,7 @@ wheels = [ [[package]] name = "mccabe" version = "0.7.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, @@ -1682,7 +1743,7 @@ wheels = [ [[package]] name = "mdurl" version = "0.1.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, @@ -1691,7 +1752,7 @@ wheels = [ [[package]] name = "more-itertools" version = "10.8.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, @@ -1700,7 +1761,7 @@ wheels = [ [[package]] name = "multidict" version = "6.7.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] @@ -1856,7 +1917,7 @@ wheels = [ [[package]] name = "mypy-extensions" version = "1.1.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, @@ -1865,7 +1926,7 @@ wheels = [ [[package]] name = "nodeenv" version = "1.10.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, @@ -1874,7 +1935,7 @@ wheels = [ [[package]] name = "notify-py" version = "0.3.43" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "jeepney", marker = "sys_platform == 'linux'" }, { name = "loguru" }, @@ -1887,7 +1948,7 @@ wheels = [ [[package]] name = "oauthlib" version = "3.2.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6d/fa/fbf4001037904031639e6bfbfc02badfc7e12f137a8afa254df6c4c8a670/oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918", size = 177352, upload-time = "2022-10-17T20:04:27.471Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", size = 151688, upload-time = "2022-10-17T20:04:24.037Z" }, @@ -1896,7 +1957,7 @@ wheels = [ [[package]] name = "packaging" version = "26.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, @@ -1905,7 +1966,7 @@ wheels = [ [[package]] name = "parso" version = "0.8.6" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, @@ -1914,7 +1975,7 @@ wheels = [ [[package]] name = "pathspec" version = "1.0.4" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, @@ -1923,7 +1984,7 @@ wheels = [ [[package]] name = "pefile" version = "2024.8.26" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/03/4f/2750f7f6f025a1507cd3b7218691671eecfd0bbebebe8b39aa0fe1d360b8/pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632", size = 76008, upload-time = "2024-08-26T20:58:38.155Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" }, @@ -1932,7 +1993,7 @@ wheels = [ [[package]] name = "pexpect" version = "4.9.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "ptyprocess" }, ] @@ -1943,17 +2004,34 @@ wheels = [ [[package]] name = "platformdirs" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/39/702094fc1434a4408783b071665d9f5d8a1d0ba4dddf9dadf3d50e6eb762/platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9", size = 14462, upload-time = "2023-02-06T21:32:30.098Z" } +version = "4.4.0" +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/24/a83a900a90105f8ad3f20df5bb5a2cde886df7125c7827e196e4ed4fa8a7/platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567", size = 14402, upload-time = "2023-02-06T21:32:28.228Z" }, + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, ] [[package]] name = "pluggy" version = "1.6.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, @@ -1962,13 +2040,13 @@ wheels = [ [[package]] name = "pre-commit" version = "4.3.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "cfgv", version = "3.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "identify", version = "2.6.15", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "cfgv", version = "3.4.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "identify", version = "2.6.15", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, { name = "nodeenv", marker = "python_full_version < '3.10'" }, { name = "pyyaml", marker = "python_full_version < '3.10'" }, { name = "virtualenv", marker = "python_full_version < '3.10'" }, @@ -1981,15 +2059,15 @@ wheels = [ [[package]] name = "pre-commit" version = "4.5.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", "python_full_version == '3.10.*'", ] dependencies = [ - { name = "cfgv", version = "3.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "identify", version = "2.6.17", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "cfgv", version = "3.5.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "identify", version = "2.6.17", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, { name = "nodeenv", marker = "python_full_version >= '3.10'" }, { name = "pyyaml", marker = "python_full_version >= '3.10'" }, { name = "virtualenv", marker = "python_full_version >= '3.10'" }, @@ -2002,7 +2080,7 @@ wheels = [ [[package]] name = "prompt-toolkit" version = "3.0.52" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "wcwidth" }, ] @@ -2014,7 +2092,7 @@ wheels = [ [[package]] name = "propcache" version = "0.4.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, @@ -2143,7 +2221,7 @@ wheels = [ [[package]] name = "ptyprocess" version = "0.7.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, @@ -2152,16 +2230,25 @@ wheels = [ [[package]] name = "pure-eval" version = "0.2.3" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, ] +[[package]] +name = "pyasn1" +version = "0.6.3" +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, +] + [[package]] name = "pycodestyle" version = "2.14.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, @@ -2170,7 +2257,7 @@ wheels = [ [[package]] name = "pycparser" version = "2.23" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version < '3.10'", ] @@ -2182,7 +2269,7 @@ wheels = [ [[package]] name = "pycparser" version = "3.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", @@ -2196,24 +2283,60 @@ wheels = [ [[package]] name = "pydantic" version = "2.11.10" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, + { name = "annotated-types", marker = "python_full_version < '3.10'" }, + { name = "pydantic-core", version = "2.33.2", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, + { name = "typing-inspection", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ae/54/ecab642b3bed45f7d5f59b38443dcb36ef50f85af192e6ece103dbfe9587/pydantic-2.11.10.tar.gz", hash = "sha256:dc280f0982fbda6c38fada4e476dc0a4f3aeaf9c6ad4c28df68a666ec3c61423", size = 788494, upload-time = "2025-10-04T10:40:41.338Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/bd/1f/73c53fcbfb0b5a78f91176df41945ca466e71e9d9d836e5c522abda39ee7/pydantic-2.11.10-py3-none-any.whl", hash = "sha256:802a655709d49bd004c31e865ef37da30b540786a46bfce02333e0e24b5fe29a", size = 444823, upload-time = "2025-10-04T10:40:39.055Z" }, ] +[package.optional-dependencies] +email = [ + { name = "email-validator", marker = "python_full_version < '3.10'" }, +] + +[[package]] +name = "pydantic" +version = "2.13.0" +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "annotated-types", marker = "python_full_version >= '3.10'" }, + { name = "pydantic-core", version = "2.46.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/6b/69fd5c7194b21ebde0f8637e2a4ddc766ada29d472bfa6a5ca533d79549a/pydantic-2.13.0.tar.gz", hash = "sha256:b89b575b6e670ebf6e7448c01b41b244f471edd276cd0b6fe02e7e7aca320070", size = 843468, upload-time = "2026-04-13T10:51:35.571Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/d7/c3a52c61f5b7be648e919005820fbac33028c6149994cd64453f49951c17/pydantic-2.13.0-py3-none-any.whl", hash = "sha256:ab0078b90da5f3e2fd2e71e3d9b457ddcb35d0350854fbda93b451e28d56baaf", size = 471872, upload-time = "2026-04-13T10:51:33.343Z" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator", marker = "python_full_version >= '3.10'" }, +] + [[package]] name = "pydantic-core" version = "2.33.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } wheels = [ @@ -2317,10 +2440,145 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d4/29/3cade8a924a61f60ccfa10842f75eb12787e1440e2b8660ceffeb26685e7/pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27", size = 2066661, upload-time = "2025-04-23T18:33:49.995Z" }, ] +[[package]] +name = "pydantic-core" +version = "2.46.0" +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/0a/9414cddf82eda3976b14048cc0fa8f5b5d1aecb0b22e1dcd2dbfe0e139b1/pydantic_core-2.46.0.tar.gz", hash = "sha256:82d2498c96be47b47e903e1378d1d0f770097ec56ea953322f39936a7cf34977", size = 471441, upload-time = "2026-04-13T09:06:33.813Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/17/fd3ba2f035ac7b3a1ae0c55e5c0f6eb5275e87ad80a9b277cb2e70317e2c/pydantic_core-2.46.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d449eae37d6b066d8a8be0e3a7d7041712d6e9152869e7d03c203795aae44ed", size = 2122942, upload-time = "2026-04-13T09:04:32.413Z" }, + { url = "https://files.pythonhosted.org/packages/01/b5/214cb10e4050f430f383a21496087c1e51d583eec3c884b0e5f55c34eb69/pydantic_core-2.46.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4f7bfc1ffee4ddc03c2db472c7607a238dbbf76f7f64104fc6a623d47fb8e310", size = 1949068, upload-time = "2026-04-13T09:05:28.803Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ab/8ab4ec2a879eead4bb51c3e9af65583e16cc504867e808909cd4f991a5ae/pydantic_core-2.46.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a30f5d1d4e1c958b44b5c777a0d1adcd930429f35101e4780281ffbe11103925", size = 1974362, upload-time = "2026-04-13T09:05:26.894Z" }, + { url = "https://files.pythonhosted.org/packages/8f/dd/dc8ef47e18ddcab169af68b3c11648e1ef85c56aa18e2f96312cc5442404/pydantic_core-2.46.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f68e12d2de32ac6313a7d3854f346d71731288184fbbfc9004e368714244d2cd", size = 2043754, upload-time = "2026-04-13T09:04:54.637Z" }, + { url = "https://files.pythonhosted.org/packages/7f/52/69195c8f6549d2b1b9ce0efbb9bf169b47dcb9a60f81ff53a67cb22d8fc7/pydantic_core-2.46.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7d1a058fb5aff8a1a221e7d8a0cf5b0133d069b2f293cb05f174c61bc7cdac34", size = 2230099, upload-time = "2026-04-13T09:04:44.37Z" }, + { url = "https://files.pythonhosted.org/packages/2a/41/48c8e7709604a4230f86f77bc17e1eb575e0894831f2c3beaecb3e8f7583/pydantic_core-2.46.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbd01128431f355e309267283e37e23704f24558e9059d930e213a377b1be919", size = 2293730, upload-time = "2026-04-13T09:04:27.583Z" }, + { url = "https://files.pythonhosted.org/packages/08/ab/f3bc576d37eb3036f7b1b2721ab0f89e4684fab48e1de1d0eca0dfef7469/pydantic_core-2.46.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7747a50d9f75fe264b9e2091a2f462a7dd400add8723a87a75240106b6f4d949", size = 2095380, upload-time = "2026-04-13T09:04:45.929Z" }, + { url = "https://files.pythonhosted.org/packages/fe/69/0f6e5bd9c5594b41deb91029ad0b16ffe5a270dd412033dd1135a40bbfa3/pydantic_core-2.46.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:1d9b841e9c82a9cdf397a720bb8a4f2d6da6780204e1eb07c2d90c4b5b791b0d", size = 2140115, upload-time = "2026-04-13T09:07:00.944Z" }, + { url = "https://files.pythonhosted.org/packages/28/7c/79cfc18d352797b84a7c5b27171d6557121843729bc637a90550d08370fd/pydantic_core-2.46.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:61d0f5951b7b86ec24e24fe0c5a2cce7c360830026dfbe004954e8fac9918b95", size = 2183044, upload-time = "2026-04-13T09:03:58.106Z" }, + { url = "https://files.pythonhosted.org/packages/59/bc/701b17bf7fd375e59e03838cffe8f6893498503b7d412d577ffd92dab56c/pydantic_core-2.46.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:aec0be48d2555ceac04905ffb8f2bb7e55a56644858891196191827b6fc656b7", size = 2185277, upload-time = "2026-04-13T09:05:52.482Z" }, + { url = "https://files.pythonhosted.org/packages/c3/43/ad927b8861ab787b4189ddb2dd70ebcdc20c5a4baf52df94934d6f87d730/pydantic_core-2.46.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:2c1ec2ced44a8a479d71a14f5be35461360acd388987873a8e0a02f7f81c8ec2", size = 2329998, upload-time = "2026-04-13T09:05:54.803Z" }, + { url = "https://files.pythonhosted.org/packages/47/33/ad11d56b97ea986f991da998d551a7513d19c06ed05a529e86520430e10e/pydantic_core-2.46.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5e157a25eed281f5e40119078e3dbf698c28b3d88ff0176eea3dd37191447b8d", size = 2369004, upload-time = "2026-04-13T09:05:14.052Z" }, + { url = "https://files.pythonhosted.org/packages/16/d1/a9a28a122f1227dc13fdd361d77a3f2df4aee64e4ac5693d7ce74a8ecfa4/pydantic_core-2.46.0-cp310-cp310-win32.whl", hash = "sha256:311929d9bfdb9fdbaf28beb39d88a1e36ca6dc5424ceca6d3bf81c9e1da2313c", size = 1982879, upload-time = "2026-04-13T09:05:19.277Z" }, + { url = "https://files.pythonhosted.org/packages/94/9a/52988a743cf7a9d84861e380c6a5496589aebbc3592d9ecdecb13c6bd0a2/pydantic_core-2.46.0-cp310-cp310-win_amd64.whl", hash = "sha256:60edfb53b13fbe7be9bb51447016b7bcd8772beb8ca216873be33e9d11b2c8e8", size = 2068907, upload-time = "2026-04-13T09:03:59.541Z" }, + { url = "https://files.pythonhosted.org/packages/ce/43/9bc38d43a6a48794209e4eb6d61e9c68395f69b7949f66842854b0cd1344/pydantic_core-2.46.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0027da787ae711f7fbd5a76cb0bb8df526acba6c10c1e44581de1b838db10b7b", size = 2121004, upload-time = "2026-04-13T09:05:17.531Z" }, + { url = "https://files.pythonhosted.org/packages/8c/1d/f43342b7107939b305b5e4efeef7d54e267a5ef51515570a5c1d77726efb/pydantic_core-2.46.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:63e288fc18d7eaeef5f16c73e65c4fd0ad95b25e7e21d8a5da144977b35eb997", size = 1947505, upload-time = "2026-04-13T09:04:48.975Z" }, + { url = "https://files.pythonhosted.org/packages/4a/cd/ccf48cbbcaf0d99ba65969459ebfbf7037600b2cfdcca3062084dd83a008/pydantic_core-2.46.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:080a3bdc6807089a1fe1fbc076519cea287f1a964725731d80b49d8ecffaa217", size = 1973301, upload-time = "2026-04-13T09:05:42.149Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ff/a7bb1e7a762fb1f40ad5ef4e6a92c012864a017b7b1fdfb71cf91faa8b73/pydantic_core-2.46.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c065f1c3e54c3e79d909927a8cb48ccbc17b68733552161eba3e0628c38e5d19", size = 2042208, upload-time = "2026-04-13T09:05:32.591Z" }, + { url = "https://files.pythonhosted.org/packages/ea/64/d3f11c6f6ace71526f3b03646df95eaab3f21edd13e00daae3f20f4e5a09/pydantic_core-2.46.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7e2db58ab46cfe602d4255381cce515585998c3b6699d5b1f909f519bc44a5aa", size = 2229046, upload-time = "2026-04-13T09:04:18.59Z" }, + { url = "https://files.pythonhosted.org/packages/d0/64/93db9a63cce71630c58b376d63de498aa93cb341c72cd5f189b5c08f5c28/pydantic_core-2.46.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c660974890ec1e4c65cff93f5670a5f451039f65463e9f9c03ad49746b49fc78", size = 2292138, upload-time = "2026-04-13T09:04:13.816Z" }, + { url = "https://files.pythonhosted.org/packages/e9/96/936fccce22f1f2ae8b2b694de651c2c929847be5f701c927a0bb3b1eb679/pydantic_core-2.46.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3be91482a8db77377c902cca87697388a4fb68addeb3e943ac74f425201a099", size = 2093333, upload-time = "2026-04-13T09:05:15.729Z" }, + { url = "https://files.pythonhosted.org/packages/75/76/c325e7fda69d589e26e772272044fe704c7e525c47d0d32a74f8345ac657/pydantic_core-2.46.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:1c72de82115233112d70d07f26a48cf6996eb86f7e143423ec1a182148455a9d", size = 2138802, upload-time = "2026-04-13T09:03:51.142Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6f/ccaa2ff7d53a017b66841e2d38edd1f38d19ae1a2d0c5efee17f2d432229/pydantic_core-2.46.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7904e58768cd79304b992868d7710bfc85dc6c7ed6163f0f68dbc1dcd72dc231", size = 2181358, upload-time = "2026-04-13T09:04:30.737Z" }, + { url = "https://files.pythonhosted.org/packages/6c/71/0c4b6303e92d63edcb81f5301695cdf70bb351775b4733eea65acdac8384/pydantic_core-2.46.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1af8d88718005f57bb4768f92f4ff16bf31a747d39dfc919b22211b84e72c053", size = 2183985, upload-time = "2026-04-13T09:04:06.792Z" }, + { url = "https://files.pythonhosted.org/packages/71/eb/f6bf255de38a4393aaa10bff224e882b630576bc26ebfb401e42bb965092/pydantic_core-2.46.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:a5b891301b02770a5852253f4b97f8bd192e5710067bc129e20d43db5403ede2", size = 2328559, upload-time = "2026-04-13T09:06:14.143Z" }, + { url = "https://files.pythonhosted.org/packages/f2/71/93895a1545f50823a24b21d7761c2bd1b1afea7a6ddc019787caec237361/pydantic_core-2.46.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:48b671fe59031fd9754c7384ac05b3ed47a0cccb7d4db0ec56121f0e6a541b90", size = 2367466, upload-time = "2026-04-13T09:05:59.613Z" }, + { url = "https://files.pythonhosted.org/packages/78/39/62331b3e71f41fb13d486621e2aec49900ba56567fb3a0ae5999fded0005/pydantic_core-2.46.0-cp311-cp311-win32.whl", hash = "sha256:0a52b7262b6cc67033823e9549a41bb77580ac299dc964baae4e9c182b2e335c", size = 1981367, upload-time = "2026-04-13T09:07:37.563Z" }, + { url = "https://files.pythonhosted.org/packages/9f/51/caac70958420e2d6115962f550676df59647c11f96a44c2fcb61662fcd16/pydantic_core-2.46.0-cp311-cp311-win_amd64.whl", hash = "sha256:4103fea1beeef6b3a9fed8515f27d4fa30c929a1973655adf8f454dc49ee0662", size = 2065942, upload-time = "2026-04-13T09:06:37.873Z" }, + { url = "https://files.pythonhosted.org/packages/b2/cf/576b2a4eb5500a1a5da485613b1ea8bc0d7279b27e0426801574b284ae65/pydantic_core-2.46.0-cp311-cp311-win_arm64.whl", hash = "sha256:3137cd88938adb8e567c5e938e486adc7e518ffc96b4ae1ec268e6a4275704d7", size = 2052532, upload-time = "2026-04-13T09:06:03.697Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d2/206c72ad47071559142a35f71efc29eb16448a4a5ae9487230ab8e4e292b/pydantic_core-2.46.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:66ccedb02c934622612448489824955838a221b3a35875458970521ef17b2f9c", size = 2117060, upload-time = "2026-04-13T09:04:47.443Z" }, + { url = "https://files.pythonhosted.org/packages/17/2c/7a53b33f91c8b77e696b1a6aa3bed609bf9374bdc0f8dcda681bc7d922b8/pydantic_core-2.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a44f27f4d2788ef9876ec47a43739b118c5904d74f418f53398f6ced3bbcacf2", size = 1951802, upload-time = "2026-04-13T09:05:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/fc/20/90e548c1f6d38800ef11c915881525770ce270d8e5e887563ff046a08674/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26a1032bcce6ca4b4670eb3f7d8195bd0a8b8f255f1307823e217ca3cfa7c27", size = 1976621, upload-time = "2026-04-13T09:04:03.909Z" }, + { url = "https://files.pythonhosted.org/packages/20/3c/9c5810ca70b60c623488cdd80f7e9ee1a0812df81e97098b64788719860f/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b8d1412f725060527e56675904b17a2d421dddcf861eecf7c75b9dda47921a4", size = 2056721, upload-time = "2026-04-13T09:04:40.992Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a3/d6e5f4cdec84278431c75540f90838c9d0a4dfe9402a8f3902073660ff28/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc3d1569edd859cabaa476cabce9eecd05049a7966af7b4a33b541bfd4ca1104", size = 2239634, upload-time = "2026-04-13T09:03:52.478Z" }, + { url = "https://files.pythonhosted.org/packages/46/42/ef58aacf330d8de6e309d62469aa1f80e945eaf665929b4037ac1bfcebc1/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:38108976f2d8afaa8f5067fd1390a8c9f5cc580175407cda636e76bc76e88054", size = 2315739, upload-time = "2026-04-13T09:05:04.971Z" }, + { url = "https://files.pythonhosted.org/packages/8b/86/c63b12fafa2d86a515bfd1840b39c23a49302f02b653161bf9c3a0566c50/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5a06d8ed01dad5575056b5187e5959b336793c6047920a3441ee5b03533836", size = 2098169, upload-time = "2026-04-13T09:07:27.151Z" }, + { url = "https://files.pythonhosted.org/packages/76/19/b5b33a2f6be4755b21a20434293c4364be255f4c1a108f125d101d4cc4ee/pydantic_core-2.46.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:04017ace142da9ce27cafd423a480872571b5c7e80382aec22f7d715ca8eb870", size = 2170830, upload-time = "2026-04-13T09:04:39.448Z" }, + { url = "https://files.pythonhosted.org/packages/99/ae/7559f99a29b7d440012ddb4da897359304988a881efaca912fd2f655652e/pydantic_core-2.46.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2629ad992ed1b1c012e6067f5ffafd3336fcb9b54569449fabb85621f1444ed3", size = 2203901, upload-time = "2026-04-13T09:04:01.048Z" }, + { url = "https://files.pythonhosted.org/packages/dd/0e/b0ef945a39aeb4ac58da316813e1106b7fbdfbf20ac141c1c27904355ac5/pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3068b1e7bd986aebc88f6859f8353e72072538dcf92a7fb9cf511a0f61c5e729", size = 2191789, upload-time = "2026-04-13T09:06:39.915Z" }, + { url = "https://files.pythonhosted.org/packages/90/f4/830484e07188c1236b013995818888ab93bab8fd88aa9689b1d8fd22220d/pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:1e366916ff69ff700aa9326601634e688581bc24c5b6b4f8738d809ec7d72611", size = 2344423, upload-time = "2026-04-13T09:05:12.252Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ba/e455c18cbdc333177af754e740be4fe9d1de173d65bbe534daf88da02ac0/pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:485a23e8f4618a1b8e23ac744180acde283fffe617f96923d25507d5cade62ec", size = 2384037, upload-time = "2026-04-13T09:06:24.503Z" }, + { url = "https://files.pythonhosted.org/packages/78/1f/b35d20d73144a41e78de0ae398e60fdd8bed91667daa1a5a92ab958551ba/pydantic_core-2.46.0-cp312-cp312-win32.whl", hash = "sha256:520940e1b702fe3b33525d0351777f25e9924f1818ca7956447dabacf2d339fd", size = 1967068, upload-time = "2026-04-13T09:05:23.374Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/4b6252e9606e8295647b848233cc4137ee0a04ebba8f0f9fb2977655b38c/pydantic_core-2.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:90d2048e0339fa365e5a66aefe760ddd3b3d0a45501e088bc5bc7f4ed9ff9571", size = 2071008, upload-time = "2026-04-13T09:05:21.392Z" }, + { url = "https://files.pythonhosted.org/packages/39/95/d08eb508d4d5560ccbd226ee5971e5ef9b749aba9b413c0c4ed6e406d4f6/pydantic_core-2.46.0-cp312-cp312-win_arm64.whl", hash = "sha256:a70247649b7dffe36648e8f34be5ce8c5fa0a27ff07b071ea780c20a738c05ce", size = 2036634, upload-time = "2026-04-13T09:05:48.299Z" }, + { url = "https://files.pythonhosted.org/packages/df/05/ab3b0742bad1d51822f1af0c4232208408902bdcfc47601f3b812e09e6c2/pydantic_core-2.46.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a05900c37264c070c683c650cbca8f83d7cbb549719e645fcd81a24592eac788", size = 2116814, upload-time = "2026-04-13T09:04:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/98/08/30b43d9569d69094a0899a199711c43aa58fce6ce80f6a8f7693673eb995/pydantic_core-2.46.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de8e482fd4f1e3f36c50c6aac46d044462615d8f12cfafc6bebeaa0909eea22", size = 1951867, upload-time = "2026-04-13T09:04:02.364Z" }, + { url = "https://files.pythonhosted.org/packages/db/a0/bf9a1ba34537c2ed3872a48195291138fdec8fe26c4009776f00d63cf0c8/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c525ecf8a4cdf198327b65030a7d081867ad8e60acb01a7214fff95cf9832d47", size = 1977040, upload-time = "2026-04-13T09:06:16.088Z" }, + { url = "https://files.pythonhosted.org/packages/71/70/0ba03c20e1e118219fc18c5417b008b7e880f0e3fb38560ec4465984d471/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f14581aeb12e61542ce73b9bfef2bca5439d65d9ab3efe1a4d8e346b61838f9b", size = 2055284, upload-time = "2026-04-13T09:05:25.125Z" }, + { url = "https://files.pythonhosted.org/packages/58/cf/1e320acefbde7fb7158a9e5def55e0adf9a4634636098ce28dc6b978e0d3/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c108067f2f7e190d0dbd81247d789ec41f9ea50ccd9265a3a46710796ac60530", size = 2238896, upload-time = "2026-04-13T09:05:01.345Z" }, + { url = "https://files.pythonhosted.org/packages/df/f5/ea8ba209756abe9eba891bb0ef3772b4c59a894eb9ad86cd5bd0dd4e3e52/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ac10967e9a7bb1b96697374513f9a1a90a59e2fb41566b5e00ee45392beac59", size = 2314353, upload-time = "2026-04-13T09:06:07.942Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f8/5885350203b72e96438eee7f94de0d8f0442f4627237ca8ef75de34db1cd/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7897078fe8a13b73623c0955dfb2b3d2c9acb7177aac25144758c9e5a5265aaa", size = 2098522, upload-time = "2026-04-13T09:04:23.239Z" }, + { url = "https://files.pythonhosted.org/packages/bf/88/5930b0e828e371db5a556dd3189565417ddc3d8316bb001058168aadcf5f/pydantic_core-2.46.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:e69ce405510a419a082a78faed65bb4249cfb51232293cc675645c12f7379bf7", size = 2168757, upload-time = "2026-04-13T09:07:12.46Z" }, + { url = "https://files.pythonhosted.org/packages/da/75/63d563d3035a0548e721c38b5b69fd5626fdd51da0f09ff4467503915b82/pydantic_core-2.46.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fd28d13eea0d8cf351dc1fe274b5070cc8e1cca2644381dee5f99de629e77cf3", size = 2202518, upload-time = "2026-04-13T09:05:44.418Z" }, + { url = "https://files.pythonhosted.org/packages/a7/53/1958eacbfddc41aadf5ae86dd85041bf054b675f34a2fa76385935f96070/pydantic_core-2.46.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:ee1547a6b8243e73dd10f585555e5a263395e55ce6dea618a078570a1e889aef", size = 2190148, upload-time = "2026-04-13T09:06:56.151Z" }, + { url = "https://files.pythonhosted.org/packages/c7/17/098cc6d3595e4623186f2bc6604a6195eb182e126702a90517236391e9ce/pydantic_core-2.46.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:c3dc68dcf62db22a18ddfc3ad4960038f72b75908edc48ae014d7ac8b391d57a", size = 2342925, upload-time = "2026-04-13T09:04:17.286Z" }, + { url = "https://files.pythonhosted.org/packages/71/a7/abdb924620b1ac535c690b36ad5b8871f376104090f8842c08625cecf1d3/pydantic_core-2.46.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:004a2081c881abfcc6854a4623da6a09090a0d7c1398a6ae7133ca1256cee70b", size = 2383167, upload-time = "2026-04-13T09:04:52.643Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c9/2ddd10f50e4b7350d2574629a0f53d8d4eb6573f9c19a6b43e6b1487a31d/pydantic_core-2.46.0-cp313-cp313-win32.whl", hash = "sha256:59d24ec8d5eaabad93097525a69d0f00f2667cb353eb6cda578b1cfff203ceef", size = 1965660, upload-time = "2026-04-13T09:06:05.877Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e7/1efc38ed6f2680c032bcefa0e3ebd496a8c77e92dfdb86b07d0f2fc632b1/pydantic_core-2.46.0-cp313-cp313-win_amd64.whl", hash = "sha256:71186dad5ac325c64d68fe0e654e15fd79802e7cc42bc6f0ff822d5ad8b1ab25", size = 2069563, upload-time = "2026-04-13T09:07:14.738Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1e/a325b4989e742bf7e72ed35fa124bc611fd76539c9f8cd2a9a7854473533/pydantic_core-2.46.0-cp313-cp313-win_arm64.whl", hash = "sha256:8e4503f3213f723842c9a3b53955c88a9cfbd0b288cbd1c1ae933aebeec4a1b4", size = 2034966, upload-time = "2026-04-13T09:04:21.629Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/914891d384cdbf9a6f464eb13713baa22ea1e453d4da80fb7da522079370/pydantic_core-2.46.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:4fc801c290342350ffc82d77872054a934b2e24163727263362170c1db5416ca", size = 2113349, upload-time = "2026-04-13T09:04:59.407Z" }, + { url = "https://files.pythonhosted.org/packages/35/95/3a0c6f65e231709fb3463e32943c69d10285cb50203a2130a4732053a06d/pydantic_core-2.46.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0a36f2cc88170cc177930afcc633a8c15907ea68b59ac16bd180c2999d714940", size = 1949170, upload-time = "2026-04-13T09:06:09.935Z" }, + { url = "https://files.pythonhosted.org/packages/d1/63/d845c36a608469fe7bee226edeff0984c33dbfe7aecd755b0e7ab5a275c4/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a3912e0c568a1f99d4d6d3e41def40179d61424c0ca1c8c87c4877d7f6fd7fb", size = 1977914, upload-time = "2026-04-13T09:04:56.16Z" }, + { url = "https://files.pythonhosted.org/packages/08/6f/f2e7a7f85931fb31671f5378d1c7fc70606e4b36d59b1b48e1bd1ef5d916/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3534c3415ed1a19ab23096b628916a827f7858ec8db49ad5d7d1e44dc13c0d7b", size = 2050538, upload-time = "2026-04-13T09:05:06.789Z" }, + { url = "https://files.pythonhosted.org/packages/8c/97/f4aa7181dd9a16dd9059a99fc48fdab0c2aab68307283a5c04cf56de68c4/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21067396fc285609323a4db2f63a87570044abe0acddfcca8b135fc7948e3db7", size = 2236294, upload-time = "2026-04-13T09:07:03.2Z" }, + { url = "https://files.pythonhosted.org/packages/24/c1/6a5042fc32765c87101b500f394702890af04239c318b6002cfd627b710d/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2afd85b7be186e2fe7cdbb09a3d964bcc2042f65bbcc64ad800b3c7915032655", size = 2312954, upload-time = "2026-04-13T09:06:11.919Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e4/566101a561492ce8454f0844ca29c3b675a6b3a7b3ff577db85ed05c8c50/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67e2c2e171b78db8154da602de72ffdc473c6ee51de8a9d80c0f1cd4051abfc7", size = 2102533, upload-time = "2026-04-13T09:06:58.664Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ac/adc11ee1646a5c4dd9abb09a00e7909e6dc25beddc0b1310ca734bb9b48e/pydantic_core-2.46.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c16ae1f3170267b1a37e16dba5c297bdf60c8b5657b147909ca8774ce7366644", size = 2169447, upload-time = "2026-04-13T09:04:11.143Z" }, + { url = "https://files.pythonhosted.org/packages/26/73/408e686b45b82d28ac19e8229e07282254dbee6a5d24c5c7cf3cf3716613/pydantic_core-2.46.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:133b69e1c1ba34d3702eed73f19f7f966928f9aa16663b55c2ebce0893cca42e", size = 2200672, upload-time = "2026-04-13T09:03:54.056Z" }, + { url = "https://files.pythonhosted.org/packages/0a/3b/807d5b035ec891b57b9079ce881f48263936c37bd0d154a056e7fd152afb/pydantic_core-2.46.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:15ed8e5bde505133d96b41702f31f06829c46b05488211a5b1c7877e11de5eb5", size = 2188293, upload-time = "2026-04-13T09:07:07.614Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ed/719b307516285099d1196c52769fdbe676fd677da007b9c349ae70b7226d/pydantic_core-2.46.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:8cfc29a1c66a7f0fcb36262e92f353dd0b9c4061d558fceb022e698a801cb8ae", size = 2335023, upload-time = "2026-04-13T09:04:05.176Z" }, + { url = "https://files.pythonhosted.org/packages/8d/90/8718e4ae98c4e8a7325afdc079be82be1e131d7a47cb6c098844a9531ffe/pydantic_core-2.46.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e1155708540f13845bf68d5ac511a55c76cfe2e057ed12b4bf3adac1581fc5c2", size = 2377155, upload-time = "2026-04-13T09:06:18.081Z" }, + { url = "https://files.pythonhosted.org/packages/dd/dc/7172789283b963f81da2fc92b186e22de55687019079f71c4d570822502b/pydantic_core-2.46.0-cp314-cp314-win32.whl", hash = "sha256:de5635a48df6b2eef161d10ea1bc2626153197333662ba4cd700ee7ec1aba7f5", size = 1963078, upload-time = "2026-04-13T09:05:30.615Z" }, + { url = "https://files.pythonhosted.org/packages/e0/69/03a7ea4b6264def3a44eabf577528bcec2f49468c5698b2044dea54dc07e/pydantic_core-2.46.0-cp314-cp314-win_amd64.whl", hash = "sha256:f07a5af60c5e7cf53dd1ff734228bd72d0dc9938e64a75b5bb308ca350d9681e", size = 2068439, upload-time = "2026-04-13T09:04:57.729Z" }, + { url = "https://files.pythonhosted.org/packages/f5/eb/1c3afcfdee2ab6634b802ab0a0f1966df4c8b630028ec56a1cb0a710dc58/pydantic_core-2.46.0-cp314-cp314-win_arm64.whl", hash = "sha256:e7a77eca3c7d5108ff509db20aae6f80d47c7ed7516d8b96c387aacc42f3ce0f", size = 2026470, upload-time = "2026-04-13T09:05:08.654Z" }, + { url = "https://files.pythonhosted.org/packages/5c/30/1177dde61b200785c4739665e3aa03a9d4b2c25d2d0408b07d585e633965/pydantic_core-2.46.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5e7cdd4398bee1aaeafe049ac366b0f887451d9ae418fd8785219c13fea2f928", size = 2107447, upload-time = "2026-04-13T09:05:46.314Z" }, + { url = "https://files.pythonhosted.org/packages/b1/60/4e0f61f99bdabbbc309d364a2791e1ba31e778a4935bc43391a7bdec0744/pydantic_core-2.46.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5c2c92d82808e27cef3f7ab3ed63d657d0c755e0dbe5b8a58342e37bdf09bd2e", size = 1926927, upload-time = "2026-04-13T09:06:20.371Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d0/67f89a8269152c1d6eaa81f04e75a507372ebd8ca7382855a065222caa80/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bab80af91cd7014b45d1089303b5f844a9d91d7da60eabf3d5f9694b32a6655", size = 1966613, upload-time = "2026-04-13T09:07:05.389Z" }, + { url = "https://files.pythonhosted.org/packages/cd/07/8dfdc3edc78f29a80fb31f366c50203ec904cff6a4c923599bf50ac0d0ff/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1e49ffdb714bc990f00b39d1ad1d683033875b5af15582f60c1f34ad3eeccfaa", size = 2032902, upload-time = "2026-04-13T09:06:42.47Z" }, + { url = "https://files.pythonhosted.org/packages/b0/2a/111c5e8fe24f99c46bcad7d3a82a8f6dbc738066e2c72c04c71f827d8c78/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca877240e8dbdeef3a66f751dc41e5a74893767d510c22a22fc5c0199844f0ce", size = 2244456, upload-time = "2026-04-13T09:05:36.484Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7c/cfc5d11c15a63ece26e148572c77cfbb2c7f08d315a7b63ef0fe0711d753/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87e6843f89ecd2f596d7294e33196c61343186255b9880c4f1b725fde8b0e20d", size = 2294535, upload-time = "2026-04-13T09:06:01.689Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2c/f0d744e3dab7bd026a3f4670a97a295157cff923a2666d30a15a70a7e3d0/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e20bc5add1dd9bc3b9a3600d40632e679376569098345500799a6ad7c5d46c72", size = 2104621, upload-time = "2026-04-13T09:04:34.388Z" }, + { url = "https://files.pythonhosted.org/packages/a7/64/e7cc4698dc024264d214b51d5a47a2404221b12060dd537d76f831b2120a/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:ee6ff79a5f0289d64a9d6696a3ce1f98f925b803dd538335a118231e26d6d827", size = 2130718, upload-time = "2026-04-13T09:04:26.23Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a8/224e655fec21f7d4441438ad2ecaccb33b5a3876ce7bb2098c74a49efc14/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:52d35cfb58c26323101c7065508d7bb69bb56338cda9ea47a7b32be581af055d", size = 2180738, upload-time = "2026-04-13T09:05:50.253Z" }, + { url = "https://files.pythonhosted.org/packages/32/7b/b3025618ed4c4e4cbaa9882731c19625db6669896b621760ea95bc1125ef/pydantic_core-2.46.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d14cc5a6f260fa78e124061eebc5769af6534fc837e9a62a47f09a2c341fa4ea", size = 2171222, upload-time = "2026-04-13T09:07:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e3/68170aa1d891920af09c1f2f34df61dc5ff3a746400027155523e3400e89/pydantic_core-2.46.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:4f7ff859d663b6635f6307a10803d07f0d09487e16c3d36b1744af51dbf948b2", size = 2320040, upload-time = "2026-04-13T09:06:35.732Z" }, + { url = "https://files.pythonhosted.org/packages/67/1b/5e65807001b84972476300c1f49aea2b4971b7e9fffb5c2654877dadd274/pydantic_core-2.46.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:8ef749be6ed0d69dba31902aaa8255a9bb269ae50c93888c4df242d8bb7acd9e", size = 2377062, upload-time = "2026-04-13T09:07:39.945Z" }, + { url = "https://files.pythonhosted.org/packages/75/03/48caa9dd5f28f7662bd52bff454d9a451f6b7e5e4af95e289e5e170749c9/pydantic_core-2.46.0-cp314-cp314t-win32.whl", hash = "sha256:d93ca72870133f86360e4bb0c78cd4e6ba2a0f9f3738a6486909ffc031463b32", size = 1951028, upload-time = "2026-04-13T09:04:20.224Z" }, + { url = "https://files.pythonhosted.org/packages/87/ed/e97ff55fe28c0e6e3cba641d622b15e071370b70e5f07c496b07b65db7c9/pydantic_core-2.46.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6ebb2668afd657e2127cb40f2ceb627dd78e74e9dfde14d9bf6cdd532a29ff59", size = 2048519, upload-time = "2026-04-13T09:05:10.464Z" }, + { url = "https://files.pythonhosted.org/packages/b6/51/e0db8267a287994546925f252e329eeae4121b1e77e76353418da5a3adf0/pydantic_core-2.46.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4864f5bbb7993845baf9209bae1669a8a76769296a018cb569ebda9dcb4241f5", size = 2026791, upload-time = "2026-04-13T09:04:37.724Z" }, + { url = "https://files.pythonhosted.org/packages/aa/68/4a3a458b55e97a32101096d670f3a78cfb8b5cf72717317b935488040074/pydantic_core-2.46.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:03b5fb37542a401f02d2e43e6d5f96fbbd432dc90ad997b1a0a8f3b99ed6e556", size = 2125397, upload-time = "2026-04-13T09:06:22.499Z" }, + { url = "https://files.pythonhosted.org/packages/a1/90/dd61f69b7505ef4c645ecca344828cf2f6c83067d9786ec956a5c317887f/pydantic_core-2.46.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b89abc4c0830ea7c0f3532bcfd33619d40fa3575f4026e58ea4fd4e243727028", size = 1960161, upload-time = "2026-04-13T09:06:44.618Z" }, + { url = "https://files.pythonhosted.org/packages/bd/aa/1bda216a4d3d8ea784432b1179496345cd256c7e59852e0b6a35479b1e24/pydantic_core-2.46.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78d30efb19efdf7e68e557147f892bb7e2ee5a564260439796c1c90cd165905e", size = 1979602, upload-time = "2026-04-13T09:06:51.661Z" }, + { url = "https://files.pythonhosted.org/packages/65/6e/7dd79f1c1ccf9f8c7d04d26e642fe5060e3406e131c28ec2bef2c7c9bffe/pydantic_core-2.46.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb8a9b16b777f78f6ee3ceacf3c1c9d9d7fb017362f94a8d999945bd5885bbdc", size = 2044691, upload-time = "2026-04-13T09:07:17.194Z" }, + { url = "https://files.pythonhosted.org/packages/1e/fc/0b9d6d5d909c1a6fd856b58bec33ca4de3833059a52d683366c993519bda/pydantic_core-2.46.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6261845a5b01d36694b1b44a840e4c37b4ab935ae898b182b48aafc4bd647b21", size = 2232707, upload-time = "2026-04-13T09:04:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1c/40f6c741b4f58f01e4c3bf5a3e5b50e4c703000a3149ac416bad791bbe04/pydantic_core-2.46.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad266ebce36cff05084095fcc02f9f26a3b351b67cfd961b2b59dabb912eb031", size = 2296233, upload-time = "2026-04-13T09:06:53.87Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ee/e5c1e4dcc6556ec7e7584f8523dc8fbfaa6ae29bafa51a1671969a0ff70b/pydantic_core-2.46.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ba11c407a2263550c4c10be1160c1a170b2f2db864f990bcc4675fbb588d096", size = 2098983, upload-time = "2026-04-13T09:04:08.3Z" }, + { url = "https://files.pythonhosted.org/packages/16/df/12c46d83aa17e12d7041fd8f50e6e447b4acacef7bae6aec89d8da03e1e3/pydantic_core-2.46.0-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:8aa610ce5cdd83d58102dcd30d7734e09c44235698db1bb04ba649594b1fb984", size = 2143142, upload-time = "2026-04-13T09:07:19.49Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dd/abca4057ea80d3548a4fee56966f19fa91c94a106f83e7c2ff3bea992875/pydantic_core-2.46.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:828a38d13c7ce073791b7c5e08e3c5db4d52d702b28f948e05346db2d264df8f", size = 2184369, upload-time = "2026-04-13T09:07:32.481Z" }, + { url = "https://files.pythonhosted.org/packages/77/cc/ff9c19ea71a0a6013a13e8eafb91a9346bcc241f206e89f2053a0b4c9671/pydantic_core-2.46.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fbfb322c511a2b571eb93850221f875c1929dde3d056c7354d64fc90b49b8bc6", size = 2186942, upload-time = "2026-04-13T09:03:55.638Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f0/caf56a7ed8ff49567870bf93c8362c9f2cc4554990a6f7fad4727f03731a/pydantic_core-2.46.0-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:ded8e373d39f5b065f437711f1021e34803657343e0ce788a709200de8c55a8a", size = 2330927, upload-time = "2026-04-13T09:04:29.315Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4a/a6aaf2b0869429e3109687b79908bcda889b964f7368013cf55339226469/pydantic_core-2.46.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d55111bc1e58251eda6ec1305dcaa3007a128afa67452781e14598c173bdcc72", size = 2370109, upload-time = "2026-04-13T09:06:49.281Z" }, + { url = "https://files.pythonhosted.org/packages/65/14/3280afe357fdbcd0add8acc0a6d15d2bfd101109df5b270af00ac521a671/pydantic_core-2.46.0-cp39-cp39-win32.whl", hash = "sha256:dd103df73baaa9903bb1a2cb6380b7ccac6a236dec263c3b9dc578c40297f376", size = 1984396, upload-time = "2026-04-13T09:07:10.217Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a0/931e12d664ca370e76e0755753f4d3eac5141ae74a047463563584be776f/pydantic_core-2.46.0-cp39-cp39-win_amd64.whl", hash = "sha256:9e2effcde35c469db7ac841ee66d204d96d57569890b20e5d2bd7a0b7d64773e", size = 2073496, upload-time = "2026-04-13T09:07:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f1/6731c2d6caf03efe822101edb4783eb3f212f34b7b005a34f039f67e76e1/pydantic_core-2.46.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:ce2e38e27de73ff6a0312a9e3304c398577c418d90bbde97f0ba1ee3ab7ac39f", size = 2121259, upload-time = "2026-04-13T09:07:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/72/fd/ac34d4c92e739e37a040be9e7ea84d116afec5f983a7db856c27135fba77/pydantic_core-2.46.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:f0d34ba062396de0be7421e6e69c9a6821bf6dc73a0ab9959a48a5a6a1e24754", size = 1945798, upload-time = "2026-04-13T09:04:24.729Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a4/f413a522c4047c46b109be6805a3095d35e5a4882fd5b4fdc0909693dfc0/pydantic_core-2.46.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4c0a12147b4026dd68789fb9f22f1a8769e457f9562783c181880848bbd6412", size = 1986062, upload-time = "2026-04-13T09:05:57.177Z" }, + { url = "https://files.pythonhosted.org/packages/91/2e/9760025ea8b0f49903c0ceebdfc2d8ef839da872426f2b03cae9de036a7c/pydantic_core-2.46.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a99896d9db56df901ab4a63cd6a36348a569cff8e05f049db35f4016a817a3d9", size = 2145344, upload-time = "2026-04-13T09:03:56.924Z" }, + { url = "https://files.pythonhosted.org/packages/74/0c/106ed5cc50393d90523f09adcc50d05e42e748eb107dc06aea971137f02d/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:bc0e2fefe384152d7da85b5c2fe8ce2bf24752f68a58e3f3ea42e28a29dfdeb2", size = 2104968, upload-time = "2026-04-13T09:06:26.967Z" }, + { url = "https://files.pythonhosted.org/packages/f5/71/b494cef3165e3413ee9bbbb5a9eedc9af0ea7b88d8638beef6c2061b110e/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:a2ab0e785548be1b4362a62c4004f9217598b7ee465f1f420fc2123e2a5b5b02", size = 1940442, upload-time = "2026-04-13T09:06:29.332Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3e/a4d578c8216c443e26a1124f8c1e07c0654264ce5651143d3883d85ff140/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16d45aecb18b8cba1c68eeb17c2bb2d38627ceed04c5b30b882fc9134e01f187", size = 1999672, upload-time = "2026-04-13T09:04:42.798Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c1/9114560468685525a21770138382fd0cb849aaf351ff2c7b97f760d121e0/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5078f6c377b002428e984259ac327ef8902aacae6c14b7de740dd4869a491501", size = 2154533, upload-time = "2026-04-13T09:04:50.868Z" }, + { url = "https://files.pythonhosted.org/packages/09/ed/fbd8127e4a19c4fdbb2f4983cf72c7b3534086df640c813c5c0ec4218177/pydantic_core-2.46.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:be3e04979ba4d68183f247202c7f4f483f35df57690b3f875c06340a1579b47c", size = 2119951, upload-time = "2026-04-13T09:04:35.923Z" }, + { url = "https://files.pythonhosted.org/packages/ec/77/df8711ebb45910412f90d75198430fa1120f5618336b71fa00303601c5a4/pydantic_core-2.46.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b1eae8d7d9b8c2a90b34d3d9014804dca534f7f40180197062634499412ea14e", size = 1953812, upload-time = "2026-04-13T09:05:40.293Z" }, + { url = "https://files.pythonhosted.org/packages/12/fe/14b35df69112bd812d6818a395eeab22eeaa2befc6f85bc54ed648430186/pydantic_core-2.46.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a95a2773680dd4b6b999d4eccdd1b577fd71c31739fb4849f6ada47eabb9c56", size = 2139585, upload-time = "2026-04-13T09:06:46.94Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f0/4fea4c14ebbdeb87e5f6edd2620735fcbd384865f06707fe229c021ce041/pydantic_core-2.46.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25988c3159bb097e06abfdf7b21b1fcaf90f187c74ca6c7bb842c1f72ce74fa8", size = 2179154, upload-time = "2026-04-13T09:04:15.639Z" }, + { url = "https://files.pythonhosted.org/packages/5c/36/6329aa79ba32b73560e6e453164fb29702b115fd3b2b650e796e1dc27862/pydantic_core-2.46.0-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:747d89bd691854c719a3381ba46b6124ef916ae85364c79e11db9c84995d8d03", size = 2182917, upload-time = "2026-04-13T09:07:24.483Z" }, + { url = "https://files.pythonhosted.org/packages/92/61/edbf7aea71052d410347846a2ea43394f74651bf6822b8fad8703ca00575/pydantic_core-2.46.0-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:909a7327b83ca93b372f7d48df0ebc7a975a5191eb0b6e024f503f4902c24124", size = 2327716, upload-time = "2026-04-13T09:06:31.681Z" }, + { url = "https://files.pythonhosted.org/packages/a4/11/aa5089b941e85294b1d5d526840b18f0d4464f842d43d8999ce50ef881c1/pydantic_core-2.46.0-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:2f7e6a3752378a69fadf3f5ee8bc5fa082f623703eec0f4e854b12c548322de0", size = 2365925, upload-time = "2026-04-13T09:05:38.338Z" }, + { url = "https://files.pythonhosted.org/packages/0c/75/e187b0ea247f71f2009d156df88b7d8449c52a38810c9a1bd55dd4871206/pydantic_core-2.46.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ef47ee0a3ac4c2bb25a083b3acafb171f65be4a0ac1e84edef79dd0016e25eaa", size = 2193856, upload-time = "2026-04-13T09:05:03.114Z" }, +] + [[package]] name = "pyfakefs" version = "5.5.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b4/c1/9aba92f9fecb2b81a83efd516d028c6ddcbd3cebdfb3ecba42b9c858f5e8/pyfakefs-5.5.0.tar.gz", hash = "sha256:7448aaa07142f892d0a4eb52a5ed3206a9f02c6599e686cd97d624c18979c154", size = 205510, upload-time = "2024-05-12T06:42:07.54Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/08/63/844e40b064d9b31c62e35029f034737929931c254a6b20312f36b14ee0e9/pyfakefs-5.5.0-py3-none-any.whl", hash = "sha256:8dbf203ab7bef1529f11f7d41b9478b898e95bf9f3b71262163aac07a518cd76", size = 219967, upload-time = "2024-05-12T06:42:04.941Z" }, @@ -2329,7 +2587,7 @@ wheels = [ [[package]] name = "pyflakes" version = "3.4.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, @@ -2338,7 +2596,7 @@ wheels = [ [[package]] name = "pygitguardian" version = "1.28.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "marshmallow" }, { name = "marshmallow-dataclass" }, @@ -2354,7 +2612,7 @@ wheels = [ [[package]] name = "pygments" version = "2.20.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, @@ -2363,7 +2621,7 @@ wheels = [ [[package]] name = "pyinstaller" version = "6.7.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "altgraph" }, { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, @@ -2392,7 +2650,7 @@ wheels = [ [[package]] name = "pyinstaller-hooks-contrib" version = "2026.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "packaging" }, @@ -2406,16 +2664,29 @@ wheels = [ [[package]] name = "pyjwt" version = "2.6.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/75/65/db64904a7f23e12dbf0565b53de01db04d848a497c6c9b87e102f74c9304/PyJWT-2.6.0.tar.gz", hash = "sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd", size = 72984, upload-time = "2022-10-20T01:09:12.296Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/40/46/505f0dd53c14096f01922bf93a7abb4e40e29a06f858abbaa791e6954324/PyJWT-2.6.0-py3-none-any.whl", hash = "sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14", size = 20316, upload-time = "2022-10-20T01:09:09.802Z" }, ] +[[package]] +name = "pyopenssl" +version = "25.1.0" +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/8c/cd89ad05804f8e3c17dea8f178c3f40eeab5694c30e0c9f5bcd49f576fc3/pyopenssl-25.1.0.tar.gz", hash = "sha256:8d031884482e0c67ee92bf9a4d8cceb08d92aba7136432ffb0703c5280fc205b", size = 179937, upload-time = "2025-05-17T16:28:31.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/28/2659c02301b9500751f8d42f9a6632e1508aa5120de5e43042b8b30f8d5d/pyopenssl-25.1.0-py3-none-any.whl", hash = "sha256:2b11f239acc47ac2e5aca04fd7fa829800aeee22a2eb30d744572a157bd8a1ab", size = 56771, upload-time = "2025-05-17T16:28:29.197Z" }, +] + [[package]] name = "pyproject-hooks" version = "1.2.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, @@ -2424,7 +2695,7 @@ wheels = [ [[package]] name = "pyright" version = "1.1.367" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "nodeenv" }, ] @@ -2436,14 +2707,14 @@ wheels = [ [[package]] name = "pytest" version = "8.4.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, - { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, { name = "packaging", marker = "python_full_version < '3.10'" }, { name = "pluggy", marker = "python_full_version < '3.10'" }, { name = "pygments", marker = "python_full_version < '3.10'" }, @@ -2457,7 +2728,7 @@ wheels = [ [[package]] name = "pytest" version = "9.0.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", @@ -2466,7 +2737,7 @@ resolution-markers = [ dependencies = [ { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, - { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, { name = "packaging", marker = "python_full_version >= '3.10'" }, { name = "pluggy", marker = "python_full_version >= '3.10'" }, { name = "pygments", marker = "python_full_version >= '3.10'" }, @@ -2480,10 +2751,10 @@ wheels = [ [[package]] name = "pytest-mock" version = "3.15.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } wheels = [ @@ -2493,10 +2764,10 @@ wheels = [ [[package]] name = "pytest-socket" version = "0.7.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/05/ff/90c7e1e746baf3d62ce864c479fd53410b534818b9437413903596f81580/pytest_socket-0.7.0.tar.gz", hash = "sha256:71ab048cbbcb085c15a4423b73b619a8b35d6a307f46f78ea46be51b1b7e11b3", size = 12389, upload-time = "2024-01-28T20:17:23.177Z" } wheels = [ @@ -2506,10 +2777,10 @@ wheels = [ [[package]] name = "pytest-voluptuous" version = "1.2.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, { name = "voluptuous" }, ] wheels = [ @@ -2519,21 +2790,36 @@ wheels = [ [[package]] name = "pytest-xdist" version = "3.8.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "execnet" }, - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, ] +[[package]] +name = "python-discovery" +version = "1.2.2" +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } +dependencies = [ + { name = "filelock", version = "3.19.1", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "filelock", version = "3.25.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "platformdirs", version = "4.9.6", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/ef/3bae0e537cfe91e8431efcba4434463d2c5a65f5a89edd47c6cf2f03c55f/python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", size = 58872, upload-time = "2026-04-07T17:28:49.249Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" }, +] + [[package]] name = "python-dotenv" version = "0.21.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f5/d7/d548e0d5a68b328a8d69af833a861be415a17cb15ce3d8f0cd850073d2e1/python-dotenv-0.21.1.tar.gz", hash = "sha256:1c93de8f636cde3ce377292818d0e440b6e45a82f215c3744979151fa8151c49", size = 35930, upload-time = "2023-01-21T10:22:47.277Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/64/62/f19d1e9023aacb47241de3ab5a5d5fedf32c78a71a9e365bb2153378c141/python_dotenv-0.21.1-py3-none-any.whl", hash = "sha256:41e12e0318bebc859fcc4d97d4db8d20ad21721a6aa5047dd59f090391cb549a", size = 19284, upload-time = "2023-01-21T10:22:45.958Z" }, @@ -2542,7 +2828,7 @@ wheels = [ [[package]] name = "pywin32-ctypes" version = "0.2.3" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, @@ -2551,7 +2837,7 @@ wheels = [ [[package]] name = "pyyaml" version = "6.0.3" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, @@ -2624,13 +2910,13 @@ wheels = [ [[package]] name = "referencing" version = "0.36.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ { name = "attrs", marker = "python_full_version < '3.10'" }, - { name = "rpds-py", version = "0.27.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "rpds-py", version = "0.27.1", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } @@ -2641,7 +2927,7 @@ wheels = [ [[package]] name = "referencing" version = "0.37.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", @@ -2649,7 +2935,7 @@ resolution-markers = [ ] dependencies = [ { name = "attrs", marker = "python_full_version >= '3.10'" }, - { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } @@ -2660,7 +2946,7 @@ wheels = [ [[package]] name = "requests" version = "2.32.5" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, @@ -2672,23 +2958,63 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "rfc3161-client" +version = "1.0.6" +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/a7/be3b086133a87d595e7b11564931d5e5283edeeabba05dfee636a34b4dab/rfc3161_client-1.0.6.tar.gz", hash = "sha256:9969262fe6c08ecce39f9fe3996cf412187793834a022a643803090db5aae6b4", size = 106710, upload-time = "2026-04-08T14:15:05.66Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b1/262af0901e1507a4e21d268091f1f7b738b44bca424b9bd481981828a7e8/rfc3161_client-1.0.6-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:9a98e9c7ff632d9571fcea25fb70bde0e8339b86368aef67a65f6a301f125733", size = 474975, upload-time = "2026-04-08T14:14:31.559Z" }, + { url = "https://files.pythonhosted.org/packages/8a/b6/da7caa10c2a3e01f244ad116ffe2cc87056fa71a7dd76453faab2fc2c6ad/rfc3161_client-1.0.6-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0b3920334f7334ec3bb9c319d53a5d08cd43b6883f75e2669cfd869cd264d53a", size = 467442, upload-time = "2026-04-08T14:14:33.556Z" }, + { url = "https://files.pythonhosted.org/packages/64/dd/5c92004ca167ae182f543aedc7a7a8bebeb0668ade7263b82212e4b4c59d/rfc3161_client-1.0.6-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1671b1be16480ea54c0d36239efd0fb62c13dd572a9865a5e91fea39f1b95303", size = 2435049, upload-time = "2026-04-08T14:14:35.062Z" }, + { url = "https://files.pythonhosted.org/packages/c4/19/c44702336aed367c6513493c4df9e880db8c4df37d5cd8d525aac9aad827/rfc3161_client-1.0.6-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:63355099d932851eac507806bb9d0937dab546a66d5857d888168799ec635f6d", size = 1850399, upload-time = "2026-04-08T14:14:36.707Z" }, + { url = "https://files.pythonhosted.org/packages/64/fa/5af9bbfd4dcd9926463a95c4de6ea137176252d7a1d0b96533f6c85f2742/rfc3161_client-1.0.6-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bc9835467f6166edd6f876470484e5b294ee141add6eff6a59f5047937aaa75", size = 2174593, upload-time = "2026-04-08T14:14:38.764Z" }, + { url = "https://files.pythonhosted.org/packages/53/3c/32a0c7de4d63a69eb97e2564e5f1d51879ec93232b0c5ac1ec444843acba/rfc3161_client-1.0.6-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3caffaebf43242b000c4a6659d60eaf19c3b161ccbe05b15634a856c9ea7e61", size = 2177997, upload-time = "2026-04-08T14:14:40.641Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8d/4253affe1d1e221369ad3043f5d697fc3f5d7fcb439d21add84c65cafc53/rfc3161_client-1.0.6-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b7ad54288a49379b01b1d0d9d15167d2b7c6c7f940332ab85eeb4a6e844da8c7", size = 2745033, upload-time = "2026-04-08T14:14:42.82Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f0/eed22d6ed52d36b1eab3dfe56b9f507e22a328e3d018b84d48877ad7abe7/rfc3161_client-1.0.6-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:85a1d71d1eb2c9bced2b3eb75e96f9fe49732ec2567b5dafa1dd889fff42b7fe", size = 2145631, upload-time = "2026-04-08T14:14:44.901Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bb/feeacd1859e364080729b9cded1129b740eeea54c6d61ff12daceed4b7dd/rfc3161_client-1.0.6-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1be4e1133f0f7fe875629f2c358285503c1cfc79cebfbc3fb4e28b8a57d6f1a4", size = 2328742, upload-time = "2026-04-08T14:14:46.743Z" }, + { url = "https://files.pythonhosted.org/packages/6a/f5/2df8e402f069a6af2db1f21d1c88d5a929398923265189a0141709a6f25c/rfc3161_client-1.0.6-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bc379167238df32cbcc1dc9c324088559c1734331030f5293d75f4fd37b5f4f6", size = 2386899, upload-time = "2026-04-08T14:14:48.59Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1b/f78c75c6c92a0f5de46a86b7f39cf9bb8fb5980fbd684a56f46f031dbce6/rfc3161_client-1.0.6-cp39-abi3-win32.whl", hash = "sha256:8631f7db7c1327bf87ee6a9a8681b4cd6bc2a90aae651388f29d045cd9ff1ac9", size = 1966796, upload-time = "2026-04-08T14:14:50.215Z" }, + { url = "https://files.pythonhosted.org/packages/26/10/6e54860ae82e4ed5665fd3f1d798a8f68f7bac321431f444640da6f93600/rfc3161_client-1.0.6-cp39-abi3-win_amd64.whl", hash = "sha256:4ef4b096abe7d55b020526e39932c2721939a6c55e9a5cd3b3e77897a0942937", size = 2386738, upload-time = "2026-04-08T14:14:52.364Z" }, + { url = "https://files.pythonhosted.org/packages/f1/59/34be30cd647de25af4ef7ca5980c124205893248db40d93831afad53dbcf/rfc3161_client-1.0.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102165201c5224cf6e6634bfd68c6a39e8f800601188216f8210face4861215", size = 2432818, upload-time = "2026-04-08T14:14:54.457Z" }, + { url = "https://files.pythonhosted.org/packages/44/f6/99d15883d564235b14c8d429fda4c8a9145af2bd5157cce506d2dcb30cc0/rfc3161_client-1.0.6-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e16ed34f6f33fd62aa3b1f83615ecf2f96e1b1f57df4e1a36570b3f895333972", size = 1848117, upload-time = "2026-04-08T14:14:56.227Z" }, + { url = "https://files.pythonhosted.org/packages/06/c8/b486981966e9d4e754c6720bdaeeef2b59bc176cbeef5f3b30aa718c89a0/rfc3161_client-1.0.6-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78cdc6bde331492cb94f69328831d5c56b271012b00c6f1784c2e4b33837d585", size = 2168684, upload-time = "2026-04-08T14:14:58.227Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c2/08be1373dd4ca4382472a4e97be300a9db3f974f35093b33e54abbced4e1/rfc3161_client-1.0.6-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:940e1fc95ec0ca734927a82bcb5363fa988ef1a085d238ff0c861f29c0cfb746", size = 2174296, upload-time = "2026-04-08T14:15:00.234Z" }, + { url = "https://files.pythonhosted.org/packages/35/3a/d3dcf1d1c6a672928baf6cc938be8e71cd10b96e6d3cffcb1f7f2a6d75d3/rfc3161_client-1.0.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3da328ba08139846b1ab3a03402ba8a5f3659a640dbe2cd6a18f7f342e99ba98", size = 2434889, upload-time = "2026-04-08T14:15:02.307Z" }, + { url = "https://files.pythonhosted.org/packages/07/c3/74734c4927776d5d3a56e70c607cdc1c45be765f3b6224e11207d256597e/rfc3161_client-1.0.6-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed6ef8e194cab85f6ec5678995b6406bb568383ebb6a4301be40e7939dd28d9", size = 1849484, upload-time = "2026-04-08T14:15:04.199Z" }, +] + +[[package]] +name = "rfc8785" +version = "0.1.4" +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/2f/fa1d2e740c490191b572d33dbca5daa180cb423c24396b856f5886371d8b/rfc8785-0.1.4.tar.gz", hash = "sha256:e545841329fe0eee4f6a3b44e7034343100c12b4ec566dc06ca9735681deb4da", size = 14321, upload-time = "2024-09-27T16:33:31.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/78/119878110660b2ad709888c8a1614fce7e2fab39080ab960656dc8605bf6/rfc8785-0.1.4-py3-none-any.whl", hash = "sha256:520d690b448ecf0703691c76e1a34a24ddcd4fc5bc41d589cb7c58ec651bcd48", size = 9240, upload-time = "2024-09-27T16:33:29.683Z" }, +] + [[package]] name = "rich" -version = "12.5.1" -source = { registry = "https://pypi.org/simple" } +version = "13.9.4" +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ - { name = "commonmark" }, + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pygments" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/2d/c902484141330ded63c6c40d66a9725f8da5e818770f67241cf429eef825/rich-12.5.1.tar.gz", hash = "sha256:63a5c5ce3673d3d5fbbf23cd87e11ab84b6b451436f1b7f19ec54b6bc36ed7ca", size = 218370, upload-time = "2022-07-11T14:36:48.092Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149, upload-time = "2024-11-01T16:43:57.873Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/39/4cb526e0d505464376e3c47a812df6e6638363ebe66e6a63618831fe47ad/rich-12.5.1-py3-none-any.whl", hash = "sha256:2eb4e6894cde1e017976d2975ac210ef515d7548bc595ba20e195fb9628acdeb", size = 235629, upload-time = "2022-07-11T14:36:45.638Z" }, + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424, upload-time = "2024-11-01T16:43:55.817Z" }, ] [[package]] name = "rpds-py" version = "0.27.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version < '3.10'", ] @@ -2853,7 +3179,7 @@ wheels = [ [[package]] name = "rpds-py" version = "0.30.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", @@ -2980,7 +3306,7 @@ wheels = [ [[package]] name = "scriv" version = "1.7.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version < '3.10'", ] @@ -2989,7 +3315,7 @@ dependencies = [ { name = "click", marker = "python_full_version < '3.10'" }, { name = "click-log", marker = "python_full_version < '3.10'" }, { name = "jinja2", marker = "python_full_version < '3.10'" }, - { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, { name = "requests", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b2/ff/c32aee5e1750e884fb83de0e01b2979d065472276ac34454b8055c45f626/scriv-1.7.0.tar.gz", hash = "sha256:7c1a8be6351d03692e5e75784deea0d8f11b2c90f91be18b0fb3a375555b525e", size = 94092, upload-time = "2025-04-20T18:29:42.381Z" } @@ -3005,7 +3331,7 @@ toml = [ [[package]] name = "scriv" version = "1.8.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", @@ -3016,7 +3342,7 @@ dependencies = [ { name = "click", marker = "python_full_version >= '3.10'" }, { name = "click-log", marker = "python_full_version >= '3.10'" }, { name = "jinja2", marker = "python_full_version >= '3.10'" }, - { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, { name = "requests", marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/14/9a/2ef2209e0672b264a2f2574dc88ea3cd9cfc9adfecbfd3165a900980ec8c/scriv-1.8.0.tar.gz", hash = "sha256:7b1a105dd411ac541998250fc8594742419f94cee984ca1257c5ebf5af21918b", size = 98160, upload-time = "2025-12-30T00:01:10.13Z" } @@ -3032,7 +3358,7 @@ toml = [ [[package]] name = "secretstorage" version = "3.3.3" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version < '3.10'", ] @@ -3048,7 +3374,7 @@ wheels = [ [[package]] name = "secretstorage" version = "3.5.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", @@ -3063,10 +3389,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, ] +[[package]] +name = "securesystemslib" +version = "1.3.1" +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/dd/d1828dce0db18aa8d34f82aee4dbcf49b0f0303cad123a1c716bb1f3bf83/securesystemslib-1.3.1.tar.gz", hash = "sha256:ca915f4b88209bb5450ac05426b859d74b7cd1421cafcf73b8dd3418a0b17486", size = 934782, upload-time = "2025-09-26T13:36:56.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/29/1c560f46b3a95d8c508e1bd8c6d0bbf53c42d412ee7d19ec2a89ceced5b9/securesystemslib-1.3.1-py3-none-any.whl", hash = "sha256:2e5414bbdde33155a91805b295cbedc4ae3f12b48dccc63e1089093537f43c81", size = 871380, upload-time = "2025-09-26T13:36:54.851Z" }, +] + [[package]] name = "seed-isort-config" version = "2.2.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "aspy-refactor-imports" }, ] @@ -3078,16 +3413,123 @@ wheels = [ [[package]] name = "setuptools" version = "82.0.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893, upload-time = "2026-02-08T15:08:40.206Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" }, ] +[[package]] +name = "sigstore" +version = "4.1.0" +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "cryptography", marker = "python_full_version < '3.10'" }, + { name = "id", marker = "python_full_version < '3.10'" }, + { name = "importlib-resources", marker = "python_full_version < '3.10'" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyasn1", marker = "python_full_version < '3.10'" }, + { name = "pydantic", version = "2.11.10", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyjwt", marker = "python_full_version < '3.10'" }, + { name = "pyopenssl", marker = "python_full_version < '3.10'" }, + { name = "requests", marker = "python_full_version < '3.10'" }, + { name = "rfc3161-client", marker = "python_full_version < '3.10'" }, + { name = "rfc8785", marker = "python_full_version < '3.10'" }, + { name = "rich", marker = "python_full_version < '3.10'" }, + { name = "sigstore-models", version = "0.0.5", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sigstore-rekor-types", marker = "python_full_version < '3.10'" }, + { name = "tuf", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/1e/8c115a155b67254b52780730bc86edf90d108d172377e526ce91e42ba9de/sigstore-4.1.0.tar.gz", hash = "sha256:312f7f73fe27127784245f523b86b6334978c555fe4ba7831be5602c089807c1", size = 87928, upload-time = "2025-10-11T12:48:14.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/02/126842beb9ce3d66f86203cdf94d99805df040cf693501a36bd6d0319cdf/sigstore-4.1.0-py3-none-any.whl", hash = "sha256:ec3ed0d92bf53ffb23261245a78d4ad3da5af5cc9af889c86461a7d02407249a", size = 109014, upload-time = "2025-10-11T12:48:12.827Z" }, +] + +[[package]] +name = "sigstore" +version = "4.2.0" +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "cryptography", marker = "python_full_version >= '3.10'" }, + { name = "id", marker = "python_full_version >= '3.10'" }, + { name = "importlib-resources", marker = "python_full_version == '3.10.*'" }, + { name = "platformdirs", version = "4.9.6", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyasn1", marker = "python_full_version >= '3.10'" }, + { name = "pydantic", version = "2.13.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyjwt", marker = "python_full_version >= '3.10'" }, + { name = "pyopenssl", marker = "python_full_version >= '3.10'" }, + { name = "requests", marker = "python_full_version >= '3.10'" }, + { name = "rfc3161-client", marker = "python_full_version >= '3.10'" }, + { name = "rfc8785", marker = "python_full_version >= '3.10'" }, + { name = "rich", marker = "python_full_version >= '3.10'" }, + { name = "sigstore-models", version = "0.0.6", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "sigstore-rekor-types", marker = "python_full_version >= '3.10'" }, + { name = "tuf", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/c3/84ec81173ade0dba5613feea577308cde4e69045cc804d02953e3a40922c/sigstore-4.2.0.tar.gz", hash = "sha256:bdbb49a42fd5f0ea6765919adb42ccee7254c482330764d0842eec4e11ad78d7", size = 88123, upload-time = "2026-01-26T15:01:35.203Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/ae/d7876024c2c84512e56528bd4de7ea444efed6d2eb74a11957a927f2532e/sigstore-4.2.0-py3-none-any.whl", hash = "sha256:ed2e0f50aae85148a8aa4fc0f57c298927fce430ad1f988f38611ce90c85829f", size = 109276, upload-time = "2026-01-26T15:01:33.944Z" }, +] + +[[package]] +name = "sigstore-models" +version = "0.0.5" +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "pydantic", version = "2.11.10", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/13/f67a87e8d8c97b9a47d4971263ca6afbd5250315a55b8056358061fc07da/sigstore_models-0.0.5.tar.gz", hash = "sha256:8eda90fe16ef3e4e624edd029f4cbbc9832a192dc5c8f66011d94ec4253f9f3f", size = 7037, upload-time = "2025-07-23T17:22:30.173Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/f6/619d9b21df2688d54e61bdb181fa2be32697def742ec7dbbd34c462778ed/sigstore_models-0.0.5-py3-none-any.whl", hash = "sha256:ac3ca1554d5dd509a6710699d83a035a09ba112d1fa180959cbfcdd5d97633b7", size = 13253, upload-time = "2025-07-23T17:22:29.259Z" }, +] + +[[package]] +name = "sigstore-models" +version = "0.0.6" +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "pydantic", version = "2.13.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/ed/5c0ff809f90b19f4e971e17c1ed11f4df60082c6010b32a82054087e91e0/sigstore_models-0.0.6.tar.gz", hash = "sha256:c766c09470c2a7e8a4a333c893f07e2001c56a3ff1757b1a246119f53169a849", size = 7037, upload-time = "2025-11-26T19:18:12.61Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/dc/7a19864a7bc9f43121ab459e12768ab0080590e3e1b60a29b2f2eb01fb85/sigstore_models-0.0.6-py3-none-any.whl", hash = "sha256:5201a68f4d7d0f8bec1e2f4378eb646b084c52609a4e31db8c385095fff68b2e", size = 13213, upload-time = "2025-11-26T19:18:11.365Z" }, +] + +[[package]] +name = "sigstore-rekor-types" +version = "0.0.18" +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } +dependencies = [ + { name = "pydantic", version = "2.11.10", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, extra = ["email"], marker = "python_full_version < '3.10'" }, + { name = "pydantic", version = "2.13.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, extra = ["email"], marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/54/102e772445c5e849b826fbdcd44eb9ad7b3d10fda17b08964658ec7027dc/sigstore_rekor_types-0.0.18.tar.gz", hash = "sha256:19aef25433218ebf9975a1e8b523cc84aaf3cd395ad39a30523b083ea7917ec5", size = 15687, upload-time = "2024-11-22T13:59:54.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/7c/f0b4e19fd424df4cc964f5d454e1d814fd2dc3b386342e6040441024318f/sigstore_rekor_types-0.0.18-py3-none-any.whl", hash = "sha256:b62bf38c5b1a62bc0d7fe0ee51a0709e49311d137c7880c329882a8f4b2d1d78", size = 20610, upload-time = "2024-11-22T13:59:52.684Z" }, +] + [[package]] name = "stack-data" version = "0.6.3" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "asttokens" }, { name = "executing" }, @@ -3101,12 +3543,12 @@ wheels = [ [[package]] name = "syrupy" version = "4.9.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8c/f8/022d8704a3314f3e96dbd6bbd16ebe119ce30e35f41aabfa92345652fceb/syrupy-4.9.1.tar.gz", hash = "sha256:b7d0fcadad80a7d2f6c4c71917918e8ebe2483e8c703dfc8d49cdbb01081f9a4", size = 52492, upload-time = "2025-03-24T01:36:37.225Z" } wheels = [ @@ -3116,14 +3558,14 @@ wheels = [ [[package]] name = "syrupy" version = "5.1.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", "python_full_version == '3.10.*'", ] dependencies = [ - { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2e/b0/24bca682d6a6337854be37f242d116cceeda9942571d5804c44bc1bdd427/syrupy-5.1.0.tar.gz", hash = "sha256:df543c7aa50d3cf1246e83d58fe490afe5f7dab7b41e74ecc0d8d23ae19bd4b8", size = 50495, upload-time = "2026-01-25T14:53:06.2Z" } wheels = [ @@ -3133,7 +3575,7 @@ wheels = [ [[package]] name = "tomli" version = "2.4.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, @@ -3187,7 +3629,7 @@ wheels = [ [[package]] name = "traitlets" version = "5.14.3" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, @@ -3196,25 +3638,38 @@ wheels = [ [[package]] name = "truststore" version = "0.10.4" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, ] +[[package]] +name = "tuf" +version = "6.0.0" +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } +dependencies = [ + { name = "securesystemslib" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/b5/377a566dfa8286b2ca27ddbc792ab1645de0b6c65dd5bf03027b3bf8cc8f/tuf-6.0.0.tar.gz", hash = "sha256:9eed0f7888c5fff45dc62164ff243a05d47fb8a3208035eb268974287e0aee8d", size = 271268, upload-time = "2025-03-11T10:48:45.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/3d/161ab4fec446048707cb13e8ba22220e05a6c4a6587d3631feeb5715037e/tuf-6.0.0-py3-none-any.whl", hash = "sha256:458f663a233d95cc76dde0e1a3d01796516a05ce2781fefafebe037f7729601a", size = 54774, upload-time = "2025-03-11T10:48:44.188Z" }, +] + [[package]] name = "typing-extensions" -version = "4.12.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321, upload-time = "2024-06-07T18:52:15.995Z" } +version = "4.15.0" +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438, upload-time = "2024-06-07T18:52:13.582Z" }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] name = "typing-inspect" version = "0.9.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "typing-extensions" }, @@ -3227,7 +3682,7 @@ wheels = [ [[package]] name = "typing-inspection" version = "0.4.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "typing-extensions" }, ] @@ -3239,7 +3694,7 @@ wheels = [ [[package]] name = "tzdata" version = "2025.3" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, @@ -3248,7 +3703,7 @@ wheels = [ [[package]] name = "urllib3" version = "2.6.3" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, @@ -3257,7 +3712,7 @@ wheels = [ [[package]] name = "vcrpy" version = "7.0.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version < '3.10'", ] @@ -3275,7 +3730,7 @@ wheels = [ [[package]] name = "vcrpy" version = "8.1.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", @@ -3292,23 +3747,26 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.21.1" -source = { registry = "https://pypi.org/simple" } +version = "21.2.4" +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "distlib" }, - { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "filelock", version = "3.25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "platformdirs" }, + { name = "filelock", version = "3.19.1", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "filelock", version = "3.25.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version < '3.10'" }, + { name = "platformdirs", version = "4.9.6", source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "python-discovery" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/b3/b9fe4d783105896f9038c170e4630a9976b92769f951c04634774c10acb1/virtualenv-20.21.1.tar.gz", hash = "sha256:4c104ccde994f8b108163cf9ba58f3d11511d9403de87fb9b4f52bf33dbc8668", size = 12108081, upload-time = "2023-04-19T20:44:11.241Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/98/3a7e644e19cb26133488caff231be390579860bbbb3da35913c49a1d0a46/virtualenv-21.2.4.tar.gz", hash = "sha256:b294ef68192638004d72524ce7ef303e9d0cf5a44c95ce2e54a7500a6381cada", size = 5850742, upload-time = "2026-04-14T22:15:31.438Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/9b/445412c769289c18a5f9c2e8c816c1369f6c5fae4802f2196e5aef7630fd/virtualenv-20.21.1-py3-none-any.whl", hash = "sha256:09ddbe1af0c8ed2bb4d6ed226b9e6415718ad18aef9fa0ba023d96b7a8356049", size = 8744928, upload-time = "2023-04-19T20:44:07.197Z" }, + { url = "https://files.pythonhosted.org/packages/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl", hash = "sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac", size = 5831232, upload-time = "2026-04-14T22:15:29.342Z" }, ] [[package]] name = "voluptuous" version = "0.14.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a1/ce/0733e4d6f870a0e5f4dbb00766b36b71ee0d25f8de33d27fb662f29154b1/voluptuous-0.14.2.tar.gz", hash = "sha256:533e36175967a310f1b73170d091232bf881403e4ebe52a9b4ade8404d151f5d", size = 50885, upload-time = "2024-02-03T11:23:57.022Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3e/21/0424844b889dccd8f1899f92f239d6eca5f4995f5c86baff094694140828/voluptuous-0.14.2-py3-none-any.whl", hash = "sha256:efc1dadc9ae32a30cc622602c1400a17b7bf8ee2770d64f70418144860739c3b", size = 31160, upload-time = "2024-02-03T11:23:55.226Z" }, @@ -3317,7 +3775,7 @@ wheels = [ [[package]] name = "wcwidth" version = "0.6.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, @@ -3326,7 +3784,7 @@ wheels = [ [[package]] name = "wheel-filename" version = "1.4.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/38/be/726dab762b770d0417e505c58e26d661aac1ec0c831e483cda4817ca2417/wheel_filename-1.4.2.tar.gz", hash = "sha256:87891c465dcbb40b40394a906f01a93214bdd51aa5d25e3a9a59cae62bc298fd", size = 7911, upload-time = "2024-12-01T13:03:16.012Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b4/0f/6e97a3bc38cdde32e3ec49f8c0903fe3559ec9ec9db181782f0bb4417717/wheel_filename-1.4.2-py3-none-any.whl", hash = "sha256:3fa599046443d4ca830d06e3d180cd0a675d5871af0a68daa5623318bb4d17e3", size = 6195, upload-time = "2024-12-01T13:03:00.536Z" }, @@ -3335,7 +3793,7 @@ wheels = [ [[package]] name = "win32-setctime" version = "1.2.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, @@ -3344,7 +3802,7 @@ wheels = [ [[package]] name = "wrapt" version = "2.1.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f7/37/ae31f40bec90de2f88d9597d0b5281e23ffe85b893a47ca5d9c05c63a4f6/wrapt-2.1.1.tar.gz", hash = "sha256:5fdcb09bf6db023d88f312bd0767594b414655d58090fc1c46b3414415f67fac", size = 81329, upload-time = "2026-02-03T02:12:13.786Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ca/21/293b657a27accfbbbb6007ebd78af0efa2083dac83e8f523272ea09b4638/wrapt-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7e927375e43fd5a985b27a8992327c22541b6dede1362fc79df337d26e23604f", size = 60554, upload-time = "2026-02-03T02:11:17.362Z" }, @@ -3425,7 +3883,7 @@ wheels = [ [[package]] name = "yarl" version = "1.22.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } dependencies = [ { name = "idna", marker = "python_full_version < '3.10'" }, { name = "multidict", marker = "python_full_version < '3.10'" }, @@ -3567,7 +4025,7 @@ wheels = [ [[package]] name = "zipp" version = "3.23.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://gitlab.gitguardian.ovh/api/v4/projects/435/packages/pypi/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, From d94d8974d86627c4b05b3d4fa54decc1e269d124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Tourri=C3=A8re?= Date: Thu, 19 Feb 2026 15:55:43 +0100 Subject: [PATCH 2/2] docs: add changelog entry for plugin signature verification --- ...155531_clement.tourriere_plugin_signing.md | 7 + ggshield/__main__.py | 16 +- ggshield/core/config/enterprise_config.py | 24 +- ggshield/core/plugin/downloader.py | 11 - tests/unit/cmd/plugin/test_install.py | 219 ++++++++++++++++++ tests/unit/core/plugin/test_downloader.py | 218 +++++++++++++++++ .../core/plugin/test_enterprise_config.py | 41 ++++ tests/unit/core/plugin/test_loader.py | 66 +++++- 8 files changed, 569 insertions(+), 33 deletions(-) create mode 100644 changelog.d/20260219_155531_clement.tourriere_plugin_signing.md diff --git a/changelog.d/20260219_155531_clement.tourriere_plugin_signing.md b/changelog.d/20260219_155531_clement.tourriere_plugin_signing.md new file mode 100644 index 0000000000..a192cef0dc --- /dev/null +++ b/changelog.d/20260219_155531_clement.tourriere_plugin_signing.md @@ -0,0 +1,7 @@ +### Added + +- Add sigstore signature verification for plugin wheels, enforcing identity-based trust via OIDC. Plugins can be verified in STRICT, WARN, or DISABLED mode, configurable through enterprise settings or the `--allow-unsigned` flag. + +### Fixed + +- Forward `signature_mode` through GitHub release and GitHub artifact download paths, ensuring signature verification is applied consistently across all install sources. diff --git a/ggshield/__main__.py b/ggshield/__main__.py index ed6ced3616..db8e36201f 100644 --- a/ggshield/__main__.py +++ b/ggshield/__main__.py @@ -70,15 +70,6 @@ def _load_plugins() -> PluginRegistry: """Load plugins at module level so commands are available.""" global _plugin_registry if _plugin_registry is None: - # Suppress signature/loader loggers during startup to avoid noisy output - # before logging is configured - sig_logger = logging.getLogger("ggshield.core.plugin.signature") - loader_logger = logging.getLogger("ggshield.core.plugin.loader") - orig_sig_level = sig_logger.level - orig_loader_level = loader_logger.level - sig_logger.setLevel(logging.CRITICAL) - loader_logger.setLevel(logging.CRITICAL) - try: enterprise_config = EnterpriseConfig.load() plugin_loader = PluginLoader(enterprise_config) @@ -86,9 +77,6 @@ def _load_plugins() -> PluginRegistry: except Exception as e: _deferred_warnings.append(f"Failed to load plugins: {e}") _plugin_registry = PluginRegistry() - finally: - sig_logger.setLevel(orig_sig_level) - loader_logger.setLevel(orig_loader_level) # Make registry available to hooks module from ggshield.core.plugin.hooks import set_plugin_registry @@ -286,14 +274,14 @@ def main(args: Optional[List[str]] = None) -> Any: `args` is only used by unit-tests. """ + log_utils.disable_logs() + _register_plugin_commands() # Required by pyinstaller when forking. # See https://pyinstaller.org/en/latest/common-issues-and-pitfalls.html#multi-processing multiprocessing.freeze_support() - log_utils.disable_logs() - if not os.getenv("GG_PLAINTEXT_OUTPUT", False) and sys.stderr.isatty(): ui.set_ui(RichGGShieldUI()) diff --git a/ggshield/core/config/enterprise_config.py b/ggshield/core/config/enterprise_config.py index c5c18b4d6d..d29131f697 100644 --- a/ggshield/core/config/enterprise_config.py +++ b/ggshield/core/config/enterprise_config.py @@ -2,16 +2,22 @@ Enterprise configuration - plugin settings. """ +from __future__ import annotations + from dataclasses import dataclass, field from pathlib import Path from typing import TYPE_CHECKING, Any, Dict, Optional +from ggshield.core.config.utils import load_yaml_dict, save_yaml_dict +from ggshield.core.dirs import get_config_dir + if TYPE_CHECKING: from ggshield.core.plugin.signature import SignatureVerificationMode -from ggshield.core.config.utils import load_yaml_dict, save_yaml_dict -from ggshield.core.dirs import get_config_dir + +_PLUGIN_SIGNATURE_MODE_KEY = "plugin_signature_mode" +_DEFAULT_SIGNATURE_MODE = "strict" def get_enterprise_config_filepath() -> Path: @@ -33,10 +39,10 @@ class EnterpriseConfig: """Enterprise configuration stored in ~/.config/ggshield/enterprise_config.yaml""" plugins: Dict[str, PluginConfig] = field(default_factory=dict) - plugin_signature_mode: str = "strict" + plugin_signature_mode: str = _DEFAULT_SIGNATURE_MODE @classmethod - def load(cls) -> "EnterpriseConfig": + def load(cls) -> EnterpriseConfig: """Load enterprise config from file.""" config_path = get_enterprise_config_filepath() data = load_yaml_dict(config_path) @@ -59,7 +65,9 @@ def load(cls) -> "EnterpriseConfig": else: plugins[name] = PluginConfig(enabled=True) - plugin_signature_mode = data.get("plugin_signature_mode", "strict") + plugin_signature_mode = data.get( + _PLUGIN_SIGNATURE_MODE_KEY, _DEFAULT_SIGNATURE_MODE + ) return cls(plugins=plugins, plugin_signature_mode=plugin_signature_mode) @@ -79,7 +87,7 @@ def save(self) -> None: } } - data["plugin_signature_mode"] = self.plugin_signature_mode + data[_PLUGIN_SIGNATURE_MODE_KEY] = self.plugin_signature_mode # Remove None values for cleaner YAML for plugin_data in data["plugins"].values(): @@ -88,8 +96,10 @@ def save(self) -> None: save_yaml_dict(data, config_path) - def get_signature_mode(self) -> "SignatureVerificationMode": + def get_signature_mode(self) -> SignatureVerificationMode: """Get the signature verification mode.""" + # Deferred import to avoid circular dependency: + # enterprise_config -> plugin.__init__ -> loader -> enterprise_config from ggshield.core.plugin.signature import SignatureVerificationMode try: diff --git a/ggshield/core/plugin/downloader.py b/ggshield/core/plugin/downloader.py index 3f3e24c538..38afc0d2f8 100644 --- a/ggshield/core/plugin/downloader.py +++ b/ggshield/core/plugin/downloader.py @@ -33,17 +33,6 @@ logger = logging.getLogger(__name__) -def parse_wheel_filename(filename: str) -> Optional[Tuple[str, str]]: - """Parse a wheel filename into (name, version). - - Returns None if the filename doesn't match the wheel naming convention. - """ - match = re.match(r"^([A-Za-z0-9_.-]+?)-(\d+[^-]*)-", filename) - if match: - return match.group(1), match.group(2) - return None - - def get_signature_label(manifest: Dict[str, Any]) -> Optional[str]: """Get a human-readable signature status label from a manifest. diff --git a/tests/unit/cmd/plugin/test_install.py b/tests/unit/cmd/plugin/test_install.py index 92b01f9f89..92b6ada11c 100644 --- a/tests/unit/cmd/plugin/test_install.py +++ b/tests/unit/cmd/plugin/test_install.py @@ -17,6 +17,7 @@ PluginSourceType, ) from ggshield.core.plugin.downloader import ChecksumMismatchError, DownloadError +from ggshield.core.plugin.signature import SignatureStatus, SignatureVerificationError class TestPluginInstall: @@ -1054,3 +1055,221 @@ def test_install_checksum_mismatch( assert result.exit_code == ExitCode.UNEXPECTED_ERROR assert "Checksum verification failed" in result.output + + +class TestSignatureVerificationHandling: + """Tests for signature verification error handling in install commands.""" + + def test_gitguardian_install_signature_error(self, cli_fs_runner) -> None: + """ + GIVEN a plugin with invalid signature + WHEN installing from GitGuardian API + THEN signature error is shown with --allow-unsigned hint + """ + mock_catalog = PluginCatalog( + plan="Enterprise", + plugins=[ + PluginInfo( + name="tokenscanner", + display_name="Token Scanner", + description="Local secret scanning", + available=True, + latest_version="1.0.0", + reason=None, + ), + ], + features={}, + ) + + mock_download_info = PluginDownloadInfo( + download_url="https://example.com/plugin.whl", + filename="tokenscanner-1.0.0.whl", + sha256="abc123", + version="1.0.0", + expires_at="2099-12-31T23:59:59Z", + ) + + with ( + mock.patch( + "ggshield.cmd.plugin.install.create_client_from_config" + ) as mock_create_client, + mock.patch( + "ggshield.cmd.plugin.install.PluginAPIClient" + ) as mock_plugin_api_client_class, + mock.patch( + "ggshield.cmd.plugin.install.PluginDownloader" + ) as mock_downloader_class, + mock.patch( + "ggshield.cmd.plugin.install.EnterpriseConfig" + ) as mock_config_class, + ): + mock_client = mock.MagicMock() + mock_create_client.return_value = mock_client + + mock_plugin_api_client = mock.MagicMock() + mock_plugin_api_client.get_available_plugins.return_value = mock_catalog + mock_plugin_api_client.get_download_info.return_value = mock_download_info + mock_plugin_api_client_class.return_value = mock_plugin_api_client + + mock_downloader = mock.MagicMock() + mock_downloader.download_and_install.side_effect = ( + SignatureVerificationError( + SignatureStatus.INVALID, "no trusted identity matched" + ) + ) + mock_downloader_class.return_value = mock_downloader + + mock_config = mock.MagicMock() + mock_config.get_signature_mode.return_value = mock.MagicMock() + mock_config_class.load.return_value = mock_config + + result = cli_fs_runner.invoke(cli, ["plugin", "install", "tokenscanner"]) + + assert result.exit_code == ExitCode.UNEXPECTED_ERROR + assert "Signature verification failed" in result.output + assert "--allow-unsigned" in result.output + + def test_local_wheel_signature_error(self, cli_fs_runner, tmp_path: Path) -> None: + """ + GIVEN a local wheel with invalid signature + WHEN installing from local wheel + THEN signature error is shown with --allow-unsigned hint + """ + wheel_path = tmp_path / "plugin.whl" + wheel_path.touch() + + with ( + mock.patch( + "ggshield.cmd.plugin.install.PluginDownloader" + ) as mock_downloader_class, + mock.patch( + "ggshield.cmd.plugin.install.EnterpriseConfig" + ) as mock_config_class, + mock.patch( + "ggshield.cmd.plugin.install.detect_source_type", + return_value=PluginSourceType.LOCAL_FILE, + ), + ): + mock_downloader = mock.MagicMock() + mock_downloader.install_from_wheel.side_effect = SignatureVerificationError( + SignatureStatus.MISSING, "No bundle found" + ) + mock_downloader_class.return_value = mock_downloader + + mock_config = mock.MagicMock() + mock_config.get_signature_mode.return_value = mock.MagicMock() + mock_config_class.load.return_value = mock_config + + result = cli_fs_runner.invoke(cli, ["plugin", "install", str(wheel_path)]) + + assert result.exit_code == ExitCode.UNEXPECTED_ERROR + assert "Signature verification failed" in result.output + assert "--allow-unsigned" in result.output + + @pytest.mark.parametrize( + "source_type, method, cli_args", + [ + pytest.param( + PluginSourceType.URL, + "download_from_url", + ["plugin", "install", "https://example.com/plugin.whl"], + id="url", + ), + pytest.param( + PluginSourceType.GITHUB_RELEASE, + "download_from_github_release", + [ + "plugin", + "install", + "https://github.com/o/r/releases/download/v1/p.whl", + ], + id="github_release", + ), + pytest.param( + PluginSourceType.GITHUB_ARTIFACT, + "download_from_github_artifact", + [ + "plugin", + "install", + "https://github.com/o/r/actions/runs/1/artifacts/2", + ], + id="github_artifact", + ), + ], + ) + def test_signature_error_all_sources( + self, cli_fs_runner, source_type, method, cli_args + ) -> None: + """Test that SignatureVerificationError is handled for all source types.""" + with ( + mock.patch( + "ggshield.cmd.plugin.install.PluginDownloader" + ) as mock_downloader_class, + mock.patch( + "ggshield.cmd.plugin.install.EnterpriseConfig" + ) as mock_config_class, + mock.patch( + "ggshield.cmd.plugin.install.detect_source_type", + return_value=source_type, + ), + ): + mock_downloader = mock.MagicMock() + getattr(mock_downloader, method).side_effect = SignatureVerificationError( + SignatureStatus.INVALID, "bad signature" + ) + mock_downloader_class.return_value = mock_downloader + + mock_config = mock.MagicMock() + mock_config.get_signature_mode.return_value = mock.MagicMock() + mock_config_class.load.return_value = mock_config + + result = cli_fs_runner.invoke(cli, cli_args) + + assert result.exit_code == ExitCode.UNEXPECTED_ERROR + assert "Signature verification failed" in result.output + assert "--allow-unsigned" in result.output + + def test_allow_unsigned_flag(self, cli_fs_runner, tmp_path: Path) -> None: + """ + GIVEN --allow-unsigned flag + WHEN installing a plugin + THEN signature mode is set to WARN (not STRICT) + """ + wheel_path = tmp_path / "plugin.whl" + wheel_path.touch() + + with ( + mock.patch( + "ggshield.cmd.plugin.install.PluginDownloader" + ) as mock_downloader_class, + mock.patch( + "ggshield.cmd.plugin.install.EnterpriseConfig" + ) as mock_config_class, + mock.patch( + "ggshield.cmd.plugin.install.detect_source_type", + return_value=PluginSourceType.LOCAL_FILE, + ), + ): + mock_downloader = mock.MagicMock() + mock_downloader.install_from_wheel.return_value = ( + "plugin", + "1.0.0", + wheel_path, + ) + mock_downloader_class.return_value = mock_downloader + + mock_config = mock.MagicMock() + mock_config.get_signature_mode.return_value = mock.MagicMock() + mock_config_class.load.return_value = mock_config + + result = cli_fs_runner.invoke( + cli, + ["plugin", "install", str(wheel_path), "--allow-unsigned"], + catch_exceptions=False, + ) + + assert result.exit_code == ExitCode.SUCCESS + call_kwargs = mock_downloader.install_from_wheel.call_args[1] + from ggshield.core.plugin.signature import SignatureVerificationMode + + assert call_kwargs["signature_mode"] == SignatureVerificationMode.WARN diff --git a/tests/unit/core/plugin/test_downloader.py b/tests/unit/core/plugin/test_downloader.py index 8967ccfba6..d672338fc9 100644 --- a/tests/unit/core/plugin/test_downloader.py +++ b/tests/unit/core/plugin/test_downloader.py @@ -21,6 +21,7 @@ InsecureSourceError, PluginDownloader, get_plugins_dir, + get_signature_label, ) from ggshield.core.plugin.signature import SignatureInfo, SignatureStatus @@ -1127,6 +1128,223 @@ def test_get_gh_token_not_installed(self, tmp_path: Path) -> None: assert token is None +class TestGetSignatureLabel: + """Tests for get_signature_label.""" + + def test_returns_none_when_no_signature(self) -> None: + manifest: dict = {"plugin_name": "test", "version": "1.0.0"} + assert get_signature_label(manifest) is None + + def test_returns_none_when_signature_is_empty(self) -> None: + manifest: dict = {"signature": {}} + assert get_signature_label(manifest) is None + + def test_returns_status_with_identity(self) -> None: + manifest: dict = { + "signature": { + "status": "valid", + "identity": "GitGuardian/satori", + } + } + assert get_signature_label(manifest) == "valid (GitGuardian/satori)" + + def test_returns_status_without_identity(self) -> None: + manifest: dict = {"signature": {"status": "missing"}} + assert get_signature_label(manifest) == "missing" + + def test_returns_unknown_when_no_status(self) -> None: + manifest: dict = {"signature": {"identity": "org/repo"}} + assert get_signature_label(manifest) == "unknown (org/repo)" + + +class TestDownloadBundle: + """Tests for _download_bundle method.""" + + def test_returns_none_when_no_signature_url(self, tmp_path: Path) -> None: + download_info = PluginDownloadInfo( + download_url="https://example.com/plugin.whl", + filename="plugin-1.0.0.whl", + sha256="abc", + version="1.0.0", + expires_at="2025-01-01T00:00:00Z", + signature_url=None, + ) + + with patch( + "ggshield.core.plugin.downloader.get_plugins_dir", return_value=tmp_path + ): + downloader = PluginDownloader() + + result = downloader._download_bundle(download_info, tmp_path) + assert result is None + + def test_downloads_bundle_successfully(self, tmp_path: Path) -> None: + download_info = PluginDownloadInfo( + download_url="https://example.com/plugin.whl", + filename="plugin-1.0.0.whl", + sha256="abc", + version="1.0.0", + expires_at="2025-01-01T00:00:00Z", + signature_url="https://example.com/plugin.whl.sigstore", + ) + + mock_response = MagicMock() + mock_response.iter_content.return_value = [b"bundle-content"] + mock_response.raise_for_status = MagicMock() + + with patch( + "ggshield.core.plugin.downloader.get_plugins_dir", return_value=tmp_path + ): + downloader = PluginDownloader() + + with patch("requests.get", return_value=mock_response): + result = downloader._download_bundle(download_info, tmp_path) + + assert result is not None + assert result.name == "plugin-1.0.0.whl.sigstore" + assert result.read_bytes() == b"bundle-content" + + def test_returns_none_on_network_error(self, tmp_path: Path) -> None: + download_info = PluginDownloadInfo( + download_url="https://example.com/plugin.whl", + filename="plugin-1.0.0.whl", + sha256="abc", + version="1.0.0", + expires_at="2025-01-01T00:00:00Z", + signature_url="https://example.com/plugin.whl.sigstore", + ) + + with patch( + "ggshield.core.plugin.downloader.get_plugins_dir", return_value=tmp_path + ): + downloader = PluginDownloader() + + with patch( + "requests.get", side_effect=requests.RequestException("Network error") + ): + result = downloader._download_bundle(download_info, tmp_path) + + assert result is None + + +class TestCleanupFailedInstall: + """Tests for _cleanup_failed_install method.""" + + def test_removes_wheel_file(self, tmp_path: Path) -> None: + wheel_path = tmp_path / "plugin-1.0.0.whl" + wheel_path.write_bytes(b"wheel") + + with patch( + "ggshield.core.plugin.downloader.get_plugins_dir", return_value=tmp_path + ): + downloader = PluginDownloader() + + downloader._cleanup_failed_install(wheel_path) + assert not wheel_path.exists() + + def test_removes_bundle_files(self, tmp_path: Path) -> None: + wheel_path = tmp_path / "plugin-1.0.0.whl" + wheel_path.write_bytes(b"wheel") + sigstore = tmp_path / "plugin-1.0.0.whl.sigstore" + sigstore.write_bytes(b"bundle") + sigstore_json = tmp_path / "plugin-1.0.0.whl.sigstore.json" + sigstore_json.write_bytes(b"bundle2") + + with patch( + "ggshield.core.plugin.downloader.get_plugins_dir", return_value=tmp_path + ): + downloader = PluginDownloader() + + downloader._cleanup_failed_install(wheel_path) + assert not wheel_path.exists() + assert not sigstore.exists() + assert not sigstore_json.exists() + + def test_handles_nonexistent_files(self, tmp_path: Path) -> None: + wheel_path = tmp_path / "nonexistent.whl" + + with patch( + "ggshield.core.plugin.downloader.get_plugins_dir", return_value=tmp_path + ): + downloader = PluginDownloader() + + # Should not raise + downloader._cleanup_failed_install(wheel_path) + + +class TestWriteManifestWithSignature: + """Tests for _write_manifest with signature_info.""" + + def test_manifest_includes_signature_when_valid(self, tmp_path: Path) -> None: + sig_info = SignatureInfo( + status=SignatureStatus.VALID, + identity="GitGuardian/satori", + ) + + with patch( + "ggshield.core.plugin.downloader.get_plugins_dir", return_value=tmp_path + ): + downloader = PluginDownloader() + + downloader._write_manifest( + plugin_dir=tmp_path, + plugin_name="test", + version="1.0.0", + wheel_filename="test-1.0.0.whl", + sha256="abc", + source=PluginSource(type=PluginSourceType.GITGUARDIAN_API), + signature_info=sig_info, + ) + + manifest = json.loads((tmp_path / "manifest.json").read_text()) + assert manifest["signature"]["status"] == "valid" + assert manifest["signature"]["identity"] == "GitGuardian/satori" + assert "message" not in manifest["signature"] + + def test_manifest_includes_signature_message(self, tmp_path: Path) -> None: + sig_info = SignatureInfo( + status=SignatureStatus.MISSING, + message="No bundle found", + ) + + with patch( + "ggshield.core.plugin.downloader.get_plugins_dir", return_value=tmp_path + ): + downloader = PluginDownloader() + + downloader._write_manifest( + plugin_dir=tmp_path, + plugin_name="test", + version="1.0.0", + wheel_filename="test-1.0.0.whl", + sha256="abc", + source=PluginSource(type=PluginSourceType.GITGUARDIAN_API), + signature_info=sig_info, + ) + + manifest = json.loads((tmp_path / "manifest.json").read_text()) + assert manifest["signature"]["status"] == "missing" + assert manifest["signature"]["message"] == "No bundle found" + + def test_manifest_omits_signature_when_none(self, tmp_path: Path) -> None: + with patch( + "ggshield.core.plugin.downloader.get_plugins_dir", return_value=tmp_path + ): + downloader = PluginDownloader() + + downloader._write_manifest( + plugin_dir=tmp_path, + plugin_name="test", + version="1.0.0", + wheel_filename="test-1.0.0.whl", + sha256="abc", + source=PluginSource(type=PluginSourceType.GITGUARDIAN_API), + ) + + manifest = json.loads((tmp_path / "manifest.json").read_text()) + assert "signature" not in manifest + + def _create_artifact_zip(content: bytes, filename: str) -> bytes: """Helper to create an artifact ZIP file in memory.""" import io diff --git a/tests/unit/core/plugin/test_enterprise_config.py b/tests/unit/core/plugin/test_enterprise_config.py index 294ee1342b..92d0cb6e2c 100644 --- a/tests/unit/core/plugin/test_enterprise_config.py +++ b/tests/unit/core/plugin/test_enterprise_config.py @@ -4,6 +4,7 @@ from unittest.mock import patch from ggshield.core.config.enterprise_config import EnterpriseConfig, PluginConfig +from ggshield.core.plugin.signature import SignatureVerificationMode class TestPluginConfig: @@ -152,6 +153,46 @@ def test_load_and_save(self, tmp_path: Path) -> None: assert loaded.plugins["test-plugin"].enabled is True assert loaded.plugins["test-plugin"].version == "1.0.0" + def test_get_signature_mode_default(self) -> None: + """Test default signature mode is strict.""" + config = EnterpriseConfig() + + assert config.get_signature_mode() == SignatureVerificationMode.STRICT + + def test_get_signature_mode_warn(self) -> None: + """Test warn signature mode.""" + config = EnterpriseConfig(plugin_signature_mode="warn") + + assert config.get_signature_mode() == SignatureVerificationMode.WARN + + def test_get_signature_mode_disabled(self) -> None: + """Test disabled signature mode.""" + config = EnterpriseConfig(plugin_signature_mode="disabled") + + assert config.get_signature_mode() == SignatureVerificationMode.DISABLED + + def test_get_signature_mode_invalid_falls_back_to_strict(self) -> None: + """Test that invalid mode falls back to strict.""" + config = EnterpriseConfig(plugin_signature_mode="invalid_value") + + assert config.get_signature_mode() == SignatureVerificationMode.STRICT + + def test_load_and_save_signature_mode(self, tmp_path: Path) -> None: + """Test that plugin_signature_mode is persisted.""" + config_path = tmp_path / "enterprise_config.yaml" + + with patch( + "ggshield.core.config.enterprise_config.get_enterprise_config_filepath" + ) as mock_path: + mock_path.return_value = config_path + + config = EnterpriseConfig(plugin_signature_mode="warn") + config.save() + + loaded = EnterpriseConfig.load() + assert loaded.plugin_signature_mode == "warn" + assert loaded.get_signature_mode() == SignatureVerificationMode.WARN + def test_load_simple_format(self, tmp_path: Path) -> None: """Test loading config with simple boolean format.""" config_path = tmp_path / "enterprise_config.yaml" diff --git a/tests/unit/core/plugin/test_loader.py b/tests/unit/core/plugin/test_loader.py index 0fc9921953..55e5e78460 100644 --- a/tests/unit/core/plugin/test_loader.py +++ b/tests/unit/core/plugin/test_loader.py @@ -14,7 +14,11 @@ parse_entry_point_from_content, ) from ggshield.core.plugin.registry import PluginRegistry -from ggshield.core.plugin.signature import SignatureVerificationMode +from ggshield.core.plugin.signature import ( + SignatureStatus, + SignatureVerificationError, + SignatureVerificationMode, +) class MockPlugin(GGShieldPlugin): @@ -589,6 +593,66 @@ def test_load_from_wheel_extracts_wheel(self, tmp_path: Path) -> None: assert extract_dir.exists() assert (extract_dir / "test_plugin" / "__init__.py").exists() + def test_load_from_wheel_rejects_on_strict_signature_error( + self, tmp_path: Path + ) -> None: + """Test that _load_from_wheel returns None when signature fails in STRICT mode.""" + import zipfile + + config = EnterpriseConfig() + loader = PluginLoader(config, signature_mode=SignatureVerificationMode.STRICT) + + wheel_path = tmp_path / "test-1.0.0.whl" + with zipfile.ZipFile(wheel_path, "w") as zf: + zf.writestr( + "test-1.0.0.dist-info/entry_points.txt", + "[ggshield.plugins]\ntest = test:Plugin\n", + ) + + with patch( + "ggshield.core.plugin.loader.verify_wheel_signature", + side_effect=SignatureVerificationError( + SignatureStatus.MISSING, "No bundle found" + ), + ): + result = loader._load_from_wheel(wheel_path) + + assert result is None + + def test_load_from_wheel_warns_on_missing_signature(self, tmp_path: Path) -> None: + """Test that _load_from_wheel proceeds with warning in WARN mode.""" + import zipfile + + from ggshield.core.plugin.signature import SignatureInfo + + config = EnterpriseConfig() + loader = PluginLoader(config, signature_mode=SignatureVerificationMode.WARN) + + wheel_path = tmp_path / "test_plugin-1.0.0.whl" + with zipfile.ZipFile(wheel_path, "w") as zf: + zf.writestr("test_plugin/__init__.py", "class TestPlugin: pass") + zf.writestr( + "test_plugin-1.0.0.dist-info/entry_points.txt", + "[ggshield.plugins]\ntest = test_plugin:TestPlugin\n", + ) + + with patch( + "ggshield.core.plugin.loader.verify_wheel_signature", + return_value=SignatureInfo( + status=SignatureStatus.MISSING, message="No bundle found" + ), + ): + with patch( + "ggshield.core.plugin.loader.importlib.import_module" + ) as mock_import: + mock_module = MagicMock() + mock_module.TestPlugin = MockPlugin + mock_import.return_value = mock_module + + result = loader._load_from_wheel(wheel_path) + + assert result is not None + def test_load_from_wheel_handles_exception(self, tmp_path: Path) -> None: """Test that _load_from_wheel returns None on exception.""" import zipfile