diff --git a/CHANGELOG.md b/CHANGELOG.md index e2ea6c0..0dfab45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2.2.87 + +- Fixed diff scan API requests so `--timeout` is passed through to the Socket SDK request layer. +- Fixed `--exclude-license-details` so the full-scan diff comparison request sends `include_license_details=false`. +- Let diff comparison API failures propagate to top-level CLI exit handling so `--disable-blocking` is honored consistently. + ## 2.2.83 - Fixed branch detection in detached-HEAD CI checkouts. When `git name-rev --name-only HEAD` returned an output with a suffix operator (e.g. `remotes/origin/master~1`, `master^0`), the `~N`/`^N` was previously passed through as the branch name and rejected by the Socket API as an invalid Git ref. The suffix is now stripped before the prefix split, producing the bare branch name. diff --git a/pyproject.toml b/pyproject.toml index 49bb294..feaa859 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "socketsecurity" -version = "2.2.86" +version = "2.2.87" requires-python = ">= 3.11" license = {"file" = "LICENSE"} dependencies = [ diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index c816fab..69d6f7b 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,3 +1,3 @@ __author__ = 'socket.dev' -__version__ = '2.2.86' +__version__ = '2.2.87' USER_AGENT = f'SocketPythonCLI/{__version__}' diff --git a/socketsecurity/core/__init__.py b/socketsecurity/core/__init__.py index daaa9a8..b442b20 100644 --- a/socketsecurity/core/__init__.py +++ b/socketsecurity/core/__init__.py @@ -919,7 +919,8 @@ def get_license_text_via_purl(self, packages: dict[str, Package], batch_size: in def get_added_and_removed_packages( self, head_full_scan_id: str, - new_full_scan_id: str + new_full_scan_id: str, + include_license_details: bool = True ) -> Tuple[Dict[str, Package], Dict[str, Package], Dict[str, Package]]: """ Get packages that were added and removed between scans. @@ -936,17 +937,17 @@ def get_added_and_removed_packages( diff_start = time.time() try: diff_report = ( - self.sdk.fullscans.stream_diff - ( + self.sdk.fullscans.stream_diff( self.config.org_slug, head_full_scan_id, new_full_scan_id, - use_types=True + use_types=True, + include_license_details=str(include_license_details).lower() ).data ) except APIFailure as e: log.error(f"API Error: {e}") - sys.exit(1) + raise except Exception as e: import traceback log.error(f"Error getting diff report: {str(e)}") @@ -1149,7 +1150,11 @@ def create_new_diff( added_packages, removed_packages, packages - ) = self.get_added_and_removed_packages(head_full_scan_id, new_full_scan.id) + ) = self.get_added_and_removed_packages( + head_full_scan_id, + new_full_scan.id, + include_license_details=getattr(params, "include_license_details", True) + ) # Separate unchanged packages from added/removed for --strict-blocking support unchanged_packages = { diff --git a/socketsecurity/socketcli.py b/socketsecurity/socketcli.py index 1f2b166..cd83d6f 100644 --- a/socketsecurity/socketcli.py +++ b/socketsecurity/socketcli.py @@ -26,6 +26,23 @@ load_dotenv() +DEFAULT_API_TIMEOUT = 1200 + + +def get_api_request_timeout(config: CliConfig) -> int: + return config.timeout if config.timeout is not None else DEFAULT_API_TIMEOUT + + +def build_socket_sdk(config: CliConfig) -> socketdev: + cli_user_agent_string = f"SocketPythonCLI/{config.version}" + return socketdev( + token=config.api_token, + timeout=get_api_request_timeout(config), + allow_unverified=config.allow_unverified, + user_agent=cli_user_agent_string + ) + + def cli(): try: main_code() @@ -63,8 +80,7 @@ def main_code(): "1. Command line: --api-token YOUR_TOKEN\n" "2. Environment variable: SOCKET_SECURITY_API_TOKEN") sys.exit(3) - cli_user_agent_string = f"SocketPythonCLI/{config.version}" - sdk = socketdev(token=config.api_token, allow_unverified=config.allow_unverified, user_agent=cli_user_agent_string) + sdk = build_socket_sdk(config) # Suppress urllib3 InsecureRequestWarning when using --allow-unverified if config.allow_unverified: @@ -83,7 +99,7 @@ def main_code(): socket_config = SocketConfig( api_key=config.api_token, allow_unverified_ssl=config.allow_unverified, - timeout=config.timeout if config.timeout is not None else 1200 # Use CLI timeout if provided + timeout=get_api_request_timeout(config) ) log.debug("loaded socket_config") client = CliClient(socket_config) diff --git a/tests/core/test_sdk_methods.py b/tests/core/test_sdk_methods.py index bb096eb..0a3ff0d 100644 --- a/tests/core/test_sdk_methods.py +++ b/tests/core/test_sdk_methods.py @@ -1,4 +1,5 @@ import pytest +from socketdev.exceptions import APIFailure from socketdev.fullscans import FullScanParams from socketsecurity.core import Core @@ -101,6 +102,7 @@ def test_get_added_and_removed_packages(core): "head", "new", use_types=True, + include_license_details="true", ) # Verify the results @@ -115,6 +117,25 @@ def test_get_added_and_removed_packages(core): assert "dp2_t1" in removed # Verify transitive dependencies are also tracked assert "pypi/direct_package_1@1.6.0" in all_packages # Unchanged package is in full package map +def test_get_added_and_removed_packages_can_exclude_license_details(core): + """Test that diff scan license detail expansion can be disabled.""" + core.get_added_and_removed_packages("head", "new", include_license_details=False) + + core.sdk.fullscans.stream_diff.assert_called_once_with( + core.config.org_slug, + "head", + "new", + use_types=True, + include_license_details="false", + ) + +def test_get_added_and_removed_packages_reraises_api_failures(core): + """Test that API failures propagate to top-level CLI exit handling.""" + core.sdk.fullscans.stream_diff.side_effect = APIFailure("upstream request timeout") + + with pytest.raises(APIFailure): + core.get_added_and_removed_packages("head", "new") + def test_empty_alerts_preserved(core): """Test that empty alerts arrays stay as empty arrays and don't become None""" # Get the scan that contains dp2 (which has empty alerts array) diff --git a/tests/unit/test_cli_config.py b/tests/unit/test_cli_config.py index 045f0e4..c26d1c5 100644 --- a/tests/unit/test_cli_config.py +++ b/tests/unit/test_cli_config.py @@ -1,6 +1,9 @@ import pytest + +from socketsecurity import socketcli from socketsecurity.config import CliConfig + class TestCliConfig: def test_api_token_from_env(self, monkeypatch): monkeypatch.setenv("SOCKET_SECURITY_API_KEY", "test-token") @@ -81,4 +84,25 @@ def test_workspace_is_independent_of_workspace_name(self): "--workspace-name", "monorepo-suffix", ]) assert config.workspace == "my-workspace" - assert config.workspace_name == "monorepo-suffix" \ No newline at end of file + assert config.workspace_name == "monorepo-suffix" + + def test_api_request_timeout_defaults_to_twenty_minutes(self): + config = CliConfig.from_args(["--api-token", "test"]) + assert socketcli.get_api_request_timeout(config) == 1200 + + def test_socket_sdk_receives_cli_timeout(self, monkeypatch): + captured = {} + + def fake_socketdev(**kwargs): + captured.update(kwargs) + return object() + + monkeypatch.setattr(socketcli, "socketdev", fake_socketdev) + config = CliConfig.from_args(["--api-token", "test", "--timeout", "1800"]) + + socketcli.build_socket_sdk(config) + + assert captured["token"] == "test" + assert captured["timeout"] == 1800 + assert captured["allow_unverified"] is False + assert captured["user_agent"] == f"SocketPythonCLI/{config.version}" diff --git a/tests/unit/test_socketcli.py b/tests/unit/test_socketcli.py new file mode 100644 index 0000000..a469c96 --- /dev/null +++ b/tests/unit/test_socketcli.py @@ -0,0 +1,36 @@ +import sys + +import pytest +from socketdev.exceptions import APIFailure + +from socketsecurity import socketcli + + +def test_cli_honors_disable_blocking_for_api_failures(monkeypatch): + def fail_main_code(): + raise APIFailure("upstream request timeout") + + monkeypatch.setattr(socketcli, "main_code", fail_main_code) + monkeypatch.setattr( + sys, + "argv", + ["socketcli", "--api-token", "test", "--disable-blocking"], + ) + + with pytest.raises(SystemExit) as exc_info: + socketcli.cli() + + assert exc_info.value.code == 0 + + +def test_cli_returns_error_for_api_failures_without_disable_blocking(monkeypatch): + def fail_main_code(): + raise APIFailure("upstream request timeout") + + monkeypatch.setattr(socketcli, "main_code", fail_main_code) + monkeypatch.setattr(sys, "argv", ["socketcli", "--api-token", "test"]) + + with pytest.raises(SystemExit) as exc_info: + socketcli.cli() + + assert exc_info.value.code == 3 diff --git a/uv.lock b/uv.lock index a90e6b2..5a67fa0 100644 --- a/uv.lock +++ b/uv.lock @@ -1168,7 +1168,7 @@ wheels = [ [[package]] name = "socketsecurity" -version = "2.2.86" +version = "2.2.87" source = { editable = "." } dependencies = [ { name = "bs4" },