diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 76ed953..edd0e77 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @SocketDev/eng \ No newline at end of file +* @SocketDev/customer-engineering \ No newline at end of file diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml new file mode 100644 index 0000000..cc986cb --- /dev/null +++ b/.github/workflows/e2e-test.yml @@ -0,0 +1,109 @@ +name: E2E Test + +on: + push: + branches: [main] + pull_request: + +jobs: + e2e-scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + with: + fetch-depth: 0 + + - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + with: + python-version: '3.12' + + - name: Install CLI from local repo + run: | + python -m pip install --upgrade pip + pip install . + + - name: Run Socket CLI scan + env: + SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_CLI_API_TOKEN }} + run: | + set -o pipefail + socketcli \ + --target-path tests/e2e/fixtures/simple-npm \ + --disable-blocking \ + --enable-debug \ + 2>&1 | tee /tmp/scan-output.log + + - name: Verify scan produced a report + run: | + if grep -q "Full scan report URL: https://socket.dev/" /tmp/scan-output.log; then + echo "PASS: Full scan report URL found" + grep "Full scan report URL:" /tmp/scan-output.log + elif grep -q "Diff Url: https://socket.dev/" /tmp/scan-output.log; then + echo "PASS: Diff URL found" + grep "Diff Url:" /tmp/scan-output.log + else + echo "FAIL: No report URL found in scan output" + cat /tmp/scan-output.log + exit 1 + fi + + e2e-reachability: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + with: + fetch-depth: 0 + + - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + with: + python-version: '3.12' + + - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af + with: + node-version: '20' + + - name: Install CLI from local repo + run: | + python -m pip install --upgrade pip + pip install . + + - name: Install uv + run: pip install uv + + - name: Run Socket CLI with reachability + env: + SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_CLI_API_TOKEN }} + run: | + set -o pipefail + socketcli \ + --target-path tests/e2e/fixtures/simple-npm \ + --reach \ + --disable-blocking \ + --enable-debug \ + 2>&1 | tee /tmp/reach-output.log + + - name: Verify reachability analysis completed + run: | + if grep -q "Reachability analysis completed successfully" /tmp/reach-output.log; then + echo "PASS: Reachability analysis completed" + grep "Reachability analysis completed successfully" /tmp/reach-output.log + grep "Results written to:" /tmp/reach-output.log || true + else + echo "FAIL: Reachability analysis did not complete successfully" + cat /tmp/reach-output.log + exit 1 + fi + + - name: Verify scan produced a report + run: | + if grep -q "Full scan report URL: https://socket.dev/" /tmp/reach-output.log; then + echo "PASS: Full scan report URL found" + grep "Full scan report URL:" /tmp/reach-output.log + elif grep -q "Diff Url: https://socket.dev/" /tmp/reach-output.log; then + echo "PASS: Diff URL found" + grep "Diff Url:" /tmp/reach-output.log + else + echo "FAIL: No report URL found in scan output" + cat /tmp/reach-output.log + exit 1 + fi diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml index 9386346..2ee9b7e 100644 --- a/.github/workflows/pr-preview.yml +++ b/.github/workflows/pr-preview.yml @@ -16,12 +16,13 @@ jobs: fetch-depth: 0 - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 with: - python-version: '3.x' + python-version: '3.13' # Install all dependencies from pyproject.toml - name: Install dependencies run: | python -m pip install --upgrade pip + pip install "virtualenv<20.36" pip install hatchling==1.27.0 hatch==1.14.0 - name: Inject full dynamic version diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 20b4bd1..99372b6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,12 +15,13 @@ jobs: fetch-depth: 0 - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 with: - python-version: '3.x' + python-version: '3.13' # Install all dependencies from pyproject.toml - name: Install dependencies run: | python -m pip install --upgrade pip + pip install "virtualenv<20.36" pip install hatchling==1.27.0 hatch==1.14.0 - name: Get Version diff --git a/README.md b/README.md index b2c2d9a..0a13219 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ These examples are production-ready and include best practices for each platform ## Monorepo Workspace Support +> **Note:** If you're looking to associate a scan with a named Socket workspace (e.g. because your repo is identified as `org/repo`), see the [`--workspace` flag](#repository) instead. The `--workspace-name` flag described in this section is an unrelated monorepo feature. + The Socket CLI supports scanning specific workspaces within monorepo structures while preserving git context from the repository root. This is useful for organizations that maintain multiple applications or services in a single repository. ### Key Features @@ -114,7 +116,7 @@ This will simultaneously generate: ## Usage ```` shell -socketcli [-h] [--api-token API_TOKEN] [--repo REPO] [--repo-is-public] [--branch BRANCH] [--integration {api,github,gitlab,azure,bitbucket}] +socketcli [-h] [--api-token API_TOKEN] [--repo REPO] [--workspace WORKSPACE] [--repo-is-public] [--branch BRANCH] [--integration {api,github,gitlab,azure,bitbucket}] [--owner OWNER] [--pr-number PR_NUMBER] [--commit-message COMMIT_MESSAGE] [--commit-sha COMMIT_SHA] [--committers [COMMITTERS ...]] [--target-path TARGET_PATH] [--sbom-file SBOM_FILE] [--license-file-name LICENSE_FILE_NAME] [--save-submitted-files-list SAVE_SUBMITTED_FILES_LIST] [--save-manifest-tar SAVE_MANIFEST_TAR] [--files FILES] [--sub-path SUB_PATH] [--workspace-name WORKSPACE_NAME] @@ -138,14 +140,21 @@ If you don't want to provide the Socket API Token every time then you can use th | --api-token | False | | Socket Security API token (can also be set via SOCKET_SECURITY_API_TOKEN env var) | #### Repository -| Parameter | Required | Default | Description | -|:-----------------|:---------|:--------|:------------------------------------------------------------------------| -| --repo | False | *auto* | Repository name in owner/repo format (auto-detected from git remote) | -| --repo-is-public | False | False | If set, flags a new repository creation as public. Defaults to false. | -| --integration | False | api | Integration type (api, github, gitlab, azure, bitbucket) | -| --owner | False | | Name of the integration owner, defaults to the socket organization slug | -| --branch | False | *auto* | Branch name (auto-detected from git) | -| --committers | False | *auto* | Committer(s) to filter by (auto-detected from git commit) | +| Parameter | Required | Default | Description | +|:-----------------|:---------|:--------|:------------------------------------------------------------------------------------------------------------------| +| --repo | False | *auto* | Repository name in owner/repo format (auto-detected from git remote) | +| --workspace | False | | The Socket workspace to associate the scan with (e.g. `my-org` in `my-org/my-repo`). See note below. | +| --repo-is-public | False | False | If set, flags a new repository creation as public. Defaults to false. | +| --integration | False | api | Integration type (api, github, gitlab, azure, bitbucket) | +| --owner | False | | Name of the integration owner, defaults to the socket organization slug | +| --branch | False | *auto* | Branch name (auto-detected from git) | +| --committers | False | *auto* | Committer(s) to filter by (auto-detected from git commit) | + +> **`--workspace` vs `--workspace-name`** — these are two distinct flags for different purposes: +> +> - **`--workspace `** maps to the Socket API's `workspace` query parameter on `CreateOrgFullScan`. Use it when your repository belongs to a named Socket workspace (e.g. an org with multiple workspace groups). Example: `--repo my-repo --workspace my-org`. Without this flag, scans are created without workspace context and may not appear under the correct workspace in the Socket dashboard. +> +> - **`--workspace-name `** is a monorepo feature. It appends a suffix to the repository slug to create a unique name in Socket (e.g. `my-repo-frontend`). It must always be paired with `--sub-path` and has nothing to do with the API `workspace` field. See [Monorepo Workspace Support](#monorepo-workspace-support) below. #### Pull Request and Commit | Parameter | Required | Default | Description | diff --git a/instructions/gitlab-commit-status/uat.md b/instructions/gitlab-commit-status/uat.md new file mode 100644 index 0000000..f3b62a8 --- /dev/null +++ b/instructions/gitlab-commit-status/uat.md @@ -0,0 +1,54 @@ +# UAT: GitLab Commit Status Integration + +## Feature +`--enable-commit-status` posts a commit status (`success`/`failed`) to GitLab after scan completes. Repo admins can then require `socket-security` as a status check on protected branches. + +## Prerequisites +- GitLab project with CI/CD configured +- `GITLAB_TOKEN` with `api` scope (or `CI_JOB_TOKEN` with sufficient permissions) +- Merge request pipeline (so `CI_MERGE_REQUEST_PROJECT_ID` is set) + +## Test Cases + +### 1. Pass scenario (no blocking alerts) +1. Create MR with no dependency changes (or only safe ones) +2. Run: `socketcli --scm gitlab --enable-commit-status` +3. **Expected**: Commit status `socket-security` = `success`, description = "No blocking issues" +4. Verify in GitLab: **Repository > Commits > (sha) > Pipelines** or **MR > Pipeline > External** tab + +### 2. Fail scenario (blocking alerts) +1. Create MR adding a package with known blocking alerts +2. Run: `socketcli --scm gitlab --enable-commit-status` +3. **Expected**: Commit status = `failed`, description = "N blocking alert(s) found" + +### 3. Flag omitted (default off) +1. Run: `socketcli --scm gitlab` (no `--enable-commit-status`) +2. **Expected**: No commit status posted + +### 4. Non-MR pipeline (push event without MR) +1. Trigger pipeline on a push (no MR context) +2. Run: `socketcli --scm gitlab --enable-commit-status` +3. **Expected**: Commit status skipped (no `mr_project_id`), no error + +### 5. API failure is non-fatal +1. Use an invalid/revoked `GITLAB_TOKEN` +2. Run: `socketcli --scm gitlab --enable-commit-status` +3. **Expected**: Error logged ("Failed to set commit status: ..."), scan still completes with correct exit code + +### 6. Non-GitLab SCM +1. Run: `socketcli --scm github --enable-commit-status` +2. **Expected**: Flag is accepted but commit status is not posted (GitHub not yet supported) + +## Blocking Merges on Failure + +### Option A: Pipelines must succeed (all GitLab tiers) +Since `socketcli` exits with code 1 when blocking alerts are found, the pipeline fails automatically. +1. Go to **Settings > General > Merge requests** +2. Under **Merge checks**, enable **"Pipelines must succeed"** +3. Save — GitLab will now prevent merging when the pipeline fails + +### Option B: External status checks (GitLab Ultimate only) +Use the `socket-security` commit status as a required external check. +1. Go to **Settings > General > Merge requests > Status checks** +2. Add an external status check with name `socket-security` +3. MRs will require Socket's `success` status to merge diff --git a/pyproject.toml b/pyproject.toml index 124646f..1a59754 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "socketsecurity" -version = "2.2.71" +version = "2.2.74" requires-python = ">= 3.10" license = {"file" = "LICENSE"} dependencies = [ @@ -16,7 +16,7 @@ dependencies = [ 'GitPython', 'packaging', 'python-dotenv', - "socketdev>=3.0.29,<4.0.0", + "socketdev>=3.0.31,<4.0.0", "bs4>=0.0.2", "markdown>=3.10", ] diff --git a/socket.yml b/socket.yml new file mode 100644 index 0000000..a8bca7f --- /dev/null +++ b/socket.yml @@ -0,0 +1,4 @@ +version: 2 + +projectIgnorePaths: + - "tests/e2e/fixtures/" diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index 03e8a4a..bee5bcf 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,3 +1,3 @@ __author__ = 'socket.dev' -__version__ = '2.2.71' +__version__ = '2.2.74' USER_AGENT = f'SocketPythonCLI/{__version__}' diff --git a/socketsecurity/config.py b/socketsecurity/config.py index 5105281..eb47772 100644 --- a/socketsecurity/config.py +++ b/socketsecurity/config.py @@ -66,6 +66,7 @@ class CliConfig: save_manifest_tar: Optional[str] = None sub_paths: List[str] = field(default_factory=list) workspace_name: Optional[str] = None + workspace: Optional[str] = None # Reachability Flags reach: bool = False reach_version: Optional[str] = None @@ -86,6 +87,7 @@ class CliConfig: only_facts_file: bool = False reach_use_only_pregenerated_sboms: bool = False max_purl_batch_size: int = 5000 + enable_commit_status: bool = False @classmethod def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig': @@ -144,6 +146,7 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig': 'save_manifest_tar': args.save_manifest_tar, 'sub_paths': args.sub_paths or [], 'workspace_name': args.workspace_name, + 'workspace': args.workspace, 'slack_webhook': args.slack_webhook, 'reach': args.reach, 'reach_version': args.reach_version, @@ -164,6 +167,7 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig': 'only_facts_file': args.only_facts_file, 'reach_use_only_pregenerated_sboms': args.reach_use_only_pregenerated_sboms, 'max_purl_batch_size': args.max_purl_batch_size, + 'enable_commit_status': args.enable_commit_status, 'version': __version__ } try: @@ -254,6 +258,12 @@ def create_argument_parser() -> argparse.ArgumentParser: help="Repository name in owner/repo format", required=False ) + repo_group.add_argument( + "--workspace", + metavar="", + help="The workspace in the Socket Organization that the repository is in to associate with the full scan.", + required=False + ) repo_group.add_argument( "--repo-is-public", dest="repo_is_public", @@ -512,6 +522,18 @@ def create_argument_parser() -> argparse.ArgumentParser: action="store_true", help=argparse.SUPPRESS ) + output_group.add_argument( + "--enable-commit-status", + dest="enable_commit_status", + action="store_true", + help="Report scan result as a commit status on GitLab (requires GitLab SCM)" + ) + output_group.add_argument( + "--enable_commit_status", + dest="enable_commit_status", + action="store_true", + help=argparse.SUPPRESS + ) # Plugin Configuration plugin_group = parser.add_argument_group('Plugin Configuration') diff --git a/socketsecurity/core/scm/gitlab.py b/socketsecurity/core/scm/gitlab.py index 70abf50..e88e050 100644 --- a/socketsecurity/core/scm/gitlab.py +++ b/socketsecurity/core/scm/gitlab.py @@ -47,8 +47,15 @@ def from_env(cls) -> 'GitlabConfig': # Determine which authentication pattern to use headers = cls._get_auth_headers(token) + # Prefer source branch SHA (real commit) over CI_COMMIT_SHA which + # may be a synthetic merge-result commit in merged-results pipelines. + commit_sha = ( + os.getenv('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA') or + os.getenv('CI_COMMIT_SHA', '') + ) + return cls( - commit_sha=os.getenv('CI_COMMIT_SHA', ''), + commit_sha=commit_sha, api_url=os.getenv('CI_API_V4_URL', ''), project_dir=os.getenv('CI_PROJECT_DIR', ''), mr_source_branch=mr_source_branch, @@ -260,6 +267,65 @@ def add_socket_comments( log.debug("No Previous version of Security Issue comment, posting") self.post_comment(security_comment) + def enable_merge_pipeline_check(self) -> None: + """Enable 'only_allow_merge_if_pipeline_succeeds' on the MR target project.""" + if not self.config.mr_project_id: + return + url = f"{self.config.api_url}/projects/{self.config.mr_project_id}" + try: + resp = requests.put( + url, + json={"only_allow_merge_if_pipeline_succeeds": True}, + headers=self.config.headers, + ) + if resp.status_code == 401: + fallback = self._get_fallback_headers(self.config.headers) + if fallback: + resp = requests.put( + url, + json={"only_allow_merge_if_pipeline_succeeds": True}, + headers=fallback, + ) + if resp.status_code >= 400: + log.error(f"GitLab enable merge check API {resp.status_code}: {resp.text}") + else: + log.info("Enabled 'pipelines must succeed' merge check on project") + except Exception as e: + log.error(f"Failed to enable merge pipeline check: {e}") + + def set_commit_status(self, state: str, description: str, target_url: str = '') -> None: + """Post a commit status to GitLab. state should be 'success' or 'failed'. + + Uses requests.post with json= directly because CliClient.request sends + data= (form-encoded) which GitLab's commit status endpoint rejects. + """ + if not self.config.mr_project_id: + log.debug("No mr_project_id, skipping commit status") + return + url = f"{self.config.api_url}/projects/{self.config.mr_project_id}/statuses/{self.config.commit_sha}" + payload = { + "state": state, + "context": "socket-security-commit-status", + "description": description, + } + if self.config.mr_source_branch: + payload["ref"] = self.config.mr_source_branch + if target_url: + payload["target_url"] = target_url + try: + log.debug(f"Posting commit status to {url}") + resp = requests.post(url, json=payload, headers=self.config.headers) + if resp.status_code == 401: + fallback = self._get_fallback_headers(self.config.headers) + if fallback: + resp = requests.post(url, json=payload, headers=fallback) + if resp.status_code >= 400: + log.error(f"GitLab commit status API {resp.status_code}: {resp.text}") + resp.raise_for_status() + log.info(f"Commit status set to '{state}' on {self.config.commit_sha[:8]}") + except Exception as e: + log.error(f"Failed to set commit status: {e}") + def remove_comment_alerts(self, comments: dict): security_alert = comments.get("security") if security_alert is not None: diff --git a/socketsecurity/socketcli.py b/socketsecurity/socketcli.py index 194da44..6f17f58 100644 --- a/socketsecurity/socketcli.py +++ b/socketsecurity/socketcli.py @@ -464,7 +464,8 @@ def main_code(): make_default_branch=is_default_branch, set_as_pending_head=is_default_branch, tmp=False, - scan_type='socket_tier1' if config.reach else 'socket' + scan_type='socket_tier1' if config.reach else 'socket', + workspace=config.workspace or None, ) params.include_license_details = not config.exclude_license_details @@ -641,6 +642,21 @@ def main_code(): log.debug("Temporarily enabling disable_blocking due to no supported manifest files") config.disable_blocking = True + # Post commit status to GitLab if enabled + if config.enable_commit_status and scm is not None: + from socketsecurity.core.scm.gitlab import Gitlab + if isinstance(scm, Gitlab) and scm.config.mr_project_id: + scm.enable_merge_pipeline_check() + passed = output_handler.report_pass(diff) + state = "success" if passed else "failed" + blocking_count = sum(1 for a in diff.new_alerts if a.error) + if passed: + description = "No blocking issues" + else: + description = f"{blocking_count} blocking alert(s) found" + target_url = diff.report_url or diff.diff_url or "" + scm.set_commit_status(state, description, target_url) + sys.exit(output_handler.return_exit_code(diff)) diff --git a/tests/e2e/fixtures/simple-npm/index.js b/tests/e2e/fixtures/simple-npm/index.js new file mode 100644 index 0000000..8057d28 --- /dev/null +++ b/tests/e2e/fixtures/simple-npm/index.js @@ -0,0 +1,13 @@ +const express = require('express') +const lodash = require('lodash') + +const app = express() + +app.get('/', (req, res) => { + const data = lodash.pick(req.query, ['name', 'age']) + res.json(data) +}) + +app.listen(3000, () => { + console.log(`Test fixture ${__filename} running on port 3000`) +}) diff --git a/tests/e2e/fixtures/simple-npm/package.json b/tests/e2e/fixtures/simple-npm/package.json new file mode 100644 index 0000000..cf70416 --- /dev/null +++ b/tests/e2e/fixtures/simple-npm/package.json @@ -0,0 +1,15 @@ +{ + "name": "reach-test-fixture", + "version": "1.0.0", + "description": "Test fixture for reachability analysis", + "main": "index.js", + "dependencies": { + "lodash": "4.17.23", + "express": "4.22.0", + "axios": "1.13.5" + }, + "devDependencies": { + "typescript": "5.0.4", + "jest": "29.5.0" + } +} diff --git a/tests/unit/test_cli_config.py b/tests/unit/test_cli_config.py index b72ed9b..045f0e4 100644 --- a/tests/unit/test_cli_config.py +++ b/tests/unit/test_cli_config.py @@ -60,4 +60,25 @@ def test_strict_blocking_with_disable_blocking(self): "--disable-blocking" ]) assert config.strict_blocking is True - assert config.disable_blocking is True \ No newline at end of file + assert config.disable_blocking is True + + def test_workspace_flag(self): + """Test that --workspace is parsed and stored correctly.""" + config = CliConfig.from_args(["--api-token", "test", "--workspace", "my-workspace"]) + assert config.workspace == "my-workspace" + + def test_workspace_default_is_none(self): + """Test that workspace defaults to None when not supplied.""" + config = CliConfig.from_args(["--api-token", "test"]) + assert config.workspace is None + + def test_workspace_is_independent_of_workspace_name(self): + """--workspace and --workspace-name are distinct flags with distinct purposes.""" + config = CliConfig.from_args([ + "--api-token", "test", + "--workspace", "my-workspace", + "--sub-path", ".", + "--workspace-name", "monorepo-suffix", + ]) + assert config.workspace == "my-workspace" + assert config.workspace_name == "monorepo-suffix" \ No newline at end of file diff --git a/tests/unit/test_gitlab_commit_status.py b/tests/unit/test_gitlab_commit_status.py new file mode 100644 index 0000000..fc57ed6 --- /dev/null +++ b/tests/unit/test_gitlab_commit_status.py @@ -0,0 +1,184 @@ +"""Tests for GitLab commit status integration""" +import os +import pytest +from unittest.mock import patch, MagicMock, call + +from socketsecurity.core.scm.gitlab import Gitlab, GitlabConfig + + +def _make_gitlab_config(**overrides): + defaults = dict( + commit_sha="abc123def456", + api_url="https://gitlab.example.com/api/v4", + project_dir="/builds/test", + mr_source_branch="feature", + mr_iid="42", + mr_project_id="99", + commit_message="test commit", + default_branch="main", + project_name="test-project", + pipeline_source="merge_request_event", + commit_author="dev@example.com", + token="glpat-test", + repository="test-project", + is_default_branch=False, + headers={"Authorization": "Bearer glpat-test", "accept": "application/json"}, + ) + defaults.update(overrides) + return GitlabConfig(**defaults) + + +class TestSetCommitStatus: + """Test Gitlab.set_commit_status()""" + + @patch("socketsecurity.core.scm.gitlab.requests.post") + def test_calls_correct_url_and_json_payload(self, mock_post): + mock_post.return_value = MagicMock(status_code=200) + config = _make_gitlab_config() + gl = Gitlab(client=MagicMock(), config=config) + + gl.set_commit_status("success", "No blocking issues", "https://app.socket.dev/report/123") + + mock_post.assert_called_once_with( + "https://gitlab.example.com/api/v4/projects/99/statuses/abc123def456", + json={ + "state": "success", + "context": "socket-security-commit-status", + "description": "No blocking issues", + "ref": "feature", + "target_url": "https://app.socket.dev/report/123", + }, + headers=config.headers, + ) + + @patch("socketsecurity.core.scm.gitlab.requests.post") + def test_failed_state_payload(self, mock_post): + mock_post.return_value = MagicMock(status_code=200) + config = _make_gitlab_config() + gl = Gitlab(client=MagicMock(), config=config) + + gl.set_commit_status("failed", "3 blocking alert(s) found") + + payload = mock_post.call_args.kwargs["json"] + assert payload["state"] == "failed" + assert payload["description"] == "3 blocking alert(s) found" + assert "target_url" not in payload + + @patch("socketsecurity.core.scm.gitlab.requests.post") + def test_skipped_when_no_mr_project_id(self, mock_post): + config = _make_gitlab_config(mr_project_id=None) + gl = Gitlab(client=MagicMock(), config=config) + + gl.set_commit_status("success", "No blocking issues") + + mock_post.assert_not_called() + + @patch("socketsecurity.core.scm.gitlab.requests.post") + def test_graceful_error_handling(self, mock_post): + mock_post.side_effect = Exception("connection error") + config = _make_gitlab_config() + gl = Gitlab(client=MagicMock(), config=config) + + # Should not raise + gl.set_commit_status("success", "No blocking issues") + + @patch("socketsecurity.core.scm.gitlab.requests.post") + def test_no_target_url_omitted_from_payload(self, mock_post): + mock_post.return_value = MagicMock(status_code=200) + config = _make_gitlab_config() + gl = Gitlab(client=MagicMock(), config=config) + + gl.set_commit_status("success", "No blocking issues", target_url="") + + payload = mock_post.call_args.kwargs["json"] + assert "target_url" not in payload + + @patch("socketsecurity.core.scm.gitlab.requests.post") + def test_auth_fallback_on_401(self, mock_post): + resp_401 = MagicMock(status_code=401) + resp_401.raise_for_status.side_effect = Exception("401") + resp_200 = MagicMock(status_code=200) + mock_post.side_effect = [resp_401, resp_200] + + config = _make_gitlab_config() + gl = Gitlab(client=MagicMock(), config=config) + + gl.set_commit_status("success", "No blocking issues") + + assert mock_post.call_count == 2 + # Second call should use fallback headers (PRIVATE-TOKEN) + fallback_headers = mock_post.call_args_list[1].kwargs["headers"] + assert "PRIVATE-TOKEN" in fallback_headers + + +class TestEnableMergePipelineCheck: + """Test Gitlab.enable_merge_pipeline_check()""" + + @patch("socketsecurity.core.scm.gitlab.requests.put") + def test_calls_correct_url_and_payload(self, mock_put): + mock_put.return_value = MagicMock(status_code=200) + config = _make_gitlab_config() + gl = Gitlab(client=MagicMock(), config=config) + + gl.enable_merge_pipeline_check() + + mock_put.assert_called_once_with( + "https://gitlab.example.com/api/v4/projects/99", + json={"only_allow_merge_if_pipeline_succeeds": True}, + headers=config.headers, + ) + + @patch("socketsecurity.core.scm.gitlab.requests.put") + def test_skipped_when_no_mr_project_id(self, mock_put): + config = _make_gitlab_config(mr_project_id=None) + gl = Gitlab(client=MagicMock(), config=config) + + gl.enable_merge_pipeline_check() + + mock_put.assert_not_called() + + @patch("socketsecurity.core.scm.gitlab.requests.put") + def test_auth_fallback_on_401(self, mock_put): + resp_401 = MagicMock(status_code=401) + resp_200 = MagicMock(status_code=200) + mock_put.side_effect = [resp_401, resp_200] + + config = _make_gitlab_config() + gl = Gitlab(client=MagicMock(), config=config) + + gl.enable_merge_pipeline_check() + + assert mock_put.call_count == 2 + fallback_headers = mock_put.call_args_list[1].kwargs["headers"] + assert "PRIVATE-TOKEN" in fallback_headers + + @patch("socketsecurity.core.scm.gitlab.requests.put") + def test_graceful_error_handling(self, mock_put): + mock_put.side_effect = Exception("connection error") + config = _make_gitlab_config() + gl = Gitlab(client=MagicMock(), config=config) + + # Should not raise + gl.enable_merge_pipeline_check() + + +class TestEnableCommitStatusCliArg: + """Test --enable-commit-status CLI argument parsing""" + + def test_default_is_false(self): + from socketsecurity.config import create_argument_parser + parser = create_argument_parser() + args = parser.parse_args([]) + assert args.enable_commit_status is False + + def test_flag_sets_true(self): + from socketsecurity.config import create_argument_parser + parser = create_argument_parser() + args = parser.parse_args(["--enable-commit-status"]) + assert args.enable_commit_status is True + + def test_underscore_alias(self): + from socketsecurity.config import create_argument_parser + parser = create_argument_parser() + args = parser.parse_args(["--enable_commit_status"]) + assert args.enable_commit_status is True diff --git a/uv.lock b/uv.lock index f8cbcd5..229b89b 100644 --- a/uv.lock +++ b/uv.lock @@ -451,51 +451,51 @@ toml = [ [[package]] name = "cryptography" -version = "46.0.3" +version = "46.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, - { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, - { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, - { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, - { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, - { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, - { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, - { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, - { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, - { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, - { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, - { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, - { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, - { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, - { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, - { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, - { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, - { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, - { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, - { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, - { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, - { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, - { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, - { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, - { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, - { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, - { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, - { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, - { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, - { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, + { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, + { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, + { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, + { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, + { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, + { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, + { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, + { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, + { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, + { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, + { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, + { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, ] [[package]] @@ -536,11 +536,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.20.1" +version = "3.20.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/23/ce7a1126827cedeb958fc043d61745754464eb56c5937c35bbf2b8e26f34/filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c", size = 19476, upload-time = "2025-12-15T23:54:28.027Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/7f/a1a97644e39e7316d850784c642093c99df1290a460df4ede27659056834/filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a", size = 16666, upload-time = "2025-12-15T23:54:26.874Z" }, + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, ] [[package]] @@ -1250,20 +1250,20 @@ wheels = [ [[package]] name = "socketdev" -version = "3.0.29" +version = "3.0.31" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/f4/a4434843e0f01da684d0d330f1b4b744abfad1ee4d6b6d5fddfa9228b122/socketdev-3.0.29.tar.gz", hash = "sha256:be201a9bd186da6ddae4725294d3cbf11b00ec76c96e46be38d78a569fde4af3", size = 170751, upload-time = "2026-01-21T09:15:57.465Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/7e/f927ebb11968f22e80d1760a0f57b967ac111bd698a478717aa3049be88a/socketdev-3.0.31.tar.gz", hash = "sha256:946b2d64f7256b2a4a848b1770770aad927fdedb470e129529ac100a87a3eee8", size = 170976, upload-time = "2026-02-26T16:48:35.518Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/08/f1cea3b342d8b9109f5792257e3f6c31c3ff53a1e42a87726a2acac65440/socketdev-3.0.29-py3-none-any.whl", hash = "sha256:c2f832a703bd61eb88a5e3f9b8079e62f7cd1352ec206a20a946c6dd34fa788e", size = 66783, upload-time = "2026-01-21T09:15:55.909Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2e/5c87e1c0c96efbd54da5d10a3d6c89cf674bfa6082f2854a515b853e64c6/socketdev-3.0.31-py3-none-any.whl", hash = "sha256:be87a48c17031a8b7aa2cfe207285f8447f502612bcb2b8ef6fcb5ede4947e9d", size = 66841, upload-time = "2026-02-26T16:48:33.748Z" }, ] [[package]] name = "socketsecurity" -version = "2.2.69" +version = "2.2.73" source = { editable = "." } dependencies = [ { name = "bs4" }, @@ -1316,7 +1316,7 @@ requires-dist = [ { name = "python-dotenv" }, { name = "requests" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.3.0" }, - { name = "socketdev", specifier = ">=3.0.29,<4.0.0" }, + { name = "socketdev", specifier = ">=3.0.31,<4.0.0" }, { name = "twine", marker = "extra == 'dev'" }, { name = "uv", marker = "extra == 'dev'", specifier = ">=0.1.0" }, ] @@ -1441,11 +1441,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.2" +version = "2.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } +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/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, + { 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" }, ] [[package]] @@ -1488,7 +1488,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.35.4" +version = "20.36.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -1496,9 +1496,9 @@ dependencies = [ { name = "platformdirs" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, ] [[package]]