From c9df8084e0140249ff5c906a33e6cae4dc9c8b86 Mon Sep 17 00:00:00 2001 From: Douglas Date: Fri, 22 Aug 2025 21:17:16 -0700 Subject: [PATCH] fix: include license violations in diff results + SDK 2.1.8 upgrade (#111) * feat: upgrade to SDK 2.1.8 with lazy loading and improved committer handling - Upgrade socket-sdk-python dependency to version 2.1.8 to support lazy file loading capabilities - Enable lazy file loading in fullscans.post() with use_lazy_loading=True and max_open_files=50 to prevent "Too many open files" errors when processing large numbers of manifest files - Remove custom lazy_file_loader module as this functionality is now handled by the SDK - Fix committer display format by implementing proper priority order: 1. CLI --committers argument (highest priority) 2. CI/CD SCM username (GITHUB_ACTOR, GITLAB_USER_LOGIN, BITBUCKET_STEP_TRIGGERER_UUID) 3. Git username extracted from email patterns (e.g., GitHub noreply emails) 4. Git email address 5. Git author name (fallback) - Add get_formatted_committer() method to Git class to properly format committer strings instead of displaying raw git.Actor objects - Include license alerts in diff processing by removing licenseSpdxDisj filter condition - Change ulimit warning messages from log.warning to log.debug to reduce noise - Update create_full_scan() method signature to accept file paths directly instead of pre-processed file objects - Remove deprecated load_files_for_sending() method as lazy loading is now handled by the SDK This update improves performance for large repositories, provides better committer identification in CI/CD environments, and ensures license violations are properly reported. * feat: add --enable-diff flag and improve license policy violation handling - Add --enable-diff flag to force differential scanning even when using --integration api - Improve license policy violation grouping and display in PR comments - Fix alert consolidation logic to prevent duplicate alerts based on manifest files - Enhance empty baseline scan creation with proper file cleanup - Add comprehensive test coverage for new enable_diff functionality - Update documentation with new scanning mode examples and usage patterns The --enable-diff flag enables differential mode without SCM integration, useful for getting diff reports while using the API integration type. License policy violations are now properly grouped by package and displayed with consistent formatting in GitHub PR comments. * changes for license processing * Fixing login issues for pushing Docker image * Another docker fix * bumping minor version since the PR ended up having a lot of changes --- .github/workflows/docker-stable.yml | 15 ++-- .github/workflows/pr-preview.yml | 14 ++-- .github/workflows/release.yml | 12 +-- README.md | 3 + pyproject.toml | 4 +- requirements.txt | 2 +- socketsecurity/__init__.py | 2 +- socketsecurity/config.py | 7 ++ socketsecurity/core/__init__.py | 115 +++++++++++++++++---------- socketsecurity/core/git_interface.py | 61 ++++++++++++++ socketsecurity/core/messages.py | 62 ++++++++++++++- socketsecurity/socketcli.py | 29 ++++++- tests/unit/test_cli_config.py | 16 +++- 13 files changed, 268 insertions(+), 74 deletions(-) diff --git a/.github/workflows/docker-stable.yml b/.github/workflows/docker-stable.yml index 4461498..2a4c92d 100644 --- a/.github/workflows/docker-stable.yml +++ b/.github/workflows/docker-stable.yml @@ -21,18 +21,18 @@ jobs: fi echo "Version ${{ inputs.version }} found on PyPI - proceeding with release" - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + - name: Login to Docker Hub with Organization Token + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build & Push Stable Docker uses: docker/build-push-action@v5 with: @@ -40,4 +40,5 @@ jobs: platforms: linux/amd64,linux/arm64 tags: socketdev/cli:stable build-args: | - CLI_VERSION=${{ inputs.version }} \ No newline at end of file + CLI_VERSION=${{ inputs.version }} + \ No newline at end of file diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml index 8c706ac..f3b142a 100644 --- a/.github/workflows/pr-preview.yml +++ b/.github/workflows/pr-preview.yml @@ -119,19 +119,19 @@ jobs: echo "success=false" >> $GITHUB_OUTPUT exit 1 - - name: Login to Docker Hub - if: steps.verify_package.outputs.success == 'true' - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + - name: Login to Docker Hub with Organization Token + if: steps.verify_package.outputs.success == 'true' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build & Push Docker Preview if: steps.verify_package.outputs.success == 'true' uses: docker/build-push-action@v5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0a5d0c6..b70d26e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -68,18 +68,18 @@ jobs: if: steps.version_check.outputs.pypi_exists != 'true' uses: pypa/gh-action-pypi-publish@v1.12.4 - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + - name: Login to Docker Hub with Organization Token + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Verify package is installable id: verify_package env: diff --git a/README.md b/README.md index 2029a45..6f9ca2b 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ If you don't want to provide the Socket API Token every time then you can use th |:-------------------------|:---------|:--------|:----------------------------------------------------------------------| | --ignore-commit-files | False | False | Ignore commit files | | --disable-blocking | False | False | Disable blocking mode | +| --enable-diff | False | False | Enable diff mode even when using --integration api (forces diff mode without SCM integration) | | --scm | False | api | Source control management type | | --timeout | False | | Timeout in seconds for API requests | | --include-module-folders | False | False | If enabled will include manifest files from folders like node_modules | @@ -205,6 +206,7 @@ The CLI determines which files to scan based on the following logic: - **Differential Mode**: When manifest files are detected in changes, performs a diff scan with PR/MR comment integration - **API Mode**: When no manifest files are in changes, creates a full scan report without PR comments but still scans the entire repository - **Force Mode**: With `--ignore-commit-files`, always performs a full scan regardless of changes +- **Forced Diff Mode**: With `--enable-diff`, forces differential mode even when using `--integration api` (without SCM integration) ### Examples @@ -212,6 +214,7 @@ The CLI determines which files to scan based on the following logic: - **Commit without manifest files**: If your commit only changes non-manifest files (like `.github/workflows/socket.yaml`), the CLI automatically switches to API mode and performs a full repository scan. - **Using `--files`**: If you specify `--files '["package.json"]'`, the CLI will check if this file exists and is a manifest file before determining scan type. - **Using `--ignore-commit-files`**: This forces a full scan of all manifest files in the target path, regardless of what's in your commit. +- **Using `--enable-diff`**: Forces diff mode without SCM integration - useful when you want differential scanning but are using `--integration api`. For example: `socketcli --integration api --enable-diff --target-path /path/to/repo` - **Auto-detection**: Most CI/CD scenarios now work with just `socketcli --target-path /path/to/repo --scm github --pr-number $PR_NUM` ## Debugging and Troubleshooting diff --git a/pyproject.toml b/pyproject.toml index 7f81e51..2f5c9e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "socketsecurity" -version = "2.1.35" +version = "2.2.0" requires-python = ">= 3.10" license = {"file" = "LICENSE"} dependencies = [ @@ -16,7 +16,7 @@ dependencies = [ 'GitPython', 'packaging', 'python-dotenv', - 'socket-sdk-python>=2.1.5,<3' + 'socket-sdk-python>=2.1.8,<3' ] readme = "README.md" description = "Socket Security CLI for CI/CD" diff --git a/requirements.txt b/requirements.txt index 9eca071..b2a6676 100644 --- a/requirements.txt +++ b/requirements.txt @@ -59,7 +59,7 @@ requests==2.32.4 # via socketsecurity smmap==5.0.2 # via gitdb -socket-sdk-python==2.1.5 +socket-sdk-python==2.1.8 # via socketsecurity typing-extensions==4.12.2 # via socket-sdk-python diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index c0d1c92..9309f87 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,2 +1,2 @@ __author__ = 'socket.dev' -__version__ = '2.1.35' +__version__ = '2.2.0' diff --git a/socketsecurity/config.py b/socketsecurity/config.py index 817c7da..a6b5b2d 100644 --- a/socketsecurity/config.py +++ b/socketsecurity/config.py @@ -48,6 +48,7 @@ class CliConfig: integration_type: IntegrationType = "api" integration_org_slug: Optional[str] = None pending_head: bool = False + enable_diff: bool = False timeout: Optional[int] = 1200 exclude_license_details: bool = False include_module_folders: bool = False @@ -421,6 +422,12 @@ def create_argument_parser() -> argparse.ArgumentParser: action="store_true", help=argparse.SUPPRESS ) + advanced_group.add_argument( + "--enable-diff", + dest="enable_diff", + action="store_true", + help="Enable diff mode even when using --integration api (forces diff mode without SCM integration)" + ) advanced_group.add_argument( "--scm", metavar="", diff --git a/socketsecurity/core/__init__.py b/socketsecurity/core/__init__.py index 7c43265..3edd097 100644 --- a/socketsecurity/core/__init__.py +++ b/socketsecurity/core/__init__.py @@ -2,6 +2,7 @@ import os import sys import tarfile +import tempfile import time import io import json @@ -30,7 +31,6 @@ from .socket_config import SocketConfig from .utils import socket_globs from .resource_utils import check_file_count_against_ulimit -from .lazy_file_loader import load_files_for_sending_lazy import importlib logging_std = importlib.import_module("logging") @@ -338,10 +338,10 @@ def find_files(self, path: str) -> List[str]: ulimit_check = check_file_count_against_ulimit(file_count) if ulimit_check["can_check"]: if ulimit_check["would_exceed"]: - log.warning(f"Found {file_count} manifest files, which may exceed the file descriptor limit (ulimit -n = {ulimit_check['soft_limit']})") - log.warning(f"Available file descriptors: {ulimit_check['available_fds']} (after {ulimit_check['buffer_size']} buffer)") - log.warning(f"Recommendation: {ulimit_check['recommendation']}") - log.warning("This may cause 'Too many open files' errors during processing") + log.debug(f"Found {file_count} manifest files, which may exceed the file descriptor limit (ulimit -n = {ulimit_check['soft_limit']})") + log.debug(f"Available file descriptors: {ulimit_check['available_fds']} (after {ulimit_check['buffer_size']} buffer)") + log.debug(f"Recommendation: {ulimit_check['recommendation']}") + log.debug("This may cause 'Too many open files' errors during processing") else: log.debug(f"File count ({file_count}) is within file descriptor limit ({ulimit_check['soft_limit']})") else: @@ -434,37 +434,29 @@ def to_case_insensitive_regex(input_string: str) -> str: return ''.join(f'[{char.lower()}{char.upper()}]' if char.isalpha() else char for char in input_string) @staticmethod - def empty_head_scan_file() -> list[tuple[str, tuple[str, Union[BinaryIO, BytesIO]]]]: - # Create an empty file for when no head full scan so that the diff endpoint can always be used - empty_file_obj = io.BytesIO(b"") - empty_filename = "initial_head_scan" - empty_full_scan_file = [(empty_filename, (empty_filename, empty_file_obj))] - return empty_full_scan_file - - @staticmethod - def load_files_for_sending(files: List[str], workspace: str) -> List[Tuple[str, Tuple[str, BinaryIO]]]: + def empty_head_scan_file() -> List[str]: """ - Prepares files for sending to the Socket API using lazy loading. + Creates a temporary empty file for baseline scans when no head scan exists. - This version uses lazy file loading to prevent "Too many open files" errors - when processing large numbers of manifest files. - - Args: - files: List of file paths from find_files() - workspace: Base directory path to make paths relative to - Returns: - List of tuples formatted for requests multipart upload: - [(field_name, (filename, file_object)), ...] + List containing path to a temporary empty file """ - return load_files_for_sending_lazy(files, workspace) + # Create a temporary empty file + temp_fd, temp_path = tempfile.mkstemp(suffix='.empty', prefix='socket_baseline_') + + # Close the file descriptor since we just need the path + # The file is already created and empty + os.close(temp_fd) + + log.debug(f"Created temporary empty file for baseline scan: {temp_path}") + return [temp_path] - def create_full_scan(self, files: list[tuple[str, tuple[str, BytesIO]]], params: FullScanParams) -> FullScan: + def create_full_scan(self, files: List[str], params: FullScanParams) -> FullScan: """ Creates a new full scan via the Socket API. Args: - files: List of files to scan + files: List of file paths to scan params: Parameters for the full scan Returns: @@ -473,7 +465,7 @@ def create_full_scan(self, files: list[tuple[str, tuple[str, BytesIO]]], params: log.info("Creating new full scan") create_full_start = time.time() - res = self.sdk.fullscans.post(files, params, use_types=True) + res = self.sdk.fullscans.post(files, params, use_types=True, use_lazy_loading=True, max_open_files=50) if not res.success: log.error(f"Error creating full scan: {res.message}, status: {res.status}") raise Exception(f"Error creating full scan: {res.message}, status: {res.status}") @@ -525,14 +517,13 @@ def create_full_scan_with_report_url( if save_manifest_tar_path and files: self.save_manifest_tar(files, save_manifest_tar_path, path) - files_for_sending = self.load_files_for_sending(files, path) if not files: return diff try: # Create new scan new_scan_start = time.time() - new_full_scan = self.create_full_scan(files_for_sending, params) + new_full_scan = self.create_full_scan(files, params) new_scan_end = time.time() log.info(f"Total time to create new full scan: {new_scan_end - new_scan_start:.2f}") except APIFailure as e: @@ -779,7 +770,15 @@ def get_added_and_removed_packages( log.info(f"Comparing scans - Head scan ID: {head_full_scan_id}, New scan ID: {new_full_scan_id}") diff_start = time.time() try: - diff_report = self.sdk.fullscans.stream_diff(self.config.org_slug, head_full_scan_id, new_full_scan_id, use_types=True).data + diff_report = ( + self.sdk.fullscans.stream_diff + ( + self.config.org_slug, + head_full_scan_id, + new_full_scan_id, + use_types=True + ).data + ) except APIFailure as e: log.error(f"API Error: {e}") sys.exit(1) @@ -877,7 +876,6 @@ def create_new_diff( if save_manifest_tar_path and files: self.save_manifest_tar(files, save_manifest_tar_path, path) - files_for_sending = self.load_files_for_sending(files, path) if not files: return Diff(id="NO_DIFF_RAN", diff_url="", report_url="") @@ -887,7 +885,9 @@ def create_new_diff( except APIResourceNotFound: head_full_scan_id = None + # If no head scan exists, create an empty baseline scan if head_full_scan_id is None: + log.info("No previous scan found - creating empty baseline scan") new_params = copy.deepcopy(params.__dict__) new_params.pop('include_license_details') tmp_params = FullScanParams(**new_params) @@ -895,13 +895,34 @@ def create_new_diff( tmp_params.tmp = True tmp_params.set_as_pending_head = False tmp_params.make_default_branch = False - head_full_scan = self.create_full_scan(Core.empty_head_scan_file(), tmp_params) - head_full_scan_id = head_full_scan.id + + # Create baseline scan with empty file + empty_files = Core.empty_head_scan_file() + try: + head_full_scan = self.create_full_scan(empty_files, tmp_params) + head_full_scan_id = head_full_scan.id + log.debug(f"Created empty baseline scan: {head_full_scan_id}") + + # Clean up the temporary empty file + for temp_file in empty_files: + try: + os.unlink(temp_file) + log.debug(f"Cleaned up temporary file: {temp_file}") + except OSError as e: + log.warning(f"Failed to clean up temporary file {temp_file}: {e}") + except Exception as e: + # Clean up temp files even if scan creation fails + for temp_file in empty_files: + try: + os.unlink(temp_file) + except OSError: + pass + raise e # Create new scan try: new_scan_start = time.time() - new_full_scan = self.create_full_scan(files_for_sending, params) + new_full_scan = self.create_full_scan(files, params) new_scan_end = time.time() log.info(f"Total time to create new full scan: {new_scan_end - new_scan_start:.2f}") except APIFailure as e: @@ -913,6 +934,7 @@ def create_new_diff( log.error(f"Stack trace:\n{traceback.format_exc()}") raise + # Handle diff generation - now we always have both scans scans_ready = self.check_full_scans_status(head_full_scan_id, new_full_scan.id) if scans_ready is False: log.error(f"Full scans did not complete within {self.config.timeout} seconds") @@ -1134,6 +1156,12 @@ def add_package_alerts_to_collection(self, package: Package, alerts_collection: alert = Alert(**alert_item) props = getattr(self.config.all_issues, alert.type, default_props) introduced_by = self.get_source_data(package, packages) + + # Handle special case for license policy violations + title = props.title + if alert.type == "licenseSpdxDisj" and not title: + title = "License Policy Violation" + issue_alert = Issue( pkg_type=package.type, pkg_name=package.name, @@ -1144,7 +1172,7 @@ def add_package_alerts_to_collection(self, package: Package, alerts_collection: type=alert.type, severity=alert.severity, description=props.description, - title=props.title, + title=title, suggestion=props.suggestion, next_step_title=props.nextStepTitle, introduced_by=introduced_by, @@ -1156,11 +1184,10 @@ def add_package_alerts_to_collection(self, package: Package, alerts_collection: action = self.config.security_policy[alert.type]['action'] setattr(issue_alert, action, True) - if issue_alert.type != 'licenseSpdxDisj': - if issue_alert.key not in alerts_collection: - alerts_collection[issue_alert.key] = [issue_alert] - else: - alerts_collection[issue_alert.key].append(issue_alert) + if issue_alert.key not in alerts_collection: + alerts_collection[issue_alert.key] = [issue_alert] + else: + alerts_collection[issue_alert.key].append(issue_alert) return alerts_collection @@ -1232,7 +1259,8 @@ def get_new_alerts( if alert_key not in removed_package_alerts: new_alerts = added_package_alerts[alert_key] for alert in new_alerts: - alert_str = f"{alert.purl},{alert.manifests},{alert.type}" + # Consolidate by package and alert type, not by manifest details + alert_str = f"{alert.purl},{alert.type}" if alert.error or alert.warn: if alert_str not in consolidated_alerts: @@ -1243,7 +1271,8 @@ def get_new_alerts( removed_alerts = removed_package_alerts[alert_key] for alert in new_alerts: - alert_str = f"{alert.purl},{alert.manifests},{alert.type}" + # Consolidate by package and alert type, not by manifest details + alert_str = f"{alert.purl},{alert.type}" # Only add if: # 1. Alert isn't in removed packages (or we're not ignoring readded alerts) diff --git a/socketsecurity/core/git_interface.py b/socketsecurity/core/git_interface.py index a10efc5..ed1fc0e 100644 --- a/socketsecurity/core/git_interface.py +++ b/socketsecurity/core/git_interface.py @@ -319,6 +319,67 @@ def commit_str(self) -> str: """Return commit SHA as a string""" return self.commit.hexsha + def get_formatted_committer(self) -> str: + """ + Get the committer in the preferred order: + 1. CLI --committers (handled in socketcli.py) + 2. CI/CD SCM username (GitHub/GitLab/BitBucket environment variables) + 3. Git username (extracted from email patterns like GitHub noreply) + 4. Git email address + 5. Git author name (fallback) + + Returns: + Formatted committer string + """ + # Check for CI/CD environment usernames first + # GitHub Actions + github_actor = os.getenv('GITHUB_ACTOR') + if github_actor: + log.debug(f"Using GitHub actor as committer: {github_actor}") + return github_actor + + # GitLab CI + gitlab_user_login = os.getenv('GITLAB_USER_LOGIN') + if gitlab_user_login: + log.debug(f"Using GitLab user login as committer: {gitlab_user_login}") + return gitlab_user_login + + # Bitbucket Pipelines + bitbucket_step_triggerer_uuid = os.getenv('BITBUCKET_STEP_TRIGGERER_UUID') + if bitbucket_step_triggerer_uuid: + log.debug(f"Using Bitbucket step triggerer as committer: {bitbucket_step_triggerer_uuid}") + return bitbucket_step_triggerer_uuid + + # Fall back to commit author/committer details + # Priority 3: Try to extract git username from email patterns first + if self.author and self.author.email and self.author.email.strip(): + email = self.author.email.strip() + + # If it's a GitHub noreply email, try to extract username + if email.endswith('@users.noreply.github.com'): + # Pattern: number+username@users.noreply.github.com + email_parts = email.split('@')[0] + if '+' in email_parts: + username = email_parts.split('+')[1] + log.debug(f"Extracted GitHub username from noreply email: {username}") + return username + + # Priority 4: Use email if available + if self.author and self.author.email and self.author.email.strip(): + email = self.author.email.strip() + log.debug(f"Using commit author email as committer: {email}") + return email + + # Priority 5: Fall back to author name as last resort + if self.author and self.author.name and self.author.name.strip(): + name = self.author.name.strip() + log.debug(f"Using commit author name as fallback committer: {name}") + return name + + # Ultimate fallback + log.debug("Using fallback committer: unknown") + return "unknown" + def get_default_branch_name(self) -> str: """ Get the default branch name from the remote origin. diff --git a/socketsecurity/core/messages.py b/socketsecurity/core/messages.py index 0b5fc62..42a4fd1 100644 --- a/socketsecurity/core/messages.py +++ b/socketsecurity/core/messages.py @@ -309,13 +309,26 @@ def security_comment_template(diff: Diff) -> str: :param diff: Diff - Contains the detected vulnerabilities and warnings. :return: str - The formatted Markdown/HTML string. """ + # Group license policy violations by PURL (ecosystem/package@version) + license_groups = {} + security_alerts = [] + + for alert in diff.new_alerts: + if alert.type == "licenseSpdxDisj": + purl_key = f"{alert.pkg_type}/{alert.pkg_name}@{alert.pkg_version}" + if purl_key not in license_groups: + license_groups[purl_key] = [] + license_groups[purl_key].append(alert) + else: + security_alerts.append(alert) + # Start of the comment comment = """ > **❗️ Caution** > **Review the following alerts detected in dependencies.** > -> According to your organization’s Security Policy, you **must** resolve all **“Block”** alerts before proceeding. It’s recommended to resolve **“Warn”** alerts too. +> According to your organization's Security Policy, you **must** resolve all **"Block"** alerts before proceeding. It's recommended to resolve **"Warn"** alerts too. > Learn more about [Socket for GitHub](https://socket.dev?utm_medium=gh). @@ -330,8 +343,8 @@ def security_comment_template(diff: Diff) -> str: """ - # Loop through alerts, dynamically generating rows - for alert in diff.new_alerts: + # Loop through security alerts (non-license), dynamically generating rows + for alert in security_alerts: severity_icon = Messages.get_severity_icon(alert.severity) action = "Block" if alert.error else "Warn" details_open = "" @@ -365,7 +378,48 @@ def security_comment_template(diff: Diff) -> str: """ - # Close table and comment + # Add license policy violation entries grouped by PURL + for purl_key, alerts in license_groups.items(): + action = "Block" if any(alert.error for alert in alerts) else "Warn" + first_alert = alerts[0] + + # Use orange diamond for license policy violations + license_icon = "🔶" + + # Build license findings list + license_findings = [] + for alert in alerts: + license_findings.append(alert.title) + + comment += f""" + + + {action} + {license_icon} + +
+ {first_alert.pkg_name}@{first_alert.pkg_version} has a License Policy Violation. +

License findings:

+
    +""" + for finding in license_findings: + comment += f"
  • {finding}
  • \n" + + comment += f"""
+

From: {first_alert.manifests}

+

ℹ️ Read more on: This package | What is a license policy violation?

+
+

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

+

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

+

Mark the package as acceptable risk: To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore {first_alert.pkg_name}@{first_alert.pkg_version}. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

+
+
+ + + + """ + + # Close table comment += """ diff --git a/socketsecurity/socketcli.py b/socketsecurity/socketcli.py index b1100d1..7731a5d 100644 --- a/socketsecurity/socketcli.py +++ b/socketsecurity/socketcli.py @@ -125,7 +125,7 @@ def main_code(): if not config.branch: config.branch = git_repo.branch if not config.committers: - config.committers = [git_repo.author] + config.committers = [git_repo.get_formatted_committer()] if not config.commit_message: config.commit_message = git_repo.commit_message except InvalidGitRepositoryError: @@ -320,6 +320,33 @@ def main_code(): 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) 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) + output_handler.handle_output(diff) + + elif config.enable_diff and force_api_mode: + # User requested diff mode but no manifest files were detected + log.warning("--enable-diff was specified but no supported manifest files were detected in the changed files. Falling back to full scan mode.") + log.info("Creating Socket Report (full scan)") + serializable_params = { + key: value if isinstance(value, (int, float, str, list, dict, bool, type(None))) else str(value) + for key, value in params.__dict__.items() + } + log.debug(f"params={serializable_params}") + diff = core.create_full_scan_with_report_url( + 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 + ) + log.info(f"Full scan created with ID: {diff.id}") + log.info(f"Full scan report URL: {diff.report_url}") + output_handler.handle_output(diff) + else: if force_api_mode: log.info("No Manifest files changed, creating Socket Report") diff --git a/tests/unit/test_cli_config.py b/tests/unit/test_cli_config.py index db7b1f5..40178d3 100644 --- a/tests/unit/test_cli_config.py +++ b/tests/unit/test_cli_config.py @@ -24,8 +24,20 @@ def test_default_values(self): @pytest.mark.parametrize("flag,attr", [ ("--enable-debug", "enable_debug"), ("--disable-blocking", "disable_blocking"), - ("--allow-unverified", "allow_unverified") + ("--allow-unverified", "allow_unverified"), + ("--enable-diff", "enable_diff") ]) def test_boolean_flags(self, flag, attr): config = CliConfig.from_args(["--api-token", "test", flag]) - assert getattr(config, attr) is True \ No newline at end of file + assert getattr(config, attr) is True + + def test_enable_diff_default_false(self): + """Test that enable_diff defaults to False""" + config = CliConfig.from_args(["--api-token", "test"]) + assert config.enable_diff is False + + def test_enable_diff_with_integration_api(self): + """Test that enable_diff can be used with integration api""" + config = CliConfig.from_args(["--api-token", "test", "--integration", "api", "--enable-diff"]) + assert config.enable_diff is True + assert config.integration_type == "api" \ No newline at end of file