socketsecurity 2.1.35__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/.github/workflows/docker-stable.yml +8 -7
  2. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/.github/workflows/pr-preview.yml +7 -7
  3. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/.github/workflows/release.yml +6 -6
  4. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/PKG-INFO +5 -2
  5. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/README.md +3 -0
  6. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/pyproject.toml +2 -2
  7. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/requirements.txt +1 -1
  8. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/__init__.py +1 -1
  9. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/config.py +7 -0
  10. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/core/__init__.py +72 -43
  11. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/core/git_interface.py +61 -0
  12. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/core/messages.py +58 -4
  13. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/socketcli.py +28 -1
  14. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/tests/unit/test_cli_config.py +14 -2
  15. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/.github/CODEOWNERS +0 -0
  16. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/.github/PULL_REQUEST_TEMPLATE/bug-fix.md +0 -0
  17. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/.github/PULL_REQUEST_TEMPLATE/feature.md +0 -0
  18. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/.github/PULL_REQUEST_TEMPLATE/improvement.md +0 -0
  19. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  20. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/.github/workflows/version-check.yml +0 -0
  21. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/.gitignore +0 -0
  22. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/.hooks/sync_version.py +0 -0
  23. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/.pre-commit-config.yaml +0 -0
  24. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/.python-version +0 -0
  25. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/Dockerfile +0 -0
  26. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/LICENSE +0 -0
  27. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/Makefile +0 -0
  28. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/Pipfile.lock +0 -0
  29. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/docs/README.md +0 -0
  30. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/pytest.ini +0 -0
  31. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/requirements-dev.lock +0 -0
  32. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/requirements-dev.txt +0 -0
  33. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/requirements.lock +0 -0
  34. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/scripts/build_container.sh +0 -0
  35. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/scripts/deploy-test-docker.sh +0 -0
  36. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/scripts/deploy-test-pypi.sh +0 -0
  37. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/scripts/run.sh +0 -0
  38. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/core/classes.py +0 -0
  39. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/core/cli_client.py +0 -0
  40. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/core/exceptions.py +0 -0
  41. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/core/helper/__init__.py +0 -0
  42. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/core/lazy_file_loader.py +0 -0
  43. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/core/logging.py +0 -0
  44. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/core/resource_utils.py +0 -0
  45. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/core/scm/__init__.py +0 -0
  46. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/core/scm/base.py +0 -0
  47. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/core/scm/client.py +0 -0
  48. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/core/scm/github.py +0 -0
  49. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/core/scm/gitlab.py +0 -0
  50. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/core/scm_comments.py +0 -0
  51. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/core/socket_config.py +0 -0
  52. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/core/utils.py +0 -0
  53. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/output.py +0 -0
  54. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/plugins/__init__.py +0 -0
  55. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/plugins/base.py +0 -0
  56. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/plugins/jira.py +0 -0
  57. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/plugins/manager.py +0 -0
  58. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/plugins/slack.py +0 -0
  59. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/plugins/teams.py +0 -0
  60. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/socketsecurity/plugins/webhook.py +0 -0
  61. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/tests/__init__.py +0 -0
  62. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/tests/core/conftest.py +0 -0
  63. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/tests/core/create_diff_input.json +0 -0
  64. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/tests/core/test_diff_generation.py +0 -0
  65. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/tests/core/test_package_and_alerts.py +0 -0
  66. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/tests/core/test_sdk_methods.py +0 -0
  67. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/tests/core/test_supporting_methods.py +0 -0
  68. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/tests/data/fullscans/create_response.json +0 -0
  69. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/tests/data/fullscans/diff/stream_diff.json +0 -0
  70. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/tests/data/fullscans/diff/stream_diff_full.json +0 -0
  71. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/tests/data/fullscans/head_scan/metadata.json +0 -0
  72. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/tests/data/fullscans/head_scan/stream_scan.json +0 -0
  73. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/tests/data/fullscans/head_scan/stream_scan_full.json +0 -0
  74. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/tests/data/fullscans/new_scan/metadata.json +0 -0
  75. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/tests/data/fullscans/new_scan/stream_scan.json +0 -0
  76. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/tests/data/repos/repo_info_error.json +0 -0
  77. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/tests/data/repos/repo_info_no_head.json +0 -0
  78. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/tests/data/repos/repo_info_success.json +0 -0
  79. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/tests/data/settings/security-policy.json +0 -0
  80. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/tests/unit/__init__.py +0 -0
  81. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/tests/unit/test_client.py +0 -0
  82. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/tests/unit/test_config.py +0 -0
  83. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/tests/unit/test_output.py +0 -0
  84. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/workflows/bitbucket-pipelines.yml +0 -0
  85. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/workflows/github-actions.yml +0 -0
  86. {socketsecurity-2.1.35 → socketsecurity-2.2.0}/workflows/gitlab-ci.yml +0 -0
