owasp-depscan 5.1.3__py3-none-any.whl → 5.1.5__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.

Potentially problematic release.


This version of owasp-depscan might be problematic. Click here for more details.

depscan/lib/analysis.py CHANGED
@@ -1,9 +1,16 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import contextlib
1
4
  import json
2
5
  import os.path
6
+ import re
3
7
  from collections import OrderedDict, defaultdict
4
8
  from dataclasses import dataclass
5
9
  from typing import Dict, List, Optional
6
10
 
11
+ import cvss
12
+ from cvss import CVSSError
13
+ from packageurl import PackageURL
7
14
  from rich import box
8
15
  from rich.markdown import Markdown
9
16
  from rich.panel import Panel
@@ -18,11 +25,11 @@ from depscan.lib import config
18
25
  from depscan.lib.logger import LOG, console
19
26
  from depscan.lib.utils import max_version
20
27
 
21
- # -*- coding: utf-8 -*-
22
-
23
28
 
24
29
  NEWLINE = "\\n"
25
30
 
31
+ CWE_SPLITTER = re.compile(r"(?<=CWE-)[0-9]\d{0,5}", re.IGNORECASE)
32
+
26
33
 
27
34
  def best_fixed_location(sug_version, orig_fixed_location):
28
35
  """
@@ -43,9 +50,9 @@ def best_fixed_location(sug_version, orig_fixed_location):
43
50
  if sug_version and orig_fixed_location:
44
51
  if sug_version == placeholder_fix_version:
45
52
  return ""
46
- tmpA = sug_version.split(".")[0]
47
- tmpB = orig_fixed_location.split(".")[0]
48
- if tmpA == tmpB:
53
+ tmp_a = sug_version.split(".")[0]
54
+ tmp_b = orig_fixed_location.split(".")[0]
55
+ if tmp_a == tmp_b:
49
56
  return sug_version
50
57
  # Handle the placeholder version used by OS distros
51
58
  if orig_fixed_location == placeholder_fix_version:
@@ -90,12 +97,21 @@ def retrieve_bom_dependency_tree(bom_file):
90
97
  bom_data = json.load(bfp)
91
98
  if bom_data:
92
99
  return bom_data.get("dependencies", []), bom_data
93
- except Exception:
100
+ except json.JSONDecodeError:
94
101
  pass
95
102
  return [], None
96
103
 
97
104
 
98
105
  def retrieve_oci_properties(bom_data):
106
+ """
107
+ Retrieves OCI properties from the given BOM data.
108
+
109
+ :param bom_data: The BOM data to retrieve OCI properties from.
110
+ :type bom_data: dict
111
+
112
+ :return: A dictionary containing the retrieved OCI properties.
113
+ :rtype: dict
114
+ """
99
115
  props = {}
100
116
  if not bom_data:
101
117
  return props
@@ -119,17 +135,14 @@ def get_pkg_display(tree_pkg, current_pkg, extra_text=None):
119
135
  tree_pkg == current_pkg or tree_pkg in current_pkg
120
136
  )
121
137
  if tree_pkg:
122
- try:
123
- if current_pkg.startswith("pkg:"):
124
- purl_obj = parse_purl(current_pkg)
125
- if purl_obj:
126
- version_used = purl_obj.get("version")
127
- if version_used:
128
- full_pkg_display = (
129
- f"""{purl_obj.get("name")}@{version_used}"""
130
- )
131
- except Exception:
132
- pass
138
+ if current_pkg.startswith("pkg:"):
139
+ purl_obj = parse_purl(current_pkg)
140
+ if purl_obj:
141
+ version_used = purl_obj.get("version")
142
+ if version_used:
143
+ full_pkg_display = (
144
+ f"""{purl_obj.get("name")}@{version_used}"""
145
+ )
133
146
  if extra_text and highlightable:
134
147
  full_pkg_display = f"{full_pkg_display} {extra_text}"
135
148
  return full_pkg_display
@@ -234,7 +247,8 @@ def prepare_vdr(options: PrepareVdrOptions):
234
247
  vulnerability details.
235
248
 
236
249
  :param options: An instance of PrepareVdrOptions containing the function parameters.
237
- :return: Tuple containing (A list of vulnerability details, prioritized list as a dict)
250
+ :return: Vulnerability details, dictionary of prioritized items
251
+ :rtype: Tuple[List, Dict]
238
252
  """
