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.
- {socketsecurity-1.0.41/socketsecurity.egg-info → socketsecurity-1.0.43}/PKG-INFO +4 -2
- {socketsecurity-1.0.41 → socketsecurity-1.0.43}/README.md +2 -1
- {socketsecurity-1.0.41 → socketsecurity-1.0.43}/pyproject.toml +2 -1
- {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity/__init__.py +1 -1
- {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity/core/__init__.py +147 -149
- {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity/core/messages.py +123 -0
- {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity/socketcli.py +38 -3
- {socketsecurity-1.0.41 → socketsecurity-1.0.43/socketsecurity.egg-info}/PKG-INFO +4 -2
- {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity.egg-info/requires.txt +1 -0
- {socketsecurity-1.0.41 → socketsecurity-1.0.43}/LICENSE +0 -0
- {socketsecurity-1.0.41 → socketsecurity-1.0.43}/setup.cfg +0 -0
- {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity/core/classes.py +0 -0
- {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity/core/exceptions.py +0 -0
- {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity/core/git_interface.py +0 -0
- {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity/core/github.py +0 -0
- {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity/core/gitlab.py +0 -0
- {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity/core/issues.py +0 -0
- {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity/core/licenses.py +0 -0
- {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity/core/scm_comments.py +0 -0
- {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity.egg-info/SOURCES.txt +0 -0
- {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity.egg-info/dependency_links.txt +0 -0
- {socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity.egg-info/entry_points.txt +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
7
|
-
import
|
|
8
|
-
from
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
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.
|
|
177
|
+
) -> requests.Response:
|
|
164
178
|
"""
|
|
165
|
-
|
|
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
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
"
|
|
233
|
-
"error": response.text,
|
|
215
|
+
"body": response.text,
|
|
234
216
|
"payload": payload,
|
|
235
|
-
"
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
333
|
-
|
|
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
|
-
|
|
336
|
-
log.
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
412
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
|
|
531
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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. |
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{socketsecurity-1.0.41 → socketsecurity-1.0.43}/socketsecurity.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|