@@ -21,18 +21,18 @@ jobs:
21
21
  fi
22
22
  echo "Version ${{ inputs.version }} found on PyPI - proceeding with release"
23
23
 
24
- - name: Login to Docker Hub
25
- uses: docker/login-action@v3
26
- with:
27
- username: ${{ secrets.DOCKERHUB_USERNAME }}
28
- password: ${{ secrets.DOCKERHUB_TOKEN }}
29
-
30
24
  - name: Set up QEMU
31
25
  uses: docker/setup-qemu-action@v3
32
26
 
33
27
  - name: Set up Docker Buildx
34
28
  uses: docker/setup-buildx-action@v3
35
29
 
30
+ - name: Login to Docker Hub with Organization Token
31
+ uses: docker/login-action@v3
32
+ with:
33
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
34
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
35
+
36
36
  - name: Build & Push Stable Docker
37
37
  uses: docker/build-push-action@v5
38
38
  with:
@@ -40,4 +40,5 @@ jobs:
40
40
  platforms: linux/amd64,linux/arm64
41
41
  tags: socketdev/cli:stable
42
42
  build-args: |
43
- CLI_VERSION=${{ inputs.version }}
43
+ CLI_VERSION=${{ inputs.version }}
44
+
@@ -119,19 +119,19 @@ jobs:
119
119
  echo "success=false" >> $GITHUB_OUTPUT
120
120
  exit 1
121
121
 
122
- - name: Login to Docker Hub
123
- if: steps.verify_package.outputs.success == 'true'
124
- uses: docker/login-action@v3
125
- with:
126
- username: ${{ secrets.DOCKERHUB_USERNAME }}
127
- password: ${{ secrets.DOCKERHUB_TOKEN }}
128
-
129
122
  - name: Set up QEMU
130
123
  uses: docker/setup-qemu-action@v3
131
124
 
132
125
  - name: Set up Docker Buildx
133
126
  uses: docker/setup-buildx-action@v3
134
127
 
128
+ - name: Login to Docker Hub with Organization Token
129
+ if: steps.verify_package.outputs.success == 'true'
130
+ uses: docker/login-action@v3
131
+ with:
132
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
133
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
134
+
135
135
  - name: Build & Push Docker Preview
136
136
  if: steps.verify_package.outputs.success == 'true'
137
137
  uses: docker/build-push-action@v5
@@ -68,18 +68,18 @@ jobs:
68
68
  if: steps.version_check.outputs.pypi_exists != 'true'
69
69
  uses: pypa/gh-action-pypi-publish@v1.12.4
70
70
 
71
- - name: Login to Docker Hub
72
- uses: docker/login-action@v3
73
- with:
74
- username: ${{ secrets.DOCKERHUB_USERNAME }}
75
- password: ${{ secrets.DOCKERHUB_TOKEN }}
76
-
77
71
  - name: Set up QEMU
78
72
  uses: docker/setup-qemu-action@v3
79
73
 
80
74
  - name: Set up Docker Buildx
81
75
  uses: docker/setup-buildx-action@v3
82
76
 
77
+ - name: Login to Docker Hub with Organization Token
78
+ uses: docker/login-action@v3
79
+ with:
80
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
81
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
82
+
83
83
  - name: Verify package is installable
84
84
  id: verify_package
85
85
  env:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: socketsecurity
3
- Version: 2.1.35
3
+ Version: 2.2.0
4
4
  Summary: Socket Security CLI for CI/CD
5
5
  Project-URL: Homepage, https://socket.dev
6
6
  Author-email: Douglas Coburn <douglas@socket.dev>
@@ -39,7 +39,7 @@ Requires-Dist: packaging
39
39
  Requires-Dist: prettytable
40
40
  Requires-Dist: python-dotenv
41
41
  Requires-Dist: requests
42
- Requires-Dist: socket-sdk-python<3,>=2.1.5
42
+ Requires-Dist: socket-sdk-python<3,>=2.1.8
43
43
  Provides-Extra: dev
44
44
  Requires-Dist: hatch; extra == 'dev'
45
45
  Requires-Dist: pip-tools>=7.4.0; extra == 'dev'
@@ -172,6 +172,7 @@ If you don't want to provide the Socket API Token every time then you can use th
172
172
  |:-------------------------|:---------|:--------|:----------------------------------------------------------------------|
173
173
  | --ignore-commit-files | False | False | Ignore commit files |
174
174
  | --disable-blocking | False | False | Disable blocking mode |
175
+ | --enable-diff | False | False | Enable diff mode even when using --integration api (forces diff mode without SCM integration) |
175
176
  | --scm | False | api | Source control management type |