239
253
  if not options.results:
240
254
  return [], {}
@@ -246,14 +260,11 @@ def prepare_vdr(options: PrepareVdrOptions):
246
260
  min_width=150,
247
261
  )
248
262
  ids_seen = {}
249
- direct_purls = options.direct_purls
250
- if not direct_purls:
251
- direct_purls = {}
252
- reached_purls = options.reached_purls
253
- if not reached_purls:
254
- reached_purls = {}
263
+ direct_purls = options.direct_purls or {}
264
+ reached_purls = options.reached_purls or {}
255
265
  required_pkgs = options.scoped_pkgs.get("required", [])
256
266
  optional_pkgs = options.scoped_pkgs.get("optional", [])
267
+ fp_count = 0
257
268
  pkg_attention_count = 0
258
269
  critical_count = 0
259
270
  has_poc_count = 0
@@ -288,6 +299,10 @@ def prepare_vdr(options: PrepareVdrOptions):
288
299
  for vuln_occ_dict in options.results:
289
300
  vid = vuln_occ_dict.get("id")
290
301
  problem_type = vuln_occ_dict.get("problem_type")
302
+ cwes = []
303
+ if problem_type:
304
+ cwes = split_cwe(problem_type)
305
+ has_flagged_cwe = False
291
306
  package_issue = vuln_occ_dict.get("package_issue")
292
307
  matched_by = vuln_occ_dict.get("matched_by")
293
308
  full_pkg = package_issue["affected_location"].get("package")
@@ -309,7 +324,6 @@ def prepare_vdr(options: PrepareVdrOptions):
309
324
  f"{vendor}:"
310
325
  f"{package_issue['affected_location'].get('package')}"
311
326
  )
312
- version = None
313
327
  if matched_by:
314
328
  version = matched_by.split("|")[-1]
315
329
  full_pkg = full_pkg + ":" + version
@@ -323,55 +337,86 @@ def prepare_vdr(options: PrepareVdrOptions):
323
337
  package_type = None
324
338
  insights = []
325
339
  plain_insights = []
340
+ purl_obj = None
341
+ vendor = None
326
342
  if purl and purl.startswith("pkg:"):
327
- try:
328
- purl_obj = parse_purl(purl)
329
- if purl_obj:
330
- version_used = purl_obj.get("version")
331
- package_type = purl_obj.get("type")
332
- qualifiers = purl_obj.get("qualifiers", {})
333
- if package_type in config.OS_PKG_TYPES:
343
+ purl_obj = parse_purl(purl)
344
+ if purl_obj:
345
+ version_used = purl_obj.get("version")
346
+ package_type = purl_obj.get("type")
347
+ qualifiers = purl_obj.get("qualifiers", {})
348
+ if package_type in config.OS_PKG_TYPES:
349
+ vendor = package_issue["affected_location"].get("vendor")
350
+ if (
351
+ vendor
352
+ and oci_product_types
353
+ and vendor not in oci_product_types
354
+ ):
355
+ # Bug #170 - do not report CVEs belonging to other distros
356
+ if vendor in config.OS_PKG_TYPES:
357
+ fp_count += 1
358
+ continue
359
+ # Some nvd data might match application CVEs for
360
+ # OS vendors which can be filtered
361
+ if package_issue["affected_location"].get("cpe_uri"):
362
+ all_parts = CPE_FULL_REGEX.match(
363
+ package_issue["affected_location"].get(
364
+ "cpe_uri"
365
+ )
366
+ )
367
+ if (
368
+ all_parts
369
+ and all_parts.group("target_sw") != "*"
370
+ and all_parts.group("target_sw")
371
+ not in config.OS_PKG_TYPES
372
+ ):
373
+ fp_count += 1
374
+ continue
375
+ insights.append(
376
+ f"[#7C8082]:telescope: Vendor {vendor}[/#7C8082]"
377
+ )
378
+ plain_insights.append(f"Vendor {vendor}")
379
+ has_os_packages = True
380
+ for acwe in cwes:
381
+ if acwe in config.OS_VULN_KEY_CWES:
382
+ has_flagged_cwe = True
383
+ break
384
+ # Don't flag the cwe for ignorable os packages
385
+ if has_flagged_cwe and (
386
+ purl_obj.get("name") in config.OS_PKG_UNINSTALLABLE
387
+ or purl_obj.get("name") in config.OS_PKG_IGNORABLE
388
+ or vendor in config.OS_PKG_IGNORABLE
389
+ ):
390
+ has_flagged_cwe = False
391
+ else:
334
392
  if (
335
- package_issue["affected_location"].get("vendor")
336
- and oci_product_types
337
- and package_issue["affected_location"].get("vendor")
338
- not in oci_product_types
393
+ purl_obj.get("name") in config.OS_PKG_IGNORABLE
394
+ or vendor in config.OS_PKG_IGNORABLE
339
395
  ):
