socketsecurity 2.2.43__tar.gz → 2.2.55__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 (88) hide show
  1. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/Dockerfile +1 -1
  2. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/PKG-INFO +2 -2
  3. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/pyproject.toml +3 -2
  4. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/__init__.py +1 -1
  5. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/config.py +28 -0
  6. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/core/__init__.py +100 -85
  7. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/core/tools/reachability.py +8 -3
  8. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/socketcli.py +63 -25
  9. socketsecurity-2.2.55/uv.lock +1541 -0
  10. socketsecurity-2.2.43/uv.lock +0 -1432
  11. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/.github/CODEOWNERS +0 -0
  12. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/.github/PULL_REQUEST_TEMPLATE/bug-fix.md +0 -0
  13. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/.github/PULL_REQUEST_TEMPLATE/feature.md +0 -0
  14. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/.github/PULL_REQUEST_TEMPLATE/improvement.md +0 -0
  15. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  16. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/.github/workflows/docker-stable.yml +0 -0
  17. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/.github/workflows/pr-preview.yml +0 -0
  18. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/.github/workflows/release.yml +0 -0
  19. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/.github/workflows/version-check.yml +0 -0
  20. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/.gitignore +0 -0
  21. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/.hooks/sync_version.py +0 -0
  22. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/.pre-commit-config.yaml +0 -0
  23. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/.python-version +0 -0
  24. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/LICENSE +0 -0
  25. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/Makefile +0 -0
  26. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/README.md +0 -0
  27. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/docs/README.md +0 -0
  28. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/pytest.ini +0 -0
  29. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/scripts/build_container.sh +0 -0
  30. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/scripts/build_container_flexible.sh +0 -0
  31. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/scripts/deploy-test-docker.sh +0 -0
  32. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/scripts/deploy-test-pypi.sh +0 -0
  33. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/scripts/docker-entrypoint.sh +0 -0
  34. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/scripts/run.sh +0 -0
  35. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/core/classes.py +0 -0
  36. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/core/cli_client.py +0 -0
  37. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/core/exceptions.py +0 -0
  38. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/core/git_interface.py +0 -0
  39. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/core/helper/__init__.py +0 -0
  40. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/core/lazy_file_loader.py +0 -0
  41. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/core/logging.py +0 -0
  42. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/core/messages.py +0 -0
  43. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/core/resource_utils.py +0 -0
  44. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/core/scm/__init__.py +0 -0
  45. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/core/scm/base.py +0 -0
  46. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/core/scm/client.py +0 -0
  47. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/core/scm/github.py +0 -0
  48. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/core/scm/gitlab.py +0 -0
  49. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/core/scm_comments.py +0 -0
  50. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/core/socket_config.py +0 -0
  51. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/core/utils.py +0 -0
  52. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/output.py +0 -0
  53. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/plugins/__init__.py +0 -0
  54. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/plugins/base.py +0 -0
  55. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/plugins/jira.py +0 -0
  56. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/plugins/manager.py +0 -0
  57. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/plugins/slack.py +0 -0
  58. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/plugins/teams.py +0 -0
  59. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/socketsecurity/plugins/webhook.py +0 -0
  60. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/tests/__init__.py +0 -0
  61. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/tests/core/conftest.py +0 -0
  62. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/tests/core/create_diff_input.json +0 -0
  63. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/tests/core/test_diff_generation.py +0 -0
  64. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/tests/core/test_package_and_alerts.py +0 -0
  65. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/tests/core/test_sdk_methods.py +0 -0
  66. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/tests/core/test_supporting_methods.py +0 -0
  67. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/tests/data/fullscans/create_response.json +0 -0
  68. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/tests/data/fullscans/diff/stream_diff.json +0 -0
  69. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/tests/data/fullscans/diff/stream_diff_full.json +0 -0
  70. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/tests/data/fullscans/head_scan/metadata.json +0 -0
  71. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/tests/data/fullscans/head_scan/stream_scan.json +0 -0
  72. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/tests/data/fullscans/head_scan/stream_scan_full.json +0 -0
  73. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/tests/data/fullscans/new_scan/metadata.json +0 -0
  74. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/tests/data/fullscans/new_scan/stream_scan.json +0 -0
  75. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/tests/data/repos/repo_info_error.json +0 -0
  76. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/tests/data/repos/repo_info_no_head.json +0 -0
  77. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/tests/data/repos/repo_info_success.json +0 -0
  78. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/tests/data/settings/security-policy.json +0 -0
  79. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/tests/unit/__init__.py +0 -0
  80. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/tests/unit/test_cli_config.py +0 -0
  81. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/tests/unit/test_client.py +0 -0
  82. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/tests/unit/test_config.py +0 -0
  83. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/tests/unit/test_gitlab_auth.py +0 -0
  84. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/tests/unit/test_gitlab_auth_fallback.py +0 -0
  85. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/tests/unit/test_output.py +0 -0
  86. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/workflows/bitbucket-pipelines.yml +0 -0
  87. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/workflows/github-actions.yml +0 -0
  88. {socketsecurity-2.2.43 → socketsecurity-2.2.55}/workflows/gitlab-ci.yml +0 -0
