@@ -69,15 +69,15 @@ def load_json_file(self, filepath: str):
6969 json_file = Path (filepath ).resolve ()
7070
7171 if not json_file .exists ():
72- self .print_stderr (f" Scan settings file not found: { filepath } " )
72+ self .print_stderr (f' Scan settings file not found: { filepath } ' )
7373 self .data = {}
7474
75- with open (json_file , "r" ) as jsonfile :
76- self .print_debug (f" Loading scan settings from: { filepath } " )
75+ with open (json_file , 'r' ) as jsonfile :
76+ self .print_debug (f' Loading scan settings from: { filepath } ' )
7777 try :
7878 self .data = json .load (jsonfile )
7979 except Exception as e :
80- self .print_stderr (f" ERROR: Problem parsing input JSON: { e } " )
80+ self .print_stderr (f' ERROR: Problem parsing input JSON: { e } ' )
8181 return self
8282
8383 def set_file_type (self , file_type : str ):
@@ -91,9 +91,7 @@ def set_file_type(self, file_type: str):
9191 """
9292 self .settings_file_type = file_type
9393 if not self ._is_valid_sbom_file :
94- raise Exception (
95- 'Invalid scan settings file, missing "components" or "bom")'
96- )
94+ raise Exception ('Invalid scan settings file, missing "components" or "bom")' )
9795 return self
9896
9997 def set_scan_type (self , scan_type : str ):
@@ -111,7 +109,7 @@ def _is_valid_sbom_file(self):
111109 Returns:
112110 bool: True if the file is valid, False otherwise
113111 """
114- if not self .data .get (" components" ) or not self .data .get (" bom" ):
112+ if not self .data .get (' components' ) or not self .data .get (' bom' ):
115113 return False
116114 return True
117115
@@ -122,46 +120,56 @@ def _get_bom(self):
122120 dict: If using scanoss.json
123121 list: If using SBOM.json
124122 """
125- if self .settings_file_type == " legacy" :
123+ if self .settings_file_type == ' legacy' :
126124 if isinstance (self .data , list ):
127125 return self .data
128- elif isinstance (self .data , dict ) and self .data .get (" components" ):
129- return self .data .get (" components" )
126+ elif isinstance (self .data , dict ) and self .data .get (' components' ):
127+ return self .data .get (' components' )
130128 else :
131129 return []
132- return self .data .get (" bom" , {})
130+ return self .data .get (' bom' , {})
133131
134132 def get_bom_include (self ) -> List [BomEntry ]:
135133 """Get the list of components to include in the scan
136134
137135 Returns:
138136 list: List of components to include in the scan
139137 """
140- if self .settings_file_type == " legacy" :
138+ if self .settings_file_type == ' legacy' :
141139 return self ._get_bom ()
142- return self ._get_bom ().get (" include" , [])
140+ return self ._get_bom ().get (' include' , [])
143141
144142 def get_bom_remove (self ) -> List [BomEntry ]:
145143 """Get the list of components to remove from the scan
146144
147145 Returns:
148146 list: List of components to remove from the scan
149147 """
150- if self .settings_file_type == " legacy" :
148+ if self .settings_file_type == ' legacy' :
151149 return self ._get_bom ()
152- return self ._get_bom ().get ("remove" , [])
150+ return self ._get_bom ().get ('remove' , [])
151+
152+ def get_bom_replace (self ) -> List [BomEntry ]:
153+ """Get the list of components to replace in the scan
154+
155+ Returns:
156+ list: List of components to replace in the scan
157+ """
158+ if self .settings_file_type == 'legacy' :
159+ return []
160+ return self ._get_bom ().get ('replace' , [])
153161
154162 def get_sbom (self ):
155163 """Get the SBOM to be sent to the SCANOSS API
156164
157165 Returns:
158- dict: SBOM
166+ dict: SBOM request payload
159167 """
160168 if not self .data :
161169 return None
162170 return {
163- " scan_type" : self .scan_type ,
164- " assets" : json .dumps (self ._get_sbom_assets ()),
171+ ' scan_type' : self .scan_type ,
172+ ' assets' : json .dumps (self ._get_sbom_assets ()),
165173 }
166174
167175 def _get_sbom_assets (self ):
@@ -170,8 +178,15 @@ def _get_sbom_assets(self):
170178 Returns:
171179 List: List of SBOM assets
172180 """
173- if self .scan_type == "identify" :
174- return self .normalize_bom_entries (self .get_bom_include ())
181+ if self .scan_type == 'identify' :
182+ include_bom_entries = self ._remove_duplicates (self .normalize_bom_entries (self .get_bom_include ()))
183+ replace_bom_entries = self ._remove_duplicates (self .normalize_bom_entries (self .get_bom_replace ()))
184+ self .print_debug (
185+ f"Scan type set to 'identify'. Adding { len (include_bom_entries ) + len (replace_bom_entries )} components as context to the scan. \n "
186+ f"From Include list: { [entry ['purl' ] for entry in include_bom_entries ]} \n "
187+ f"From Replace list: { [entry ['purl' ] for entry in replace_bom_entries ]} \n "
188+ )
189+ return include_bom_entries + replace_bom_entries
175190 return self .normalize_bom_entries (self .get_bom_remove ())
176191
177192 @staticmethod
@@ -188,7 +203,26 @@ def normalize_bom_entries(bom_entries) -> List[BomEntry]:
188203 for entry in bom_entries :
189204 normalized_bom_entries .append (
190205 {
191- " purl" : entry .get (" purl" , "" ),
206+ ' purl' : entry .get (' purl' , '' ),
192207 }
193208 )
194209 return normalized_bom_entries
210+
211+ @staticmethod
212+ def _remove_duplicates (bom_entries : List [BomEntry ]) -> List [BomEntry ]:
213+ """Remove duplicate BOM entries
214+
215+ Args:
216+ bom_entries (List[Dict]): List of BOM entries
217+
218+ Returns:
219+ List: List of unique BOM entries
220+ """
221+ already_added = set ()
222+ unique_entries = []
223+ for entry in bom_entries :
224+ entry_tuple = tuple (entry .items ())
225+ if entry_tuple not in already_added :
226+ already_added .add (entry_tuple )
227+ unique_entries .append (entry )
228+ return unique_entries
0 commit comments