340
- # Some nvd data might match application CVEs for OS vendors which can be filtered
341
- if package_issue["affected_location"].get(
342
- "cpe_uri"
343
- ):
344
- all_parts = CPE_FULL_REGEX.match(
345
- package_issue["affected_location"].get(
346
- "cpe_uri"
347
- )
348
- )
349
- if (
350
- all_parts
351
- and all_parts.group("target_sw") != "*"
352
- and all_parts.group("target_sw")
353
- not in config.OS_PKG_TYPES
354
- ):
355
- continue
356
- # Some vendors like suse leads to FP and can be turned off if our image do not have those types
357
- # Some os packages might match application packages in NVD
358
- if package_issue["affected_location"].get(
359
- "vendor"
360
- ) not in ("suse",):
361
- insights.append(
362
- f"[#7C8082]:telescope: Vendor {package_issue['affected_location'].get('vendor')}"
363
- )
364
- plain_insights.append(
365
- f"Vendor {package_issue['affected_location'].get('vendor')}"
366
- )
367
- has_os_packages = True
396
+ insights.append(
397
+ "[#7C8082]:mute: Suppress for containers[/#7C8082]"
398
+ )
399
+ plain_insights.append("Suppress for containers")
400
+ elif (
401
+ purl_obj.get("name") in config.OS_PKG_UNINSTALLABLE
402
+ ):
403
+ insights.append(
404
+ "[#7C8082]:scissors: Uninstall candidate[/#7C8082]"
405
+ )
406
+ plain_insights.append("Uninstall candidate")
407
+ # If the flag remains after all the suppressions then add it as an insight
408
+ if has_flagged_cwe:
409
+ insights.append(
410
+ "[#7C8082]:triangular_flag: Flagged weakness[/#7C8082]"
411
+ )
412
+ plain_insights.append("Flagged weakness")
413
+ if qualifiers:
368
414
  if "ubuntu" in qualifiers.get("distro", ""):
369
415
  has_ubuntu_packages = True
370
416
  if "rhel" in qualifiers.get("distro", ""):
371
417
  has_redhat_packages = True
372
- except Exception:
373
- pass
374
418
  if ids_seen.get(vid + purl):
419
+ fp_count += 1
375
420
  continue
376
421
  # Mark this CVE + pkg as seen to avoid duplicates
377
422
  ids_seen[vid + purl] = True
@@ -421,7 +466,11 @@ def prepare_vdr(options: PrepareVdrOptions):
421
466
  )
422
467
  if is_required and package_type not in config.OS_PKG_TYPES:
423
468
  if direct_purls.get(purl):
424
- package_usage = f":direct_hit: Used in [info]{str(direct_purls.get(purl))}[/info] locations"
469
+ package_usage = (
470
+ f":direct_hit: Used in [info]"
471
+ f"{str(direct_purls.get(purl))}"
472
+ f"[/info] locations"
473
+ )
425
474
  plain_package_usage = (
426
475
  f"Used in {str(direct_purls.get(purl))} locations"
427
476
  )
@@ -442,7 +491,10 @@ def prepare_vdr(options: PrepareVdrOptions):
442
491
  plain_package_usage = "Local install"
