From 40fc69e6c699e0f23e05ef59e44cc6248c195051 Mon Sep 17 00:00:00 2001 From: Douglas Date: Thu, 11 Sep 2025 21:28:50 -0700 Subject: [PATCH] feat: add monorepo workspace support with --sub-path and --workspace-name (#120) - Add --sub-path option to scan manifest files in a subdirectory while preserving git context from target-path - Add --workspace-name option to append suffix to repository name (repo-name-workspace_name) - Require both options to be used together with validation - Update scanning logic to use combined target_path + sub_path for manifest file detection - Modify repository naming to include workspace suffix when provided - Preserve git repository context (commits, branches, etc.) from main target-path - Enable Socket CLI to work with monorepo structures where manifests are in subdirectories This allows users to scan specific workspaces within a monorepo while maintaining proper git context and --- pyproject.toml | 2 +- socketsecurity/__init__.py | 2 +- socketsecurity/config.py | 24 +++++++++++++++++ socketsecurity/socketcli.py | 51 ++++++++++++++++++++++++++++++++----- 4 files changed, 70 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ff7c2cb..d8747c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "socketsecurity" -version = "2.2.8" +version = "2.2.9" requires-python = ">= 3.10" license = {"file" = "LICENSE"} dependencies = [ diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index 8625d2e..2ba817a 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,2 +1,2 @@ __author__ = 'socket.dev' -__version__ = '2.2.8' +__version__ = '2.2.9' diff --git a/socketsecurity/config.py b/socketsecurity/config.py index a6b5b2d..72a2327 100644 --- a/socketsecurity/config.py +++ b/socketsecurity/config.py @@ -60,6 +60,8 @@ class CliConfig: license_file_name: str = "license_output.json" save_submitted_files_list: Optional[str] = None save_manifest_tar: Optional[str] = None + sub_path: Optional[str] = None + workspace_name: Optional[str] = None @classmethod def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig': @@ -106,6 +108,8 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig': 'license_file_name': args.license_file_name, 'save_submitted_files_list': args.save_submitted_files_list, 'save_manifest_tar': args.save_manifest_tar, + 'sub_path': args.sub_path, + 'workspace_name': args.workspace_name, 'version': __version__ } try: @@ -129,6 +133,14 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig': if args.owner: config_args['integration_org_slug'] = args.owner + # Validate that sub_path and workspace_name are used together + if args.sub_path and not args.workspace_name: + logging.error("--sub-path requires --workspace-name to be specified") + exit(1) + if args.workspace_name and not args.sub_path: + logging.error("--workspace-name requires --sub-path to be specified") + exit(1) + return cls(**config_args) def to_dict(self) -> dict: @@ -285,6 +297,18 @@ def create_argument_parser() -> argparse.ArgumentParser: default="[]", help="Files to analyze (JSON array string)" ) + path_group.add_argument( + "--sub-path", + dest="sub_path", + metavar="", + help="Sub-path within target-path for manifest file scanning (while preserving git context from target-path)" + ) + path_group.add_argument( + "--workspace-name", + dest="workspace_name", + metavar="", + help="Workspace name suffix to append to repository name (repo-name-workspace_name)" + ) path_group.add_argument( "--excluded-ecosystems", diff --git a/socketsecurity/socketcli.py b/socketsecurity/socketcli.py index a0d071e..15ae755 100644 --- a/socketsecurity/socketcli.py +++ b/socketsecurity/socketcli.py @@ -136,13 +136,35 @@ def main_code(): raise Exception(f"Unable to find path {config.target_path}") if not config.repo: - config.repo = "socket-default-repo" + base_repo_name = "socket-default-repo" + if config.workspace_name: + config.repo = f"{base_repo_name}-{config.workspace_name}" + else: + config.repo = base_repo_name log.debug(f"Using default repository name: {config.repo}") if not config.branch: config.branch = "socket-default-branch" log.debug(f"Using default branch name: {config.branch}") + # Calculate the scan path - combine target_path with sub_path if provided + scan_path = config.target_path + if config.sub_path: + import os + scan_path = os.path.join(config.target_path, config.sub_path) + log.debug(f"Using sub-path for scanning: {scan_path}") + # Verify the scan path exists + if not os.path.exists(scan_path): + raise Exception(f"Sub-path does not exist: {scan_path}") + + # Modify repository name if workspace_name is provided + if config.workspace_name and config.repo: + config.repo = f"{config.repo}-{config.workspace_name}" + log.debug(f"Modified repository name with workspace suffix: {config.repo}") + elif config.workspace_name and not config.repo: + # If no repo name was set but workspace_name is provided, we'll use it later + log.debug(f"Workspace name provided: {config.workspace_name}") + scm = None if config.scm == "github": from socketsecurity.core.scm.github import Github, GithubConfig @@ -179,6 +201,21 @@ def main_code(): # Check if we have supported manifest files has_supported_files = files_to_check and core.has_manifest_files(files_to_check) + # If using sub_path, we need to check if manifest files exist in the scan path + if config.sub_path and not files_explicitly_specified: + # Override file checking to look in the scan path instead + import os + from pathlib import Path + + # Get manifest files from the scan path + try: + scan_files = core.find_files(scan_path) + has_supported_files = len(scan_files) > 0 + log.debug(f"Found {len(scan_files)} manifest files in scan path: {scan_path}") + except Exception as e: + log.debug(f"Error finding files in scan path {scan_path}: {e}") + has_supported_files = False + # Case 3: If no supported files or files are empty, force API mode (no PR comments) if not has_supported_files: force_api_mode = True @@ -264,7 +301,7 @@ def main_code(): log.info("Push initiated flow") if scm.check_event_type() == "diff": log.info("Starting comment logic for PR/MR event") - diff = core.create_new_diff(config.target_path, params, no_change=should_skip_scan, save_files_list_path=config.save_submitted_files_list, save_manifest_tar_path=config.save_manifest_tar) + diff = core.create_new_diff(scan_path, params, no_change=should_skip_scan, save_files_list_path=config.save_submitted_files_list, save_manifest_tar_path=config.save_manifest_tar) comments = scm.get_comments_for_pr() log.debug("Removing comment alerts") @@ -317,14 +354,14 @@ def main_code(): ) else: log.info("Starting non-PR/MR flow") - diff = core.create_new_diff(config.target_path, params, no_change=should_skip_scan, save_files_list_path=config.save_submitted_files_list, save_manifest_tar_path=config.save_manifest_tar) + diff = core.create_new_diff(scan_path, params, no_change=should_skip_scan, save_files_list_path=config.save_submitted_files_list, save_manifest_tar_path=config.save_manifest_tar) output_handler.handle_output(diff) elif config.enable_diff and not force_api_mode: # New logic: --enable-diff forces diff mode even with --integration api (no SCM) log.info("Diff mode enabled without SCM integration") - diff = core.create_new_diff(config.target_path, params, no_change=should_skip_scan, save_files_list_path=config.save_submitted_files_list, save_manifest_tar_path=config.save_manifest_tar) + diff = core.create_new_diff(scan_path, params, no_change=should_skip_scan, save_files_list_path=config.save_submitted_files_list, save_manifest_tar_path=config.save_manifest_tar) output_handler.handle_output(diff) elif config.enable_diff and force_api_mode: @@ -337,7 +374,7 @@ def main_code(): } log.debug(f"params={serializable_params}") diff = core.create_full_scan_with_report_url( - config.target_path, + scan_path, params, no_change=should_skip_scan, save_files_list_path=config.save_submitted_files_list, @@ -356,7 +393,7 @@ def main_code(): } log.debug(f"params={serializable_params}") diff = core.create_full_scan_with_report_url( - config.target_path, + scan_path, params, no_change=should_skip_scan, save_files_list_path=config.save_submitted_files_list, @@ -367,7 +404,7 @@ def main_code(): else: log.info("API Mode") diff = core.create_new_diff( - config.target_path, params, + scan_path, params, no_change=should_skip_scan, save_files_list_path=config.save_submitted_files_list, save_manifest_tar_path=config.save_manifest_tar