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 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,8 @@
1
+ Metadata-Version: 2.1
2
+ Name: socketsecurity
3
+ Version: 0.0.38
4
+ Requires-Dist: click
5
+ Requires-Dist: mdutils
6
+ Requires-Dist: requests
7
+ Requires-Dist: prettytable
8
+
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.41.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ my_cli_app = socketcli.socketcli:cli
@@ -0,0 +1 @@
1
+ core