443
492
  has_os_packages = True
444
493
  else:
445
- package_usage = "[spring_green4]:notebook: Indirect dependency[/spring_green4]"
494
+ package_usage = (
495
+ "[spring_green4]:notebook: Indirect "
496
+ "dependency[/spring_green4]"
497
+ )
446
498
  plain_package_usage = "Indirect dependency"
447
499
  if package_usage != "N/A":
448
500
  insights.append(package_usage)
@@ -483,10 +535,26 @@ def prepare_vdr(options: PrepareVdrOptions):
483
535
  )
484
536
  plain_insights.append("Reachable and Exploitable")
485
537
  has_reachable_exploit_count += 1
486
- # Fail safe. Packages with exploits and direct usage without a reachable flow
487
- # are still considered reachable to reduce false negatives
538
+ # Fail safe. Packages with exploits and direct usage without
539
+ # a reachable flow are still considered reachable to reduce
540
+ # false negatives
488
541
  if not reached_purls.get(purl):
489
542
  reached_purls[purl] = 1
543
+ elif has_flagged_cwe:
544
+ if (vendor and vendor in ("gnu",)) or (
545
+ purl_obj and purl_obj.get("name") in ("glibc", "openssl")
546
+ ):
547
+ insights.append(
548
+ "[bright_red]:exclamation_mark: Reachable and Exploitable[/bright_red]"
549
+ )
550
+ plain_insights.append("Reachable and Exploitable")
551
+ has_reachable_exploit_count += 1
552
+ else:
553
+ insights.append(
554
+ "[bright_red]:exclamation_mark: Exploitable[/bright_red]"
555
+ )
556
+ plain_insights.append("Exploitable")
557
+ has_exploit_count += 1
490
558
  else:
491
559
  insights.append(
492
560
  "[bright_red]:exclamation_mark: Known Exploits[/bright_red]"
@@ -553,74 +621,54 @@ def prepare_vdr(options: PrepareVdrOptions):
553
621
  "state": "in_triage",
554
622
  "detail": f"Dependency Tree: {json.dumps(pkg_tree_list)}",
555
623
  }
556
- score = 2.0
557
- try:
558
- score = float(vuln_occ_dict.get("cvss_score"))
559
- except Exception:
560
- pass
561
- sev_to_use = pkg_severity.lower()
562
- if sev_to_use not in (
563
- "critical",
564
- "high",
565
- "medium",
566
- "low",
567
- "info",
568
- "none",
569
- ):
570
- sev_to_use = "unknown"
571
- ratings = [
624
+ ratings = cvss_to_vdr_rating(vuln_occ_dict)
625
+ properties = [
572
626
  {
573
- "score": score,
574
- "severity": sev_to_use,
575
- "method": "CVSSv31",
576
- }
627
+ "name": "depscan:insights",
628
+ "value": "\\n".join(plain_insights),
629
+ },
630
+ {
631
+ "name": "depscan:prioritized",
632
+ "value": "true" if pkg_group_rows.get(purl) else "false",
633
+ },
577
634
  ]
635
+ affected_version_range = get_version_range(package_issue, purl)
636
+ if affected_version_range:
637
+ properties.append(affected_version_range)
578
638
  advisories = []
579
639
  for k, v in clinks.items():
580
640
  advisories.append({"title": k, "url": v})