@@ -57,7 +57,7 @@ RUN if [ "$DOTNET_VERSION" = "6" ]; then \
57
57
  fi
58
58
 
59
59
  # Install additional tools
60
- RUN npm install @coana-tech/cli -g && \
60
+ RUN npm install @coana-tech/cli socket -g && \
61
61
  gem install bundler && \
62
62
  curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && \
63
63
  . ~/.cargo/env && \
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: socketsecurity
3
- Version: 2.2.43
3
+ Version: 2.2.55
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>
@@ -40,7 +40,7 @@ Requires-Dist: packaging
40
40
  Requires-Dist: prettytable
41
41
  Requires-Dist: python-dotenv
42
42
  Requires-Dist: requests
43
- Requires-Dist: socketdev<4.0.0,>=3.0.21
43
+ Requires-Dist: socketdev<4.0.0,>=3.0.22
44
44
  Provides-Extra: dev
45
45
  Requires-Dist: hatch; extra == 'dev'
46
46
  Requires-Dist: pre-commit; extra == 'dev'
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "socketsecurity"
9
- version = "2.2.43"
9
+ version = "2.2.55"
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
- 'socketdev>=3.0.21,<4.0.0',
19
+ 'socketdev>=3.0.22,<4.0.0',
20
20
  "bs4>=0.0.2",
21
21
  ]
22
22
  readme = "README.md"
@@ -165,3 +165,4 @@ include = ["socketsecurity", "LICENSE"]
165
165
  dev = [
166
166
  "pre-commit>=4.3.0",
167
167
  ]
168
+
@@ -1,3 +1,3 @@
1
1
  __author__ = 'socket.dev'
2
- __version__ = '2.2.43'
2
+ __version__ = '2.2.55'
3
3
  USER_AGENT = f'SocketPythonCLI/{__version__}'
@@ -77,6 +77,8 @@ class CliConfig:
77
77
  reach_concurrency: Optional[int] = None
78
78
  reach_additional_params: Optional[List[str]] = None
79
79
  only_facts_file: bool = False
80
+ reach_use_only_pregenerated_sboms: bool = False
81
+ max_purl_batch_size: int = 5000
80
82
 
81
83
  @classmethod
82
84
  def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
@@ -105,6 +107,7 @@ class CliConfig:
105
107
  'commit_sha': args.commit_sha,
106
108
  'generate_license': args.generate_license,
107
109
  'enable_debug': args.enable_debug,
110
+ 'enable_diff': args.enable_diff,
108
111
  'allow_unverified': args.allow_unverified,
109
112
  'enable_json': args.enable_json,
110
113
  'enable_sarif': args.enable_sarif,
@@ -139,6 +142,8 @@ class CliConfig:
139
142
  'reach_concurrency': args.reach_concurrency,
140
143
  'reach_additional_params': args.reach_additional_params,
141
144
  'only_facts_file': args.only_facts_file,
145
+ 'reach_use_only_pregenerated_sboms': args.reach_use_only_pregenerated_sboms,
146
+ 'max_purl_batch_size': args.max_purl_batch_size,
142
147
  'version': __version__
143
148
  }
144
149
  try:
@@ -175,11 +180,21 @@ class CliConfig:
175
180
  logging.error("--only-facts-file requires --reach to be specified")
176
181
  exit(1)
177
182
 
183
+ # Validate that reach_use_only_pregenerated_sboms requires reach
184
+ if args.reach_use_only_pregenerated_sboms and not args.reach:
185
+ logging.error("--reach-use-only-pregenerated-sboms requires --reach to be specified")
186
+ exit(1)
187
+
178
188
  # Validate reach_concurrency is >= 1 if provided
179
189
  if args.reach_concurrency is not None and args.reach_concurrency < 1:
180
190
  logging.error("--reach-concurrency must be >= 1")
181
191
  exit(1)
182
192
 
