diff --git a/README.md b/README.md index e83ef6e..8b09ea5 100644 --- a/README.md +++ b/README.md @@ -496,3 +496,34 @@ Implementation targets: #### GitLab Integration - `GITLAB_TOKEN`: GitLab API token for GitLab integration (supports both Bearer and PRIVATE-TOKEN authentication) - `CI_JOB_TOKEN`: GitLab CI job token (automatically provided in GitLab CI environments) + +### Manual Development Environment Setup + +For manual setup without using the Make targets, follow these steps: + +1. **Create a virtual environment:** +```bash +python -m venv .venv +``` + +2. **Activate the virtual environment:** +```bash +source .venv/bin/activate +``` + +3. **Sync dependencies with uv:** +```bash +uv sync +``` + +4. **Install pre-commit:** +```bash +uv add --dev pre-commit +``` + +5. **Register the pre-commit hook:** +```bash +pre-commit install +``` + +> **Note**: This manual setup is an alternative to the streamlined Make targets described above. For most development workflows, using `make first-time-setup` or `make first-time-local-setup` is recommended. diff --git a/pyproject.toml b/pyproject.toml index 86fb4bd..a167d1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "socketsecurity" -version = "2.2.38" +version = "2.2.40" requires-python = ">= 3.10" license = {"file" = "LICENSE"} dependencies = [ @@ -16,7 +16,7 @@ dependencies = [ 'GitPython', 'packaging', 'python-dotenv', - 'socketdev>=3.0.19,<4.0.0', + 'socketdev>=3.0.21,<4.0.0', "bs4>=0.0.2", ] readme = "README.md" diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index db4d3e0..58461e0 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,3 +1,3 @@ __author__ = 'socket.dev' -__version__ = '2.2.38' +__version__ = '2.2.40' USER_AGENT = f'SocketPythonCLI/{__version__}' diff --git a/socketsecurity/core/__init__.py b/socketsecurity/core/__init__.py index 443c5cf..fd8774e 100644 --- a/socketsecurity/core/__init__.py +++ b/socketsecurity/core/__init__.py @@ -10,7 +10,10 @@ from glob import glob from io import BytesIO from pathlib import PurePath -from typing import BinaryIO, Dict, List, Tuple, Set, Union +from typing import BinaryIO, Dict, List, Tuple, Set, Union, TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from socketsecurity.config import CliConfig from socketdev import socketdev from socketdev.exceptions import APIFailure from socketdev.fullscans import FullScanParams, SocketArtifact @@ -59,11 +62,13 @@ class Core: config: SocketConfig sdk: socketdev + cli_config: Optional['CliConfig'] - def __init__(self, config: SocketConfig, sdk: socketdev) -> None: + def __init__(self, config: SocketConfig, sdk: socketdev, cli_config: Optional['CliConfig'] = None) -> None: """Initialize Core with configuration and SDK instance.""" self.config = config self.sdk = sdk + self.cli_config = cli_config self.set_org_vars() def set_org_vars(self) -> None: @@ -453,7 +458,61 @@ def empty_head_scan_file() -> List[str]: log.debug(f"Created temporary empty file for baseline scan: {temp_path}") return [temp_path] - def create_full_scan(self, files: List[str], params: FullScanParams, base_paths: List[str] = None) -> FullScan: + def finalize_tier1_scan(self, full_scan_id: str, facts_file_path: str) -> bool: + """ + Finalize a tier 1 reachability scan by associating it with a full scan. + + This function reads the tier1ReachabilityScanId from the facts file and + calls the SDK to link it with the specified full scan. + + Linking the tier 1 scan to the full scan helps the Socket team debug potential issues. + + Args: + full_scan_id: The ID of the full scan to associate with the tier 1 scan + facts_file_path: Path to the .socket.facts.json file containing the tier1ReachabilityScanId + + Returns: + True if successful, False otherwise + """ + log.debug(f"Finalizing tier 1 scan for full scan {full_scan_id}") + + # Read the tier1ReachabilityScanId from the facts file + try: + if not os.path.exists(facts_file_path): + log.debug(f"Facts file not found: {facts_file_path}") + return False + + with open(facts_file_path, 'r') as f: + facts = json.load(f) + + tier1_scan_id = facts.get('tier1ReachabilityScanId') + if not tier1_scan_id: + log.debug(f"No tier1ReachabilityScanId found in {facts_file_path}") + return False + + tier1_scan_id = tier1_scan_id.strip() + log.debug(f"Found tier1ReachabilityScanId: {tier1_scan_id}") + + except (json.JSONDecodeError, IOError) as e: + log.debug(f"Failed to read tier1ReachabilityScanId from {facts_file_path}: {e}") + return False + + # Call the SDK to finalize the tier 1 scan + try: + success = self.sdk.fullscans.finalize_tier1( + full_scan_id=full_scan_id, + tier1_reachability_scan_id=tier1_scan_id, + ) + + if success: + log.debug(f"Successfully finalized tier 1 scan {tier1_scan_id} for full scan {full_scan_id}") + return success + + except Exception as e: + log.debug(f"Unable to finalize tier 1 scan: {e}") + return False + + def create_full_scan(self, files: List[str], params: FullScanParams, base_paths: Optional[List[str]] = None) -> FullScan: """ Creates a new full scan via the Socket API. @@ -478,6 +537,19 @@ def create_full_scan(self, files: List[str], params: FullScanParams, base_paths: total_time = create_full_end - create_full_start log.debug(f"New Full Scan created in {total_time:.2f} seconds") + # Finalize tier1 scan if reachability analysis was enabled + if self.cli_config and self.cli_config.reach: + facts_file_path = self.cli_config.reach_output_file or ".socket.facts.json" + log.debug(f"Reachability analysis enabled, finalizing tier1 scan for full scan {full_scan.id}") + try: + success = self.finalize_tier1_scan(full_scan.id, facts_file_path) + if success: + log.debug(f"Successfully finalized tier1 scan for full scan {full_scan.id}") + else: + log.debug(f"Failed to finalize tier1 scan for full scan {full_scan.id}") + except Exception as e: + log.warning(f"Error finalizing tier1 scan for full scan {full_scan.id}: {e}") + return full_scan def create_full_scan_with_report_url( @@ -485,9 +557,9 @@ def create_full_scan_with_report_url( paths: List[str], params: FullScanParams, no_change: bool = False, - save_files_list_path: str = None, - save_manifest_tar_path: str = None, - base_paths: List[str] = None + save_files_list_path: Optional[str] = None, + save_manifest_tar_path: Optional[str] = None, + base_paths: Optional[List[str]] = None ) -> Diff: """Create a new full scan and return with html_report_url. @@ -881,9 +953,9 @@ def create_new_diff( paths: List[str], params: FullScanParams, no_change: bool = False, - save_files_list_path: str = None, - save_manifest_tar_path: str = None, - base_paths: List[str] = None + save_files_list_path: Optional[str] = None, + save_manifest_tar_path: Optional[str] = None, + base_paths: Optional[List[str]] = None ) -> Diff: """Create a new diff using the Socket SDK. @@ -1130,6 +1202,7 @@ def create_purl(self, package_id: str, packages: dict[str, Package]) -> Purl: ) return purl + @staticmethod def get_source_data(package: Package, packages: dict) -> list: """ diff --git a/socketsecurity/socketcli.py b/socketsecurity/socketcli.py index 244402e..c244a2e 100644 --- a/socketsecurity/socketcli.py +++ b/socketsecurity/socketcli.py @@ -1,4 +1,5 @@ import json +import os import sys import traceback import shutil @@ -81,7 +82,7 @@ def main_code(): client = CliClient(socket_config) sdk.api.api_url = socket_config.api_url log.debug("loaded client") - core = Core(socket_config, sdk) + core = Core(socket_config, sdk, config) log.debug("loaded core") # Check for required dependencies if reachability analysis is enabled @@ -207,7 +208,6 @@ def main_code(): base_paths = [config.target_path] # Always use target_path as the single base path if config.sub_paths: - import os for sub_path in config.sub_paths: full_scan_path = os.path.join(config.target_path, sub_path) log.debug(f"Using sub-path for scanning: {full_scan_path}") @@ -299,7 +299,6 @@ def main_code(): # If only-facts-file mode, mark the facts file for submission if config.only_facts_file: - import os facts_file_to_submit = os.path.abspath(output_path) log.info(f"Only-facts-file mode: will submit only {facts_file_to_submit}") @@ -355,9 +354,6 @@ def main_code(): # If using sub_paths, we need to check if manifest files exist in the scan paths if config.sub_paths and not files_explicitly_specified: # Override file checking to look in the scan paths instead - import os - from pathlib import Path - # Get manifest files from all scan paths try: all_scan_files = [] @@ -569,7 +565,7 @@ def main_code(): ) output_handler.handle_output(diff) - # Handle license generation + # Handle license generation if not should_skip_scan and diff.id != "NO_DIFF_RAN" and diff.id != "NO_SCAN_RAN" and config.generate_license: all_packages = {} for purl in diff.packages: