@@ -32,19 +32,106 @@ def extract_reference_link(text):
32
32
match = re .search (r"(https?://[^\s)]+)" , text )
33
33
return match .group (1 ) if match else None
34
34
35
- finding_description = (
36
- f"**Library Name**: { lib_name } \n "
37
- f"**Library Version**: { lib_version } \n "
38
- f"**Library Path**: { lib_path } \n "
39
- f"**Vulnerability Name**: { vuln_name } \n "
40
- f"**Fixed Version**: { fixed_version } \n "
41
- f"**Source**: { source } \n "
42
- f"**Description**: { description } \n "
43
- f"**Score**: { score } \n "
44
- f"**Exploitability Score**: { exploitability_score } \n "
45
- f"**Has Exploit**: { has_exploit } \n "
46
- f"**Has CISA KEV Exploit**: { has_cisa_kev_exploit } \n "
47
- )
35
+ @staticmethod
36
+ def _generate_unique_id (components : list ) -> str :
37
+ """
38
+ Generates a stable unique ID for findings.
39
+
40
+ Args:
41
+ components: List of components to use for ID generation
42
+
43
+ """
44
+ # Filter out None and empty values
45
+ filtered_components = [str (c ).strip () for c in components if c is not None and str (c ).strip ()]
46
+
47
+ # Sort components for consistent order regardless of input order
48
+ filtered_components = sorted (filtered_components )
49
+
50
+ id_string = "|" .join (filtered_components )
51
+ hash_object = hashlib .sha256 (id_string .encode ("utf-8" ))
52
+ return hash_object .hexdigest ()
53
+
54
+ @staticmethod
55
+ def parse_libraries (libraries_data , test ):
56
+ """
57
+ Parses library vulnerability data into granular DefectDojo findings.
58
+ Creates one finding per unique vulnerability (CVE/ID) per library instance (name/version/path).
59
+ """
60
+ findings_list = []
61
+ if not libraries_data :
62
+ return findings_list
63
+
64
+ for lib_item in libraries_data :
65
+ lib_name = lib_item .get ("name" , "N/A" )
66
+ lib_version = lib_item .get ("version" , "N/A" )
67
+ lib_path = lib_item .get ("path" , "N/A" )
68
+ lib_line = lib_item .get ("startLine" )
69
+
70
+ vulnerabilities_in_lib_instance = lib_item .get ("vulnerabilities" , [])
71
+ if not vulnerabilities_in_lib_instance :
72
+ continue
73
+
74
+ for vuln_data in vulnerabilities_in_lib_instance :
75
+ vuln_name = vuln_data .get ("name" , "N/A" )
76
+ severity_str = vuln_data .get ("severity" )
77
+ severity = WizcliParsers .get_severity (severity_str )
78
+ fixed_version = vuln_data .get ("fixedVersion" )
79
+ source_url = vuln_data .get ("source" , "N/A" )
80
+ vuln_description_from_wiz = vuln_data .get ("description" )
81
+ score_str = vuln_data .get ("score" )
82
+ has_exploit = vuln_data .get ("hasExploit" , False )
83
+ has_cisa_kev_exploit = vuln_data .get ("hasCisaKevExploit" , False )
84
+
85
+ title = f"{ lib_name } { lib_version } - { vuln_name } "
86
+
87
+ description_parts = [
88
+ f"**Vulnerability**: `{ vuln_name } `" ,
89
+ f"**Severity**: { severity } " ,
90
+ f"**Library**: `{ lib_name } `" ,
91
+ f"**Version**: `{ lib_version } `" ,
92
+ f"**Path/Manifest**: `{ lib_path } `" ,
93
+ ]
94
+ if lib_line is not None :
95
+ description_parts .append (f"**Line in Manifest**: { lib_line } " )
96
+
97
+ if fixed_version :
98
+ description_parts .append (f"**Fixed Version**: { fixed_version } " )
99
+ mitigation = f"Update `{ lib_name } ` to version `{ fixed_version } ` or later in path/manifest `{ lib_path } `."
100
+ else :
101
+ description_parts .append ("**Fixed Version**: N/A" )
102
+ mitigation = f"No fixed version available from Wiz. Investigate `{ vuln_name } ` for `{ lib_name } ` in `{ lib_path } ` and apply vendor guidance or risk acceptance."
103
+
104
+ description_parts .append (f"**Source**: { source_url } " )
105
+ if vuln_description_from_wiz :
106
+ description_parts .append (f"\n **Details from Wiz**:\n { vuln_description_from_wiz } \n " )
107
+ if score_str is not None :
108
+ description_parts .append (f"**CVSS Score (from Wiz)**: { score_str } " )
109
+ description_parts .extend ([
110
+ f"**Has Exploit (Known)**: { has_exploit } " ,
111
+ f"**In CISA KEV**: { has_cisa_kev_exploit } " ,
112
+ ])
113
+
114
+ failed_policies = vuln_data .get ("failedPolicyMatches" , [])
115
+ if failed_policies :
116
+ description_parts .append ("\n **Failed Policies**:" )
117
+ for match in failed_policies :
118
+ policy = match .get ("policy" , {})
119
+ description_parts .append (f"- { policy .get ('name' , 'N/A' )} (ID: { policy .get ('id' , 'N/A' )} )" )
120
+ ignored_policies = vuln_data .get ("ignoredPolicyMatches" , [])
121
+ if ignored_policies :
122
+ description_parts .append ("\n **Ignored Policies**:" )
123
+ for match in ignored_policies :
124
+ policy = match .get ("policy" , {})
125
+ reason = match .get ("ignoreReason" , "N/A" )
126
+ description_parts .append (f"- { policy .get ('name' , 'N/A' )} (ID: { policy .get ('id' , 'N/A' )} ), Reason: { reason } " )
127
+
128
+ full_description = "\n " .join (description_parts )
129
+ references = source_url if source_url != "N/A" else None
130
+
131
+ # Generate unique ID using stable components including file path
132
+ unique_id = WizcliParsers ._generate_unique_id (
133
+ [lib_name , lib_version , vuln_name , lib_path ],
134
+ )
48
135
49
136
finding = Finding (
50
137
title = f"{ lib_name } - { vuln_name } " ,
0 commit comments