193
+ # Validate max_purl_batch_size is within allowed range
194
+ if args.max_purl_batch_size < 1 or args.max_purl_batch_size > 9999:
195
+ logging.error("--max-purl-batch-size must be between 1 and 9999")
196
+ exit(1)
197
+
183
198
  return cls(**config_args)
184
199
 
185
200
  def to_dict(self) -> dict:
@@ -439,6 +454,13 @@ def create_argument_parser() -> argparse.ArgumentParser:
439
454
  action="store_true",
440
455
  help="Exclude license details from the diff report (boosts performance for large repos)"
441
456
  )
457
+ output_group.add_argument(
458
+ "--max-purl-batch-size",
459
+ dest="max_purl_batch_size",
460
+ type=int,
461
+ default=5000,
462
+ help="Maximum batch size for PURL endpoint calls when generating license info (default: 5000, min: 1, max: 9999)"
463
+ )
442
464
 
443
465
  output_group.add_argument(
444
466
  "--disable-security-issue",
@@ -602,6 +624,12 @@ def create_argument_parser() -> argparse.ArgumentParser:
602
624
  action="store_true",
603
625
  help="Submit only the .socket.facts.json file when creating full scan (requires --reach)"
604
626
  )
627
+ reachability_group.add_argument(
628
+ "--reach-use-only-pregenerated-sboms",
629
+ dest="reach_use_only_pregenerated_sboms",
630
+ action="store_true",
631
+ help="When using this option, the scan is created based only on pre-generated CDX and SPDX files in your project. (requires --reach)"
632
+ )
605
633
 
606
634
  parser.add_argument(
607
635
  '--version',
@@ -281,12 +281,13 @@ class Core:
281
281
  except Exception as e:
282
282
  log.error(f"Failed to save manifest tar.gz to {output_path}: {e}")
283
283
 
284
- def find_files(self, path: str) -> List[str]:
284
+ def find_files(self, path: str, ecosystems: Optional[List[str]] = None) -> List[str]:
285
285
  """
286
286
  Finds supported manifest files in the given path.
287
287
 
288
288
  Args:
289
289
  path: Path to search for manifest files.
290
+ ecosystems: Optional list of ecosystems to include. If None, all ecosystems are included.
290
291
 
291
292
  Returns:
292
293
  List of found manifest file paths.
@@ -299,6 +300,9 @@ class Core:
299
300
  patterns = self.get_supported_patterns()
300
301
 
301
302
  for ecosystem in patterns:
303
+ # If ecosystems filter is provided, only include specified ecosystems
304
+ if ecosystems is not None and ecosystem not in ecosystems:
305
+ continue
302
306
  if ecosystem in self.config.excluded_ecosystems:
303
307
  continue
304
308
  log.debug(f'Scanning ecosystem: {ecosystem}')
@@ -343,6 +347,23 @@ class Core:
343
347
 
344
348
  return file_list
345
349
 
350
+ def find_sbom_files(self, path: str) -> List[str]:
351
+ """
352
+ Finds only pre-generated SBOM files (CDX and SPDX) in the given path.
353
+
354
+ This is used with --reach-use-only-pregenerated-sboms to find only
355
+ pre-computed CycloneDX and SPDX manifest files.
356
+
357
+ Args:
358
+ path: Path to search for SBOM files.
359
+
360
+ Returns:
361
+ List of found CDX and SPDX file paths.
362
+ """
363
+ log.debug("Starting Find SBOM Files (CDX and SPDX only)")
364
+ sbom_ecosystems = ['cdx', 'spdx']
365
+ return self.find_files(path, ecosystems=sbom_ecosystems)
366
+
346
367
  def get_supported_patterns(self) -> Dict:
347
368
  """
348
369
  Gets supported file patterns from the Socket API.
@@ -547,7 +568,8 @@ class Core:
547
568
  no_change: bool = False,
548
569
  save_files_list_path: Optional[str] = None,
549
570
  save_manifest_tar_path: Optional[str] = None,
550
- base_paths: Optional[List[str]] = None
571
+ base_paths: Optional[List[str]] = None,
572
+ explicit_files: Optional[List[str]] = None
551
573
  ) -> Diff:
