socketsecurity 2.0.6__tar.gz → 2.0.8__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 (32) hide show
  1. {socketsecurity-2.0.6/socketsecurity.egg-info → socketsecurity-2.0.8}/PKG-INFO +5 -2
  2. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/README.md +2 -0
  3. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/pyproject.toml +3 -2
  4. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/socketsecurity/__init__.py +1 -1
  5. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/socketsecurity/config.py +8 -0
  6. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/socketsecurity/core/__init__.py +172 -142
  7. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/socketsecurity/core/classes.py +7 -1
  8. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/socketsecurity/core/scm/github.py +3 -1
  9. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/socketsecurity/core/utils.py +23 -0
  10. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/socketsecurity/socketcli.py +9 -4
  11. {socketsecurity-2.0.6 → socketsecurity-2.0.8/socketsecurity.egg-info}/PKG-INFO +5 -2
  12. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/socketsecurity.egg-info/requires.txt +2 -1
  13. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/LICENSE +0 -0
  14. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/setup.cfg +0 -0
  15. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/socketsecurity/core/cli_client.py +0 -0
  16. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/socketsecurity/core/exceptions.py +0 -0
  17. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/socketsecurity/core/git_interface.py +0 -0
  18. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/socketsecurity/core/issues.py +0 -0
  19. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/socketsecurity/core/licenses.py +0 -0
  20. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/socketsecurity/core/logging.py +0 -0
  21. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/socketsecurity/core/messages.py +0 -0
  22. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/socketsecurity/core/scm/__init__.py +0 -0
  23. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/socketsecurity/core/scm/base.py +0 -0
  24. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/socketsecurity/core/scm/client.py +0 -0
  25. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/socketsecurity/core/scm/gitlab.py +0 -0
  26. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/socketsecurity/core/scm_comments.py +0 -0
  27. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/socketsecurity/core/socket_config.py +0 -0
  28. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/socketsecurity/output.py +0 -0
  29. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/socketsecurity.egg-info/SOURCES.txt +0 -0
  30. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/socketsecurity.egg-info/dependency_links.txt +0 -0
  31. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/socketsecurity.egg-info/entry_points.txt +0 -0
  32. {socketsecurity-2.0.6 → socketsecurity-2.0.8}/socketsecurity.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: socketsecurity
3
- Version: 2.0.6
3
+ Version: 2.0.8
4
4
  Summary: Socket Security CLI for CI/CD
5
5
  Author-email: Douglas Coburn <douglas@socket.dev>
6
6
  Maintainer-email: Douglas Coburn <douglas@socket.dev>
@@ -19,7 +19,7 @@ Requires-Dist: prettytable
19
19
  Requires-Dist: GitPython
20
20
  Requires-Dist: packaging
21
21
  Requires-Dist: python-dotenv
22
- Requires-Dist: socket-sdk-python>=2.0.5
22
+ Requires-Dist: socket-sdk-python>=2.0.8
23
23
  Provides-Extra: test
24
24
  Requires-Dist: pytest>=7.4.0; extra == "test"
25
25
  Requires-Dist: pytest-cov>=4.1.0; extra == "test"
@@ -28,6 +28,7 @@ Requires-Dist: pytest-asyncio>=0.23.0; extra == "test"
28
28
  Requires-Dist: pytest-watch>=4.2.0; extra == "test"
29
29
  Provides-Extra: dev
30
30
  Requires-Dist: ruff>=0.3.0; extra == "dev"
31
+ Requires-Dist: twine; extra == "dev"
31
32
  Requires-Dist: pip-tools>=7.4.0; extra == "dev"
32
33
 
33
34
  # Socket Security CLI
