socketsecurity 1.0.41__tar.gz → 1.0.43__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 (23) hide show
  1. {socketsecurity-1.0.41/socketsecurity.egg-info → socketsecurity-1.0.43}/PKG-INFO +4 -2
  2. {socketsecurity-1.0.41 → socketsecurity-1.0.43}/README.md +2 -1
  3. {socketsecurity-1.0.41 → socketsecurity-1.0.43}/pyproject.toml +2 -1
  4. {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity/__init__.py +1 -1
  5. {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity/core/__init__.py +147 -149
  6. {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity/core/messages.py +123 -0
  7. {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity/socketcli.py +38 -3
  8. {socketsecurity-1.0.41 → socketsecurity-1.0.43/socketsecurity.egg-info}/PKG-INFO +4 -2
  9. {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity.egg-info/requires.txt +1 -0
  10. {socketsecurity-1.0.41 → socketsecurity-1.0.43}/LICENSE +0 -0
  11. {socketsecurity-1.0.41 → socketsecurity-1.0.43}/setup.cfg +0 -0
  12. {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity/core/classes.py +0 -0
  13. {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity/core/exceptions.py +0 -0
  14. {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity/core/git_interface.py +0 -0
  15. {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity/core/github.py +0 -0
  16. {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity/core/gitlab.py +0 -0
  17. {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity/core/issues.py +0 -0
  18. {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity/core/licenses.py +0 -0
  19. {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity/core/scm_comments.py +0 -0
  20. {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity.egg-info/SOURCES.txt +0 -0
  21. {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity.egg-info/dependency_links.txt +0 -0
  22. {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity.egg-info/entry_points.txt +0 -0
  23. {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: socketsecurity
3
- Version: 1.0.41
3
+ Version: 1.0.43
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>
@@ -20,6 +20,7 @@ Requires-Dist: mdutils
20
20
  Requires-Dist: prettytable
21
21
  Requires-Dist: GitPython
22
22
  Requires-Dist: packaging
23
+ Requires-Dist: socket-sdk-python<2.0.0,>=1.0.15
23
24
 
24
25
  # Socket Security CLI
25
26
 
@@ -30,7 +31,7 @@ The Socket Security CLI was created to enable integrations with other tools like
30
31
  ```` shell
31
32
  socketcli [-h] [--api_token API_TOKEN] [--repo REPO] [--branch BRANCH] [--committer COMMITTER] [--pr_number PR_NUMBER]
32
33
  [--commit_message COMMIT_MESSAGE] [--default_branch] [--target_path TARGET_PATH] [--scm {api,github,gitlab}] [--sbom-file SBOM_FILE]
33
- [--commit-sha COMMIT_SHA] [--generate-license GENERATE_LICENSE] [-v] [--enable-debug] [--enable-json] [--disable-overview]
34
+ [--commit-sha COMMIT_SHA] [--generate-license GENERATE_LICENSE] [-v] [--enable-debug] [--enable-json] [--enable-sarif] [--disable-overview]
34
35
  [--disable-security-issue] [--files FILES] [--ignore-commit-files] [--timeout]
35
36
  ````
36
37
 
@@ -56,6 +57,7 @@ If you don't want to provide the Socket API Token every time then you can use th
56
57
  | --commit-sha | | False | | The commit hash for the commit |
57
58
  | --generate-license | | False | False | If enabled with `--sbom-file` will include license details |
58
59
  | --enable-json | | False | False | If enabled will change the console output format to JSON |
60
+ | --enable-sarif | | False | False | If enabled will change the console output format to SARIF |
59
61
  | --disable-overview | | False | False | If enabled will disable Dependency Overview comments |
60
62
  | --disable-security-issue | | False | False | If enabled will disable Security Issue Comments |
61
63
  | --files | | False | | If provided in the format of `["file1", "file2"]` will be used to determine if there have been supported file changes. This is used if it isn't a git repo and you would like to only run if it supported files have changed. |
@@ -7,7 +7,7 @@ The Socket Security CLI was created to enable integrations with other tools like
7
7
  ```` shell
8
8
  socketcli [-h] [--api_token API_TOKEN] [--repo REPO] [--branch BRANCH] [--committer COMMITTER] [--pr_number PR_NUMBER]
9
9
  [--commit_message COMMIT_MESSAGE] [--default_branch] [--target_path TARGET_PATH] [--scm {api,github,gitlab}] [--sbom-file SBOM_FILE]
10
- [--commit-sha COMMIT_SHA] [--generate-license GENERATE_LICENSE] [-v] [--enable-debug] [--enable-json] [--disable-overview]
10
+ [--commit-sha COMMIT_SHA] [--generate-license GENERATE_LICENSE] [-v] [--enable-debug] [--enable-json] [--enable-sarif] [--disable-overview]
11
11
  [--disable-security-issue] [--files FILES] [--ignore-commit-files] [--timeout]
12
12
  ````
13
13
 
@@ -33,6 +33,7 @@ If you don't want to provide the Socket API Token every time then you can use th
33
33
  | --commit-sha | | False | | The commit hash for the commit |
34
34
  | --generate-license | | False | False | If enabled with `--sbom-file` will include license details |
35
35
  | --enable-json | | False | False | If enabled will change the console output format to JSON |
36
+ | --enable-sarif | | False | False | If enabled will change the console output format to SARIF |
36
37
  | --disable-overview | | False | False | If enabled will disable Dependency Overview comments |
37
38
  | --disable-security-issue | | False | False | If enabled will disable Security Issue Comments |
38
39
  | --files | | False | | If provided in the format of `["file1", "file2"]` will be used to determine if there have been supported file changes. This is used if it isn't a git repo and you would like to only run if it supported files have changed. |
@@ -12,6 +12,7 @@ dependencies = [
12
12
  'prettytable',
13
13
  'GitPython',
14
14
  'packaging',
15
+ 'socket-sdk-python>=1.0.15,<2.0.0'
15
16
  ]
16
17
  readme = "README.md"
17
18
  description = "Socket Security CLI for CI/CD"
@@ -45,4 +46,4 @@ include = [
45
46
  ]
46
47
 
47
48
  [tool.setuptools.dynamic]
48
- version = {attr = "socketsecurity.__version__"}
49
+ version = {attr = "socketsecurity.__version__"}
@@ -1,2 +1,2 @@
1
1
  __author__ = 'socket.dev'
2
- __version__ = '1.0.41'
2
+ __version__ = '1.0.43'
@@ -1,36 +1,39 @@
1
+ import base64
2
+ import json
1
3
  import logging
4
+ import platform
5
+ import time
6
+ from glob import glob
2
7
  from pathlib import PurePath
3
- from requests.exceptions import ReadTimeout
4
- import requests
5
8
  from urllib.parse import urlencode
6
- import base64
7
- import json
8
- from socketsecurity.core.exceptions import (
9
- APIFailure,
10
- APIKeyMissing,
11
- APIAccessDenied,
12
- APIInsufficientQuota,
13
- APIResourceNotFound,
14
- APICloudflareError,
15
- RequestTimeoutExceeded
16
- )
9
+
10
+ import requests
11
+ from requests.exceptions import ReadTimeout
12
+ from socketdev import socketdev
13
+
17
14
  from socketsecurity import __version__
18
- from socketsecurity.core.licenses import Licenses
19
- from socketsecurity.core.issues import AllIssues
20
15
  from socketsecurity.core.classes import (
21
- Report,
22
- Issue,
23
- Package,
24
16
  Alert,
17
+ Diff,
25
18
  FullScan,
26
19
  FullScanParams,
20
+ Issue,
21
+ Package,
22
+ Purl,
23
+ Report,
27
24
  Repository,
28
- Diff,
29
- Purl
30
25
  )
31
- import platform
32
- from glob import glob
33
- import time
26
+ from socketsecurity.core.exceptions import (
27
+ APIAccessDenied,
28
+ APICloudflareError,
29
+ APIFailure,
30
+ APIInsufficientQuota,
31
+ APIKeyMissing,
32
+ APIResourceNotFound,
33
+ RequestTimeoutExceeded,
34
+ )
35
+ from socketsecurity.core.issues import AllIssues
36
+ from socketsecurity.core.licenses import Licenses
34
37
 
35
38
  __all__ = [
36
39
  "Core",
@@ -55,6 +58,8 @@ allow_unverified_ssl = False
55
58
  log = logging.getLogger("socketdev")
56
59
  log.addHandler(logging.NullHandler())
57
60
 
61
+ socket_sdk = None
62
+
58
63
  socket_globs = {
59
64
  "spdx": {
60
65
  "spdx.json": {
@@ -153,6 +158,15 @@ def encode_key(token: str) -> None:
153
158
  encoded_key = base64.b64encode(token.encode()).decode('ascii')
154
159
 
155
160
 
161
+ class SCMRequestError(Exception):
162
+ """Generic exception for SCM API request failures"""
163
+ def __init__(self, status_code: int, message: str, url: str):
164
+ self.status_code = status_code
165
+ self.message = message
166
+ self.url = url
167
+ super().__init__(f"SCM API request failed: {status_code} - {message} (URL: {url})")
168
+
169
+
156
170
  def do_request(
157
171
  path: str,
158
172
  headers: dict = None,
@@ -160,34 +174,24 @@ def do_request(
160
174
  files: list = None,
161
175
  method: str = "GET",
162
176
  base_url: str = None,
163
- ) -> requests.request:
177
+ ) -> requests.Response:
164
178
  """
165
- do_requests is the shared function for making HTTP calls
166
- :param base_url:
179
+ Shared function for making HTTP calls to SCM providers (GitHub/GitLab)
180
+ :param base_url: Base URL for the SCM provider API
167
181
  :param path: Required path for the request
168
- :param headers: Optional dictionary of headers. If not set will use a default set
182
+ :param headers: Optional dictionary of headers
169
183
  :param payload: Optional dictionary or string of the payload to pass
170
184
  :param files: Optional list of files to upload
171
185
  :param method: Optional method to use, defaults to GET
172
- :return:
186
+ :return: Response object
187
+ :raises: SCMRequestError if the request fails
173
188
  """
189
+ if base_url is None:
190
+ raise ValueError("base_url is required for SCM API calls")
191
+
192
+ url = f"{base_url}/{path}"
193
+ verify = not allow_unverified_ssl
174
194
 
175
- if base_url is not None:
176
- url = f"{base_url}/{path}"
177
- else:
178
- if encoded_key is None or encoded_key == "":
179
- raise APIKeyMissing
180
- url = f"{api_url}/{path}"
181
-
182
- if headers is None:
183
- headers = {
184
- 'Authorization': f"Basic {encoded_key}",
185
- 'User-Agent': f'SocketPythonCLI/{__version__}',
186
- "accept": "application/json"
187
- }
188
- verify = True
189
- if allow_unverified_ssl:
190
- verify = False
191
195
  try:
192
196
  response = requests.request(
193
197
  method.upper(),
@@ -198,43 +202,49 @@ def do_request(
198
202
  timeout=timeout,
199
203
  verify=verify
200
204
  )
201
- except ReadTimeout:
202
- raise RequestTimeoutExceeded(f"Configured timeout {timeout} reached for request for path {url}")
203
- output_headers = headers.copy()
204
- output_headers['Authorization'] = "API_KEY_REDACTED"
205
- output = {
206
- "url": url,
207
- "headers": output_headers,
208
- "status_code": response.status_code,
209
- "body": response.text,
210
- "payload": payload,
211
- "files": files,
212
- "timeout": timeout
213
- }
214
- log.debug(output)
215
- if response.status_code <= 399:
216
- return response
217
- elif response.status_code == 400:
218
- raise APIFailure(output)
219
- elif response.status_code == 401:
220
- raise APIAccessDenied("Unauthorized")
221
- elif response.status_code == 403:
222
- raise APIInsufficientQuota("Insufficient max_quota for API method")
223
- elif response.status_code == 404:
224
- raise APIResourceNotFound(f"Path not found {path}")
225
- elif response.status_code == 429:
226
- raise APIInsufficientQuota("Insufficient quota for API route")
227
- elif response.status_code == 524:
228
- raise APICloudflareError(response.text)
229
- else:
230
- msg = {
205
+
206
+ # Log request details (with redacted auth)
207
+ output_headers = headers.copy() if headers else {}
208
+ if 'Authorization' in output_headers:
209
+ output_headers['Authorization'] = "TOKEN_REDACTED"
210
+
211
+ log.debug({
212
+ "url": url,
213
+ "headers": output_headers,
231
214
  "status_code": response.status_code,
232
- "UnexpectedError": "There was an unexpected error using the API",
233
- "error": response.text,
215
+ "body": response.text,
234
216
  "payload": payload,
235
- "url": url
236
- }
237
- raise APIFailure(msg)
217
+ "files": files,
218
+ "timeout": timeout
219
+ })
220
+
221
+ if response.status_code < 400:
222
+ return response
223
+
224
+ # Try to get error message from response
225
+ try:
226
+ error_msg = response.json().get('message', response.text)
227
+ except (json.JSONDecodeError, AttributeError):
228
+ error_msg = response.text
229
+
230
+ raise SCMRequestError(
231
+ status_code=response.status_code,
232
+ message=error_msg,
233
+ url=url
234
+ )
235
+
236
+ except ReadTimeout:
237
+ raise SCMRequestError(
238
+ status_code=408,
239
+ message=f"Request timed out after {timeout} seconds",
240
+ url=url
241
+ )
242
+ except requests.RequestException as e:
243
+ raise SCMRequestError(
244
+ status_code=500,
245
+ message=str(e),
246
+ url=url
247
+ )
238
248
 
239
249
 
240
250
  class Core:
@@ -251,9 +261,10 @@ class Core:
251
261
  enable_all_alerts: bool = False,
252
262
  allow_unverified: bool = False
253
263
  ):
254
- global allow_unverified_ssl
264
+ global allow_unverified_ssl, socket_sdk
255
265
  allow_unverified_ssl = allow_unverified
256
266
  self.token = token + ":"
267
+ socket_sdk = socketdev(self.token, timeout=request_timeout)
257
268
  encode_key(self.token)
258
269
  self.socket_date_format = "%Y-%m-%dT%H:%M:%S.%fZ"
259
270
  self.base_api_url = base_api_url
@@ -311,9 +322,7 @@ class Core:
311
322
  Gets the Org ID and Org Slug for the API Token
312
323
  :return:
313
324
  """
314
- path = "organizations"
315
- response = do_request(path)
316
- data = response.json()
325
+ data = socket_sdk.org.get()
317
326
  organizations = data.get("organizations")
318
327
  new_org_id = None
319
328
  new_org_slug = None
@@ -325,23 +334,30 @@ class Core:
325
334
 
326
335
  @staticmethod
327
336
  def get_sbom_data(full_scan_id: str) -> list:
328
- path = f"orgs/{org_slug}/full-scans/{full_scan_id}"
329
- response = do_request(path)
330
- results = []
337
+ """
338
+ Gets SBOM data for a full scan using the Socket SDK
339
+ :param full_scan_id: str - ID of the full scan to get SBOM data for
340
+ :return: list of SBOM artifacts
341
+ """
331
342
  try:
332
- data = response.json()
333
- results = data.get("sbom_artifacts") or []
343
+ result = socket_sdk.fullscans.stream(org_slug, full_scan_id)
344
+ if result.get("success", False):
345
+ # Remove metadata properties before returning artifacts
346
+ result.pop("success", None)
347
+ result.pop("status", None)
348
+ # The SDK returns a dict with the SBOM artifacts as values, so we need to convert it to a list
349
+ return list(result.values())
350
+ else:
351
+ # TODO: In future ticket, throw appropriate error here instead of returning empty list
352
+ log.error(f"Failed to get SBOM data for scan {full_scan_id}")
353
+ log.error(f"Status: {result.get('status')}")
354
+ log.error(f"Message: {result.get('message')}")
355
+ return []
334
356
  except Exception as error:
335
- log.debug("Failed with old style full-scan API using new format")
336
- log.debug(error)
337
- data = response.text
338
- data.strip('"')
339
- data.strip()
340
- for line in data.split("\n"):
341
- if line != '"' and line != "" and line is not None:
342
- item = json.loads(line)
343
- results.append(item)
344
- return results
357
+ # TODO: In future ticket, throw appropriate error here instead of returning empty list
358
+ log.error(f"Unexpected error getting SBOM data for scan {full_scan_id}")
359
+ log.error(error)
360
+ return []
345
361
 
346
362
  @staticmethod
347
363
  def get_security_policy() -> dict:
@@ -349,14 +365,7 @@ class Core:
349
365
  Get the Security policy and determine the effective Org security policy
350
366
  :return:
351
367
  """
352
- path = "settings"
353
- payload = [
354
- {
355
- "organization": org_id
356
- }
357
- ]
358
- response = do_request(path, payload=json.dumps(payload), method="POST")
359
- data = response.json()
368
+ data = socket_sdk.settings.get(org_id)
360
369
  defaults = data.get("defaults")
361
370
  default_rules = defaults.get("issueRules")
362
371
  entries = data.get("entries")
@@ -400,16 +409,15 @@ class Core:
400
409
 
401
410
  @staticmethod
402
411
  def create_sbom_output(diff: Diff) -> dict:
403
- base_path = f"orgs/{org_slug}/export/cdx"
404
- path = f"{base_path}/{diff.id}"
405
- result = do_request(path=path)
406
- try:
407
- sbom = result.json()
408
- except Exception as error:
412
+ result = socket_sdk.export.cdx_bom(org_slug, diff.id)
413
+
414
+ if not result.get("success", False):
409
415
  log.error(f"Unable to get CycloneDX Output for {diff.id}")
410
- log.error(error)
411
- sbom = {}
412
- return sbom
416
+ log.error(result.get("message", "No error message provided"))
417
+ return {}
418
+
419
+ result.pop("success", None)
420
+ return result
413
421
 
414
422
  @staticmethod
415
423
  def match_supported_files(files: list) -> bool:
@@ -462,6 +470,7 @@ class Core:
462
470
  end_time = time.time()
463
471
  total_time = end_time - start_time
464
472
  log.info(f"Found {len(files)} in {total_time:.2f} seconds")
473
+ log.debug(f"Files found: {list(files)}")
465
474
  return list(files)
466
475
 
467
476
  @staticmethod
@@ -473,38 +482,18 @@ class Core:
473
482
  :param workspace: str - Path of workspace
474
483
  :return:
475
484
  """
476
- send_files = []
477
485
  create_full_start = time.time()
478
486
  log.debug("Creating new full scan")
479
- for file in files:
480
- if platform.system() == "Windows":
481
- file = file.replace("\\", "/")
482
- if "/" in file:
483
- path, name = file.rsplit("/", 1)
484
- else:
485
- path = "."
486
- name = file
487
- full_path = f"{path}/{name}"
488
- if full_path.startswith(workspace):
489
- key = full_path[len(workspace):]
490
- else:
491
- key = full_path
492
- key = key.lstrip("/")
493
- key = key.lstrip("./")
494
- payload = (
495
- key,
496
- (
497
- name,
498
- open(full_path, 'rb')
499
- )
500
- )
501
- send_files.append(payload)
502
- query_params = urlencode(params.__dict__)
503
- full_uri = f"{full_scan_path}?{query_params}"
504
- response = do_request(full_uri, method="POST", files=send_files)
505
- results = response.json()
487
+
488
+ # Convert params to dict and add org_slug
489
+ params_dict = params.__dict__.copy()
490
+ params_dict['org_slug'] = org_slug
491
+
492
+ results = socket_sdk.fullscans.post(files=files, params=params_dict, workspace=workspace)
493
+
506
494
  full_scan = FullScan(**results)
507
495
  full_scan.sbom_artifacts = Core.get_sbom_data(full_scan.id)
496
+
508
497
  create_full_end = time.time()
509
498
  total_time = create_full_end - create_full_start
510
499
  log.debug(f"New Full Scan created in {total_time:.2f} seconds")
@@ -527,9 +516,8 @@ class Core:
527
516
  :param repo_slug: Str - Repo slug for the repository that is being diffed
528
517
  :return:
529
518
  """
530
- repo_path = f"{repository_path}/{repo_slug}"
531
- response = do_request(repo_path)
532
- results = response.json()
519
+ results = socket_sdk.repos.repo(org_slug, repo_name=repo_slug)
520
+
533
521
  repository = Repository(**results)
534
522
  return repository.head_full_scan_id
535
523
 
@@ -540,9 +528,7 @@ class Core:
540
528
  :param full_scan_id: str - ID of the full scan to pull
541
529
  :return:
542
530
  """
543
- full_scan_url = f"{full_scan_path}/{full_scan_id}"
544
- response = do_request(full_scan_url)
545
- results = response.json()
531
+ results = socket_sdk.fullscans.metadata(org_slug, full_scan_id)
546
532
  full_scan = FullScan(**results)
547
533
  full_scan.sbom_artifacts = Core.get_sbom_data(full_scan.id)
548
534
  return full_scan
@@ -876,6 +862,18 @@ class Core:
876
862
  log.debug(f"Orphaned top level package id {package_id} for packages {details}")
877
863
  else:
878
864
  packages[package_id].transitives = top_level_count[package_id]
865
+
866
+ # Check for potential API truncation
867
+ top_levels_len = len(top_levels)
868
+ packages_len = len(packages)
869
+ difference = top_levels_len - packages_len
870
+
871
+ if difference > 10 and difference > (packages_len * 0.5):
872
+ raise APIFailure(
873
+ f"Potential API truncation detected: Found {top_levels_len} top-level ancestors but only {packages_len} packages. "
874
+ f"This suggests the SBOM data may be incomplete."
875
+ )
876
+
879
877
  return packages
880
878
 
881
879
  @staticmethod
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import os
2
3
 
3
4
  from mdutils import MdUtils
4
5
  from socketsecurity.core.classes import Diff, Purl, Issue
@@ -7,6 +8,128 @@ from prettytable import PrettyTable
7
8
 
8
9
  class Messages:
9
10
 
11
+ @staticmethod
12
+ def map_severity_to_sarif(severity: str) -> str:
13
+ """
14
+ Map Socket severity levels to SARIF levels (GitHub code scanning).
15
+ """
16
+ severity_mapping = {
17
+ "low": "note",
18
+ "medium": "warning",
19
+ "middle": "warning", # older data might say "middle"
20
+ "high": "error",
21
+ "critical": "error",
22
+ }
23
+ return severity_mapping.get(severity.lower(), "note")
24
+
25
+
26
+ @staticmethod
27
+ def find_line_in_file(pkg_name: str, manifest_file: str) -> tuple[int, str]:
28
+ """
29
+ Search 'manifest_file' for 'pkg_name'.
30
+ Return (line_number, line_content) if found, else (1, fallback).
31
+ """
32
+ if not manifest_file or not os.path.isfile(manifest_file):
33
+ return 1, f"[No {manifest_file or 'manifest'} found in repo]"
34
+ try:
35
+ with open(manifest_file, "r", encoding="utf-8") as f:
36
+ lines = f.readlines()
37
+ for i, line in enumerate(lines, start=1):
38
+ if pkg_name.lower() in line.lower():
39
+ return i, line.rstrip("\n")
40
+ except Exception as e:
41
+ return 1, f"[Error reading {manifest_file}: {e}]"
42
+ return 1, f"[Package '{pkg_name}' not found in {manifest_file}]"
43
+
44
+ @staticmethod
45
+ def create_security_comment_sarif(diff: Diff) -> dict:
46
+ """
47
+ Create SARIF-compliant output from the diff report.
48
+ """
49
+ scan_failed = False
50
+ if len(diff.new_alerts) == 0:
51
+ for alert in diff.new_alerts:
52
+ alert: Issue
53
+ if alert.error:
54
+ scan_failed = True
55
+ break
56
+
57
+ # Basic SARIF structure
58
+ sarif_data = {
59
+ "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
60
+ "version": "2.1.0",
61
+ "runs": [
62
+ {
63
+ "tool": {
64
+ "driver": {
65
+ "name": "Socket Security",
66
+ "informationUri": "https://socket.dev",
67
+ "rules": []
68
+ }
69
+ },
70
+ "results": []
71
+ }
72
+ ]
73
+ }
74
+
75
+ rules_map = {}
76
+ results_list = []
77
+
78
+ for alert in diff.new_alerts:
79
+ alert: Issue
80
+ pkg_name = alert.pkg_name
81
+ pkg_version = alert.pkg_version
82
+ rule_id = f"{pkg_name}=={pkg_version}"
83
+ severity = alert.severity
84
+
85
+ # Title and descriptions
86
+ title = f"Alert generated for {pkg_name}=={pkg_version} by Socket Security"
87
+ full_desc = f"{alert.title} - {alert.description}"
88
+ short_desc = f"{alert.props.get('note', '')}\r\n\r\nSuggested Action:\r\n{alert.suggestion}"
89
+
90
+ # Find the manifest file and line details
91
+ introduced_list = alert.introduced_by
92
+ if introduced_list and isinstance(introduced_list[0], list) and len(introduced_list[0]) > 1:
93
+ manifest_file = introduced_list[0][1]
94
+ else:
95
+ manifest_file = alert.manifests or "requirements.txt"
96
+
97
+ line_number, line_content = Messages.find_line_in_file(pkg_name, manifest_file)
98
+
99
+ # Define the rule if not already defined
100
+ if rule_id not in rules_map:
101
+ rules_map[rule_id] = {
102
+ "id": rule_id,
103
+ "name": f"{pkg_name}=={pkg_version}",
104
+ "shortDescription": {"text": title},
105
+ "fullDescription": {"text": full_desc},
106
+ "helpUri": alert.url,
107
+ "defaultConfiguration": {"level": Messages.map_severity_to_sarif(severity)},
108
+ }
109
+
110
+ # Add the result
111
+ result_obj = {
112
+ "ruleId": rule_id,
113
+ "message": {"text": short_desc},
114
+ "locations": [
115
+ {
116
+ "physicalLocation": {
117
+ "artifactLocation": {"uri": manifest_file},
118
+ "region": {
119
+ "startLine": line_number,
120
+ "snippet": {"text": line_content},
121
+ },
122
+ }
123
+ }
124
+ ],
125
+ }
126
+ results_list.append(result_obj)
127
+
128
+ sarif_data["runs"][0]["tool"]["driver"]["rules"] = list(rules_map.values())
129
+ sarif_data["runs"][0]["results"] = results_list
130
+
131
+ return sarif_data
132
+
10
133
  @staticmethod
11
134
  def create_security_comment_json(diff: Diff) -> dict:
12
135
  scan_failed = False
@@ -1,5 +1,6 @@
1
1
  import argparse
2
2
  import json
3
+ import traceback
3
4
 
4
5
  import socketsecurity.core
5
6
  from socketsecurity.core import Core, __version__
@@ -169,7 +170,12 @@ parser.add_argument(
169
170
  type=float
170
171
  )
171
172
 
172
-
173
+ parser.add_argument(
174
+ '--enable-sarif',
175
+ help='Enable SARIF output of results instead of table or JSON format',
176
+ action='store_true',
177
+ default=False
178
+ )
173
179
 
174
180
 
175
181
  def output_console_comments(diff_report: Diff, sbom_file_name: str = None) -> None:
@@ -190,6 +196,25 @@ def output_console_comments(diff_report: Diff, sbom_file_name: str = None) -> No
190
196
  else:
191
197
  log.info("No New Security issues detected by Socket Security")
192
198
 
199
+ def output_console_sarif(diff_report: Diff, sbom_file_name: str = None) -> None:
200
+ """
201
+ Generate SARIF output from the diff report and save it to a file.
202
+ """
203
+ if diff_report.id != "NO_DIFF_RAN":
204
+ # Generate the SARIF structure using Messages
205
+ console_security_comment = Messages.create_security_comment_sarif(diff_report)
206
+
207
+ # Save the SARIF output to the specified SBOM file name or fallback to a default
208
+ save_sbom_file(diff_report, sbom_file_name)
209
+ # Print the SARIF output to the console in JSON format
210
+ print(json.dumps(console_security_comment, indent=2))
211
+
212
+ # Handle exit codes based on alert severity
213
+ if not report_pass(diff_report) and not blocking_disabled:
214
+ sys.exit(1)
215
+ elif len(diff_report.new_alerts) > 0 and not blocking_disabled:
216
+ # Warning alerts without blocking
217
+ sys.exit(5)
193
218
 
194
219
  def output_console_json(diff_report: Diff, sbom_file_name: str = None) -> None:
195
220
  if diff_report.id != "NO_DIFF_RAN":
@@ -231,6 +256,8 @@ def cli():
231
256
  except Exception as error:
232
257
  log.error("Unexpected error when running the cli")
233
258
  log.error(error)
259
+ log.error("Traceback:")
260
+ log.error(traceback.format_exc())
234
261
  if not blocking_disabled:
235
262
  sys.exit(3)
236
263
  else:
@@ -257,6 +284,7 @@ def main_code():
257
284
  sbom_file = arguments.sbom_file
258
285
  license_mode = arguments.generate_license
259
286
  enable_json = arguments.enable_json
287
+ enable_sarif = arguments.enable_sarif
260
288
  disable_overview = arguments.disable_overview
261
289
  disable_security_issue = arguments.disable_security_issue
262
290
  ignore_commit_files = arguments.ignore_commit_files
@@ -401,7 +429,10 @@ def main_code():
401
429
  else:
402
430
  log.info("Starting non-PR/MR flow")
403
431
  diff = core.create_new_diff(target_path, params, workspace=target_path, no_change=no_change)
404
- if enable_json:
432
+ if enable_sarif:
433
+ log.debug("Outputting SARIF Results")
434
+ output_console_sarif(diff, sbom_file)
435
+ elif enable_json:
405
436
  log.debug("Outputting JSON Results")
406
437
  output_console_json(diff, sbom_file)
407
438
  else:
@@ -410,7 +441,11 @@ def main_code():
410
441
  log.info("API Mode")
411
442
  diff: Diff
412
443
  diff = core.create_new_diff(target_path, params, workspace=target_path, no_change=no_change)
413
- if enable_json:
444
+ if enable_sarif:
445
+ log.debug("Outputting SARIF Results")
446
+ output_console_sarif(diff, sbom_file)
447
+ elif enable_json:
448
+ log.debug("Outputting JSON Results")
414
449
  output_console_json(diff, sbom_file)
415
450
  else:
416
451
  output_console_comments(diff, sbom_file)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: socketsecurity
3
- Version: 1.0.41
3
+ Version: 1.0.43
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>
@@ -20,6 +20,7 @@ Requires-Dist: mdutils
20
20
  Requires-Dist: prettytable
21
21
  Requires-Dist: GitPython
22
22
  Requires-Dist: packaging
23
+ Requires-Dist: socket-sdk-python<2.0.0,>=1.0.15
23
24
 
24
25
  # Socket Security CLI
25
26
 
@@ -30,7 +31,7 @@ The Socket Security CLI was created to enable integrations with other tools like
30
31
  ```` shell
31
32
  socketcli [-h] [--api_token API_TOKEN] [--repo REPO] [--branch BRANCH] [--committer COMMITTER] [--pr_number PR_NUMBER]
32
33
  [--commit_message COMMIT_MESSAGE] [--default_branch] [--target_path TARGET_PATH] [--scm {api,github,gitlab}] [--sbom-file SBOM_FILE]
33
- [--commit-sha COMMIT_SHA] [--generate-license GENERATE_LICENSE] [-v] [--enable-debug] [--enable-json] [--disable-overview]
34
+ [--commit-sha COMMIT_SHA] [--generate-license GENERATE_LICENSE] [-v] [--enable-debug] [--enable-json] [--enable-sarif] [--disable-overview]
34
35
  [--disable-security-issue] [--files FILES] [--ignore-commit-files] [--timeout]
35
36
  ````
36
37
 
@@ -56,6 +57,7 @@ If you don't want to provide the Socket API Token every time then you can use th
56
57
  | --commit-sha | | False | | The commit hash for the commit |
57
58
  | --generate-license | | False | False | If enabled with `--sbom-file` will include license details |
58
59
  | --enable-json | | False | False | If enabled will change the console output format to JSON |
60
+ | --enable-sarif | | False | False | If enabled will change the console output format to SARIF |
59
61
  | --disable-overview | | False | False | If enabled will disable Dependency Overview comments |
60
62
  | --disable-security-issue | | False | False | If enabled will disable Security Issue Comments |
61
63
  | --files | | False | | If provided in the format of `["file1", "file2"]` will be used to determine if there have been supported file changes. This is used if it isn't a git repo and you would like to only run if it supported files have changed. |
@@ -3,3 +3,4 @@ mdutils
3
3
  prettytable
4
4
  GitPython
5
5
  packaging
6
+ socket-sdk-python<2.0.0,>=1.0.15
File without changes