From fab2f79ba9d92bdcca95c14fc307781a234596e2 Mon Sep 17 00:00:00 2001 From: Douglas Date: Fri, 12 Sep 2025 08:50:01 -0700 Subject: [PATCH] feat: Add support for base_paths parameter in fullscans and diffscans (#48) - Add base_paths parameter to Utils.load_files_for_sending_lazy() to support multiple base paths for file key stripping - Update FullScans.post() method to accept base_paths parameter with lazy loading support - Update DiffScans.create_from_repo() method to accept base_paths parameter - Maintain backward compatibility with existing base_path parameter - Fix test issues: correct fullscans.get() usage in comprehensive integration test - Update test expectations to match correct API path handling (path segments vs query params) The base_paths parameter takes precedence over base_path when both are provided, allowing users to specify multiple directory paths to strip from uploaded file keys for cleaner file organization in Socket scans. --- pyproject.toml | 2 +- socketdev/diffscans/__init__.py | 7 ++-- socketdev/fullscans/__init__.py | 5 ++- socketdev/utils/__init__.py | 41 +++++++++++++++---- socketdev/version.py | 2 +- .../test_comprehensive_integration.py | 2 +- tests/unit/test_working_endpoints_unit.py | 10 ++--- 7 files changed, 48 insertions(+), 21 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3d01a19..1afc226 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "socketdev" -version = "3.0.5" +version = "3.0.6" requires-python = ">= 3.9" dependencies = [ 'requests', diff --git a/socketdev/diffscans/__init__.py b/socketdev/diffscans/__init__.py index 12b297a..28075f2 100644 --- a/socketdev/diffscans/__init__.py +++ b/socketdev/diffscans/__init__.py @@ -1,6 +1,6 @@ import json import logging -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, List, Optional, Union from ..utils import Utils log = logging.getLogger("socketdev") @@ -30,7 +30,7 @@ def get(self, org_slug: str, diff_scan_id: str) -> dict: log.error(f"Error fetching diff scan: {response.status_code}, message: {response.text}") return {} - def create_from_repo(self, org_slug: str, repo_slug: str, files: list, params: Optional[Dict[str, Any]] = None, use_lazy_loading: bool = False, workspace: str = None, max_open_files: int = 100, base_path: str = None) -> dict: + def create_from_repo(self, org_slug: str, repo_slug: str, files: list, params: Optional[Dict[str, Any]] = None, use_lazy_loading: bool = False, workspace: str = None, max_open_files: int = 100, base_path: str = None, base_paths: list = None) -> dict: """ Create a diff scan from repo HEAD, uploading files as multipart form data. @@ -46,6 +46,7 @@ def create_from_repo(self, org_slug: str, repo_slug: str, files: list, params: O max_open_files: Maximum number of files to keep open simultaneously when using lazy loading. Useful for systems with low ulimit values (default: 100) base_path: Optional base path to strip from key names for cleaner file organization + base_paths: Optional list of base paths to strip from key names (takes precedence over base_path) Returns: dict: API response containing diff scan results @@ -64,7 +65,7 @@ def create_from_repo(self, org_slug: str, repo_slug: str, files: list, params: O # Use lazy loading if requested if use_lazy_loading: - prepared_files = Utils.load_files_for_sending_lazy(files, workspace, max_open_files, base_path) + prepared_files = Utils.load_files_for_sending_lazy(files, workspace, max_open_files, base_path, base_paths) else: prepared_files = files diff --git a/socketdev/fullscans/__init__.py b/socketdev/fullscans/__init__.py index 38d8964..94439f9 100644 --- a/socketdev/fullscans/__init__.py +++ b/socketdev/fullscans/__init__.py @@ -728,7 +728,7 @@ def get(self, org_slug: str, params: dict, use_types: bool = False) -> Union[dic ) return {} - def post(self, files: list, params: FullScanParams, use_types: bool = False, use_lazy_loading: bool = False, workspace: str = None, max_open_files: int = 100, base_path: str = None) -> Union[dict, CreateFullScanResponse]: + def post(self, files: list, params: FullScanParams, use_types: bool = False, use_lazy_loading: bool = False, workspace: str = None, max_open_files: int = 100, base_path: str = None, base_paths: List[str] = None) -> Union[dict, CreateFullScanResponse]: """ Create a new full scan by uploading manifest files. @@ -743,6 +743,7 @@ def post(self, files: list, params: FullScanParams, use_types: bool = False, use max_open_files: Maximum number of files to keep open simultaneously when using lazy loading. Useful for systems with low ulimit values (default: 100) base_path: Optional base path to strip from key names for cleaner file organization + base_paths: Optional list of base paths to strip from key names (takes precedence over base_path) Returns: dict or CreateFullScanResponse: API response containing scan results @@ -763,7 +764,7 @@ def post(self, files: list, params: FullScanParams, use_types: bool = False, use # Use lazy loading if requested if use_lazy_loading: - prepared_files = Utils.load_files_for_sending_lazy(files, workspace, max_open_files, base_path) + prepared_files = Utils.load_files_for_sending_lazy(files, workspace, max_open_files, base_path, base_paths) else: prepared_files = files diff --git a/socketdev/utils/__init__.py b/socketdev/utils/__init__.py index e9f556c..c18060e 100644 --- a/socketdev/utils/__init__.py +++ b/socketdev/utils/__init__.py @@ -233,7 +233,7 @@ def validate_integration_type(integration_type: str) -> IntegrationType: return integration_type # type: ignore @staticmethod - def load_files_for_sending_lazy(files: List[str], workspace: str = None, max_open_files: int = 100, base_path: str = None) -> List[Tuple[str, Tuple[str, LazyFileLoader]]]: + def load_files_for_sending_lazy(files: List[str], workspace: str = None, max_open_files: int = 100, base_path: str = None, base_paths: List[str] = None) -> List[Tuple[str, Tuple[str, LazyFileLoader]]]: """ Prepares files for sending to the Socket API using lazy loading. @@ -247,6 +247,7 @@ def load_files_for_sending_lazy(files: List[str], workspace: str = None, max_ope workspace: Base directory path to make paths relative to max_open_files: Maximum number of files to keep open simultaneously (default: 100) base_path: Optional base path to strip from key names for cleaner file organization + base_paths: Optional list of base paths to strip from key names (takes precedence over base_path) Returns: List of tuples formatted for requests multipart upload: @@ -260,6 +261,8 @@ def load_files_for_sending_lazy(files: List[str], workspace: str = None, max_ope workspace = workspace.replace("\\", "/") if base_path and "\\" in base_path: base_path = base_path.replace("\\", "/") + if base_paths: + base_paths = [bp.replace("\\", "/") if "\\" in bp else bp for bp in base_paths] for file_path in files: # Normalize file path @@ -270,29 +273,51 @@ def load_files_for_sending_lazy(files: List[str], workspace: str = None, max_ope # Calculate the key name for the form data key = file_path + path_stripped = False - # If base_path is provided, strip it from the file path to create the key - if base_path: + # If base_paths is provided, try to strip one of the paths from the file path + if base_paths: + for bp in base_paths: + # Normalize base_path to ensure consistent handling of trailing slashes + normalized_base_path = bp.rstrip("/") + "/" if not bp.endswith("/") else bp + if key.startswith(normalized_base_path): + key = key[len(normalized_base_path):] + path_stripped = True + break + elif key.startswith(bp.rstrip("/")): + # Handle case where base_path matches exactly without trailing slash + stripped_base = bp.rstrip("/") + if key.startswith(stripped_base + "/") or key == stripped_base: + key = key[len(stripped_base):] + key = key.lstrip("/") + path_stripped = True + break + + # If base_path is provided and no base_paths matched, use single base_path + elif base_path: # Normalize base_path to ensure consistent handling of trailing slashes normalized_base_path = base_path.rstrip("/") + "/" if not base_path.endswith("/") else base_path if key.startswith(normalized_base_path): key = key[len(normalized_base_path):] + path_stripped = True elif key.startswith(base_path.rstrip("/")): # Handle case where base_path matches exactly without trailing slash stripped_base = base_path.rstrip("/") if key.startswith(stripped_base + "/") or key == stripped_base: key = key[len(stripped_base):] key = key.lstrip("/") + path_stripped = True - # If workspace is provided and base_path wasn't used, fall back to workspace logic - elif workspace and file_path.startswith(workspace): + # If workspace is provided and no base paths matched, fall back to workspace logic + if not path_stripped and workspace and file_path.startswith(workspace): key = file_path[len(workspace):] key = key.lstrip("/") key = key.lstrip("./") + path_stripped = True - # If neither base_path nor workspace matched, clean up the key - if key == file_path: - # No base_path or workspace stripping occurred, clean up leading parts + # If no path stripping occurred, clean up the key + if not path_stripped: + # No base_path, base_paths, or workspace stripping occurred, clean up leading parts key = key.lstrip("/") key = key.lstrip("./") diff --git a/socketdev/version.py b/socketdev/version.py index e94f36f..6ed0182 100644 --- a/socketdev/version.py +++ b/socketdev/version.py @@ -1 +1 @@ -__version__ = "3.0.5" +__version__ = "3.0.6" diff --git a/tests/integration/test_comprehensive_integration.py b/tests/integration/test_comprehensive_integration.py index 0ecf3e2..137723b 100644 --- a/tests/integration/test_comprehensive_integration.py +++ b/tests/integration/test_comprehensive_integration.py @@ -152,7 +152,7 @@ def test_fullscans_basic_workflow(self): self.created_scan_ids.append(scan_id) # Get the scan - scan_info = self.sdk.fullscans.get(self.org_slug, scan_id) + scan_info = self.sdk.fullscans.get(self.org_slug, {"id": scan_id}) self.assertIsInstance(scan_info, dict) # List scans diff --git a/tests/unit/test_working_endpoints_unit.py b/tests/unit/test_working_endpoints_unit.py index bc20e41..e9d9d50 100644 --- a/tests/unit/test_working_endpoints_unit.py +++ b/tests/unit/test_working_endpoints_unit.py @@ -346,15 +346,15 @@ def test_fullscans_get_corrected_unit(self): """Test fullscans get - CORRECTED PARAMETER HANDLING.""" expected_data = {"id": "scan-123", "status": "completed"} self._mock_response(expected_data) - - # The actual API uses query parameters, not path parameters + + # When getting a specific scan by ID, it uses path parameters result = self.sdk.fullscans.get("test-org", {"id": "scan-123"}) - + self.assertEqual(result, expected_data) call_args = self.mock_requests.request.call_args self.assertEqual(call_args[0][0], "GET") - # Path includes query params, not path segments - self.assertIn("/orgs/test-org/full-scans?id=scan-123", call_args[0][1]) + # Single ID param creates path segment, not query params + self.assertIn("/orgs/test-org/full-scans/scan-123", call_args[0][1]) def test_diffscans_create_from_repo_corrected_unit(self): """Test diffscans creation from repo - CORRECTED PATH."""