552
574
  """Create a new full scan and return with html_report_url.
553
575
 
@@ -558,6 +580,7 @@ class Core:
558
580
  save_files_list_path: Optional path to save submitted files list for debugging
559
581
  save_manifest_tar_path: Optional path to save manifest files tar.gz archive
560
582
  base_paths: List of base paths for the scan (optional)
583
+ explicit_files: Optional list of explicit files to use instead of discovering files
561
584
 
562
585
  Returns:
563
586
  Dict with full scan data including html_report_url
@@ -571,11 +594,15 @@ class Core:
571
594
  if no_change:
572
595
  return diff
573
596
 
574
- # Find manifest files from all paths
575
- all_files = []
576
- for path in paths:
577
- files = self.find_files(path)
578
- all_files.extend(files)
597
+ # Use explicit files if provided, otherwise find manifest files from all paths
598
+ if explicit_files is not None:
599
+ all_files = explicit_files
600
+ log.debug(f"Using {len(all_files)} explicit files instead of discovering files")
601
+ else:
602
+ all_files = []
603
+ for path in paths:
604
+ files = self.find_files(path)
605
+ all_files.extend(files)
579
606
 
580
607
  # Save submitted files list if requested
581
608
  if save_files_list_path and all_files:
@@ -632,54 +659,6 @@ class Core:
632
659
  # Return result in the format expected by the user
633
660
  return diff
634
661
 
635
- def check_full_scans_status(self, head_full_scan_id: str, new_full_scan_id: str) -> bool:
636
- is_ready = False
637
- current_timeout = self.config.timeout
638
- self.sdk.set_timeout(0.5)
639
- try:
640
- self.sdk.fullscans.stream(self.config.org_slug, head_full_scan_id)
641
- except Exception:
642
- log.debug(f"Queued up full scan for processing ({head_full_scan_id})")
643
-
644
- try:
645
- self.sdk.fullscans.stream(self.config.org_slug, new_full_scan_id)
646
- except Exception:
647
- log.debug(f"Queued up full scan for processing ({new_full_scan_id})")
648
- self.sdk.set_timeout(current_timeout)
649
- start_check = time.time()
650
- head_is_ready = False
651
- new_is_ready = False
652
- while not is_ready:
653
- head_full_scan_metadata = self.sdk.fullscans.metadata(self.config.org_slug, head_full_scan_id)
654
- if head_full_scan_metadata:
655
- head_state = head_full_scan_metadata.get("scan_state")
656
- else:
657
- head_state = None
658
- new_full_scan_metadata = self.sdk.fullscans.metadata(self.config.org_slug, new_full_scan_id)
659
- if new_full_scan_metadata:
660
- new_state = new_full_scan_metadata.get("scan_state")
661
- else:
662
- new_state = None
663
- if head_state and head_state == "resolve":
664
- head_is_ready = True
665
- if new_state and new_state == "resolve":
666
- new_is_ready = True
667
- if head_is_ready and new_is_ready:
668
- is_ready = True
669
- current_time = time.time()
670
- if current_time - start_check >= self.config.timeout:
671
- log.debug(
672
- f"Timeout reached while waiting for full scans to be ready "
673
- f"({head_full_scan_id}, {new_full_scan_id})"
674
- )
675
- break
676
- total_time = time.time() - start_check
677
- if is_ready:
678
- log.info(f"Full scans are ready in {total_time:.2f} seconds")
679
- else:
680
- log.warning(f"Full scans are not ready yet ({head_full_scan_id}, {new_full_scan_id})")
681
- return is_ready
682
-
683
662
  def get_full_scan(self, full_scan_id: str) -> FullScan:
684
663
  """
685
664
  Get a FullScan object for an existing full scan including sbom_artifacts and packages.
@@ -819,28 +798,54 @@ class Core:
819
798
  pkg.url += f"/{pkg.name}/overview/{pkg.version}"
820
799
  return pkg
821
800
 
822
- def get_license_text_via_purl(self, packages: dict[str, Package]) -> dict:
823
- components = []
801
+ def get_license_text_via_purl(self, packages: dict[str, Package], batch_size: int = 5000) -> dict:
802
+ """Get license attribution and details via PURL endpoint in batches.
803
+
804
+ Args:
805
+ packages: Dictionary of packages to get license info for
806
+ batch_size: Maximum number of packages to process per API call (1-9999)
807
+
808
+ Returns:
809
+ Updated packages dictionary with licenseAttrib and licenseDetails populated
810
+ """
811
+ # Validate batch size
812
+ batch_size = max(1, min(9999, batch_size))
813
+
814
+ # Build list of all components
815
+ all_components = []
824
816
  for purl in packages:
825
817
  full_purl = f"pkg:/{purl}"