176
177
  | --timeout | False | | Timeout in seconds for API requests |
177
178
  | --include-module-folders | False | False | If enabled will include manifest files from folders like node_modules |
@@ -261,6 +262,7 @@ The CLI determines which files to scan based on the following logic:
261
262
  - **Differential Mode**: When manifest files are detected in changes, performs a diff scan with PR/MR comment integration
262
263
  - **API Mode**: When no manifest files are in changes, creates a full scan report without PR comments but still scans the entire repository
263
264
  - **Force Mode**: With `--ignore-commit-files`, always performs a full scan regardless of changes
265
+ - **Forced Diff Mode**: With `--enable-diff`, forces differential mode even when using `--integration api` (without SCM integration)
264
266
 
265
267
  ### Examples
266
268
 
@@ -268,6 +270,7 @@ The CLI determines which files to scan based on the following logic:
268
270
  - **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.
269
271
  - **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.
270
272
  - **Using `--ignore-commit-files`**: This forces a full scan of all manifest files in the target path, regardless of what's in your commit.
273
+ - **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`
271
274
  - **Auto-detection**: Most CI/CD scenarios now work with just `socketcli --target-path /path/to/repo --scm github --pr-number $PR_NUM`
272
275
 
273
276
  ## Debugging and Troubleshooting
@@ -116,6 +116,7 @@ If you don't want to provide the Socket API Token every time then you can use th
116
116
  |:-------------------------|:---------|:--------|:----------------------------------------------------------------------|
117
117
  | --ignore-commit-files | False | False | Ignore commit files |
118
118
  | --disable-blocking | False | False | Disable blocking mode |
119
+ | --enable-diff | False | False | Enable diff mode even when using --integration api (forces diff mode without SCM integration) |
119
120
  | --scm | False | api | Source control management type |
120
121
  | --timeout | False | | Timeout in seconds for API requests |
121
122
  | --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:
205
206
  - **Differential Mode**: When manifest files are detected in changes, performs a diff scan with PR/MR comment integration
206
207
  - **API Mode**: When no manifest files are in changes, creates a full scan report without PR comments but still scans the entire repository
207
208
  - **Force Mode**: With `--ignore-commit-files`, always performs a full scan regardless of changes
209
+ - **Forced Diff Mode**: With `--enable-diff`, forces differential mode even when using `--integration api` (without SCM integration)
208
210
 
209
211
  ### Examples
210
212
 
@@ -212,6 +214,7 @@ The CLI determines which files to scan based on the following logic:
212
214
  - **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.
213
215
  - **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.
214
216
  - **Using `--ignore-commit-files`**: This forces a full scan of all manifest files in the target path, regardless of what's in your commit.
217
+ - **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`
215
218
  - **Auto-detection**: Most CI/CD scenarios now work with just `socketcli --target-path /path/to/repo --scm github --pr-number $PR_NUM`
216
219
 
217
220
  ## Debugging and Troubleshooting
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "socketsecurity"
9
- version = "2.1.35"
9
+ version = "2.2.0"
10
10
  requires-python = ">= 3.10"
11
11
  license = {"file" = "LICENSE"}
12
12
  dependencies = [
@@ -16,7 +16,7 @@ dependencies = [
16
16
  'GitPython',
17
17
  'packaging',
18
18
  'python-dotenv',
19
- 'socket-sdk-python>=2.1.5,<3'
19
+ 'socket-sdk-python>=2.1.8,<3'
20
20
  ]
21
21
  readme = "README.md"
22
22
  description = "Socket Security CLI for CI/CD"
@@ -59,7 +59,7 @@ requests==2.32.4
59
59
  # via socketsecurity
60
60
  smmap==5.0.2
61
61
  # via gitdb
62
- socket-sdk-python==2.1.5
62
+ socket-sdk-python==2.1.8
63
63
  # via socketsecurity
64
64
  typing-extensions==4.12.2
65
65
  # via socket-sdk-python
@@ -1,2 +1,2 @@
1
1
  __author__ = 'socket.dev'
2
- __version__ = '2.1.35'
2
+ __version__ = '2.2.0'
@@ -48,6 +48,7 @@ class CliConfig:
48
48
  integration_type: IntegrationType = "api"
49
49
  integration_org_slug: Optional[str] = None
50
50
  pending_head: bool = False
51
+ enable_diff: bool = False
51
52
  timeout: Optional[int] = 1200
52
53
  exclude_license_details: bool = False
53
54
  include_module_folders: bool = False
@@ -421,6 +422,12 @@ def create_argument_parser() -> argparse.ArgumentParser:
421
422
  action="store_true",
422
423
  help=argparse.SUPPRESS
423
424
  )