@@ -42,6 +43,7 @@ socketcli [-h] [--api-token API_TOKEN] [--repo REPO] [--integration {api,github,
42
43
  [--target-path TARGET_PATH] [--sbom-file SBOM_FILE] [--files FILES] [--default-branch] [--pending-head]
43
44
  [--generate-license] [--enable-debug] [--enable-json] [--enable-sarif] [--disable-overview] [--disable-security-issue]
44
45
  [--allow-unverified] [--ignore-commit-files] [--disable-blocking] [--scm SCM] [--timeout TIMEOUT]
46
+ [--exclude-license-details]
45
47
  ````
46
48
 
47
49
  If you don't want to provide the Socket API Token every time then you can use the environment variable `SOCKET_SECURITY_API_KEY`
@@ -90,6 +92,7 @@ If you don't want to provide the Socket API Token every time then you can use th
90
92
  | --enable-json | False | False | Output in JSON format |
91
93
  | --enable-sarif | False | False | Enable SARIF output of results instead of table or JSON format|
92
94
  | --disable-overview | False | False | Disable overview output |
95
+ | --exclude-license-details | False | False | Exclude license details from the diff report (boosts performance for large repos) |
93
96
 
94
97
  #### Security Configuration
95
98
  | Parameter | Required | Default | Description |
@@ -10,6 +10,7 @@ socketcli [-h] [--api-token API_TOKEN] [--repo REPO] [--integration {api,github,
10
10
  [--target-path TARGET_PATH] [--sbom-file SBOM_FILE] [--files FILES] [--default-branch] [--pending-head]
11
11
  [--generate-license] [--enable-debug] [--enable-json] [--enable-sarif] [--disable-overview] [--disable-security-issue]
12
12
  [--allow-unverified] [--ignore-commit-files] [--disable-blocking] [--scm SCM] [--timeout TIMEOUT]
13
+ [--exclude-license-details]
13
14
  ````
14
15
 
15
16
  If you don't want to provide the Socket API Token every time then you can use the environment variable `SOCKET_SECURITY_API_KEY`
@@ -58,6 +59,7 @@ If you don't want to provide the Socket API Token every time then you can use th
58
59
  | --enable-json | False | False | Output in JSON format |
59
60
  | --enable-sarif | False | False | Enable SARIF output of results instead of table or JSON format|
60
61
  | --disable-overview | False | False | Disable overview output |
62
+ | --exclude-license-details | False | False | Exclude license details from the diff report (boosts performance for large repos) |
61
63
 
62
64
  #### Security Configuration
63
65
  | Parameter | Required | Default | Description |
@@ -12,8 +12,8 @@ dependencies = [
12
12
  'prettytable',
13
13
  'GitPython',
14
14
  'packaging',
15
- 'python-dotenv',
16
- 'socket-sdk-python>=2.0.5'
15
+ 'python-dotenv',
16
+ 'socket-sdk-python>=2.0.8'
17
17
  ]
18
18
  readme = "README.md"
19
19
  description = "Socket Security CLI for CI/CD"
@@ -41,6 +41,7 @@ test = [
41
41
  ]
42
42
  dev = [
43
43
  "ruff>=0.3.0",
44
+ "twine", # for building
44
45
  "pip-tools>=7.4.0", # for pip-compile
45
46
  ]
46
47
 
@@ -1,2 +1,2 @@
1
1
  __author__ = 'socket.dev'
2
- __version__ = '2.0.6'
2
+ __version__ = '2.0.8'
@@ -33,6 +33,7 @@ class CliConfig:
33
33
  integration_org_slug: Optional[str] = None
34
34
  pending_head: bool = False
35
35
  timeout: Optional[int] = 1200
36
+ exclude_license_details: bool = False
36
37
  @classmethod
37
38
  def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
38
39
  parser = create_argument_parser()
@@ -71,6 +72,7 @@ class CliConfig:
71
72
  'integration_type': args.integration,
72
73
  'pending_head': args.pending_head,
73
74
  'timeout': args.timeout,
75
+ 'exclude_license_details': args.exclude_license_details,
74
76
  }
75
77
 
76
78
  if args.owner:
@@ -283,6 +285,12 @@ def create_argument_parser() -> argparse.ArgumentParser:
283
285
  action="store_true",
284
286
  help=argparse.SUPPRESS
285
287
  )
288
+ output_group.add_argument(
289
+ "--exclude-license-details",
290
+ dest="exclude_license_details",
291
+ action="store_true",
292
+ help="Exclude license details from the diff report (boosts performance for large repos)"
293
+ )
286
294
 
287
295
  # Security Configuration
288
296
  security_group = parser.add_argument_group('Security Configuration')
@@ -1,18 +1,14 @@
1
- import base64
2
- import json
3
1
  import logging
2
+ import sys
4
3
  import time
5
4
  from dataclasses import asdict
6
5
  from glob import glob
7
6
  from pathlib import PurePath
8
- from typing import BinaryIO, Dict, List, Optional, Tuple
7
+ from typing import BinaryIO, Dict, List, Tuple
9
8
 
10
9
  from socketdev import socketdev
11
- from socketdev.fullscans import (
12
- FullScanParams,
13
- SocketArtifact,
14
- DiffArtifact,
15
- )
10
+ from socketdev.exceptions import APIFailure
11
+ from socketdev.fullscans import FullScanParams, SocketArtifact
16
12
  from socketdev.org import Organization
17
13
  from socketdev.repos import RepositoryInfo
18
14
  from socketdev.settings import SecurityPolicyRule
@@ -26,9 +22,7 @@ from socketsecurity.core.classes import (
26
22
  Package,
27
23
  Purl,
28
24
  )
29
- from socketsecurity.core.exceptions import (
30
- APIResourceNotFound,
31
- )
25
+ from socketsecurity.core.exceptions import APIResourceNotFound
32
26
  from socketsecurity.core.licenses import Licenses
33
27
 
34
28
  from .socket_config import SocketConfig
@@ -45,11 +39,11 @@ log = logging.getLogger("socketdev")
45
39
 
46
40
  class Core:
47
41
  """Main class for interacting with Socket Security API and processing scan results."""
48
-
42
+
49
43
  ALERT_TYPE_TO_CAPABILITY = {
50
44
  "envVars": "Environment Variables",
51
45
  "networkAccess": "Network Access",
52
- "filesystemAccess": "File System Access",
46
+ "filesystemAccess": "File System Access",
53
47
  "shellAccess": "Shell Access",
54
48
  "usesEval": "Uses Eval",
55
49
  "unsafe": "Unsafe"
@@ -79,7 +73,7 @@ class Core:
79
73
 
80
74
  def get_org_id_slug(self) -> Tuple[str, str]:
81
75
  """Gets the Org ID and Org Slug for the API Token."""
82
- response = self.sdk.org.get()
76
+ response = self.sdk.org.get(use_types=True)
83
77
  organizations: Dict[str, Organization] = response.get("organizations", {})
84
78
 
85
79
  if len(organizations) == 1:
@@ -89,33 +83,33 @@ class Core:
89
83
 
90
84
  def get_sbom_data(self, full_scan_id: str) -> Dict[str, SocketArtifact]:
91
85
  """Returns the list of SBOM artifacts for a full scan."""
92
- response = self.sdk.fullscans.stream(self.config.org_slug, full_scan_id)
86
+ response = self.sdk.fullscans.stream(self.config.org_slug, full_scan_id, use_types=True)
93
87
  if not response.success:
94
88
  log.debug(f"Failed to get SBOM data for full-scan {full_scan_id}")
95
89
  log.debug(response.message)
96
90
  return {}
97
91
 
98
92
  return response.artifacts
99
-
93
+
100
94
  def get_sbom_data_list(self, artifacts_dict: Dict[str, SocketArtifact]) -> list[SocketArtifact]:
101
95
  """Converts artifacts dictionary to a list."""
102
96
  return list(artifacts_dict.values())
103
97
 
104
98
  def get_security_policy(self) -> Dict[str, SecurityPolicyRule]:
105
99
  """Gets the organization's security policy."""
106
- response = self.sdk.settings.get(self.config.org_slug)
107
-
100
+ response = self.sdk.settings.get(self.config.org_slug, use_types=True)
101
+
108
102
  if not response.success:
109
103
  log.error(f"Failed to get security policy: {response.status}")
110
104
  log.error(response.message)
111
105
  raise Exception(f"Failed to get security policy: {response.status}, message: {response.message}")
112
-
106
+
113
107
  return response.securityPolicyRules
114
108
 
115
109
  def create_sbom_output(self, diff: Diff) -> dict:
116
110
  """Creates CycloneDX output for a given diff."""
117
111
  try:
118
- result = self.sdk.export.cdx_bom(self.config.org_slug, diff.id)
112
+ result = self.sdk.export.cdx_bom(self.config.org_slug, diff.id, use_types=True)
119
113
  if not result.success:
120
114
  log.error(f"Failed to get CycloneDX Output for full-scan {diff.id}")
121
115
  log.error(result.message)
@@ -123,7 +117,7 @@ class Core:
123
117
 
124
118
  result.pop("success", None)
125
119
  return result
126
- except Exception as error:
120
+ except Exception:
127
121
  log.error(f"Unable to get CycloneDX Output for {diff.id}")
128
122
  log.error(result.get("message", "No error message provided"))
129
123
  return {}
@@ -132,23 +126,23 @@ class Core:
132
126
  def find_files(path: str) -> List[str]:
133
127
  """
134
128
  Finds supported manifest files in the given path.
135
-
129
+
136
130
  Args:
137
131
  path: Path to search for manifest files
138
-
132
+
139
133
  Returns:
140
134
  List of found manifest file paths
141
135
  """
142
136
  log.debug("Starting Find Files")
143
137
  start_time = time.time()
144
138
  files = set()
145
-
139
+
146
140
  for ecosystem in socket_globs:
147
141
  patterns = socket_globs[ecosystem]
148
142
  for file_name in patterns:
149
143
  pattern = Core.to_case_insensitive_regex(patterns[file_name]["pattern"])
150
144
  file_path = f"{path}/**/{pattern}"
151
- log.debug(f"Globbing {file_path}")
145
+ #log.debug(f"Globbing {file_path}")
152
146
  glob_start = time.time()
153
147
  glob_files = glob(file_path, recursive=True)
154
148
  for glob_file in glob_files:
@@ -156,26 +150,29 @@ class Core:
156
150
  files.add(glob_file)
157
151
  glob_end = time.time()
158
152
  glob_total_time = glob_end - glob_start
159
- log.debug(f"Glob for pattern {file_path} took {glob_total_time:.2f} seconds")
153
+ #log.debug(f"Glob for pattern {file_path} took {glob_total_time:.2f} seconds")
160
154
 
161
155
  log.debug("Finished Find Files")
162
156
  end_time = time.time()
163
157
  total_time = end_time - start_time
164
- log.info(f"Found {len(files)} in {total_time:.2f} seconds")
165
- log.debug(f"Files found: {list(files)}")
158
+ files_list = list(files)
159
+ if len(files_list) > 5:
160
+ log.debug(f"{len(files_list)} Files found ({total_time:.2f}s): {', '.join(files_list[:5])}, ...")
161
+ else:
162
+ log.debug(f"{len(files_list)} Files found ({total_time:.2f}s): {', '.join(files_list)}")
166
163
  return list(files)
167
-
164
+
168
165
  @staticmethod
169
166
  def to_case_insensitive_regex(input_string: str) -> str:
170
167
  """
171
168
  Converts a string into a case-insensitive regex pattern.
172
-
169
+
173
170
  Args:
174
171
  input_string: String to convert
175
-
172
+
176
173
  Returns:
177
174
  Case-insensitive regex pattern
178
-
175
+
179
176
  Example:
180
177
  "pipfile" -> "[Pp][Ii][Pp][Ff][Ii][Ll][Ee]"
181
178
  """
@@ -185,79 +182,79 @@ class Core:
185
182
  def load_files_for_sending(files: List[str], workspace: str) -> List[Tuple[str, Tuple[str, BinaryIO]]]:
186
183
  """
187
184
  Prepares files for sending to the Socket API.
188
-
185
+
189
186
  Args:
190
187
  files: List of file paths from find_files()
191
188
  workspace: Base directory path to make paths relative to
192
-
189
+
193
190
  Returns:
194
191
  List of tuples formatted for requests multipart upload:
195
192
  [(field_name, (filename, file_object)), ...]
196
193
  """
197
194
  send_files = []
198
-
195
+
199
196
  for file_path in files:
200
197
  if "/" in file_path:
201
198
  _, name = file_path.rsplit("/", 1)
202
199
  else:
203
200
  name = file_path
204
-
201
+
205
202
  if file_path.startswith(workspace):
206
203
  key = file_path[len(workspace):]
207
204
  else:
208
205
  key = file_path
209
-
206
+
210
207
  key = key.lstrip("/")
211
208
  key = key.lstrip("./")
212
-
209
+
213
210
  f = open(file_path, 'rb')
214
211
  payload = (key, (name, f))
215
212
  send_files.append(payload)
216
-
213
+
217
214
  return send_files
218
215
 
219
- def create_full_scan(self, files: List[str], params: FullScanParams) -> FullScan:
216
+ def create_full_scan(self, files: List[str], params: FullScanParams, has_head_scan: bool = False) -> FullScan:
220
217
  """
221
218
  Creates a new full scan via the Socket API.
222
-
219
+
223
220
  Args:
224
221
  files: List of files to scan
225
222
  params: Parameters for the full scan
226
-
223
+
227
224
  Returns:
228
225
  FullScan object with scan results
229
226
  """
230
227
  log.debug("Creating new full scan")
231
228
  create_full_start = time.time()
232
229
 
233
- res = self.sdk.fullscans.post(files, params)
230
+ res = self.sdk.fullscans.post(files, params, use_types=True)
234
231
  if not res.success:
235
232
  log.error(f"Error creating full scan: {res.message}, status: {res.status}")
236
233
  raise Exception(f"Error creating full scan: {res.message}, status: {res.status}")
237
234
 
238
235
  full_scan = FullScan(**asdict(res.data))
239
-
240
- full_scan_artifacts_dict = self.get_sbom_data(full_scan.id)
241
- full_scan.sbom_artifacts = self.get_sbom_data_list(full_scan_artifacts_dict)
242
- full_scan.packages = self.create_packages_dict(full_scan.sbom_artifacts)
236
+ if not has_head_scan:
237
+ full_scan_artifacts_dict = self.get_sbom_data(full_scan.id)
238
+ full_scan.sbom_artifacts = self.get_sbom_data_list(full_scan_artifacts_dict)
239
+ full_scan.packages = self.create_packages_dict(full_scan.sbom_artifacts)
243
240
 
244
241
  create_full_end = time.time()
245
242
  total_time = create_full_end - create_full_start
246
243
  log.debug(f"New Full Scan created in {total_time:.2f} seconds")
247
-
244
+
248
245
  return full_scan
249
246
 
250
247
  def get_full_scan(self, full_scan_id: str) -> FullScan:
251
248
  """
252
249
  Get a FullScan object for an existing full scan including sbom_artifacts and packages.
253
-
250
+
254
251
  Args:
255
252
  full_scan_id: The ID of the full scan to get
256
-
253
+
257
254
  Returns:
258
255
  The FullScan object with populated artifacts and packages
259
256
  """
260
- full_scan_metadata = self.sdk.fullscans.metadata(self.config.org_slug, full_scan_id)
257
+ full_scan_metadata = self.sdk.fullscans.metadata(self.config.org_slug, full_scan_id, use_types=True)
261
258
  full_scan = FullScan(**asdict(full_scan_metadata.data))
262
259
  full_scan_artifacts_dict = self.get_sbom_data(full_scan_id)
263
260
  full_scan.sbom_artifacts = self.get_sbom_data_list(full_scan_artifacts_dict)
@@ -267,10 +264,10 @@ class Core:
267
264
  def create_packages_dict(self, sbom_artifacts: list[SocketArtifact]) -> dict[str, Package]:
268
265
  """
269
266
  Creates a dictionary of Package objects from SBOM artifacts.
270
-
267
+
271
268
  Args:
272
269
  sbom_artifacts: List of SBOM artifacts from the scan
273
-
270
+
274
271
  Returns:
275
272
  Dictionary mapping package IDs to Package objects
276
273
  """
@@ -288,87 +285,122 @@ class Core:
288
285
  top_level_count[top_id] = 1
289
286
  else:
290
287
  top_level_count[top_id] += 1
291
-
288
+
292
289
  for package_id, package in packages.items():
293
290
  package.transitives = top_level_count.get(package_id, 0)
294
291
 
295
292
  return packages
296
-
293
+
297
294
  def get_package_license_text(self, package: Package) -> str:
298
295
  """
299
296
  Gets the license text for a package if available.
300
-
297
+
301
298
  Args:
302
299
  package: Package object to get license text for
303
-
300
+
304
301
  Returns:
305
302
  License text if found, empty string otherwise
306
303
  """
307
304
  if package.license is None:
308
305
  return ""
309
-
306
+
310
307
  license_raw = package.license
311
308
  all_licenses = Licenses()
312
309
  license_str = Licenses.make_python_safe(license_raw)
313
-
310
+
314
311
  if license_str is not None and hasattr(all_licenses, license_str):
315
312
  license_obj = getattr(all_licenses, license_str)
316
313
  return license_obj.licenseText
317
-
314
+
318
315
  return ""
319
316
 
320
- def get_repo_info(self, repo_slug: str) -> RepositoryInfo:
317
+ def get_repo_info(self, repo_slug: str, default_branch: str = "socket-default-branch") -> RepositoryInfo:
321
318
  """
322
319
  Gets repository information from the Socket API.
323
-
320
+
324
321
  Args:
325
322
  repo_slug: Repository slug to get info for
326
-
323
+ default_branch: Default branch string to use if the repo doesn't exist
324
+
327
325
  Returns:
328
326
  RepositoryInfo object
329
-
327
+
330
328
  Raises:
331
329
  Exception: If API request fails
332
330
  """
333
- response = self.sdk.repos.repo(self.config.org_slug, repo_slug)
334
- if not response.success:
335
- log.error(f"Failed to get repository: {response.status}")
336
- log.error(response.message)
337
- raise Exception(f"Failed to get repository info: {response.status}, message: {response.message}")
331
+ try:
332
+ response = self.sdk.repos.repo(self.config.org_slug, repo_slug, use_types=True)
333
+ if not response.success:
334
+ log.error(f"Failed to get repository: {response.status}")
335
+ log.error(response.message)
336
+ # raise Exception(f"Failed to get repository info: {response.status}, message: {response.message}")
337
+ except APIFailure:
338
+ log.warning(f"Failed to get repository {repo_slug}, attempting to create it")
339
+ create_response = self.sdk.repos.post(self.config.org_slug, name=repo_slug, default_branch=default_branch)
340
+ if not create_response.success:
341
+ log.error(f"Failed to create repository: {create_response.status}")
342
+ log.error(create_response.message)
343
+ raise Exception(
344
+ f"Failed to create repository: {create_response.status}, message: {create_response.message}"
345
+ )
346
+ else:
347
+ return create_response.data
338
348
  return response.data
339
349
 
340
350
  def get_head_scan_for_repo(self, repo_slug: str) -> str:
341
351
  """
342
352
  Gets the head scan ID for a repository.
343
-
353
+
344
354
  Args:
345
355
  repo_slug: Repository slug to get head scan for
346
-
356
+
347
357
  Returns:
348
358
  Head scan ID if it exists, None otherwise
349
359
  """
350
360
  repo_info = self.get_repo_info(repo_slug)
351
361
  return repo_info.head_full_scan_id if repo_info.head_full_scan_id else None
352
362
 
353
- def get_added_and_removed_packages(self, head_full_scan: Optional[FullScan], new_full_scan: FullScan) -> Tuple[Dict[str, Package], Dict[str, Package]]:
363
+ @staticmethod
364
+ def update_package_values(pkg: Package) -> Package:
365
+ pkg.purl = f"{pkg.name}@{pkg.version}"
366
+ pkg.url = f"https://socket.dev/{pkg.type}/package"
367
+ if pkg.namespace:
368
+ pkg.purl = f"{pkg.namespace}/{pkg.purl}"
369
+ pkg.url += f"/{pkg.namespace}"
370
+ pkg.url += f"/{pkg.name}/overview/{pkg.version}"
371
+ return pkg
372
+
373
+ def get_added_and_removed_packages(self, head_full_scan_id: str, new_full_scan: FullScan) -> Tuple[Dict[str, Package], Dict[str, Package]]:
354
374
  """
355
375
  Get packages that were added and removed between scans.
356
-
376
+
357
377
  Args:
358
378
  head_full_scan: Previous scan (may be None if first scan)
359
- new_full_scan: New scan just created
360
-
379
+ head_full_scan_id: New scan just created
380
+
361
381
  Returns:
362
382
  Tuple of (added_packages, removed_packages) dictionaries
363
383
  """
364
- if head_full_scan is None:
384
+ if head_full_scan_id is None:
365
385
  log.info(f"No head scan found. New scan ID: {new_full_scan.id}")
366
386
  return new_full_scan.packages, {}
367
-
368
- log.info(f"Comparing scans - Head scan ID: {head_full_scan.id}, New scan ID: {new_full_scan.id}")
369
- diff_report = self.sdk.fullscans.stream_diff(self.config.org_slug, head_full_scan.id, new_full_scan.id).data
370
-
371
- log.info(f"Diff report artifact counts:")
387
+
388
+ log.info(f"Comparing scans - Head scan ID: {head_full_scan_id}, New scan ID: {new_full_scan.id}")
389
+ diff_start = time.time()
390
+ try:
391
+ diff_report = self.sdk.fullscans.stream_diff(self.config.org_slug, head_full_scan_id, new_full_scan.id, use_types=True).data
392
+ except APIFailure as e:
393
+ log.error(f"API Error: {e}")
394
+ sys.exit(1)
395
+ except Exception as e:
396
+ import traceback
397
+ log.error(f"Error getting diff report: {str(e)}")
398
+ log.error(f"Stack trace:\n{traceback.format_exc()}")
399
+ raise
400
+
401
+ diff_end = time.time()
402
+ log.info(f"Diff Report Gathered in {diff_end - diff_start:.2f} seconds")
403
+ log.info("Diff report artifact counts:")
372
404
  log.info(f"Added: {len(diff_report.artifacts.added)}")
373
405
  log.info(f"Removed: {len(diff_report.artifacts.removed)}")
374
406
  log.info(f"Unchanged: {len(diff_report.artifacts.unchanged)}")
@@ -384,32 +416,24 @@ class Core:
384
416
  for artifact in added_artifacts:
385
417
  try:
386
418
  pkg = Package.from_diff_artifact(asdict(artifact))
419
+ pkg = Core.update_package_values(pkg)
387
420
  added_packages[artifact.id] = pkg
388
421
  except KeyError:
389
422
  log.error(f"KeyError: Could not create package from added artifact {artifact.id}")
390
423
  log.error(f"Artifact details - name: {artifact.name}, version: {artifact.version}")
391
- matches = [p for p in new_full_scan.packages.values() if p.name == artifact.name and p.version == artifact.version]
392
- if matches:
393
- log.error(f"Found {len(matches)} packages with matching name/version:")
394
- for m in matches:
395
- log.error(f" ID: {m.id}, name: {m.name}, version: {m.version}")
396
- else:
397
- log.error("No matching packages found in new_full_scan")
424
+ log.error("No matching packages found in new_full_scan")
398
425
 
399
426
  for artifact in removed_artifacts:
400
427
  try:
401
428
  pkg = Package.from_diff_artifact(asdict(artifact))
429
+ pkg = Core.update_package_values(pkg)
430
+ if pkg.namespace:
431
+ pkg.purl += f"{pkg.namespace}/{pkg.purl}"
402
432
  removed_packages[artifact.id] = pkg
403
433
  except KeyError:
404
434
  log.error(f"KeyError: Could not create package from removed artifact {artifact.id}")
405
435
  log.error(f"Artifact details - name: {artifact.name}, version: {artifact.version}")
406
- matches = [p for p in head_full_scan.packages.values() if p.name == artifact.name and p.version == artifact.version]
407
- if matches:
408
- log.error(f"Found {len(matches)} packages with matching name/version:")
409
- for m in matches:
410
- log.error(f" ID: {m.id}, name: {m.name}, version: {m.version}")
411
- else:
412
- log.error("No matching packages found in head_full_scan")
436
+ log.error("No matching packages found in head_full_scan")
413
437
 
414
438
  return added_packages, removed_packages
415
439
 
@@ -424,7 +448,6 @@ class Core:
424
448
  Args:
425
449
  path: Path to look for manifest files
426
450
  params: Query params for the Full Scan endpoint
427
-
428
451
  no_change: If True, return empty diff
429
452
  """
430
453
  log.debug(f"starting create_new_diff with no_change: {no_change}")
@@ -435,36 +458,44 @@ class Core:
435
458
  files = self.find_files(path)
436
459
  files_for_sending = self.load_files_for_sending(files, path)
437
460
 
438
- log.debug(f"files: {files} found at path {path}")
439
461
  if not files:
440
462
  return Diff(id="no_diff_id")
441
463
 
442
- head_full_scan_id = None
443
-
444
464
  try:
445
465
  # Get head scan ID
446
466
  head_full_scan_id = self.get_head_scan_for_repo(params.repo)
467
+ has_head_scan = True
447
468
  except APIResourceNotFound:
448
469
  head_full_scan_id = None
470
+ has_head_scan = False
449
471
 
450
472
  # Create new scan
451
- new_scan_start = time.time()
452
- new_full_scan = self.create_full_scan(files_for_sending, params)
453
- new_scan_end = time.time()
454
- log.info(f"Total time to create new full scan: {new_scan_end - new_scan_start:.2f}")
455
-
456
-
457
- head_full_scan = None
458
- if head_full_scan_id:
459
- head_full_scan = self.get_full_scan(head_full_scan_id)
473
+ try:
474
+ new_scan_start = time.time()
475
+ new_full_scan = self.create_full_scan(files_for_sending, params, has_head_scan)
476
+ new_scan_end = time.time()
477
+ log.info(f"Total time to create new full scan: {new_scan_end - new_scan_start:.2f}")
478
+ except APIFailure as e:
479
+ log.error(f"API Error: {e}")
480
+ sys.exit(1)
481
+ except Exception as e:
482
+ import traceback
483
+ log.error(f"Error creating new full scan: {str(e)}")
484
+ log.error(f"Stack trace:\n{traceback.format_exc()}")
485
+ raise
460
486
 
461
- added_packages, removed_packages = self.get_added_and_removed_packages(head_full_scan, new_full_scan)
487
+ added_packages, removed_packages = self.get_added_and_removed_packages(head_full_scan_id, new_full_scan)
462
488
 
463
489
  diff = self.create_diff_report(added_packages, removed_packages)
464
490
 
465
491
  base_socket = "https://socket.dev/dashboard/org"
466
492
  diff.id = new_full_scan.id
467
- diff.report_url = f"{base_socket}/{self.config.org_slug}/sbom/{diff.id}"
493
+
494
+ report_url = f"{base_socket}/{self.config.org_slug}/sbom/{diff.id}"
495
+ if not params.include_license_details:
496
+ report_url += "?include_license_details=false"
497
+ diff.report_url = report_url
498
+
468
499
  if head_full_scan_id is not None:
469
500
  diff.diff_url = f"{base_socket}/{self.config.org_slug}/diff/{diff.id}/{head_full_scan_id}"
470
501
  else:
@@ -473,25 +504,25 @@ class Core:
473
504
  return diff
474
505
 
475
506
  def create_diff_report(
476
- self,
477
- added_packages: Dict[str, Package],
507
+ self,
508
+ added_packages: Dict[str, Package],
478
509
  removed_packages: Dict[str, Package],
479
510
  direct_only: bool = True
480
511
  ) -> Diff:
481
512
  """
482
513
  Creates a diff report comparing two sets of packages.
483
-
514
+
484
515
  Takes packages that were added and removed between two scans and:
485
516
  1. Records new/removed packages (direct only by default)
486
517
  2. Collects alerts from both sets of packages
487
518
  3. Determines new capabilities introduced
488
-
519
+
489
520
  Args:
490
521
  added_packages: Dict of packages added in new scan
491
522
  removed_packages: Dict of packages removed in new scan
492
523
  direct_only: If True, only direct dependencies are included in new/removed lists
493
524
  (but alerts are still processed for all packages)
494
-
525
+
495
526
  Returns:
496
527
  Diff object containing the comparison results
497
528
  """
@@ -546,11 +577,11 @@ class Core:
546
577
  def create_purl(package_id: str, packages: dict[str, Package]) -> Purl:
547
578
  """
548
579
  Creates the extended PURL data for package identification and tracking.
549
-
580
+
550
581
  Args:
551
582
  package_id: Package ID to create PURL data for
552
583
  packages: Dictionary of all packages for transitive dependency lookup
553
-
584
+
554
585
  Returns:
555
586
  Purl object containing package metadata and dependency information
556
587
  """
@@ -575,14 +606,14 @@ class Core:
575
606
  def get_source_data(package: Package, packages: dict) -> list:
576
607
  """
577
608
  Determines how a package was introduced into the dependency tree.
578
-
609
+
579
610
  For direct dependencies, records the manifest file.
580
611
  For transitive dependencies, records the top-level package that introduced it.
581
-
612
+
582
613
  Args:
583
614
  package: Package to analyze
584
615
  packages: Dictionary of all packages for ancestor lookup
585
-
616
+
586
617
  Returns:
587
618
  List of tuples containing (source, manifest_file) information
588
619
  """
@@ -609,14 +640,15 @@ class Core:
609
640
  source = (top_purl, manifests)
610
641
  introduced_by.append(source)
611
642
  else:
612
- log.debug(f"Unable to get top level package info for {top_id}")
643
+ pass
644
+ # log.debug(f"Unable to get top level package info for {top_id}")
613
645
  return introduced_by
614
646
 
615
647
  @staticmethod
616
648
  def add_purl_capabilities(diff: Diff) -> None:
617
649
  """
618
650
  Adds capability information to each package in the diff's new_packages list.
619
-
651
+
620
652
  Args:
621
653
  diff: Diff object to update with capability information
622
654
  """
@@ -630,18 +662,18 @@ class Core:
630
662
  new_packages.append(new_purl)
631
663
  else:
632
664
  new_packages.append(purl)
633
-
665
+
634
666
  diff.new_packages = new_packages
635
667
 
636
668
  def add_package_alerts_to_collection(self, package: Package, alerts_collection: dict, packages: dict) -> dict:
637
669
  """
638
670
  Processes alerts from a package and adds them to a shared alerts collection.
639
-
671
+
640
672
  Args:
641
673
  package: Package to process alerts from
642
674
  alerts_collection: Dictionary to store processed alerts
643
675
  packages: Dictionary of all packages for dependency lookup
644
-
676
+
645
677
  Returns:
646
678
  Updated alerts collection dictionary
647
679
  """
@@ -691,11 +723,11 @@ class Core:
691
723
  def save_file(file_name: str, content: str) -> None:
692
724
  """
693
725
  Saves content to a file, raising an error if the save fails.
694
-
726
+
695
727
  Args:
696
728
  file_name: Path to save the file
697
729
  content: Content to write to the file
698
-
730
+
699
731
  Raises:
700
732
  IOError: If file cannot be written
701
733
  """
@@ -710,10 +742,10 @@ class Core:
710
742
  def has_manifest_files(files: list) -> bool:
711
743
  """
712
744
  Checks if any files in the list are supported manifest files.
713
-
745
+
714
746
  Args:
715
747
  files: List of file paths to check
716
-
748
+
717
749
  Returns:
718
750
  True if any files match manifest patterns, False otherwise
719
751
  """
@@ -732,41 +764,41 @@ class Core:
732
764
  def get_capabilities_for_added_packages(added_packages: Dict[str, Package]) -> Dict[str, List[str]]:
733
765
  """
734
766
  Maps added packages to their capabilities based on their alerts.
735
-
767
+
736
768
  Args:
737
769
  added_packages: Dictionary of packages added in new scan
738
-
770
+
739
771
  Returns:
740
772
  Dictionary mapping package IDs to their capability lists
741
773
  """
742
774
  capabilities: Dict[str, List[str]] = {}
743
-
775
+
744
776
  for package_id, package in added_packages.items():
745
777
  for alert in package.alerts:
746
778
  if alert["type"] in Core.ALERT_TYPE_TO_CAPABILITY:
747
779
  value = Core.ALERT_TYPE_TO_CAPABILITY[alert["type"]]
748
-
780
+
749
781
  if package_id not in capabilities:
750
782
  capabilities[package_id] = [value]
751
783
  elif value not in capabilities[package_id]:
752
784
  capabilities[package_id].append(value)
753
-
785
+
754
786
  return capabilities
755
787
 
756
788
  @staticmethod
757
789
  def get_new_alerts(
758
- added_package_alerts: Dict[str, List[Issue]],
790
+ added_package_alerts: Dict[str, List[Issue]],
759
791
  removed_package_alerts: Dict[str, List[Issue]],
760
792
  ignore_readded: bool = True
761
793
  ) -> List[Issue]:
762
794
  """
763
795
  Find alerts that are new or changed between added and removed packages.
764
-
796
+
765
797
  Args:
766
798
  added_package_alerts: Dictionary of alerts from packages that were added
767
799
  removed_package_alerts: Dictionary of alerts from packages that were removed
768
800
  ignore_readded: If True, don't report alerts that were both removed and added
769
-
801
+
770
802
  Returns:
771
803
  List of newly found alerts
772
804
  """
@@ -778,7 +810,7 @@ class Core:
778
810
  new_alerts = added_package_alerts[alert_key]
779
811
  for alert in new_alerts:
780
812
  alert_str = f"{alert.purl},{alert.manifests},{alert.type}"
781
-
813
+
782
814
  if alert.error or alert.warn:
783
815
  if alert_str not in consolidated_alerts:
784
816
  alerts.append(alert)
@@ -786,10 +818,10 @@ class Core:
786
818
  else:
787
819
  new_alerts = added_package_alerts[alert_key]
788
820
  removed_alerts = removed_package_alerts[alert_key]
789
-
821
+
790
822
  for alert in new_alerts:
791
823
  alert_str = f"{alert.purl},{alert.manifests},{alert.type}"
792
-
824
+
793
825
  # Only add if:
794
826
  # 1. Alert isn't in removed packages (or we're not ignoring readded alerts)
795
827
  # 2. We haven't already recorded this alert
@@ -800,5 +832,3 @@ class Core:
800
832
  consolidated_alerts.add(alert_str)
801
833
 
802
834
  return alerts
803
-
804
-
@@ -115,6 +115,7 @@ class Package(SocketArtifactLink):
115
115
  author: List[str] = field(default_factory=list)
116
116
  size: Optional[int] = None
117
117
  license: Optional[str] = None
118
+ namespace: Optional[str] = None
118
119
 
119
120
  # Package-specific fields
120
121
  license_text: str = ""
@@ -122,6 +123,10 @@ class Package(SocketArtifactLink):
122
123
  transitives: int = 0
123
124
  url: str = ""
124
125
 
126
+ # Artifact-specific fields
127
+ licenseDetails: Optional[list] = None
128
+
129
+
125
130
  @classmethod
126
131
  def from_socket_artifact(cls, data: dict) -> "Package":
127
132
  """
@@ -187,7 +192,8 @@ class Package(SocketArtifactLink):
187
192
  direct=ref.get("direct", False),
188
193
  manifestFiles=ref.get("manifestFiles", []),
189
194
  dependencies=ref.get("dependencies"),
190
- artifact=ref.get("artifact")
195
+ artifact=ref.get("artifact"),
196
+ namespace=data.get('namespace', None)
191
197
  )
192
198
 
193
199
  class Issue:
@@ -54,7 +54,9 @@ class GithubConfig:
54
54
  owner = repository.split('/')[0]
55
55
  repository = repository.split('/')[1]
56
56
 
57
- is_default = os.getenv('DEFAULT_BRANCH', '').lower() == 'true'
57
+ default_branch_env = os.getenv('DEFAULT_BRANCH')
58
+ # Consider the variable truthy if it exists and isn't explicitly 'false'
59
+ is_default = default_branch_env is not None and default_branch_env.lower() != 'false'
58
60
  return cls(
59
61
  sha=os.getenv('GITHUB_SHA', ''),
60
62
  api_url=os.getenv('GITHUB_API_URL', ''),
@@ -81,5 +81,28 @@ socket_globs = {
81
81
  "pom.xml": {
82
82
  "pattern": "pom.xml"
83
83
  }
84
+ },
85
+ ".net": {
86
+ "proj": {
87
+ "pattern": "*.*proj"
88
+ },
89
+ "props": {
90
+ "pattern": "*.props"
91
+ },
92
+ "targets": {
93
+ "pattern": "*.targets"
94
+ },
95
+ "nuspec": {
96
+ "pattern": "*.nuspec"
97
+ },
98
+ "nugetConfig": {
99
+ "pattern": "nuget.config"
100
+ },
101
+ "packagesConfig": {
102
+ "pattern": "packages.config"
103
+ },
104
+ "packagesLock": {
105
+ "pattern": "packages.lock.json"
106
+ }
84
107
  }
85
108
  }
@@ -48,6 +48,13 @@ def main_code():
48
48
  log.debug(f"config: {config.to_dict()}")
49
49
  output_handler = OutputHandler(config)
50
50
 
51
+ # Validate API token
52
+ if not config.api_token:
53
+ log.info("Socket API Token not found. Please set it using either:\n"
54
+ "1. Command line: --api-token YOUR_TOKEN\n"
55
+ "2. Environment variable: SOCKET_SECURITY_API_KEY")
56
+ sys.exit(3)
57
+
51
58
  sdk = socketdev(token=config.api_token)
52
59
  log.debug("sdk loaded")
53
60
 
@@ -55,10 +62,6 @@ def main_code():
55
62
  set_debug_mode(True)
56
63
  log.debug("Debug logging enabled")
57
64
 
58
- # Validate API token
59
- if not config.api_token:
60
- log.info("Unable to find Socket API Token")
61
- sys.exit(3)
62
65
 
63
66
  # Initialize Socket core components
64
67
  socket_config = SocketConfig(
@@ -160,6 +163,8 @@ def main_code():
160
163
  set_as_pending_head=True
161
164
  )
162
165
 
166
+ params.include_license_details = not config.exclude_license_details
167
+
163
168
  # Initialize diff
164
169
  diff = Diff()
165
170
  diff.id = "NO_DIFF_RAN"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: socketsecurity
3
- Version: 2.0.6
3
+ Version: 2.0.8
4
4
  Summary: Socket Security CLI for CI/CD
5
5
  Author-email: Douglas Coburn <douglas@socket.dev>
6
6
  Maintainer-email: Douglas Coburn <douglas@socket.dev>
@@ -19,7 +19,7 @@ Requires-Dist: prettytable
19
19
  Requires-Dist: GitPython
20
20
  Requires-Dist: packaging
21
21
  Requires-Dist: python-dotenv
22
- Requires-Dist: socket-sdk-python>=2.0.5
22
+ Requires-Dist: socket-sdk-python>=2.0.8
23
23
  Provides-Extra: test
24
24
  Requires-Dist: pytest>=7.4.0; extra == "test"
25
25
  Requires-Dist: pytest-cov>=4.1.0; extra == "test"
@@ -28,6 +28,7 @@ Requires-Dist: pytest-asyncio>=0.23.0; extra == "test"
28
28
  Requires-Dist: pytest-watch>=4.2.0; extra == "test"
29
29
  Provides-Extra: dev
30
30
  Requires-Dist: ruff>=0.3.0; extra == "dev"
31
+ Requires-Dist: twine; extra == "dev"
31
32
  Requires-Dist: pip-tools>=7.4.0; extra == "dev"
32
33
 
33
34
  # Socket Security CLI
@@ -42,6 +43,7 @@ socketcli [-h] [--api-token API_TOKEN] [--repo REPO] [--integration {api,github,
42
43
  [--target-path TARGET_PATH] [--sbom-file SBOM_FILE] [--files FILES] [--default-branch] [--pending-head]
43
44
  [--generate-license] [--enable-debug] [--enable-json] [--enable-sarif] [--disable-overview] [--disable-security-issue]
44
45
  [--allow-unverified] [--ignore-commit-files] [--disable-blocking] [--scm SCM] [--timeout TIMEOUT]
46
+ [--exclude-license-details]
45
47
  ````
46
48
 
47
49
  If you don't want to provide the Socket API Token every time then you can use the environment variable `SOCKET_SECURITY_API_KEY`
@@ -90,6 +92,7 @@ If you don't want to provide the Socket API Token every time then you can use th
90
92
  | --enable-json | False | False | Output in JSON format |
91
93
  | --enable-sarif | False | False | Enable SARIF output of results instead of table or JSON format|
92
94
  | --disable-overview | False | False | Disable overview output |
95
+ | --exclude-license-details | False | False | Exclude license details from the diff report (boosts performance for large repos) |
93
96
 
94
97
  #### Security Configuration
95
98
  | Parameter | Required | Default | Description |
@@ -4,10 +4,11 @@ prettytable
4
4
  GitPython
5
5
  packaging
6
6
  python-dotenv
7
- socket-sdk-python>=2.0.5
7
+ socket-sdk-python>=2.0.8
8
8
 
9
9
  [dev]
10
10
  ruff>=0.3.0
11
+ twine
11
12
  pip-tools>=7.4.0
12
13
 
13
14
  [test]
File without changes
File without changes