826
- components.append({"purl": full_purl})
827
- results = self.sdk.purl.post(
828
- license=True,
829
- components=components,
830
- licenseattrib=True,
831
- licensedetails=True
832
- )
833
- purl_packages = []
834
- for result in results:
835
- ecosystem = result["type"]
836
- name = result["name"]
837
- package_version = result["version"]
838
- licenseDetails = result.get("licenseDetails")
839
- licenseAttrib = result.get("licenseAttrib")
840
- purl = f"{ecosystem}/{name}@{package_version}"
841
- if purl not in purl_packages and purl in packages:
842
- packages[purl].licenseAttrib = licenseAttrib
843
- packages[purl].licenseDetails = licenseDetails
818
+ all_components.append({"purl": full_purl})
819
+
820
+ # Process in batches
821
+ total_components = len(all_components)
822
+ log.debug(f"Processing {total_components} packages in batches of {batch_size}")
823
+
824
+ for i in range(0, total_components, batch_size):
825
+ batch_components = all_components[i:i + batch_size]
826
+ batch_num = (i // batch_size) + 1
827
+ total_batches = (total_components + batch_size - 1) // batch_size
828
+ log.debug(f"Processing batch {batch_num}/{total_batches} ({len(batch_components)} packages)")
829
+
830
+ results = self.sdk.purl.post(
831
+ license=True,
832
+ components=batch_components,
833
+ licenseattrib=True,
834
+ licensedetails=True
835
+ )
836
+
837
+ purl_packages = []
838
+ for result in results:
839
+ ecosystem = result["type"]
840
+ name = result["name"]
841
+ package_version = result["version"]
842
+ licenseDetails = result.get("licenseDetails")
843
+ licenseAttrib = result.get("licenseAttrib")
844
+ purl = f"{ecosystem}/{name}@{package_version}"
845
+ if purl not in purl_packages and purl in packages:
846
+ packages[purl].licenseAttrib = licenseAttrib
847
+ packages[purl].licenseDetails = licenseDetails
848
+
844
849
  return packages
845
850
 
846
851
  def get_added_and_removed_packages(
@@ -933,7 +938,14 @@ class Core:
933
938
  log.error(f"Artifact details - name: {artifact.name}, version: {artifact.version}")
934
939
  log.error("No matching packages found in head_full_scan")
935
940
 
936
- packages = self.get_license_text_via_purl(packages)
941
+ # Only fetch license details if generate_license is enabled
942
+ if self.cli_config and self.cli_config.generate_license:
943
+ log.debug("Fetching license details via PURL endpoint")
944
+ batch_size = self.cli_config.max_purl_batch_size if self.cli_config else 5000
945
+ packages = self.get_license_text_via_purl(packages, batch_size=batch_size)
946
+ else:
947
+ log.debug("Skipping PURL endpoint call (--generate-license not set)")
948
+
937
949
  return added_packages, removed_packages, packages
938
950
 
939
951
  def create_new_diff(
@@ -943,7 +955,8 @@ class Core:
943
955
  no_change: bool = False,
944
956
  save_files_list_path: Optional[str] = None,
945
957
  save_manifest_tar_path: Optional[str] = None,
946
- base_paths: Optional[List[str]] = None
958
+ base_paths: Optional[List[str]] = None,
959
+ explicit_files: Optional[List[str]] = None
947
960
  ) -> Diff:
948
961
  """Create a new diff using the Socket SDK.
949
962
 
@@ -954,16 +967,21 @@ class Core:
954
967
  save_files_list_path: Optional path to save submitted files list for debugging
955
968
  save_manifest_tar_path: Optional path to save manifest files tar.gz archive
956
969
  base_paths: List of base paths for the scan (optional)
970
+ explicit_files: Optional list of explicit files to use instead of discovering files
957
971
  """
958
972
  log.debug(f"starting create_new_diff with no_change: {no_change}")
959
973
  if no_change:
960
974
  return Diff(id="NO_DIFF_RAN", diff_url="", report_url="")
961
975
 
962
- # Find manifest files from all paths
963
- all_files = []
964
- for path in paths:
965
- files = self.find_files(path)
966
- all_files.extend(files)
976
+ # Use explicit files if provided, otherwise find manifest files from all paths
977
+ if explicit_files is not None:
978
+ all_files = explicit_files
979
+ log.debug(f"Using {len(all_files)} explicit files instead of discovering files")
980
+ else:
981
+ all_files = []
982
+ for path in paths:
983
+ files = self.find_files(path)
984
+ all_files.extend(files)
967
985
 
968
986
  # Save submitted files list if requested
969
987
  if save_files_list_path and all_files:
@@ -1059,9 +1077,6 @@ class Core:
1059
1077
  log.warning(f"Failed to clean up temporary file {temp_file}: {e}")
1060
1078
 
1061
1079
  # Handle diff generation - now we always have both scans
1062
- scans_ready = self.check_full_scans_status(head_full_scan_id, new_full_scan.id)
1063
- if scans_ready is False:
1064
- log.error(f"Full scans did not complete within {self.config.timeout} seconds")
1065
1080
  (
1066
1081
  added_packages,
1067
1082
  removed_packages,
@@ -101,10 +101,11 @@ class ReachabilityAnalyzer:
101
101
  additional_params: Optional[List[str]] = None,
102
102
  allow_unverified: bool = False,
103
103
  enable_debug: bool = False,
104
+ use_only_pregenerated_sboms: bool = False,
104
105
  ) -> Dict[str, Any]:
105
106
  """
106
107
  Run reachability analysis.
107
-
108
+
108
109
  Args:
109
110
  org_slug: Socket organization slug
110
111
  target_directory: Directory to analyze
@@ -125,7 +126,8 @@ class ReachabilityAnalyzer:
125
126
  additional_params: Additional parameters to pass to coana CLI
126
127
  allow_unverified: Disable SSL certificate verification (sets NODE_TLS_REJECT_UNAUTHORIZED=0)
127
128
  enable_debug: Enable debug mode (passes -d flag to coana CLI)
128
-
129
+ use_only_pregenerated_sboms: Use only pre-generated CDX and SPDX files for the scan
130
+
129
131
  Returns:
130
132
  Dict containing scan_id and report_path
131
133
  """
@@ -179,7 +181,10 @@ class ReachabilityAnalyzer:
179
181
 
180
182
  if enable_debug:
181
183
  cmd.append("-d")
182
-
184
+
185
+ if use_only_pregenerated_sboms:
186
+ cmd.append("--use-only-pregenerated-sboms")
187
+
183
188
  # Add any additional parameters provided by the user
184
189
  if additional_params:
185
190
  cmd.extend(additional_params)
@@ -167,6 +167,8 @@ def main_code():
167
167
 
168
168
  # Variable to track if we need to override files with facts file
169
169
  facts_file_to_submit = None
170
+ # Variable to track SBOM files to submit when using --reach-use-only-pregenerated-sboms
171
+ sbom_files_to_submit = None
170
172
 
171
173
  # Git setup
172
174
  is_repo = False
@@ -230,12 +232,14 @@ def main_code():
230
232
  # Run reachability analysis if enabled
231
233
  if config.reach:
232
234
  from socketsecurity.core.tools.reachability import ReachabilityAnalyzer
233
-
235
+
234
236
  log.info("Starting reachability analysis...")
235
-
237
+
236
238
  # Find manifest files in scan paths (excluding .socket.facts.json to avoid circular dependency)
237
239
  log.info("Finding manifest files for reachability analysis...")
238
240
  manifest_files = []
241
+
242
+ # Always find all manifest files for the tar hash upload
239
243
  for scan_path in scan_paths:
240
244
  scan_manifests = core.find_files(scan_path)
241
245
  # Filter out .socket.facts.json files from manifest upload
@@ -289,7 +293,8 @@ def main_code():
289
293
  concurrency=config.reach_concurrency,
290
294
  additional_params=config.reach_additional_params,
291
295
  allow_unverified=config.allow_unverified,
292
- enable_debug=config.enable_debug
296
+ enable_debug=config.enable_debug,
297
+ use_only_pregenerated_sboms=config.reach_use_only_pregenerated_sboms
293
298
  )
294
299
 
295
300
  log.info(f"Reachability analysis completed successfully")
@@ -301,6 +306,17 @@ def main_code():
301
306
  if config.only_facts_file:
302
307
  facts_file_to_submit = os.path.abspath(output_path)
303
308
  log.info(f"Only-facts-file mode: will submit only {facts_file_to_submit}")
309
+
310
+ # If reach-use-only-pregenerated-sboms mode, submit CDX, SPDX, and facts file
311
+ if config.reach_use_only_pregenerated_sboms:
312
+ # Find only CDX and SPDX files for the final scan submission
313
+ sbom_files_to_submit = []
314
+ for scan_path in scan_paths:
315
+ sbom_files_to_submit.extend(core.find_sbom_files(scan_path))
316
+ # Use relative path for facts file
317
+ if os.path.exists(output_path):
318
+ sbom_files_to_submit.append(output_path)
319
+ log.info(f"Pre-generated SBOMs mode: will submit {len(sbom_files_to_submit)} files (CDX, SPDX, and facts file)")
304
320
 
305
321
  except Exception as e:
306
322
  log.error(f"Reachability analysis failed: {str(e)}")
@@ -331,9 +347,16 @@ def main_code():
331
347
  files_explicitly_specified = True
332
348
  log.debug(f"Overriding files to only submit facts file: {facts_file_to_submit}")
333
349
 
350
+ # Override files if reach-use-only-pregenerated-sboms mode is active
351
+ if sbom_files_to_submit:
352
+ specified_files = sbom_files_to_submit
353
+ files_explicitly_specified = True
354
+ log.debug(f"Overriding files to submit only SBOM files (CDX, SPDX, and facts): {sbom_files_to_submit}")
355
+
334
356
  # Determine files to check based on the new logic
335
357
  files_to_check = []
336
358
  force_api_mode = False
359
+ force_diff_mode = False
337
360
 
338
361
  if files_explicitly_specified:
339
362
  # Case 2: Files are specified - use them and don't check commit details
@@ -343,10 +366,21 @@ def main_code():
343
366
  # Case 1: Files not specified and --ignore-commit-files not set - try to find changed files from commit
344
367
  files_to_check = git_repo.changed_files
345
368
  log.debug(f"Using changed files from commit: {files_to_check}")
369
+ elif config.ignore_commit_files and is_repo:
370
+ # Case 3: Git repo with --ignore-commit-files - force diff mode
371
+ files_to_check = []
372
+ force_diff_mode = True
373
+ log.debug("Git repo with --ignore-commit-files: forcing diff mode")
346
374
  else:
347
- # ignore_commit_files is set or not a repo - scan everything but force API mode if no supported files
375
+ # Case 4: Not a git repo (ignore_commit_files was auto-set to True)
348
376
  files_to_check = []
349
- log.debug("No files to check from commit (ignore_commit_files=True or not a repo)")
377
+ # If --enable-diff is set, force diff mode for non-git repos
378
+ log.debug(f"Case 4: Non-git repo - config.enable_diff={config.enable_diff}, type={type(config.enable_diff)}")
379
+ if config.enable_diff:
380
+ force_diff_mode = True
381
+ log.debug("Non-git repo with --enable-diff: forcing diff mode")
382
+ else:
383
+ log.debug("Non-git repo without --enable-diff: will use full scan mode")
350
384
 
351
385
  # Check if we have supported manifest files
352
386
  has_supported_files = files_to_check and core.has_manifest_files(files_to_check)
@@ -367,22 +401,21 @@ def main_code():
367
401
  has_supported_files = False
368
402
 
369
403
  # Case 3: If no supported files or files are empty, force API mode (no PR comments)
370
- if not has_supported_files:
404
+ # BUT: Don't force API mode if we're in force_diff_mode
405
+ log.debug(f"files_to_check={files_to_check}, has_supported_files={has_supported_files}, force_diff_mode={force_diff_mode}, config.enable_diff={config.enable_diff}")
406
+ if not has_supported_files and not force_diff_mode:
371
407
  force_api_mode = True
372
408
  log.debug("No supported manifest files found, forcing API mode")
409
+ log.debug(f"force_api_mode={force_api_mode}")
373
410
 
374
411
  # Determine scan behavior
375
412
  should_skip_scan = False # Always perform scan, but behavior changes based on supported files
376
- if config.ignore_commit_files and not files_explicitly_specified:
377
- # Force full scan when ignoring commit files and no explicit files
378
- should_skip_scan = False
379
- log.debug("Forcing full scan due to ignore_commit_files")
380
- elif not has_supported_files:
381
- # No supported files - still scan but in API mode
413
+ if not has_supported_files and not force_diff_mode:
414
+ # No supported files and not forcing diff - still scan but in API mode
382
415
  should_skip_scan = False
383
416
  log.debug("No supported files but will scan in API mode")
384
417
  else:
385
- log.debug("Found supported manifest files, proceeding with normal scan")
418
+ log.debug("Found supported manifest files or forcing diff mode, proceeding with normal scan")
386
419
 
387
420
  org_slug = core.config.org_slug
388
421
  if config.repo_is_public:
@@ -435,6 +468,7 @@ def main_code():
435
468
  diff.report_url = ""
436
469
 
437
470
  # Handle SCM-specific flows
471
+ log.debug(f"Flow decision: scm={scm is not None}, force_diff_mode={force_diff_mode}, force_api_mode={force_api_mode}, enable_diff={config.enable_diff}")
438
472
  if scm is not None and scm.check_event_type() == "comment":
439
473
  # FIXME: This entire flow should be a separate command called "filter_ignored_alerts_in_comments"
440
474
  # It's not related to scanning or diff generation - it just:
@@ -452,7 +486,7 @@ def main_code():
452
486
  log.info("Push initiated flow")
453
487
  if scm.check_event_type() == "diff":
454
488
  log.info("Starting comment logic for PR/MR event")
455
- diff = core.create_new_diff(scan_paths, params, no_change=should_skip_scan, save_files_list_path=config.save_submitted_files_list, save_manifest_tar_path=config.save_manifest_tar, base_paths=base_paths)
489
+ diff = core.create_new_diff(scan_paths, params, no_change=should_skip_scan, save_files_list_path=config.save_submitted_files_list, save_manifest_tar_path=config.save_manifest_tar, base_paths=base_paths, explicit_files=sbom_files_to_submit)
456
490
  comments = scm.get_comments_for_pr()
457
491
  log.debug("Removing comment alerts")
458
492
 
@@ -505,18 +539,19 @@ def main_code():
505
539
  )