425
+ advanced_group.add_argument(
426
+ "--enable-diff",
427
+ dest="enable_diff",
428
+ action="store_true",
429
+ help="Enable diff mode even when using --integration api (forces diff mode without SCM integration)"
430
+ )
424
431
  advanced_group.add_argument(
425
432
  "--scm",
426
433
  metavar="<type>",
@@ -2,6 +2,7 @@ import logging
2
2
  import os
3
3
  import sys
4
4
  import tarfile
5
+ import tempfile
5
6
  import time
6
7
  import io
7
8
  import json
@@ -30,7 +31,6 @@ from socketsecurity.core.exceptions import APIResourceNotFound
30
31
  from .socket_config import SocketConfig
31
32
  from .utils import socket_globs
32
33
  from .resource_utils import check_file_count_against_ulimit
33
- from .lazy_file_loader import load_files_for_sending_lazy
34
34
  import importlib
35
35
  logging_std = importlib.import_module("logging")
36
36
 
@@ -338,10 +338,10 @@ class Core:
338
338
  ulimit_check = check_file_count_against_ulimit(file_count)
339
339
  if ulimit_check["can_check"]:
340
340
  if ulimit_check["would_exceed"]:
341
- log.warning(f"Found {file_count} manifest files, which may exceed the file descriptor limit (ulimit -n = {ulimit_check['soft_limit']})")
342
- log.warning(f"Available file descriptors: {ulimit_check['available_fds']} (after {ulimit_check['buffer_size']} buffer)")
343
- log.warning(f"Recommendation: {ulimit_check['recommendation']}")
344
- log.warning("This may cause 'Too many open files' errors during processing")
341
+ log.debug(f"Found {file_count} manifest files, which may exceed the file descriptor limit (ulimit -n = {ulimit_check['soft_limit']})")
342
+ log.debug(f"Available file descriptors: {ulimit_check['available_fds']} (after {ulimit_check['buffer_size']} buffer)")
343
+ log.debug(f"Recommendation: {ulimit_check['recommendation']}")
344
+ log.debug("This may cause 'Too many open files' errors during processing")
345
345
  else:
346
346
  log.debug(f"File count ({file_count}) is within file descriptor limit ({ulimit_check['soft_limit']})")
347
347
  else:
@@ -434,37 +434,29 @@ class Core:
434
434
  return ''.join(f'[{char.lower()}{char.upper()}]' if char.isalpha() else char for char in input_string)
435
435
 
436
436
  @staticmethod
437
- def empty_head_scan_file() -> list[tuple[str, tuple[str, Union[BinaryIO, BytesIO]]]]:
438
- # Create an empty file for when no head full scan so that the diff endpoint can always be used
439
- empty_file_obj = io.BytesIO(b"")
440
- empty_filename = "initial_head_scan"
441
- empty_full_scan_file = [(empty_filename, (empty_filename, empty_file_obj))]
442
- return empty_full_scan_file
443
-
444
- @staticmethod
445
- def load_files_for_sending(files: List[str], workspace: str) -> List[Tuple[str, Tuple[str, BinaryIO]]]:
437
+ def empty_head_scan_file() -> List[str]:
446
438
  """
447
- Prepares files for sending to the Socket API using lazy loading.
439
+ Creates a temporary empty file for baseline scans when no head scan exists.
448
440
 
449
- This version uses lazy file loading to prevent "Too many open files" errors
450
- when processing large numbers of manifest files.
451
-
452
- Args:
453
- files: List of file paths from find_files()
454
- workspace: Base directory path to make paths relative to
455
-
456
441
  Returns:
457
- List of tuples formatted for requests multipart upload:
458
- [(field_name, (filename, file_object)), ...]
442
+ List containing path to a temporary empty file
459
443
  """
460
- return load_files_for_sending_lazy(files, workspace)
444
+ # Create a temporary empty file
445
+ temp_fd, temp_path = tempfile.mkstemp(suffix='.empty', prefix='socket_baseline_')
446
+
447
+ # Close the file descriptor since we just need the path
448
+ # The file is already created and empty
449
+ os.close(temp_fd)
450
+
451
+ log.debug(f"Created temporary empty file for baseline scan: {temp_path}")
452
+ return [temp_path]
461
453
 
462
- def create_full_scan(self, files: list[tuple[str, tuple[str, BytesIO]]], params: FullScanParams) -> FullScan:
454
+ def create_full_scan(self, files: List[str], params: FullScanParams) -> FullScan:
463
455
  """
464
456
  Creates a new full scan via the Socket API.
465
457
 
466
458
  Args:
467
- files: List of files to scan
459
+ files: List of file paths to scan
468
460
  params: Parameters for the full scan
469
461
 
470
462
  Returns:
@@ -473,7 +465,7 @@ class Core:
473
465
  log.info("Creating new full scan")
474
466
  create_full_start = time.time()
475
467
 
476
- res = self.sdk.fullscans.post(files, params, use_types=True)
468
+ res = self.sdk.fullscans.post(files, params, use_types=True, use_lazy_loading=True, max_open_files=50)
477
469
  if not res.success:
478
470
  log.error(f"Error creating full scan: {res.message}, status: {res.status}")
479
471
  raise Exception(f"Error creating full scan: {res.message}, status: {res.status}")
@@ -525,14 +517,13 @@ class Core:
525
517
  if save_manifest_tar_path and files:
526
518
  self.save_manifest_tar(files, save_manifest_tar_path, path)
527
519
 
528
- files_for_sending = self.load_files_for_sending(files, path)
529
520
  if not files:
530
521
  return diff
531
522
 
532
523
  try:
533
524
  # Create new scan
534
525
  new_scan_start = time.time()
535
- new_full_scan = self.create_full_scan(files_for_sending, params)
526
+ new_full_scan = self.create_full_scan(files, params)
536
527
  new_scan_end = time.time()
537
528
  log.info(f"Total time to create new full scan: {new_scan_end - new_scan_start:.2f}")
538
529
  except APIFailure as e:
@@ -779,7 +770,15 @@ class Core:
779
770
  log.info(f"Comparing scans - Head scan ID: {head_full_scan_id}, New scan ID: {new_full_scan_id}")
780
771
  diff_start = time.time()
781
772
  try:
782
- diff_report = self.sdk.fullscans.stream_diff(self.config.org_slug, head_full_scan_id, new_full_scan_id, use_types=True).data
773
+ diff_report = (
774
+ self.sdk.fullscans.stream_diff
775
+ (
776
+ self.config.org_slug,
777
+ head_full_scan_id,
778
+ new_full_scan_id,
779
+ use_types=True
780
+ ).data
781
+ )
783
782
  except APIFailure as e:
784
783
  log.error(f"API Error: {e}")
785
784
  sys.exit(1)
@@ -877,7 +876,6 @@ class Core:
877
876
  if save_manifest_tar_path and files:
878
877
  self.save_manifest_tar(files, save_manifest_tar_path, path)
879
878
 
880
- files_for_sending = self.load_files_for_sending(files, path)
881
879
  if not files:
882
880
  return Diff(id="NO_DIFF_RAN", diff_url="", report_url="")
883
881
 
@@ -887,7 +885,9 @@ class Core:
887
885
  except APIResourceNotFound:
888
886
  head_full_scan_id = None
889
887
 
888
+ # If no head scan exists, create an empty baseline scan
890
889
  if head_full_scan_id is None:
890
+ log.info("No previous scan found - creating empty baseline scan")
891
891
  new_params = copy.deepcopy(params.__dict__)
892
892
  new_params.pop('include_license_details')
893
893
  tmp_params = FullScanParams(**new_params)
@@ -895,13 +895,34 @@ class Core:
895
895
  tmp_params.tmp = True
896
896
  tmp_params.set_as_pending_head = False
897
897
  tmp_params.make_default_branch = False
898
- head_full_scan = self.create_full_scan(Core.empty_head_scan_file(), tmp_params)
899
- head_full_scan_id = head_full_scan.id
898
+
899
+ # Create baseline scan with empty file
900
+ empty_files = Core.empty_head_scan_file()
901
+ try:
902
+ head_full_scan = self.create_full_scan(empty_files, tmp_params)
903
+ head_full_scan_id = head_full_scan.id
904
+ log.debug(f"Created empty baseline scan: {head_full_scan_id}")
905
+
906
+ # Clean up the temporary empty file
907
+ for temp_file in empty_files:
908
+ try:
909
+ os.unlink(temp_file)
910
+ log.debug(f"Cleaned up temporary file: {temp_file}")
911
+ except OSError as e:
912
+ log.warning(f"Failed to clean up temporary file {temp_file}: {e}")
913
+ except Exception as e:
914
+ # Clean up temp files even if scan creation fails
915
+ for temp_file in empty_files:
916
+ try:
917
+ os.unlink(temp_file)
918
+ except OSError:
919
+ pass
920
+ raise e
900
921
 
901
922
  # Create new scan
902
923
  try:
903
924
  new_scan_start = time.time()
904
- new_full_scan = self.create_full_scan(files_for_sending, params)
925
+ new_full_scan = self.create_full_scan(files, params)
905
926
  new_scan_end = time.time()
906
927
  log.info(f"Total time to create new full scan: {new_scan_end - new_scan_start:.2f}")
907
928
  except APIFailure as e:
@@ -913,6 +934,7 @@ class Core:
913
934
  log.error(f"Stack trace:\n{traceback.format_exc()}")
914
935
  raise
915
936
 
937
+ # Handle diff generation - now we always have both scans
916
938
  scans_ready = self.check_full_scans_status(head_full_scan_id, new_full_scan.id)
917
939
  if scans_ready is False:
918
940
  log.error(f"Full scans did not complete within {self.config.timeout} seconds")
@@ -1134,6 +1156,12 @@ class Core:
1134
1156
  alert = Alert(**alert_item)
1135
1157
  props = getattr(self.config.all_issues, alert.type, default_props)
1136
1158
  introduced_by = self.get_source_data(package, packages)
1159
+
1160
+ # Handle special case for license policy violations
1161
+ title = props.title
1162
+ if alert.type == "licenseSpdxDisj" and not title:
1163
+ title = "License Policy Violation"
1164
+
1137
1165
  issue_alert = Issue(
1138
1166
  pkg_type=package.type,
1139
1167
  pkg_name=package.name,
@@ -1144,7 +1172,7 @@ class Core:
1144
1172
  type=alert.type,
1145
1173
  severity=alert.severity,
1146
1174
  description=props.description,
1147
- title=props.title,
1175
+ title=title,
1148
1176
  suggestion=props.suggestion,
1149
1177
  next_step_title=props.nextStepTitle,
1150
1178
  introduced_by=introduced_by,
@@ -1156,11 +1184,10 @@ class Core:
1156
1184
  action = self.config.security_policy[alert.type]['action']
1157
1185
  setattr(issue_alert, action, True)
1158
1186
 
1159
- if issue_alert.type != 'licenseSpdxDisj':
1160
- if issue_alert.key not in alerts_collection:
1161
- alerts_collection[issue_alert.key] = [issue_alert]
1162
- else:
1163
- alerts_collection[issue_alert.key].append(issue_alert)
1187
+ if issue_alert.key not in alerts_collection:
1188
+ alerts_collection[issue_alert.key] = [issue_alert]
1189
+ else:
1190
+ alerts_collection[issue_alert.key].append(issue_alert)
1164
1191
 
1165
1192
  return alerts_collection
1166
1193
 
@@ -1232,7 +1259,8 @@ class Core:
1232
1259
  if alert_key not in removed_package_alerts:
1233
1260
  new_alerts = added_package_alerts[alert_key]
1234
1261
  for alert in new_alerts:
1235
- alert_str = f"{alert.purl},{alert.manifests},{alert.type}"
1262
+ # Consolidate by package and alert type, not by manifest details
1263
+ alert_str = f"{alert.purl},{alert.type}"
1236
1264
 
1237
1265
  if alert.error or alert.warn:
1238
1266
  if alert_str not in consolidated_alerts:
@@ -1243,7 +1271,8 @@ class Core:
1243
1271
  removed_alerts = removed_package_alerts[alert_key]
1244
1272
 
1245
1273
  for alert in new_alerts:
1246
- alert_str = f"{alert.purl},{alert.manifests},{alert.type}"
1274
+ # Consolidate by package and alert type, not by manifest details
1275
+ alert_str = f"{alert.purl},{alert.type}"
1247
1276
 
1248
1277
  # Only add if:
1249
1278
  # 1. Alert isn't in removed packages (or we're not ignoring readded alerts)
@@ -319,6 +319,67 @@ class Git:
319
319
  """Return commit SHA as a string"""
320
320
  return self.commit.hexsha
321
321
 
322
+ def get_formatted_committer(self) -> str:
323
+ """
324
+ Get the committer in the preferred order:
325
+ 1. CLI --committers (handled in socketcli.py)
326
+ 2. CI/CD SCM username (GitHub/GitLab/BitBucket environment variables)
327
+ 3. Git username (extracted from email patterns like GitHub noreply)
328
+ 4. Git email address
329
+ 5. Git author name (fallback)
330
+
331
+ Returns:
332
+ Formatted committer string
333
+ """
334
+ # Check for CI/CD environment usernames first
335
+ # GitHub Actions
336
+ github_actor = os.getenv('GITHUB_ACTOR')
337
+ if github_actor:
338
+ log.debug(f"Using GitHub actor as committer: {github_actor}")
339
+ return github_actor
340
+
341
+ # GitLab CI
342
+ gitlab_user_login = os.getenv('GITLAB_USER_LOGIN')
343
+ if gitlab_user_login:
344
+ log.debug(f"Using GitLab user login as committer: {gitlab_user_login}")
345
+ return gitlab_user_login
346
+
347
+ # Bitbucket Pipelines
348
+ bitbucket_step_triggerer_uuid = os.getenv('BITBUCKET_STEP_TRIGGERER_UUID')
349
+ if bitbucket_step_triggerer_uuid:
350
+ log.debug(f"Using Bitbucket step triggerer as committer: {bitbucket_step_triggerer_uuid}")
351
+ return bitbucket_step_triggerer_uuid
352
+
353
+ # Fall back to commit author/committer details
354
+ # Priority 3: Try to extract git username from email patterns first
355
+ if self.author and self.author.email and self.author.email.strip():
356
+ email = self.author.email.strip()
357
+
358
+ # If it's a GitHub noreply email, try to extract username
359
+ if email.endswith('@users.noreply.github.com'):
360
+ # Pattern: number+username@users.noreply.github.com
361
+ email_parts = email.split('@')[0]
362
+ if '+' in email_parts:
363
+ username = email_parts.split('+')[1]
364
+ log.debug(f"Extracted GitHub username from noreply email: {username}")
365
+ return username
366
+
367
+ # Priority 4: Use email if available
368
+ if self.author and self.author.email and self.author.email.strip():
369
+ email = self.author.email.strip()
370
+ log.debug(f"Using commit author email as committer: {email}")
371
+ return email
372
+
373
+ # Priority 5: Fall back to author name as last resort
374
+ if self.author and self.author.name and self.author.name.strip():
375
+ name = self.author.name.strip()
376
+ log.debug(f"Using commit author name as fallback committer: {name}")
377
+ return name
378
+
379
+ # Ultimate fallback
380
+ log.debug("Using fallback committer: unknown")
381
+ return "unknown"
382
+
322
383
  def get_default_branch_name(self) -> str:
323
384
  """
324
385
  Get the default branch name from the remote origin.
@@ -309,13 +309,26 @@ class Messages:
309
309
  :param diff: Diff - Contains the detected vulnerabilities and warnings.
310
310
  :return: str - The formatted Markdown/HTML string.
311
311
  """
312
+ # Group license policy violations by PURL (ecosystem/package@version)
313
+ license_groups = {}
314
+ security_alerts = []
315
+
316
+ for alert in diff.new_alerts:
317
+ if alert.type == "licenseSpdxDisj":
318
+ purl_key = f"{alert.pkg_type}/{alert.pkg_name}@{alert.pkg_version}"
319
+ if purl_key not in license_groups:
320
+ license_groups[purl_key] = []
321
+ license_groups[purl_key].append(alert)
322
+ else:
323
+ security_alerts.append(alert)
324
+
312
325
  # Start of the comment
313
326
  comment = """<!-- socket-security-comment-actions -->
314
327
 
315
328
  > **❗️ Caution**
316
329
  > **Review the following alerts detected in dependencies.**
317
330
  >
318
- > According to your organizations Security Policy, you **must** resolve all **“Block”** alerts before proceeding. Its recommended to resolve **“Warn”** alerts too.
331
+ > According to your organization's Security Policy, you **must** resolve all **"Block"** alerts before proceeding. It's recommended to resolve **"Warn"** alerts too.
319
332
  > Learn more about [Socket for GitHub](https://socket.dev?utm_medium=gh).
320
333
 
321
334
  <!-- start-socket-updated-alerts-table -->
@@ -330,8 +343,8 @@ class Messages:
330
343
  <tbody>
331
344
  """
332
345
 
333
- # Loop through alerts, dynamically generating rows
334
- for alert in diff.new_alerts:
346
+ # Loop through security alerts (non-license), dynamically generating rows
347
+ for alert in security_alerts:
335
348
  severity_icon = Messages.get_severity_icon(alert.severity)
336
349
  action = "Block" if alert.error else "Warn"
337
350
  details_open = ""
@@ -365,7 +378,48 @@ class Messages:
365
378
  <!-- end-socket-alert-{alert.pkg_name}@{alert.pkg_version} -->
366
379
  """
367
380
 
368
- # Close table and comment
381
+ # Add license policy violation entries grouped by PURL
382
+ for purl_key, alerts in license_groups.items():
383
+ action = "Block" if any(alert.error for alert in alerts) else "Warn"
384
+ first_alert = alerts[0]
385
+
386
+ # Use orange diamond for license policy violations
387
+ license_icon = "🔶"
388
+
389
+ # Build license findings list
390
+ license_findings = []
391
+ for alert in alerts:
392
+ license_findings.append(alert.title)
393
+
394
+ comment += f"""
395
+ <!-- start-socket-alert-{first_alert.pkg_name}@{first_alert.pkg_version} -->
396
+ <tr>
397
+ <td><strong>{action}</strong></td>
398
+ <td align="center">{license_icon}</td>
399
+ <td>
400
+ <details>
401
+ <summary>{first_alert.pkg_name}@{first_alert.pkg_version} has a License Policy Violation.</summary>
402
+ <p><strong>License findings:</strong></p>
403
+ <ul>
404
+ """
405
+ for finding in license_findings:
406
+ comment += f" <li>{finding}</li>\n"
407
+
408
+ comment += f""" </ul>
409
+ <p><strong>From:</strong> {first_alert.manifests}</p>
410
+ <p>ℹ️ Read more on: <a href="{first_alert.purl}">This package</a> | <a href="https://socket.dev/alerts/license">What is a license policy violation?</a></p>
411
+ <blockquote>
412
+ <p><em>Next steps:</em> 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 <strong>support@socket.dev</strong>.</p>
413
+ <p><em>Suggestion:</em> Find a package that does not violate your license policy or adjust your policy to allow this package's license.</p>
414
+ <p><em>Mark the package as acceptable risk:</em> To ignore this alert only in this pull request, reply with the comment <code>@SocketSecurity ignore {first_alert.pkg_name}@{first_alert.pkg_version}</code>. You can also ignore all packages with <code>@SocketSecurity ignore-all</code>. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.</p>
415
+ </blockquote>
416
+ </details>
417
+ </td>
418
+ </tr>
419
+ <!-- end-socket-alert-{first_alert.pkg_name}@{first_alert.pkg_version} -->
420
+ """
421
+
422
+ # Close table
369
423
  comment += """
370
424
  </tbody>
371
425
  </table>
@@ -125,7 +125,7 @@ def main_code():
125
125
  if not config.branch:
126
126
  config.branch = git_repo.branch
127
127
  if not config.committers:
128
- config.committers = [git_repo.author]
128
+ config.committers = [git_repo.get_formatted_committer()]
129
129
  if not config.commit_message:
130
130
  config.commit_message = git_repo.commit_message
131
131
  except InvalidGitRepositoryError:
@@ -320,6 +320,33 @@ def main_code():
320
320
  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)
321
321
 
322
322
  output_handler.handle_output(diff)
323
+
324
+ elif config.enable_diff and not force_api_mode:
325
+ # New logic: --enable-diff forces diff mode even with --integration api (no SCM)
326
+ log.info("Diff mode enabled without SCM integration")
327
+ 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)
328
+ output_handler.handle_output(diff)
329
+
330
+ elif config.enable_diff and force_api_mode:
331
+ # User requested diff mode but no manifest files were detected
332
+ log.warning("--enable-diff was specified but no supported manifest files were detected in the changed files. Falling back to full scan mode.")
333
+ log.info("Creating Socket Report (full scan)")
334
+ serializable_params = {
335
+ key: value if isinstance(value, (int, float, str, list, dict, bool, type(None))) else str(value)
336
+ for key, value in params.__dict__.items()
337
+ }
338
+ log.debug(f"params={serializable_params}")
339
+ diff = core.create_full_scan_with_report_url(
340
+ config.target_path,
341
+ params,
342
+ no_change=should_skip_scan,
343
+ save_files_list_path=config.save_submitted_files_list,
344
+ save_manifest_tar_path=config.save_manifest_tar
345
+ )
346
+ log.info(f"Full scan created with ID: {diff.id}")
347
+ log.info(f"Full scan report URL: {diff.report_url}")
348
+ output_handler.handle_output(diff)
349
+
323
350
  else:
324
351
  if force_api_mode:
325
352
  log.info("No Manifest files changed, creating Socket Report")
@@ -24,8 +24,20 @@ class TestCliConfig:
24
24
  @pytest.mark.parametrize("flag,attr", [
25
25
  ("--enable-debug", "enable_debug"),
26
26
  ("--disable-blocking", "disable_blocking"),
27
- ("--allow-unverified", "allow_unverified")
27
+ ("--allow-unverified", "allow_unverified"),
28
+ ("--enable-diff", "enable_diff")
28
29
  ])
29
30
  def test_boolean_flags(self, flag, attr):
30
31
  config = CliConfig.from_args(["--api-token", "test", flag])
31
- assert getattr(config, attr) is True
32
+ assert getattr(config, attr) is True
33
+
34
+ def test_enable_diff_default_false(self):
35
+ """Test that enable_diff defaults to False"""
36
+ config = CliConfig.from_args(["--api-token", "test"])
37
+ assert config.enable_diff is False
38
+
39
+ def test_enable_diff_with_integration_api(self):
40
+ """Test that enable_diff can be used with integration api"""
41
+ config = CliConfig.from_args(["--api-token", "test", "--integration", "api", "--enable-diff"])
42
+ assert config.enable_diff is True
43
+ assert config.integration_type == "api"
File without changes
File without changes