581
- cwes = []
582
- if problem_type:
583
- try:
584
- acwe = int(problem_type.lower().replace("cwe-", ""))
585
- cwes = [acwe]
586
- except Exception:
587
- pass
588
- pkg_vulnerabilities.append(
589
- {
590
- "bom-ref": f"{vid}/{purl}",
591
- "id": vid,
592
- "source": source,
593
- "ratings": ratings,
594
- "cwes": cwes,
595
- "description": vuln_occ_dict.get("short_description"),
596
- "recommendation": recommendation,
597
- "advisories": advisories,
598
- "analysis": analysis,
599
- "affects": affects,
600
- "properties": [
601
- {
602
- "name": "depscan:insights",
603
- "value": "\\n".join(plain_insights),
604
- },
605
- {
606
- "name": "depscan:prioritized",
607
- "value": "true"
608
- if pkg_group_rows.get(purl)
609
- else "false",
610
- },
611
- ],
612
- }
613
- )
641
+ vuln = {
642
+ "bom-ref": f"{vid}/{purl}",
643
+ "id": vid,
644
+ "source": source,
645
+ "ratings": ratings,
646
+ "cwes": cwes,
647
+ "description": vuln_occ_dict.get("short_description"),
648
+ "recommendation": recommendation,
649
+ "advisories": advisories,
650
+ "analysis": analysis,
651
+ "affects": affects,
652
+ "properties": properties,
653
+ }
654
+ if source_orig_time := vuln_occ_dict.get("source_orig_time"):
655
+ vuln["published"] = source_orig_time
656
+ if source_update_time := vuln_occ_dict.get("source_update_time"):
657
+ vuln["updated"] = source_update_time
658
+ pkg_vulnerabilities.append(vuln)
659
+
614
660
  if not options.no_vuln_table:
615
661
  console.print()
616
662
  console.print(table)
617
- console.print()
618
663
  if pkg_group_rows:
619
664
  psection = Markdown(
620
- """## Next Steps
665
+ """
666
+ Next Steps
667
+ ----------
621
668
 
622
669
  Below are the vulnerabilities prioritized by depscan. Follow your team's remediation workflow to mitigate these findings.
623
- """
670
+ """,
671
+ justify="left",
624
672
  )
625
673
  console.print(psection)
626
674
  utable = Table(
@@ -653,14 +701,14 @@ Below are the vulnerabilities prioritized by depscan. Follow your team's remedia
653
701
  if has_reachable_exploit_count:
654
702
  rmessage = (
655
703
  f":point_right: [magenta]{has_reachable_exploit_count}"
656
- f"[/magenta] out of {len(options.results)} vulnerabilities "
704
+ f"[/magenta] out of {len(pkg_vulnerabilities)} vulnerabilities "
657
705
  f"have [dark magenta]reachable[/dark magenta] exploits and requires your ["
658
706
  f"magenta]immediate[/magenta] attention."
659
707
  )
660
708
  else:
661
709
  rmessage = (
662
710
  f":point_right: [magenta]{has_exploit_count}"
663
- f"[/magenta] out of {len(options.results)} vulnerabilities "
711
+ f"[/magenta] out of {len(pkg_vulnerabilities)} vulnerabilities "
664
712
  f"have known exploits and requires your ["
665
713
  f"magenta]immediate[/magenta] attention."
666
714
  )
@@ -677,19 +725,25 @@ Below are the vulnerabilities prioritized by depscan. Follow your team's remedia
677
725
  )
678
726
  else:
679
727
  rmessage += (
680
- "\nConsider trimming this image by removing any "
728
+ "\n:scissors: Consider trimming this image by removing any "
681
729
  "unwanted packages. Alternatively, use a slim "
682
730
  "base image."
683
731
  )
684
732
  if distro_packages_count and distro_packages_count < len(
685
- options.results
733
+ pkg_vulnerabilities
686
734
  ):
687
- rmessage += (
688
- f"\nNOTE: [magenta]{distro_packages_count}"
689
- f"[/magenta] distro-specific vulnerabilities "
690
- f"out of {len(options.results)} could be prioritized "
691
- f"for updates."
692
- )
735
+ if (
736
+ len(pkg_vulnerabilities)
737
+ > config.max_distro_vulnerabilities
738
+ ):
739
+ rmessage += f"\nNOTE: Check if the base image or the kernel version used is End-of-Life (EOL)."
740
+ else:
741
+ rmessage += (
742
+ f"\nNOTE: [magenta]{distro_packages_count}"
743
+ f"[/magenta] distro-specific vulnerabilities "
744
+ f"out of {len(pkg_vulnerabilities)} could be prioritized "
745
+ f"for updates."
746
+ )
693
747
  if has_redhat_packages:
694
748
  rmessage += """\nNOTE: Vulnerabilities in RedHat packages with status "out of support" or "won't fix" are excluded from this result."""
695
749
  if has_ubuntu_packages:
@@ -715,7 +769,7 @@ Below are the vulnerabilities prioritized by depscan. Follow your team's remedia
715
769
  else:
716
770
  rmessage = (
717
771
  f":point_right: [info]{pkg_attention_count}"
718
- f"[/info] out of {len(options.results)} vulnerabilities "
772
+ f"[/info] out of {len(pkg_vulnerabilities)} vulnerabilities "
719
773
  f"requires your attention."
720
774
  )
721
775
  if fix_version_count:
@@ -727,10 +781,15 @@ Below are the vulnerabilities prioritized by depscan. Follow your team's remedia
727
781
  "remediate."
728
782
  )
729
783
  else:
784
+ v_text = (
785
+ "vulnerability"
786
+ if fix_version_count == 1
787
+ else "vulnerabilities"
788
+ )
730
789
  rmessage += (
731
790
  f"\nYou can remediate [bright_green]"
732
791
  f"{fix_version_count}[/bright_green] "
733
- f"{'vulnerability' if fix_version_count == 1 else 'vulnerabilities'} "
792
+ f"{v_text} "
734
793
  f"by updating the packages using the fix "
735
794
  f"version :thumbsup:"
736
795
  )
@@ -756,11 +815,7 @@ Below are the vulnerabilities prioritized by depscan. Follow your team's remedia
756
815
  rmessage = (
757
816
  ":white_medium_small_square: Prioritize any vulnerabilities in libraries such "
758
817
  "as glibc, openssl, or libcurl.\nAdditionally, "
759
- "prioritize the vulnerabilities in packages that "
760
- "provide executable binaries when there is a "
761
- "Remote Code Execution or File Write "
762
- "vulnerability in the containerized application "
763
- "or service."
818
+ "prioritize the vulnerabilities with 'Flagged weakness' under insights."
764
819
  )
765
820
  rmessage += (
766
821
  "\nVulnerabilities in Linux Kernel packages can "
@@ -817,10 +872,13 @@ Below are the vulnerabilities prioritized by depscan. Follow your team's remedia
817
872
  (k, v) for v, k in sorted_reached_purls
818
873
  )
819
874
  rsection = Markdown(
820
- """## Proactive Measures
875
+ """
876
+ Proactive Measures
877
+ ------------------
821
878
 
822
879
  Below are the top reachable packages identified by depscan. Setup alerts and notifications to actively monitor these packages for new vulnerabilities and exploits.
823
- """
880
+ """,
881
+ justify="left",
824
882
  )
825
883
  console.print(rsection)