506
540
  else:
507
541
  log.info("Starting non-PR/MR flow")
508
- diff = core.create_new_diff(scan_paths, params, no_change=should_skip_scan, save_files_list_path=config.save_submitted_files_list, save_manifest_tar_path=config.save_manifest_tar, base_paths=base_paths)
542
+ diff = core.create_new_diff(scan_paths, params, no_change=should_skip_scan, save_files_list_path=config.save_submitted_files_list, save_manifest_tar_path=config.save_manifest_tar, base_paths=base_paths, explicit_files=sbom_files_to_submit)
509
543
 
510
544
  output_handler.handle_output(diff)
511
-
512
- elif config.enable_diff and not force_api_mode:
513
- # New logic: --enable-diff forces diff mode even with --integration api (no SCM)
545
+
546
+ elif (config.enable_diff or force_diff_mode) and not force_api_mode:
547
+ # New logic: --enable-diff or force_diff_mode (from --ignore-commit-files in git repos) forces diff mode
514
548
  log.info("Diff mode enabled without SCM integration")
515
- diff = core.create_new_diff(scan_paths, params, no_change=should_skip_scan, save_files_list_path=config.save_submitted_files_list, save_manifest_tar_path=config.save_manifest_tar, base_paths=base_paths)
549
+ diff = core.create_new_diff(scan_paths, params, no_change=should_skip_scan, save_files_list_path=config.save_submitted_files_list, save_manifest_tar_path=config.save_manifest_tar, base_paths=base_paths, explicit_files=sbom_files_to_submit)
516
550
  output_handler.handle_output(diff)
