socketsecurity 0.0.38__py3-none-any.whl
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.
- core/__init__.py +644 -0
- core/classes.py +351 -0
- core/exceptions.py +31 -0
- core/messages.py +301 -0
- socketsecurity-0.0.38.dist-info/METADATA +8 -0
- socketsecurity-0.0.38.dist-info/RECORD +9 -0
- socketsecurity-0.0.38.dist-info/WHEEL +5 -0
- socketsecurity-0.0.38.dist-info/entry_points.txt +2 -0
- socketsecurity-0.0.38.dist-info/top_level.txt +1 -0
core/__init__.py
ADDED
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import requests
|
|
3
|
+
from urllib.parse import urlencode
|
|
4
|
+
import base64
|
|
5
|
+
import json
|
|
6
|
+
from core.exceptions import APIFailure, APIKeyMissing, APIAccessDenied, APIInsufficientQuota, APIResourceNotFound
|
|
7
|
+
from core.classes import (
|
|
8
|
+
Report,
|
|
9
|
+
Issue,
|
|
10
|
+
Package,
|
|
11
|
+
Alert,
|
|
12
|
+
FullScan,
|
|
13
|
+
FullScanParams,
|
|
14
|
+
Repository,
|
|
15
|
+
Diff,
|
|
16
|
+
Purl
|
|
17
|
+
)
|
|
18
|
+
import platform
|
|
19
|
+
from glob import glob
|
|
20
|
+
import time
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
__author__ = 'socket.dev'
|
|
24
|
+
__version__ = '0.0.38'
|
|
25
|
+
__all__ = [
|
|
26
|
+
"Core",
|
|
27
|
+
"log"
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
global encoded_key
|
|
32
|
+
version = __version__
|
|
33
|
+
api_url = "https://api.socket.dev/v0"
|
|
34
|
+
timeout = 30
|
|
35
|
+
full_scan_path = ""
|
|
36
|
+
repository_path = ""
|
|
37
|
+
org_id = None
|
|
38
|
+
org_slug = None
|
|
39
|
+
all_new_alerts = False
|
|
40
|
+
issue_file = "core/issues.json"
|
|
41
|
+
issue_text = {}
|
|
42
|
+
security_policy = {}
|
|
43
|
+
log = logging.getLogger("socketdev")
|
|
44
|
+
log.addHandler(logging.NullHandler())
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def load_issue_data() -> dict:
|
|
48
|
+
"""
|
|
49
|
+
Loads the issue data json and returns it as a dictionary. This data is used in the alert messages.
|
|
50
|
+
:return:
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
file = open(issue_file, 'r')
|
|
54
|
+
data = json.load(file)
|
|
55
|
+
return data
|
|
56
|
+
except Exception as error:
|
|
57
|
+
print("Unable to load issue data")
|
|
58
|
+
print(error)
|
|
59
|
+
exit(1)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def encode_key(token: str) -> None:
|
|
63
|
+
"""
|
|
64
|
+
encode_key takes passed token string and does a base64 encoding. It sets this as a global variable
|
|
65
|
+
:param token: str of the Socket API Security Token
|
|
66
|
+
:return:
|
|
67
|
+
"""
|
|
68
|
+
global encoded_key
|
|
69
|
+
encoded_key = base64.b64encode(token.encode()).decode('ascii')
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def do_request(
|
|
73
|
+
path: str,
|
|
74
|
+
headers: dict = None,
|
|
75
|
+
payload: [dict, str] = None,
|
|
76
|
+
files: list = None,
|
|
77
|
+
method: str = "GET",
|
|
78
|
+
) -> requests.request:
|
|
79
|
+
"""
|
|
80
|
+
do_requests is the shared function for making HTTP calls
|
|
81
|
+
|
|
82
|
+
:param path: Required path for the request
|
|
83
|
+
:param headers: Optional dictionary of headers. If not set will use a default set
|
|
84
|
+
:param payload: Optional dictionary or string of the payload to pass
|
|
85
|
+
:param files: Optional list of files to upload
|
|
86
|
+
:param method: Optional method to use, defaults to GET
|
|
87
|
+
:return:
|
|
88
|
+
"""
|
|
89
|
+
if encoded_key is None or encoded_key == "":
|
|
90
|
+
raise APIKeyMissing
|
|
91
|
+
|
|
92
|
+
if headers is None:
|
|
93
|
+
headers = {
|
|
94
|
+
'Authorization': f"Basic {encoded_key}",
|
|
95
|
+
'User-Agent': 'SocketPythonScript/0.0.1',
|
|
96
|
+
"accept": "application/json"
|
|
97
|
+
}
|
|
98
|
+
url = f"{api_url}/{path}"
|
|
99
|
+
response = requests.request(
|
|
100
|
+
method.upper(),
|
|
101
|
+
url,
|
|
102
|
+
headers=headers,
|
|
103
|
+
data=payload,
|
|
104
|
+
files=files,
|
|
105
|
+
timeout=timeout
|
|
106
|
+
)
|
|
107
|
+
if response.status_code <= 399:
|
|
108
|
+
return response
|
|
109
|
+
elif response.status_code == 400:
|
|
110
|
+
print(f"url={url}")
|
|
111
|
+
print(f"payload={payload}")
|
|
112
|
+
print(f"files={files}")
|
|
113
|
+
error = {
|
|
114
|
+
"msg": "bad request",
|
|
115
|
+
"error": response.text
|
|
116
|
+
}
|
|
117
|
+
raise APIFailure(error)
|
|
118
|
+
elif response.status_code == 401:
|
|
119
|
+
raise APIAccessDenied("Unauthorized")
|
|
120
|
+
elif response.status_code == 403:
|
|
121
|
+
raise APIInsufficientQuota("Insufficient max_quota for API method")
|
|
122
|
+
elif response.status_code == 404:
|
|
123
|
+
raise APIResourceNotFound(f"Path not found {path}")
|
|
124
|
+
elif response.status_code == 429:
|
|
125
|
+
raise APIInsufficientQuota("Insufficient quota for API route")
|
|
126
|
+
else:
|
|
127
|
+
msg = {
|
|
128
|
+
"status_code": 524,
|
|
129
|
+
"UnexpectedError": "There was an unexpected error using the API"
|
|
130
|
+
}
|
|
131
|
+
raise APIFailure(msg)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class Core:
|
|
135
|
+
token: str
|
|
136
|
+
base_api_url: str
|
|
137
|
+
request_timeout: int
|
|
138
|
+
reports: list
|
|
139
|
+
|
|
140
|
+
def __init__(self, token: str, base_api_url=None, request_timeout=None, enable_all_alerts=False):
|
|
141
|
+
self.token = token + ":"
|
|
142
|
+
encode_key(self.token)
|
|
143
|
+
global issue_text
|
|
144
|
+
issue_text = load_issue_data()
|
|
145
|
+
self.socket_date_format = "%Y-%m-%dT%H:%M:%S.%fZ"
|
|
146
|
+
self.base_api_url = base_api_url
|
|
147
|
+
if self.base_api_url is not None:
|
|
148
|
+
Core.set_api_url(self.base_api_url)
|
|
149
|
+
self.request_timeout = request_timeout
|
|
150
|
+
if self.request_timeout is not None:
|
|
151
|
+
Core.set_timeout(self.request_timeout)
|
|
152
|
+
if enable_all_alerts:
|
|
153
|
+
global all_new_alerts
|
|
154
|
+
all_new_alerts = True
|
|
155
|
+
Core.set_org_vars()
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def set_org_vars() -> None:
|
|
159
|
+
"""
|
|
160
|
+
Sets the main shared global variables
|
|
161
|
+
:return:
|
|
162
|
+
"""
|
|
163
|
+
global org_id, org_slug, full_scan_path, repository_path, security_policy
|
|
164
|
+
org_id, org_slug = Core.get_org_id_slug()
|
|
165
|
+
base_path = f"orgs/{org_slug}"
|
|
166
|
+
full_scan_path = f"{base_path}/full-scans"
|
|
167
|
+
repository_path = f"{base_path}/repos"
|
|
168
|
+
security_policy = Core.get_security_policy()
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def set_api_url(base_url: str):
|
|
172
|
+
"""
|
|
173
|
+
Set the global API URl if provided
|
|
174
|
+
:param base_url:
|
|
175
|
+
:return:
|
|
176
|
+
"""
|
|
177
|
+
global api_url
|
|
178
|
+
api_url = base_url
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def set_timeout(request_timeout: int):
|
|
182
|
+
"""
|
|
183
|
+
Set the global Requests timeout
|
|
184
|
+
:param request_timeout:
|
|
185
|
+
:return:
|
|
186
|
+
"""
|
|
187
|
+
global timeout
|
|
188
|
+
timeout = request_timeout
|
|
189
|
+
|
|
190
|
+
@staticmethod
|
|
191
|
+
def get_org_id_slug() -> (str, str):
|
|
192
|
+
"""
|
|
193
|
+
Gets the Org ID and Org Slug for the API Token
|
|
194
|
+
:return:
|
|
195
|
+
"""
|
|
196
|
+
path = "organizations"
|
|
197
|
+
response = do_request(path)
|
|
198
|
+
data = response.json()
|
|
199
|
+
organizations = data.get("organizations")
|
|
200
|
+
new_org_id = None
|
|
201
|
+
new_org_slug = None
|
|
202
|
+
if len(organizations) == 1:
|
|
203
|
+
for key in organizations:
|
|
204
|
+
new_org_id = key
|
|
205
|
+
new_org_slug = organizations[key].get('slug')
|
|
206
|
+
return new_org_id, new_org_slug
|
|
207
|
+
|
|
208
|
+
@staticmethod
|
|
209
|
+
def get_sbom_data(full_scan_id: str) -> list:
|
|
210
|
+
path = f"orgs/{org_slug}/full-scans/{full_scan_id}"
|
|
211
|
+
response = do_request(path)
|
|
212
|
+
results = []
|
|
213
|
+
try:
|
|
214
|
+
data = response.json()
|
|
215
|
+
results = data.get("sbom_artifacts") or []
|
|
216
|
+
except Exception as error:
|
|
217
|
+
log.info("Failed with old style full-scan API using new format")
|
|
218
|
+
log.info(error)
|
|
219
|
+
data = response.text
|
|
220
|
+
data.strip('"')
|
|
221
|
+
data.strip()
|
|
222
|
+
for line in data.split("\n"):
|
|
223
|
+
if line != '"' and line != "" and line is not None:
|
|
224
|
+
item = json.loads(line)
|
|
225
|
+
results.append(item)
|
|
226
|
+
return results
|
|
227
|
+
|
|
228
|
+
@staticmethod
|
|
229
|
+
def get_security_policy() -> dict:
|
|
230
|
+
"""
|
|
231
|
+
Get the Security policy and determine the effective Org security policy
|
|
232
|
+
:return:
|
|
233
|
+
"""
|
|
234
|
+
path = "settings"
|
|
235
|
+
payload = [
|
|
236
|
+
{
|
|
237
|
+
"organization": org_id
|
|
238
|
+
}
|
|
239
|
+
]
|
|
240
|
+
response = do_request(path, payload=json.dumps(payload), method="POST")
|
|
241
|
+
data = response.json()
|
|
242
|
+
defaults = data.get("defaults")
|
|
243
|
+
default_rules = defaults.get("issueRules")
|
|
244
|
+
entries = data.get("entries")
|
|
245
|
+
org_rules = {}
|
|
246
|
+
for org_set in entries:
|
|
247
|
+
settings = org_set.get("settings")
|
|
248
|
+
if settings is not None:
|
|
249
|
+
org_details = settings.get("organization")
|
|
250
|
+
org_rules = org_details.get("issueRules")
|
|
251
|
+
for default in default_rules:
|
|
252
|
+
if default not in org_rules:
|
|
253
|
+
action = default_rules[default]["action"]
|
|
254
|
+
org_rules[default] = {
|
|
255
|
+
"action": action
|
|
256
|
+
}
|
|
257
|
+
return org_rules
|
|
258
|
+
|
|
259
|
+
@staticmethod
|
|
260
|
+
def find_files(path: str) -> list:
|
|
261
|
+
"""
|
|
262
|
+
Globs the path for supported manifest files.
|
|
263
|
+
Note: Might move the source to a JSON file
|
|
264
|
+
:param path: Str - path to where the manifest files are located
|
|
265
|
+
:param workspace: str - workspace path to be stripped from the name
|
|
266
|
+
:return:
|
|
267
|
+
"""
|
|
268
|
+
socket_globs = {
|
|
269
|
+
"general": {
|
|
270
|
+
"readme": {
|
|
271
|
+
"pattern": "*readme*"
|
|
272
|
+
},
|
|
273
|
+
"notice": {
|
|
274
|
+
"pattern": "*notice*"
|
|
275
|
+
},
|
|
276
|
+
"license": {
|
|
277
|
+
"pattern": "{licen{s,c}e{,-*},copying}"
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
"npm": {
|
|
281
|
+
"package.json": {
|
|
282
|
+
"pattern": "package.json"
|
|
283
|
+
},
|
|
284
|
+
"package-lock.json": {
|
|
285
|
+
"pattern": "package-lock.json"
|
|
286
|
+
},
|
|
287
|
+
"npm-shrinkwrap.json": {
|
|
288
|
+
"pattern": "npm-shrinkwrap.json"
|
|
289
|
+
},
|
|
290
|
+
"yarn.lock": {
|
|
291
|
+
"pattern": "yarn.lock"
|
|
292
|
+
},
|
|
293
|
+
"pnpm-lock.yaml": {
|
|
294
|
+
"pattern": "pnpm-lock.yaml"
|
|
295
|
+
},
|
|
296
|
+
"pnpm-lock.yml": {
|
|
297
|
+
"pattern": "pnpm-lock.yml"
|
|
298
|
+
},
|
|
299
|
+
"pnpm-workspace.yaml": {
|
|
300
|
+
"pattern": "pnpm-workspace.yaml"
|
|
301
|
+
},
|
|
302
|
+
"pnpm-workspace.yml": {
|
|
303
|
+
"pattern": "pnpm-workspace.yml"
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
"pypi": {
|
|
307
|
+
"pipfile": {
|
|
308
|
+
"pattern": "pipfile"
|
|
309
|
+
},
|
|
310
|
+
"pyproject.toml": {
|
|
311
|
+
"pattern": "pyproject.toml"
|
|
312
|
+
},
|
|
313
|
+
"requirements.txt": {
|
|
314
|
+
"pattern": "*requirements.txt"
|
|
315
|
+
},
|
|
316
|
+
"requirements": {
|
|
317
|
+
"pattern": "requirements/*.txt"
|
|
318
|
+
},
|
|
319
|
+
"requirements-*.txt": {
|
|
320
|
+
"pattern": "requirements-*.txt"
|
|
321
|
+
},
|
|
322
|
+
"requirements_*.txt": {
|
|
323
|
+
"pattern": "requirements_*.txt"
|
|
324
|
+
},
|
|
325
|
+
"requirements.frozen": {
|
|
326
|
+
"pattern": "requirements.frozen"
|
|
327
|
+
},
|
|
328
|
+
"setup.py": {
|
|
329
|
+
"pattern": "setup.py"
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
"golang": {
|
|
333
|
+
"go.mod": {
|
|
334
|
+
"pattern": "go.mod"
|
|
335
|
+
},
|
|
336
|
+
"go.sum": {
|
|
337
|
+
"pattern": "go.sum"
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
all_files = []
|
|
342
|
+
for ecosystem in socket_globs:
|
|
343
|
+
patterns = socket_globs[ecosystem]
|
|
344
|
+
for file_name in patterns:
|
|
345
|
+
pattern = patterns[file_name]["pattern"]
|
|
346
|
+
file_path = f"{path}/**/{pattern}"
|
|
347
|
+
files = glob(file_path, recursive=True)
|
|
348
|
+
for file in files:
|
|
349
|
+
if platform.system() == "Windows":
|
|
350
|
+
file = file.replace("\\", "/")
|
|
351
|
+
found_path, file_name = file.rsplit("/", 1)
|
|
352
|
+
details = (found_path, file_name)
|
|
353
|
+
all_files.append(details)
|
|
354
|
+
return all_files
|
|
355
|
+
|
|
356
|
+
@staticmethod
|
|
357
|
+
def create_full_scan(files: list, params: FullScanParams, workspace: str) -> FullScan:
|
|
358
|
+
"""
|
|
359
|
+
Calls the full scan API to create a new Full Scan
|
|
360
|
+
:param files: list - Globbed files of manifest files
|
|
361
|
+
:param params: FullScanParams - Set of query params to pass to the endpoint
|
|
362
|
+
:param workspace: str - Path of workspace
|
|
363
|
+
:return:
|
|
364
|
+
"""
|
|
365
|
+
send_files = []
|
|
366
|
+
for path, name in files:
|
|
367
|
+
full_path = f"{path}/{name}"
|
|
368
|
+
if full_path.startswith(workspace):
|
|
369
|
+
key = full_path[len(workspace):]
|
|
370
|
+
else:
|
|
371
|
+
key = full_path
|
|
372
|
+
key = key.lstrip("/")
|
|
373
|
+
key = key.lstrip("./")
|
|
374
|
+
payload = (
|
|
375
|
+
key,
|
|
376
|
+
(
|
|
377
|
+
name,
|
|
378
|
+
open(full_path, 'rb')
|
|
379
|
+
)
|
|
380
|
+
)
|
|
381
|
+
send_files.append(payload)
|
|
382
|
+
query_params = urlencode(params.__dict__)
|
|
383
|
+
full_uri = f"{full_scan_path}?{query_params}"
|
|
384
|
+
response = do_request(full_uri, method="POST", files=send_files)
|
|
385
|
+
results = response.json()
|
|
386
|
+
full_scan = FullScan(**results)
|
|
387
|
+
full_scan.sbom_artifacts = Core.get_sbom_data(full_scan.id)
|
|
388
|
+
return full_scan
|
|
389
|
+
|
|
390
|
+
@staticmethod
|
|
391
|
+
def get_head_scan_for_repo(repo_slug: str):
|
|
392
|
+
"""
|
|
393
|
+
Get the head scan ID for a repository to use for the diff
|
|
394
|
+
:param repo_slug: Str - Repo slug for the repository that is being diffed
|
|
395
|
+
:return:
|
|
396
|
+
"""
|
|
397
|
+
repo_path = f"{repository_path}/{repo_slug}"
|
|
398
|
+
response = do_request(repo_path)
|
|
399
|
+
results = response.json()
|
|
400
|
+
repository = Repository(**results)
|
|
401
|
+
return repository.head_full_scan_id
|
|
402
|
+
|
|
403
|
+
@staticmethod
|
|
404
|
+
def get_full_scan(full_scan_id: str) -> FullScan:
|
|
405
|
+
"""
|
|
406
|
+
Get the specified full scan and return a FullScan object
|
|
407
|
+
:param full_scan_id: str - ID of the full scan to pull
|
|
408
|
+
:return:
|
|
409
|
+
"""
|
|
410
|
+
full_scan_url = f"{full_scan_path}/{full_scan_id}"
|
|
411
|
+
response = do_request(full_scan_url)
|
|
412
|
+
results = response.json()
|
|
413
|
+
full_scan = FullScan(**results)
|
|
414
|
+
full_scan.sbom_artifacts = Core.get_sbom_data(full_scan.id)
|
|
415
|
+
return full_scan
|
|
416
|
+
|
|
417
|
+
@staticmethod
|
|
418
|
+
def create_new_diff(path: str, params: FullScanParams, workspace: str) -> Diff:
|
|
419
|
+
"""
|
|
420
|
+
1. Get the head full scan. If it isn't present because this repo doesn't exist yet return an Empty full scan.
|
|
421
|
+
2. Create a new Full scan for the current run
|
|
422
|
+
3. Compare the head and new Full scan
|
|
423
|
+
4. Return a Diff report
|
|
424
|
+
:param path: Str - path of where to look for manifest files for the new Full Scan
|
|
425
|
+
:param params: FullScanParams - Query params for the Full Scan endpoint
|
|
426
|
+
:param workspace: str - Path for workspace
|
|
427
|
+
:return:
|
|
428
|
+
"""
|
|
429
|
+
files = Core.find_files(path)
|
|
430
|
+
try:
|
|
431
|
+
head_full_scan_id = Core.get_head_scan_for_repo(params.repo)
|
|
432
|
+
if head_full_scan_id is None or head_full_scan_id == "":
|
|
433
|
+
head_full_scan = []
|
|
434
|
+
else:
|
|
435
|
+
head_start = time.time()
|
|
436
|
+
head_full_scan = Core.get_sbom_data(head_full_scan_id)
|
|
437
|
+
head_end = time.time()
|
|
438
|
+
total_head_time = head_end - head_start
|
|
439
|
+
print(f"Total time to get head sbom {total_head_time: .2f}")
|
|
440
|
+
except APIResourceNotFound:
|
|
441
|
+
head_full_scan = []
|
|
442
|
+
if files is not None and len(files) > 0:
|
|
443
|
+
new_scan_start = time.time()
|
|
444
|
+
new_full_scan = Core.create_full_scan(files, params, workspace)
|
|
445
|
+
new_scan_end = time.time()
|
|
446
|
+
total_new_time = new_scan_end - new_scan_start
|
|
447
|
+
print(f"Total time to get new sbom {total_new_time: .2f}")
|
|
448
|
+
diff_report = Core.compare_sboms(new_full_scan.sbom_artifacts, head_full_scan)
|
|
449
|
+
else:
|
|
450
|
+
diff_report = Diff()
|
|
451
|
+
return diff_report
|
|
452
|
+
|
|
453
|
+
@staticmethod
|
|
454
|
+
def compare_sboms(new_scan: list, head_scan: list) -> Diff:
|
|
455
|
+
"""
|
|
456
|
+
compare the SBOMs of the new full Scan and the head full scan. Return a Diff report with new packages,
|
|
457
|
+
removed packages, and new alerts for the new full scan compared to the head.
|
|
458
|
+
:param new_scan: FullScan - Newly created FullScan for this execution
|
|
459
|
+
:param head_scan: FullScan - Current head FullScan for the repository
|
|
460
|
+
:return:
|
|
461
|
+
"""
|
|
462
|
+
diff: Diff
|
|
463
|
+
diff = Diff()
|
|
464
|
+
new_packages = Core.create_sbom_dict(new_scan)
|
|
465
|
+
head_packages = Core.create_sbom_dict(head_scan)
|
|
466
|
+
new_scan_alerts = {}
|
|
467
|
+
head_scan_alerts = {}
|
|
468
|
+
|
|
469
|
+
for package_id in new_packages:
|
|
470
|
+
purl, package = Core.create_purl(package_id, new_packages)
|
|
471
|
+
if package_id not in head_packages:
|
|
472
|
+
diff.new_packages.append(purl)
|
|
473
|
+
new_scan_alerts = Core.create_issue_alerts(package, new_scan_alerts, new_packages)
|
|
474
|
+
for package_id in head_packages:
|
|
475
|
+
purl, package = Core.create_purl(package_id, head_packages)
|
|
476
|
+
if package_id not in new_packages:
|
|
477
|
+
diff.removed_packages.append(purl)
|
|
478
|
+
head_scan_alerts = Core.create_issue_alerts(package, head_scan_alerts, head_packages)
|
|
479
|
+
diff.new_alerts = Core.compare_issue_alerts(new_scan_alerts, head_scan_alerts, diff.new_alerts)
|
|
480
|
+
return diff
|
|
481
|
+
|
|
482
|
+
@staticmethod
|
|
483
|
+
def compare_issue_alerts(new_scan_alerts: dict, head_scan_alerts: dict, alerts: list) -> list:
|
|
484
|
+
"""
|
|
485
|
+
Compare the issue alerts from the new full scan and the head full scans. Return a list of new alerts that
|
|
486
|
+
are in the new full scan and not in the head full scan
|
|
487
|
+
:param new_scan_alerts: dictionary of alerts from the new full scan
|
|
488
|
+
:param head_scan_alerts: dictionary of alerts from the new head scan
|
|
489
|
+
:param alerts: List of new alerts that are only in the new Full Scan
|
|
490
|
+
:return:
|
|
491
|
+
"""
|
|
492
|
+
for alert_key in new_scan_alerts:
|
|
493
|
+
if alert_key not in head_scan_alerts:
|
|
494
|
+
new_alerts = new_scan_alerts[alert_key]
|
|
495
|
+
for alert in new_alerts:
|
|
496
|
+
if Core.is_error(alert):
|
|
497
|
+
alerts.append(alert)
|
|
498
|
+
else:
|
|
499
|
+
new_alerts = new_scan_alerts[alert_key]
|
|
500
|
+
head_alerts = head_scan_alerts[alert_key]
|
|
501
|
+
for alert in new_alerts:
|
|
502
|
+
if alert not in head_alerts and Core.is_error(alert):
|
|
503
|
+
alerts.append(alert)
|
|
504
|
+
return alerts
|
|
505
|
+
|
|
506
|
+
@staticmethod
|
|
507
|
+
def is_error(alert: Alert):
|
|
508
|
+
"""
|
|
509
|
+
Compare the current alert against the Security Policy to determine if it should be included. Can be overridden
|
|
510
|
+
with all_new_alerts Global setting if desired to return all alerts and not just the error category from the
|
|
511
|
+
security policy.
|
|
512
|
+
:param alert:
|
|
513
|
+
:return:
|
|
514
|
+
"""
|
|
515
|
+
if all_new_alerts or (alert.type in security_policy and security_policy[alert.type]['action'] == "error"):
|
|
516
|
+
return True
|
|
517
|
+
else:
|
|
518
|
+
return False
|
|
519
|
+
|
|
520
|
+
@staticmethod
|
|
521
|
+
def create_issue_alerts(package: Package, alerts: dict, packages: dict) -> dict:
|
|
522
|
+
"""
|
|
523
|
+
Create the Issue Alerts from the package and base alert data.
|
|
524
|
+
:param package: Package - Current package that is being looked at for Alerts
|
|
525
|
+
:param alerts: Dict - All found Issue Alerts across all packages
|
|
526
|
+
:param packages: Dict - All packages detected in the SBOM and needed to find top level packages
|
|
527
|
+
:return:
|
|
528
|
+
"""
|
|
529
|
+
for item in package.alerts:
|
|
530
|
+
alert = Alert(**item)
|
|
531
|
+
props = issue_text.get(alert.type)
|
|
532
|
+
if props is not None:
|
|
533
|
+
description = props.get("description")
|
|
534
|
+
title = props.get("title")
|
|
535
|
+
suggestion = props.get("suggestion")
|
|
536
|
+
next_step_title = props.get("nextStepTitle")
|
|
537
|
+
else:
|
|
538
|
+
description = ""
|
|
539
|
+
title = ""
|
|
540
|
+
suggestion = ""
|
|
541
|
+
next_step_title = ""
|
|
542
|
+
introduced_by = Core.get_source_data(package, packages)
|
|
543
|
+
issue_alert = Issue(
|
|
544
|
+
pkg_type=package.type,
|
|
545
|
+
pkg_name=package.name,
|
|
546
|
+
pkg_version=package.version,
|
|
547
|
+
pkg_id=package.id,
|
|
548
|
+
type=alert.type,
|
|
549
|
+
severity=alert.severity,
|
|
550
|
+
key=alert.key,
|
|
551
|
+
props=alert.props,
|
|
552
|
+
description=description,
|
|
553
|
+
title=title,
|
|
554
|
+
suggestion=suggestion,
|
|
555
|
+
next_step_title=next_step_title,
|
|
556
|
+
introduced_by=introduced_by
|
|
557
|
+
)
|
|
558
|
+
if issue_alert.key not in alerts:
|
|
559
|
+
alerts[issue_alert.key] = [issue_alert]
|
|
560
|
+
else:
|
|
561
|
+
alerts[issue_alert.key].append(issue_alert)
|
|
562
|
+
return alerts
|
|
563
|
+
|
|
564
|
+
@staticmethod
|
|
565
|
+
def get_source_data(package: Package, packages: dict) -> list:
|
|
566
|
+
"""
|
|
567
|
+
Creates the properties for source data of the source manifest file(s) and top level packages.
|
|
568
|
+
:param package: Package - Current package being evaluated
|
|
569
|
+
:param packages: Dict - All packages, used to determine top level package information for transitive packages
|
|
570
|
+
:return:
|
|
571
|
+
"""
|
|
572
|
+
introduced_by = []
|
|
573
|
+
if package.direct:
|
|
574
|
+
manifests = ""
|
|
575
|
+
for manifest_data in package.manifestFiles:
|
|
576
|
+
manifest_file = manifest_data.get("file")
|
|
577
|
+
manifests += f"{manifest_file};"
|
|
578
|
+
manifests = manifests.rstrip(";")
|
|
579
|
+
source = ("direct", manifests)
|
|
580
|
+
introduced_by.append(source)
|
|
581
|
+
else:
|
|
582
|
+
for top_id in package.topLevelAncestors:
|
|
583
|
+
top_package: Package
|
|
584
|
+
top_package = packages[top_id]
|
|
585
|
+
manifests = ""
|
|
586
|
+
top_purl = f"{top_package.type}/{top_package.name}@{top_package.version}"
|
|
587
|
+
for manifest_data in top_package.manifestFiles:
|
|
588
|
+
manifest_file = manifest_data.get("file")
|
|
589
|
+
manifests += f"{manifest_file};"
|
|
590
|
+
manifests = manifests.rstrip(";")
|
|
591
|
+
source = (top_purl, manifests)
|
|
592
|
+
introduced_by.append(source)
|
|
593
|
+
return introduced_by
|
|
594
|
+
|
|
595
|
+
@staticmethod
|
|
596
|
+
def create_purl(package_id: str, packages: dict) -> (Purl, Package):
|
|
597
|
+
"""
|
|
598
|
+
Creates the extended PURL data to use in the added or removed package details. Primarily used for outputting
|
|
599
|
+
data in the results for detections.
|
|
600
|
+
:param package_id: Str - Package ID of the package to create the PURL data
|
|
601
|
+
:param packages: dict - All packages to use for look up from transitive packages
|
|
602
|
+
:return:
|
|
603
|
+
"""
|
|
604
|
+
package: Package
|
|
605
|
+
package = packages[package_id]
|
|
606
|
+
introduced_by = Core.get_source_data(package, packages)
|
|
607
|
+
purl = Purl(
|
|
608
|
+
id=package.id,
|
|
609
|
+
name=package.name,
|
|
610
|
+
version=package.version,
|
|
611
|
+
ecosystem=package.type,
|
|
612
|
+
direct=package.direct,
|
|
613
|
+
introduced_by=introduced_by,
|
|
614
|
+
author=package.author or '',
|
|
615
|
+
size=package.size,
|
|
616
|
+
transitives=package.transitives
|
|
617
|
+
)
|
|
618
|
+
return purl, package
|
|
619
|
+
|
|
620
|
+
@staticmethod
|
|
621
|
+
def create_sbom_dict(sbom: list) -> dict:
|
|
622
|
+
"""
|
|
623
|
+
Converts the SBOM Artifacts from the FulLScan into a Dictionary for parsing
|
|
624
|
+
:param sbom: list - Raw artifacts for the SBOM
|
|
625
|
+
:return:
|
|
626
|
+
"""
|
|
627
|
+
packages = {}
|
|
628
|
+
top_level_count = {}
|
|
629
|
+
for item in sbom:
|
|
630
|
+
package = Package(**item)
|
|
631
|
+
if package.id in packages:
|
|
632
|
+
print("Duplicate package?")
|
|
633
|
+
else:
|
|
634
|
+
packages[package.id] = package
|
|
635
|
+
for top_id in package.topLevelAncestors:
|
|
636
|
+
if top_id not in top_level_count:
|
|
637
|
+
top_level_count[top_id] = 1
|
|
638
|
+
else:
|
|
639
|
+
top_level_count[top_id] += 1
|
|
640
|
+
if len(top_level_count) > 0:
|
|
641
|
+
for package_id in top_level_count:
|
|
642
|
+
packages[package_id].transitives = top_level_count[package_id]
|
|
643
|
+
return packages
|
|
644
|
+
|
core/classes.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"Report",
|
|
6
|
+
"Score",
|
|
7
|
+
"Package",
|
|
8
|
+
"Issue",
|
|
9
|
+
"YamlFile",
|
|
10
|
+
"Alert",
|
|
11
|
+
"FullScan",
|
|
12
|
+
"FullScanParams",
|
|
13
|
+
"Repository",
|
|
14
|
+
"Diff",
|
|
15
|
+
"Purl",
|
|
16
|
+
"GithubComment"
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Report:
|
|
21
|
+
branch: str
|
|
22
|
+
commit: str
|
|
23
|
+
id: str
|
|
24
|
+
pull_requests: list
|
|
25
|
+
url: str
|
|
26
|
+
repo: str
|
|
27
|
+
processed: bool
|
|
28
|
+
owner: str
|
|
29
|
+
created_at: str
|
|
30
|
+
sbom: list
|
|
31
|
+
|
|
32
|
+
def __init__(self, **kwargs):
|
|
33
|
+
self.sbom = []
|
|
34
|
+
if kwargs:
|
|
35
|
+
for key, value in kwargs.items():
|
|
36
|
+
setattr(self, key, value)
|
|
37
|
+
if not hasattr(self, "processed"):
|
|
38
|
+
self.processed = False
|
|
39
|
+
if hasattr(self, "pull_requests"):
|
|
40
|
+
if self.pull_requests is not None:
|
|
41
|
+
self.pull_requests = json.loads(str(self.pull_requests))
|
|
42
|
+
|
|
43
|
+
def __str__(self):
|
|
44
|
+
return json.dumps(self.__dict__)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Score:
|
|
48
|
+
supplyChain: float
|
|
49
|
+
quality: float
|
|
50
|
+
maintenance: float
|
|
51
|
+
license: float
|
|
52
|
+
overall: float
|
|
53
|
+
vulnerability: float
|
|
54
|
+
|
|
55
|
+
def __init__(self, **kwargs):
|
|
56
|
+
if kwargs:
|
|
57
|
+
for key, value in kwargs.items():
|
|
58
|
+
setattr(self, key, value)
|
|
59
|
+
for score_name in self.__dict__:
|
|
60
|
+
score = getattr(self, score_name)
|
|
61
|
+
if score <= 1:
|
|
62
|
+
score = score * 100
|
|
63
|
+
setattr(self, score_name, score)
|
|
64
|
+
|
|
65
|
+
def __str__(self):
|
|
66
|
+
return json.dumps(self.__dict__)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Package:
|
|
70
|
+
type: str
|
|
71
|
+
name: str
|
|
72
|
+
version: str
|
|
73
|
+
release: str
|
|
74
|
+
id: str
|
|
75
|
+
direct: bool
|
|
76
|
+
manifestFiles: list
|
|
77
|
+
author: list
|
|
78
|
+
size: int
|
|
79
|
+
score: dict
|
|
80
|
+
scores: Score
|
|
81
|
+
alerts: list
|
|
82
|
+
error_alerts: list
|
|
83
|
+
alert_counts: dict
|
|
84
|
+
topLevelAncestors: list
|
|
85
|
+
url: str
|
|
86
|
+
transitives: int
|
|
87
|
+
|
|
88
|
+
def __init__(self, **kwargs):
|
|
89
|
+
if kwargs:
|
|
90
|
+
for key, value in kwargs.items():
|
|
91
|
+
setattr(self, key, value)
|
|
92
|
+
if not hasattr(self, "direct"):
|
|
93
|
+
self.direct = False
|
|
94
|
+
else:
|
|
95
|
+
if str(self.direct).lower() == "true":
|
|
96
|
+
self.direct = True
|
|
97
|
+
self.url = f"https://socket.dev/{self.type}/package/{self.name}/overview/{self.version}"
|
|
98
|
+
if hasattr(self, 'score'):
|
|
99
|
+
self.scores = Score(**self.score)
|
|
100
|
+
if not hasattr(self, "alerts"):
|
|
101
|
+
self.alerts = []
|
|
102
|
+
if not hasattr(self, "author"):
|
|
103
|
+
self.author = []
|
|
104
|
+
if not hasattr(self, "size"):
|
|
105
|
+
self.size = 0
|
|
106
|
+
if not hasattr(self, "topLevelAncestors"):
|
|
107
|
+
self.topLevelAncestors = []
|
|
108
|
+
if not hasattr(self, "manifestFiles"):
|
|
109
|
+
self.manifestFiles = []
|
|
110
|
+
if not hasattr(self, "transitives"):
|
|
111
|
+
self.transitives = 0
|
|
112
|
+
self.alert_counts = {
|
|
113
|
+
"critical": 0,
|
|
114
|
+
"high": 0,
|
|
115
|
+
"middle": 0,
|
|
116
|
+
"low": 0
|
|
117
|
+
}
|
|
118
|
+
self.error_alerts = []
|
|
119
|
+
|
|
120
|
+
def __str__(self):
|
|
121
|
+
return json.dumps(self.__dict__)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class Issue:
|
|
125
|
+
pkg_type: str
|
|
126
|
+
pkg_name: str
|
|
127
|
+
pkg_version: str
|
|
128
|
+
category: str
|
|
129
|
+
type: str
|
|
130
|
+
severity: str
|
|
131
|
+
pkg_id: str
|
|
132
|
+
props: dict
|
|
133
|
+
key: str
|
|
134
|
+
is_error: bool
|
|
135
|
+
description: str
|
|
136
|
+
title: str
|
|
137
|
+
emoji: str
|
|
138
|
+
next_step_title: str
|
|
139
|
+
suggestion: str
|
|
140
|
+
introduced_by: list
|
|
141
|
+
manifests: str
|
|
142
|
+
|
|
143
|
+
def __init__(self, **kwargs):
|
|
144
|
+
if kwargs:
|
|
145
|
+
for key, value in kwargs.items():
|
|
146
|
+
setattr(self, key, value)
|
|
147
|
+
|
|
148
|
+
if hasattr(self, "created_at"):
|
|
149
|
+
self.created_at = self.created_at.strip(" (Coordinated Universal Time)")
|
|
150
|
+
if not hasattr(self, "introduced_by"):
|
|
151
|
+
self.introduced_by = []
|
|
152
|
+
if not hasattr(self, "manifests"):
|
|
153
|
+
self.manifests = ""
|
|
154
|
+
|
|
155
|
+
def __str__(self):
|
|
156
|
+
return json.dumps(self.__dict__)
|
|
157
|
+
|
|
158
|
+
def __eq__(self, other):
|
|
159
|
+
return self.__dict__ == other.__dict__
|
|
160
|
+
|
|
161
|
+
def __ne__(self, other):
|
|
162
|
+
return self.__dict__ != other.__dict__
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class YamlFile:
|
|
166
|
+
path: str
|
|
167
|
+
name: str
|
|
168
|
+
team: list
|
|
169
|
+
module: list
|
|
170
|
+
production: bool
|
|
171
|
+
pii: bool
|
|
172
|
+
alerts: dict
|
|
173
|
+
error_ids: list
|
|
174
|
+
|
|
175
|
+
def __init__(
|
|
176
|
+
self,
|
|
177
|
+
**kwargs
|
|
178
|
+
):
|
|
179
|
+
self.alerts = {}
|
|
180
|
+
self.error_ids = []
|
|
181
|
+
|
|
182
|
+
if kwargs:
|
|
183
|
+
for key, value in kwargs.items():
|
|
184
|
+
setattr(self, key, value)
|
|
185
|
+
|
|
186
|
+
def __str__(self):
|
|
187
|
+
alerts = {}
|
|
188
|
+
for issue_key in self.alerts:
|
|
189
|
+
issue: Issue
|
|
190
|
+
issue = self.alerts[issue_key]["issue"]
|
|
191
|
+
manifests = self.alerts[issue_key]["manifests"]
|
|
192
|
+
new_alert = {
|
|
193
|
+
"issue": json.loads(str(issue)),
|
|
194
|
+
"manifests": manifests
|
|
195
|
+
}
|
|
196
|
+
alerts[issue_key] = new_alert
|
|
197
|
+
|
|
198
|
+
dump_object = self
|
|
199
|
+
dump_object.alerts = alerts
|
|
200
|
+
return json.dumps(dump_object.__dict__)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class Alert:
|
|
204
|
+
key: str
|
|
205
|
+
type: str
|
|
206
|
+
severity: str
|
|
207
|
+
category: str
|
|
208
|
+
props: dict
|
|
209
|
+
|
|
210
|
+
def __init__(self, **kwargs):
|
|
211
|
+
if kwargs:
|
|
212
|
+
for key, value in kwargs.items():
|
|
213
|
+
setattr(self, key, value)
|
|
214
|
+
if not hasattr(self, "props"):
|
|
215
|
+
self.props = {}
|
|
216
|
+
|
|
217
|
+
def __str__(self):
|
|
218
|
+
return json.dumps(self.__dict__)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class FullScan:
|
|
222
|
+
id: str
|
|
223
|
+
created_at: str
|
|
224
|
+
updated_at: str
|
|
225
|
+
organization_id: str
|
|
226
|
+
repository_id: str
|
|
227
|
+
branch: str
|
|
228
|
+
commit_message: str
|
|
229
|
+
commit_hash: str
|
|
230
|
+
pull_request: int
|
|
231
|
+
sbom_artifacts: list
|
|
232
|
+
|
|
233
|
+
def __init__(self, **kwargs):
|
|
234
|
+
if kwargs:
|
|
235
|
+
for key, value in kwargs.items():
|
|
236
|
+
setattr(self, key, value)
|
|
237
|
+
if not hasattr(self, "sbom_artifacts"):
|
|
238
|
+
self.sbom_artifacts = []
|
|
239
|
+
|
|
240
|
+
def __str__(self):
|
|
241
|
+
return json.dumps(self.__dict__)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class Repository:
|
|
245
|
+
id: str
|
|
246
|
+
created_at: str
|
|
247
|
+
updated_at: str
|
|
248
|
+
head_full_scan_id: str
|
|
249
|
+
name: str
|
|
250
|
+
description: str
|
|
251
|
+
homepage: str
|
|
252
|
+
visibility: str
|
|
253
|
+
archived: bool
|
|
254
|
+
default_branch: str
|
|
255
|
+
|
|
256
|
+
def __init__(self, **kwargs):
|
|
257
|
+
if kwargs:
|
|
258
|
+
for key, value in kwargs.items():
|
|
259
|
+
setattr(self, key, value)
|
|
260
|
+
|
|
261
|
+
def __str__(self):
|
|
262
|
+
return json.dumps(self.__dict__)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class FullScanParams:
|
|
266
|
+
repo: str
|
|
267
|
+
branch: str
|
|
268
|
+
commit_message: str
|
|
269
|
+
commit_hash: str
|
|
270
|
+
pull_request: int
|
|
271
|
+
committer: str
|
|
272
|
+
make_default_branch: bool
|
|
273
|
+
set_as_pending_head: bool
|
|
274
|
+
|
|
275
|
+
def __init__(self, **kwargs):
|
|
276
|
+
if kwargs:
|
|
277
|
+
for key, value in kwargs.items():
|
|
278
|
+
setattr(self, key, value)
|
|
279
|
+
|
|
280
|
+
def __str__(self):
|
|
281
|
+
return json.dumps(self.__dict__)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class Diff:
|
|
285
|
+
new_packages: list
|
|
286
|
+
removed_packages: list
|
|
287
|
+
new_alerts: list
|
|
288
|
+
id: str
|
|
289
|
+
sbom: str
|
|
290
|
+
|
|
291
|
+
def __init__(self, **kwargs):
|
|
292
|
+
if kwargs:
|
|
293
|
+
for key, value in kwargs.items():
|
|
294
|
+
setattr(self, key, value)
|
|
295
|
+
if not hasattr(self, "new_packages"):
|
|
296
|
+
self.new_packages = []
|
|
297
|
+
if not hasattr(self, "removed_packages"):
|
|
298
|
+
self.removed_packages = []
|
|
299
|
+
if not hasattr(self, "new_alerts"):
|
|
300
|
+
self.new_alerts = []
|
|
301
|
+
|
|
302
|
+
def __str__(self):
|
|
303
|
+
return json.dumps(self.__dict__)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class Purl:
|
|
307
|
+
id: str
|
|
308
|
+
name: str
|
|
309
|
+
version: str
|
|
310
|
+
ecosystem: str
|
|
311
|
+
direct: bool
|
|
312
|
+
author: str
|
|
313
|
+
size: int
|
|
314
|
+
transitives: int
|
|
315
|
+
introduced_by: list
|
|
316
|
+
|
|
317
|
+
def __init__(self, **kwargs):
|
|
318
|
+
if kwargs:
|
|
319
|
+
for key, value in kwargs.items():
|
|
320
|
+
setattr(self, key, value)
|
|
321
|
+
if not hasattr(self, "introduced_by"):
|
|
322
|
+
self.new_packages = []
|
|
323
|
+
|
|
324
|
+
def __str__(self):
|
|
325
|
+
return json.dumps(self.__dict__)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
class GithubComment:
|
|
329
|
+
url: str
|
|
330
|
+
html_url: str
|
|
331
|
+
issue_url: str
|
|
332
|
+
id: int
|
|
333
|
+
node_id: str
|
|
334
|
+
user: dict
|
|
335
|
+
created_at: str
|
|
336
|
+
updated_at: str
|
|
337
|
+
author_association: str
|
|
338
|
+
body: str
|
|
339
|
+
body_list: list
|
|
340
|
+
reactions: dict
|
|
341
|
+
performed_via_github_app: dict
|
|
342
|
+
|
|
343
|
+
def __init__(self, **kwargs):
|
|
344
|
+
if kwargs:
|
|
345
|
+
for key, value in kwargs.items():
|
|
346
|
+
setattr(self, key, value)
|
|
347
|
+
if not hasattr(self, "body_list"):
|
|
348
|
+
self.body_list = []
|
|
349
|
+
|
|
350
|
+
def __str__(self):
|
|
351
|
+
return json.dumps(self.__dict__)
|
core/exceptions.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"APIFailure",
|
|
3
|
+
"APIKeyMissing",
|
|
4
|
+
"APIAccessDenied",
|
|
5
|
+
"APIInsufficientQuota",
|
|
6
|
+
"APIResourceNotFound"
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class APIKeyMissing(Exception):
|
|
11
|
+
"""Raised when the api key is not passed and the headers are empty"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class APIFailure(Exception):
|
|
15
|
+
"""Raised when there is an error using the API"""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class APIAccessDenied(Exception):
|
|
20
|
+
"""Raised when access is denied to the API"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class APIInsufficientQuota(Exception):
|
|
25
|
+
"""Raised when access is denied to the API"""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class APIResourceNotFound(Exception):
|
|
30
|
+
"""Raised when access is denied to the API"""
|
|
31
|
+
pass
|
core/messages.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
from mdutils import MdUtils
|
|
2
|
+
from prettytable import PrettyTable
|
|
3
|
+
from core.classes import Diff, Purl, Issue
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Messages:
|
|
7
|
+
|
|
8
|
+
@staticmethod
|
|
9
|
+
def security_comment_template(diff: Diff) -> str:
|
|
10
|
+
"""
|
|
11
|
+
Creates the security comment template
|
|
12
|
+
:param diff: Diff - Diff report with the data needed for the template
|
|
13
|
+
:return:
|
|
14
|
+
"""
|
|
15
|
+
md = MdUtils(file_name="markdown_security_temp.md")
|
|
16
|
+
md.new_line("<!-- socket-security-comment-actions -->")
|
|
17
|
+
md.new_header(level=1, title="Socket Security: Issues Report")
|
|
18
|
+
md.new_line("Potential security issues detected. Learn more about [socket.dev](https://socket.dev)")
|
|
19
|
+
md.new_line("To accept the risk, merge this PR and you will not be notified again.")
|
|
20
|
+
md.new_line()
|
|
21
|
+
md.new_line("<!-- start-socket-alerts-table -->")
|
|
22
|
+
md, ignore_commands, next_steps = Messages.create_security_alert_table(diff, md)
|
|
23
|
+
md.new_line("<!-- end-socket-alerts-table -->")
|
|
24
|
+
md.new_line()
|
|
25
|
+
md = Messages.create_next_steps(md, next_steps)
|
|
26
|
+
md = Messages.create_deeper_look(md)
|
|
27
|
+
md = Messages.create_remove_package(md)
|
|
28
|
+
md = Messages.create_acceptable_risk(md, ignore_commands)
|
|
29
|
+
md.create_md_file()
|
|
30
|
+
return md.file_data_text.lstrip()
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def create_next_steps(md: MdUtils, next_steps: dict):
|
|
34
|
+
"""
|
|
35
|
+
Creates the next steps section of the security comment template
|
|
36
|
+
:param md: MdUtils - Main markdown variable
|
|
37
|
+
:param next_steps: Dict - Contains the detected next steps to include
|
|
38
|
+
:return:
|
|
39
|
+
"""
|
|
40
|
+
for step in next_steps:
|
|
41
|
+
detail = next_steps[step]
|
|
42
|
+
md.new_line("<details>")
|
|
43
|
+
md.new_line(f"<summary>{step}</summary>")
|
|
44
|
+
for line in detail:
|
|
45
|
+
md.new_paragraph(line)
|
|
46
|
+
md.new_line("</details>")
|
|
47
|
+
return md
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def create_deeper_look(md: MdUtils) -> MdUtils:
|
|
51
|
+
"""
|
|
52
|
+
Creates the deeper look section for the Security Comment Template
|
|
53
|
+
:param md: MdUtils - Main markdown variable
|
|
54
|
+
:return:
|
|
55
|
+
"""
|
|
56
|
+
md.new_line("<details>")
|
|
57
|
+
md.new_line("<summary>Take a deeper look at the dependency</summary>")
|
|
58
|
+
md.new_paragraph(
|
|
59
|
+
"Take a moment to review the security alert above. Review the linked package source code to understand the "
|
|
60
|
+
"potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, "
|
|
61
|
+
"reach out to your security team or ask the Socket team for help at support [AT] socket [DOT] dev."
|
|
62
|
+
)
|
|
63
|
+
md.new_line("</details>")
|
|
64
|
+
return md
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def create_remove_package(md: MdUtils) -> MdUtils:
|
|
68
|
+
"""
|
|
69
|
+
Creates the remove packages suggestion section for the Security Comment Template
|
|
70
|
+
:param md:
|
|
71
|
+
:return:
|
|
72
|
+
"""
|
|
73
|
+
md.new_line("<details>")
|
|
74
|
+
md.new_line("<summary>Remove the package</summary>")
|
|
75
|
+
md.new_paragraph(
|
|
76
|
+
"If you happen to install a dependency that Socket reports as "
|
|
77
|
+
"[https://socket.dev/npm/issue/malware](Known Malware) you should immediately remove it and select a "
|
|
78
|
+
"different dependency. For other alert types, you may may wish to investigate alternative packages or "
|
|
79
|
+
"consider if there are other ways to mitigate the specific risk posed by the dependency."
|
|
80
|
+
)
|
|
81
|
+
md.new_line("</details>")
|
|
82
|
+
return md
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def create_acceptable_risk(md: MdUtils, ignore_commands: list) -> MdUtils:
|
|
86
|
+
"""
|
|
87
|
+
Creates the comment on how to accept risk for the Security Comment Template
|
|
88
|
+
:param md: MdUtils - Main markdown variable
|
|
89
|
+
:param ignore_commands: List of detected ignore commands based on the alerts associated purls
|
|
90
|
+
:return:
|
|
91
|
+
"""
|
|
92
|
+
md.new_line("<details>")
|
|
93
|
+
md.new_line("<summary>Mark a package as acceptable risk</summary>")
|
|
94
|
+
md.new_paragraph(
|
|
95
|
+
"To ignore an alert, reply with a comment starting with `SocketSecurity ignore` followed by a space "
|
|
96
|
+
"separated list of `ecosystem/package-name@version` specifiers. e.g. `SocketSecurity ignore npm/foo@1.0.0`"
|
|
97
|
+
" or ignore all packages with `SocketSecurity ignore-all`"
|
|
98
|
+
)
|
|
99
|
+
md.new_list(ignore_commands)
|
|
100
|
+
md.new_line("</details>")
|
|
101
|
+
return md
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def create_security_alert_table(diff: Diff, md: MdUtils) -> (MdUtils, list, dict):
|
|
105
|
+
"""
|
|
106
|
+
Creates the detected issues table based on the Security Policy
|
|
107
|
+
:param diff: Diff - Diff report with the detected issues
|
|
108
|
+
:param md: MdUtils - Main markdown variable
|
|
109
|
+
:return:
|
|
110
|
+
"""
|
|
111
|
+
alert_table = [
|
|
112
|
+
"Alert",
|
|
113
|
+
"Package",
|
|
114
|
+
"Introduced by",
|
|
115
|
+
"Manifest File"
|
|
116
|
+
]
|
|
117
|
+
num_of_alert_columns = len(alert_table)
|
|
118
|
+
next_steps = {}
|
|
119
|
+
ignore_commands = []
|
|
120
|
+
for alert in diff.new_alerts:
|
|
121
|
+
alert: Issue
|
|
122
|
+
if alert.next_step_title not in next_steps:
|
|
123
|
+
next_steps[alert.next_step_title] = [
|
|
124
|
+
alert.description,
|
|
125
|
+
alert.suggestion
|
|
126
|
+
]
|
|
127
|
+
package_url, purl = Messages.create_package_link(alert)
|
|
128
|
+
ignore = f"`SocketSecurity ignore {purl}`"
|
|
129
|
+
if ignore not in ignore_commands:
|
|
130
|
+
ignore_commands.append(ignore)
|
|
131
|
+
manifest_str, sources = Messages.create_sources(alert)
|
|
132
|
+
row = [
|
|
133
|
+
alert.title,
|
|
134
|
+
package_url,
|
|
135
|
+
", ".join(sources),
|
|
136
|
+
manifest_str
|
|
137
|
+
]
|
|
138
|
+
alert_table.extend(row)
|
|
139
|
+
num_of_alert_rows = len(diff.new_alerts) + 1
|
|
140
|
+
md.new_table(
|
|
141
|
+
columns=num_of_alert_columns,
|
|
142
|
+
rows=num_of_alert_rows,
|
|
143
|
+
text=alert_table,
|
|
144
|
+
text_align="left"
|
|
145
|
+
)
|
|
146
|
+
return md, ignore_commands, next_steps
|
|
147
|
+
|
|
148
|
+
@staticmethod
|
|
149
|
+
def dependency_overview_template(diff: Diff) -> str:
|
|
150
|
+
"""
|
|
151
|
+
Creates the dependency Overview comment and returns a dict of the results
|
|
152
|
+
:param diff: Diff - Diff report with the added & removed packages
|
|
153
|
+
:return:
|
|
154
|
+
"""
|
|
155
|
+
md = MdUtils(file_name="markdown_overview_temp.md")
|
|
156
|
+
md.new_line("<!-- socket-overview-comment-actions -->")
|
|
157
|
+
md.new_header(level=1, title="Socket Security: Dependency Overview")
|
|
158
|
+
md.new_line("New and removed dependencies detected. Learn more about [socket.dev](https://socket.dev)")
|
|
159
|
+
md.new_line()
|
|
160
|
+
md = Messages.create_added_table(diff, md)
|
|
161
|
+
if len(diff.removed_packages) > 0:
|
|
162
|
+
md = Messages.create_remove_line(diff, md)
|
|
163
|
+
md.create_md_file()
|
|
164
|
+
return md.file_data_text.lstrip()
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def create_remove_line(diff: Diff, md: MdUtils) -> MdUtils:
|
|
168
|
+
"""
|
|
169
|
+
Creates the removed packages line for the Dependency Overview template
|
|
170
|
+
:param diff: Diff - Diff report with the removed packages
|
|
171
|
+
:param md: MdUtils - Main markdown variable
|
|
172
|
+
:return:
|
|
173
|
+
"""
|
|
174
|
+
removed_line = "Removed packages:"
|
|
175
|
+
for removed in diff.removed_packages:
|
|
176
|
+
removed: Purl
|
|
177
|
+
package_url = Messages.create_purl_link(removed)
|
|
178
|
+
removed_line += f" {package_url},"
|
|
179
|
+
removed_line = removed_line.rstrip(",")
|
|
180
|
+
md.new_line(removed_line)
|
|
181
|
+
return md
|
|
182
|
+
|
|
183
|
+
@staticmethod
|
|
184
|
+
def create_added_table(diff: Diff, md: MdUtils) -> MdUtils:
|
|
185
|
+
"""
|
|
186
|
+
Create the Added packages table for the Dependency Overview template
|
|
187
|
+
:param diff: Diff - Diff report with the Added packages information
|
|
188
|
+
:param md: MdUtils - Main markdown variable
|
|
189
|
+
:return:
|
|
190
|
+
"""
|
|
191
|
+
overview_table = [
|
|
192
|
+
"Package",
|
|
193
|
+
"Direct",
|
|
194
|
+
"Transitives",
|
|
195
|
+
"Size",
|
|
196
|
+
"Author"
|
|
197
|
+
]
|
|
198
|
+
num_of_overview_columns = len(overview_table)
|
|
199
|
+
for added in diff.new_packages:
|
|
200
|
+
added: Purl
|
|
201
|
+
package_url = Messages.create_purl_link(added)
|
|
202
|
+
row = [
|
|
203
|
+
package_url,
|
|
204
|
+
added.direct,
|
|
205
|
+
added.transitives,
|
|
206
|
+
f"{added.size} KB",
|
|
207
|
+
Messages.generate_author_data(added)
|
|
208
|
+
]
|
|
209
|
+
overview_table.extend(row)
|
|
210
|
+
num_of_overview_rows = len(diff.new_packages) + 1
|
|
211
|
+
md.new_table(
|
|
212
|
+
columns=num_of_overview_columns,
|
|
213
|
+
rows=num_of_overview_rows,
|
|
214
|
+
text=overview_table,
|
|
215
|
+
text_align="left"
|
|
216
|
+
)
|
|
217
|
+
return md
|
|
218
|
+
|
|
219
|
+
@staticmethod
|
|
220
|
+
def generate_author_data(package: Purl):
|
|
221
|
+
"""
|
|
222
|
+
Creates the Author links for the Dependency Overview Template
|
|
223
|
+
:param package:
|
|
224
|
+
:return:
|
|
225
|
+
"""
|
|
226
|
+
authors = ""
|
|
227
|
+
for author in package.author:
|
|
228
|
+
author_url = f"https://socket.dev/{package.ecosystem}/user/{author}"
|
|
229
|
+
authors += f"[{author}]({author_url}),"
|
|
230
|
+
authors = authors.rstrip(",")
|
|
231
|
+
return authors
|
|
232
|
+
|
|
233
|
+
@staticmethod
|
|
234
|
+
def create_purl_link(details: Purl) -> str:
|
|
235
|
+
"""
|
|
236
|
+
Creates the Purl link for the Dependency Overview Comment for the added packages
|
|
237
|
+
:param details: Purl - Details about the package needed to create the URLs
|
|
238
|
+
:return:
|
|
239
|
+
"""
|
|
240
|
+
purl = f"{details.ecosystem}/{details.name}@{details.version}"
|
|
241
|
+
package_url = f"[{purl}](https://socket.dev/{details.ecosystem}/{details.name}/overview/{details.version})"
|
|
242
|
+
return package_url
|
|
243
|
+
|
|
244
|
+
@staticmethod
|
|
245
|
+
def create_package_link(details: Issue) -> (str, str):
|
|
246
|
+
"""
|
|
247
|
+
Creates the package link for the Security Comment Template
|
|
248
|
+
:param details: Purl - Details about the package needed to create the URLs
|
|
249
|
+
:return:
|
|
250
|
+
"""
|
|
251
|
+
purl = f"{details.pkg_name}@{details.pkg_version}"
|
|
252
|
+
package_url = (
|
|
253
|
+
f"[{purl}]"
|
|
254
|
+
f"(https://socket.dev/{details.pkg_type}/{details.pkg_name}/overview/{details.pkg_version})"
|
|
255
|
+
)
|
|
256
|
+
return package_url, purl
|
|
257
|
+
|
|
258
|
+
@staticmethod
|
|
259
|
+
def create_console_security_alert_table(diff: Diff) -> PrettyTable:
|
|
260
|
+
"""
|
|
261
|
+
Creates the detected issues table based on the Security Policy
|
|
262
|
+
:param diff: Diff - Diff report with the detected issues
|
|
263
|
+
:return:
|
|
264
|
+
"""
|
|
265
|
+
alert_table = PrettyTable(
|
|
266
|
+
[
|
|
267
|
+
"Alert",
|
|
268
|
+
"Package",
|
|
269
|
+
"Introduced by",
|
|
270
|
+
"Manifest File"
|
|
271
|
+
]
|
|
272
|
+
)
|
|
273
|
+
for alert in diff.new_alerts:
|
|
274
|
+
alert: Issue
|
|
275
|
+
package_url, purl = Messages.create_package_link(alert)
|
|
276
|
+
manifest_str, sources = Messages.create_sources(alert, "console")
|
|
277
|
+
row = [
|
|
278
|
+
alert.title,
|
|
279
|
+
package_url,
|
|
280
|
+
", ".join(sources),
|
|
281
|
+
manifest_str
|
|
282
|
+
]
|
|
283
|
+
alert_table.add_row(row)
|
|
284
|
+
return alert_table
|
|
285
|
+
|
|
286
|
+
@staticmethod
|
|
287
|
+
def create_sources(alert: Issue, style="md") -> [str, list]:
|
|
288
|
+
sources = []
|
|
289
|
+
manifests = []
|
|
290
|
+
for source, manifest in alert.introduced_by:
|
|
291
|
+
sources.append(source)
|
|
292
|
+
if style == "md":
|
|
293
|
+
manifests.append(f"<li>{manifest}</li>")
|
|
294
|
+
else:
|
|
295
|
+
manifests.append(manifest)
|
|
296
|
+
manifest_list = "".join(manifests)
|
|
297
|
+
if style == "md":
|
|
298
|
+
manifest_str = f"<ul>{manifest_list}</ul>"
|
|
299
|
+
else:
|
|
300
|
+
manifest_str = manifest_list
|
|
301
|
+
return manifest_str, sources
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
core/__init__.py,sha256=yI-NGfEU1xsALOUwofGbyeNjCNr9phF5pJEVirNpoaE,23060
|
|
2
|
+
core/classes.py,sha256=yj3zw1J5CySLycCUN5AmUmEW_W6wzJgXwswNVPlJLEU,8053
|
|
3
|
+
core/exceptions.py,sha256=C7q0D3KlnE3ff4FyTnlY9TxU3G-7xaRZcxpanUPvCzc,626
|
|
4
|
+
core/messages.py,sha256=EE7LnmPfDf1LPs8Q-sAA41CU9ac1g32QCHSyP3fWRLk,11580
|
|
5
|
+
socketsecurity-0.0.38.dist-info/METADATA,sha256=vGws6Bo_3bkKqVtd0-90cRh4bJCu70t3GaFmQWbuU9E,155
|
|
6
|
+
socketsecurity-0.0.38.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
|
|
7
|
+
socketsecurity-0.0.38.dist-info/entry_points.txt,sha256=wD1xn8iECNjmxPbeYUlOPAmyqVPU6CO3S0zVigbW-HY,55
|
|
8
|
+
socketsecurity-0.0.38.dist-info/top_level.txt,sha256=0ebp8k35dOTHVwbY3UcokRsjC7cWLN_iaJ70tIcmODU,5
|
|
9
|
+
socketsecurity-0.0.38.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
core
|