826
884
  rtable = Table(
@@ -840,6 +898,99 @@ Below are the top reachable packages identified by depscan. Setup alerts and not
840
898
  return pkg_vulnerabilities, pkg_group_rows
841
899
 
842
900
 
901
+ def get_version_range(package_issue, purl):
902
+ """
903
+ Generates a version range object for inclusion in the vdr file.
904
+
905
+ :param package_issue: Vulnerability data dict
906
+ :param purl: Package URL string
907
+
908
+ :return: A list containing a dictionary with version range information.
909
+ """
910
+ new_prop = {}
911
+ if (affected_location := package_issue.get("affected_location")) and (
912
+ affected_version := affected_location.get("version")
913
+ ):
914
+ try:
915
+ ppurl = PackageURL.from_string(purl)
916
+ new_prop = {
917
+ "name": "affectedVersionRange",
918
+ "value": f"{ppurl.name}@" f"{affected_version}",
919
+ }
920
+ if ppurl.namespace:
921
+ new_prop["value"] = f'{ppurl.namespace}/{new_prop["value"]}'
922
+ except ValueError:
923
+ ppurl = purl.split("@")
924
+ if len(ppurl) == 2:
925
+ new_prop = {
926
+ "name": "affectedVersionRange",
927
+ "value": f"{ppurl[0]}@{affected_version}",
928
+ }
929
+
930
+ return new_prop
931
+
932
+
933
+ def cvss_to_vdr_rating(vuln_occ_dict):
934
+ """
935
+ Generates a rating object for inclusion in the vdr file.
936
+
937
+ :param vuln_occ_dict: Vulnerability data
938
+
939
+ :return: A list containing a dictionary with CVSS score information.
940
+ """
941
+ cvss_score = vuln_occ_dict.get("cvss_score", 2.0)
942
+ with contextlib.suppress(ValueError, TypeError):
943
+ cvss_score = float(cvss_score)
944
+ if (pkg_severity := vuln_occ_dict.get("severity", "").lower()) not in (
945
+ "critical",
946
+ "high",
947
+ "medium",
948
+ "low",
949
+ "info",
950
+ "none",
951
+ ):
952
+ pkg_severity = "unknown"
953
+ ratings = [
954
+ {
955
+ "score": cvss_score,
956
+ "severity": pkg_severity,
957
+ }
958
+ ]
959
+ method = "31"
960
+ if vuln_occ_dict.get("cvss_v3") and (
961
+ vector_string := vuln_occ_dict["cvss_v3"].get("vector_string")
962
+ ):
963
+ ratings[0]["vector"] = vector_string
964
+ with contextlib.suppress(CVSSError):
965
+ method = cvss.CVSS3(vector_string).as_json().get("version")
966
+ method = method.replace(".", "").replace("0", "")
967
+ ratings[0]["method"] = f"CVSSv{method}"
968
+
969
+ return ratings
970
+
971
+
972
+ def split_cwe(cwe):
973
+ """
974
+ Split the given CWE string into a list of CWE IDs.
975
+
976
+ :param cwe: The problem issue taken from a vulnerability object
977
+
978
+ :return: A list of CWE IDs
979
+ :rtype: list
980
+ """
981
+ cwe_ids = []
982
+
983
+ if isinstance(cwe, str):
984
+ cwe_ids = re.findall(CWE_SPLITTER, cwe)
985
+ elif isinstance(cwe, list):
986
+ cwes = "|".join(cwe)
987
+ cwe_ids = re.findall(CWE_SPLITTER, cwes)
988
+
989
+ with contextlib.suppress(ValueError, TypeError):
990
+ cwe_ids = [int(cwe_id) for cwe_id in cwe_ids]
991
+ return cwe_ids
992
+
993
+
843
994
  def summary_stats(results):
844
995
  """
845
996
  Generate summary stats
@@ -875,7 +1026,8 @@ def jsonl_report(
875
1026
  reached_purls,
876
1027
  ):
877
1028
  """
878
- Produce vulnerability occurrence report in jsonlines format
1029
+ DEPRECATED: Produce vulnerability occurrence report in jsonlines format
1030
+ This method should use the pkg_vulnerabilities from prepare_vdr
879
1031
 
880
1032
  :param scoped_pkgs: A dict of lists of required/optional/excluded packages.
881
1033
  :param sug_version_dict: A dict mapping package names to suggested versions.
@@ -884,6 +1036,8 @@ def jsonl_report(
884
1036
  :param results: List of vulnerabilities found
885
1037
  :param pkg_aliases: Package alias
886
1038
  :param out_file_name: Output filename
1039
+ :param direct_purls: A list of direct purls
1040
+ :param reached_purls: A list of reached purls
887
1041
  """
888
1042
  ids_seen = {}
889
1043
  required_pkgs = scoped_pkgs.get("required", [])
@@ -916,18 +1070,15 @@ def jsonl_report(
916
1070
  version_used = package_issue["affected_location"].get("version")
917
1071
  purl = purl_aliases.get(full_pkg, full_pkg)
918
1072
  if purl:
919
- try:
920
- purl_obj = parse_purl(purl)
921
- if purl_obj:
922
- version_used = purl_obj.get("version")
923
- if purl_obj.get("namespace"):
924
- full_pkg = f"""{purl_obj.get("namespace")}/
925
- {purl_obj.get("name")}@{purl_obj.get("version")}"""
926
- else:
927
- full_pkg = f"""{purl_obj.get("name")}@{purl_obj
928
- .get("version")}"""
929
- except Exception:
930
- pass
1073
+ purl_obj = parse_purl(purl)
1074
+ if purl_obj:
1075
+ version_used = purl_obj.get("version")
1076
+ if purl_obj.get("namespace"):
1077
+ full_pkg = f"""{purl_obj.get("namespace")}/
1078
+ {purl_obj.get("name")}@{purl_obj.get("version")}"""
1079
+ else:
1080
+ full_pkg = f"""{purl_obj.get("name")}@{purl_obj
1081
+ .get("version")}"""
931
1082
  if ids_seen.get(vid + purl):
932
1083
  continue
933
1084
  # On occasions, this could still result in duplicates if the
@@ -1143,6 +1294,7 @@ def suggest_version(results, pkg_aliases=None, purl_aliases=None):
1143
1294
 
1144
1295
  :param results: List of package issue objects or dicts
1145
1296
  :param pkg_aliases: Dict of package names and aliases
1297
+ :param purl_aliases: Dict of purl names and aliases
1146
1298
  :return: Dict mapping each package to its suggested version
1147
1299
  """
1148
1300
  pkg_fix_map = {}
@@ -1166,7 +1318,6 @@ def suggest_version(results, pkg_aliases=None, purl_aliases=None):
1166
1318
  f"{package_issue.affected_location.vendor}:"
1167
1319
  f"{package_issue.affected_location.package}"
1168
1320
  )
1169
- version = None
1170
1321
  if matched_by:
1171
1322
  version = matched_by.split("|")[-1]
1172
1323
  full_pkg = full_pkg + ":" + version
@@ -1176,9 +1327,9 @@ def suggest_version(results, pkg_aliases=None, purl_aliases=None):
1176
1327
  else:
1177
1328
  full_pkg = pkg_aliases.get(full_pkg, full_pkg)
1178
1329
  version_upgrades = pkg_fix_map.get(full_pkg, set())
1179
- if (
1180
- fixed_location != placeholder_fix_version
1181
- and fixed_location != placeholder_exclude_version
1330
+ if fixed_location not in (
1331
+ placeholder_fix_version,
1332
+ placeholder_exclude_version,
1182
1333
  ):
1183
1334
  version_upgrades.add(fixed_location)
1184
1335
  pkg_fix_map[full_pkg] = version_upgrades
@@ -1261,6 +1412,8 @@ def classify_links(related_urls):
1261
1412
  clinks["Bug Bounty"] = rurl
1262
1413
  elif "cwe.mitre.org" in rurl:
1263
1414
  clinks["cwe"] = rurl
1415
+ else:
1416
+ clinks["other"] = rurl
1264
1417
  return clinks
1265
1418
 
1266
1419
 
@@ -1268,12 +1421,16 @@ def find_purl_usages(bom_file, src_dir, reachables_slices_file):
1268
1421
  """
1269
1422
  Generates a list of reachable elements based on the given BOM file.
1270
1423
 
1271
- :param bom_file (str): The path to the BOM file.
1272
- :param src_dir (str): Source directory
1424
+ :param bom_file: The path to the BOM file.
1425
+ :type bom_file: str
1426
+ :param src_dir: Source directory
1427
+ :type src_dir: str
1273
1428
  :param reachables_slices_file: Path to the reachables slices file
1429
+ :type reachables_slices_file: str
1274
1430
 
1275
- :return: Tuple of direct_purls and reached_purls based on the occurrence and callstack evidences from the BOM.
1276
- If reachables slices json were found, the file would be read first.
1431
+ :return: Tuple of direct_purls and reached_purls based on the occurrence and
1432
+ callstack evidences from the BOM. If reachables slices json were
1433
+ found, the file is read first.
1277
1434
  """
1278
1435
  direct_purls = defaultdict(int)
1279
1436
  reached_purls = defaultdict(int)
@@ -1297,7 +1454,6 @@ def find_purl_usages(bom_file, src_dir, reachables_slices_file):
1297
1454
 
1298
1455
  for c in data["components"]:
1299
1456
  purl = c["purl"]
1300
- if c.get("evidence"):
1301
- if c["evidence"].get("occurrences"):
1302
- direct_purls[purl] += len(c["evidence"].get("occurrences"))
1457
+ if c.get("evidence") and c["evidence"].get("occurrences"):
1458
+ direct_purls[purl] += len(c["evidence"].get("occurrences"))
1303
1459
  return dict(direct_purls), dict(reached_purls)