517
551
 
518
- elif config.enable_diff and force_api_mode:
519
- # User requested diff mode but no manifest files were detected
552
+ elif (config.enable_diff or force_diff_mode) and force_api_mode:
553
+ # User requested diff mode but no manifest files were detected - this should not happen with new logic
554
+ # but keeping as a safety net
520
555
  log.warning("--enable-diff was specified but no supported manifest files were detected in the changed files. Falling back to full scan mode.")
521
556
  log.info("Creating Socket Report (full scan)")
522
557
  serializable_params = {
@@ -530,12 +565,13 @@ def main_code():
530
565
  no_change=should_skip_scan,
531
566
  save_files_list_path=config.save_submitted_files_list,
532
567
  save_manifest_tar_path=config.save_manifest_tar,
533
- base_paths=base_paths
568
+ base_paths=base_paths,
569
+ explicit_files=sbom_files_to_submit
534
570
  )
535
571
  log.info(f"Full scan created with ID: {diff.id}")
536
572
  log.info(f"Full scan report URL: {diff.report_url}")
537
573
  output_handler.handle_output(diff)
538
-
574
+
539
575
  else:
540
576
  if force_api_mode:
541
577
  log.info("No Manifest files changed, creating Socket Report")
@@ -550,7 +586,8 @@ def main_code():
550
586
  no_change=should_skip_scan,
551
587
  save_files_list_path=config.save_submitted_files_list,
552
588
  save_manifest_tar_path=config.save_manifest_tar,
553
- base_paths=base_paths
589
+ base_paths=base_paths,
590
+ explicit_files=sbom_files_to_submit
554
591
  )
555
592
  log.info(f"Full scan created with ID: {diff.id}")
556
593
  log.info(f"Full scan report URL: {diff.report_url}")
@@ -561,7 +598,8 @@ def main_code():
561
598
  no_change=should_skip_scan,
562
599
  save_files_list_path=config.save_submitted_files_list,
563
600
  save_manifest_tar_path=config.save_manifest_tar,
564
- base_paths=base_paths
601
+ base_paths=base_paths,
602
+ explicit_files=sbom_files_to_submit
565
603
  )
566
604
  output_handler.handle_output(diff)
567
605