2424import dataclasses
2525import datetime
2626import logging
27- from itertools import chain
2827from typing import Dict
2928from typing import List
3029from typing import Set
4948
5049
5150@dataclasses .dataclass (frozen = True )
52- class VulnerabilityReference_inserter :
51+ class VulnerabilityReferenceInserter :
5352 vulnerability : models .Vulnerability
5453 reference_id : Optional [str ] = ''
5554 url : Optional [str ] = ''
5655
5756 def __post_init__ (self ):
5857 if not any ([self .reference_id , self .url ]):
5958 raise TypeError (
60- "VulnerabilityReference_inserter expects either reference_id or url" )
59+ "VulnerabilityReferenceInserter expects either reference_id or url" )
6160
6261 def to_model_object (self ):
6362 return models .VulnerabilityReference (** dataclasses .asdict (self ))
@@ -69,7 +68,7 @@ def to_model_object(self):
6968
7069
7170@dataclasses .dataclass (frozen = True )
72- class PackageRelatedVulnerability_inserter :
71+ class PackageRelatedVulnerabilityInserter :
7372 vulnerability : models .Vulnerability
7473 is_vulnerable : bool
7574 package : models .Package
@@ -111,28 +110,22 @@ def run(self, cutoff_date: datetime.datetime = None) -> None:
111110 from all Linux distributions that package this kernel version.
112111 """
113112 logger .debug (f'Starting import for { self .importer .name } .' )
114- data_source = self .importer .make_data_source (
115- self .batch_size , cutoff_date = cutoff_date )
113+ data_source = self .importer .make_data_source (self .batch_size , cutoff_date = cutoff_date )
116114 with data_source :
117115 _process_added_advisories (data_source )
118116 _process_updated_advisories (data_source )
119- self .importer .last_run = datetime .datetime .now (
120- tz = datetime .timezone .utc )
117+ self .importer .last_run = datetime .datetime .now (tz = datetime .timezone .utc )
121118 self .importer .data_source_cfg = dataclasses .asdict (data_source .config )
122119 self .importer .save ()
123120
124121 logger .debug (f'Successfully finished import for { self .importer .name } .' )
125122
126123
127124def _process_updated_advisories (data_source : DataSource ) -> None :
128- """
129- TODO: Break this method into smaller functions
130- """
131125 bulk_create_vuln_refs = set ()
132126 bulk_create_vuln_pkg_refs = set ()
133127 for batch in data_source .updated_advisories ():
134128 for advisory in batch :
135-
136129 vuln , vuln_created , references = _create_vulnerability_and_references (
137130 advisory )
138131 bulk_create_vuln_refs .update (references )
@@ -142,6 +135,11 @@ def _process_updated_advisories(data_source: DataSource) -> None:
142135 vuln , vuln_created , advisory .resolved_package_urls , is_vulnerable = False )
143136 bulk_create_vuln_pkg_refs .update (inew_refs .union (rnew_refs ))
144137
138+ # FIXME: _create_pkg_vuln_refs handles conflicts between the data we encounter via
139+ # updated_advisories() and the data which already exists in the DB. It is not designed
140+ # to handle the conflicting data present within the entries of updated_advisories() itself.
141+ # This can be done by filtering bulk_create_vuln_pkg_refs for pairs (vulnerability, package)
142+ # occurring more than once. Also update the constraints in the models after this is fixed.
145143 models .VulnerabilityReference .objects .bulk_create (
146144 [i .to_model_object () for i in bulk_create_vuln_refs ])
147145 models .PackageRelatedVulnerability .objects .bulk_create (
@@ -170,63 +168,68 @@ def _create_vulnerability_and_references(advisory: Advisory):
170168 vuln_references = set ()
171169
172170 if vuln_created :
173- # This means vulnerability didn't previously exist in the db, so bulk create
174- # is used without any hesitation
175- for id_ in set (advisory .reference_ids ):
176- vuln_references .add (VulnerabilityReference_inserter (
177- vulnerability = vuln , reference_id = id_ ))
171+ # This means vulnerability didn't previously exist in the DB, so add
172+ # the references to bulk create queue without any hesitation
173+ for id_ in advisory .reference_ids :
174+ vuln_references .add (VulnerabilityReferenceInserter (vulnerability = vuln , reference_id = id_ )) # nopep8
178175
179- for url in set (advisory .reference_urls ):
180- vuln_references .add (VulnerabilityReference_inserter (
181- vulnerability = vuln , url = url ))
176+ for url in advisory .reference_urls :
177+ vuln_references .add (VulnerabilityReferenceInserter (vulnerability = vuln , url = url ))
182178
183179 else :
184- vuln_refs_qs = models .VulnerabilityReference .objects .filter (
185- vulnerability = vuln )
180+ vuln_refs_qs = models .VulnerabilityReference .objects .filter (vulnerability = vuln )
186181 vuln_ids = {ref .reference_id for ref in vuln_refs_qs }
187182 vuln_urls = {ref .url for ref in vuln_refs_qs }
188183
189184 for id_ in advisory .reference_ids :
190185 # Add the item preventing duplicates pass to through.
191186 if id_ not in vuln_ids :
192187 vuln_ids .add (id_ )
193- vuln_references .add (VulnerabilityReference_inserter (
188+ vuln_references .add (VulnerabilityReferenceInserter (
194189 vulnerability = vuln , reference_id = id_ ))
195190
196191 for url in advisory .reference_urls :
197192 # Add the item preventing duplicates pass to through.
198193 if url not in vuln_urls :
199194 vuln_urls .add (url )
200- vuln_references .add (VulnerabilityReference_inserter (
195+ vuln_references .add (VulnerabilityReferenceInserter (
201196 vulnerability = vuln , url = url ))
202197
203198 return vuln , vuln_created , vuln_references
204199
205200
206201def _create_pkg_vuln_refs (vuln : models .Vulnerability , vuln_created : bool , purls : Sequence [PackageURL ], is_vulnerable : bool ): # nopep8
207- new_refs , updated_refs = set (), set ()
202+ new_refs = set ()
208203 for purl in purls :
209204 pkg , pkg_created = _get_or_create_package (purl )
210- vuln_pkg_ref = PackageRelatedVulnerability_inserter (
211- package = pkg , vulnerability = vuln , is_vulnerable = is_vulnerable )
205+ vuln_pkg_ref = PackageRelatedVulnerabilityInserter (
206+ package = pkg , vulnerability = vuln , is_vulnerable = is_vulnerable )
207+
212208 if pkg_created or vuln_created :
213209 new_refs .add (vuln_pkg_ref )
214210
215211 else :
216- qs = models .PackageRelatedVulnerability .objects .filter (
212+ existing_pkg_vuln_refs = models .PackageRelatedVulnerability .objects .filter (
217213 package = pkg , vulnerability = vuln )
218- if not qs :
214+ if not existing_pkg_vuln_refs :
219215 new_refs .add (vuln_pkg_ref )
220216 else :
221217 # Note: PackageRelatedVulnerability has constraints
222218 # unique_together = ('package', 'vulnerability', 'is_vulnerable')
223- # This fact is used below.
224- vuln_impact = {i .is_vulnerable for i in qs }
225- if len (vuln_impact ) == 2 or is_vulnerable not in vuln_impact :
226- conflicts = [i for i in qs ]
219+ vuln_impact = {i .is_vulnerable for i in existing_pkg_vuln_refs }
220+ # is_vulnerable is a boolean which indicates the relationship of
221+ # a vulnerability's impact on a package. vuln_impact is a set of
222+ # all such booleans for a pair of (vulnerability, package). In cases
223+ # where vuln_impact == {True, False}, we know that conflicting relationships
224+ # of (vulnerability, package) ALREADY EXIST in the DB.
225+ # The other check `is_vulnerable not in vuln_impact` is used to know whether the
226+ # data we just found is not conflicting with the data already existing in DB.
227+ # In any of the above two cases we move the entries involved in ImportProblem
228+ if vuln_impact == {True , False } or is_vulnerable not in vuln_impact :
229+ conflicts = existing_pkg_vuln_refs [:]
227230 conflicts .append (vuln_pkg_ref .to_model_object ())
228231 handle_conflicts (conflicts )
229- qs .delete ()
232+ existing_pkg_vuln_refs .delete ()
230233
231234 return new_refs
232235
@@ -278,28 +281,32 @@ def _get_or_create_package(p: PackageURL) -> Tuple[models.Package, bool]:
278281
279282
280283def _bulk_insert_packages (
281- impacted : List [PackageURL ],
282- resolved : List [PackageURL ]
283- ) -> Tuple [Dict [PackageURL , int ], Dict [PackageURL , int ]]:
284+ impacted : Set [PackageURL ],
285+ resolved : Set [PackageURL ]
286+ ) -> Tuple [Dict [PackageURL , models .Package ], Dict [PackageURL , models .Package ]]:
287+
288+ packages = [_package_url_to_package (p ) for p in impacted .union (resolved )]
289+ packages = models .Package .objects .bulk_create (packages )
290+
291+ impacted_packages , resolved_packages = {}, {}
284292
285- impacted_packages = models .Package .objects .bulk_create (
286- [_package_url_to_package (p ) for p in impacted ])
287- resolved_packages = models .Package .objects .bulk_create (
288- [_package_url_to_package (p ) for p in resolved ])
293+ for pkg in packages :
294+ # Unfortunately, PackageURLMixin.package_url returns a string, not a PackageURL
295+ purl = PackageURL .from_string (pkg .package_url )
289296
290- impacted_packages = dict (
291- zip ( impacted , [ pkg . id for pkg in impacted_packages ]))
292- resolved_packages = dict (
293- zip ( resolved , [ pkg . id for pkg in resolved_packages ]))
297+ if purl in impacted :
298+ impacted_packages [ purl ] = pkg
299+ elif purl in resolved :
300+ resolved_packages [ purl ] = pkg
294301
295302 return impacted_packages , resolved_packages
296303
297304
298305def _bulk_insert_impacted_and_resolved_packages (
299306 batch : Set [Advisory ],
300307 vulnerabilities : Set [models .Vulnerability ],
301- impacted_packages : Dict [PackageURL , int ],
302- resolved_packages : Dict [PackageURL , int ],
308+ impacted_packages : Dict [PackageURL , models . Package ],
309+ resolved_packages : Dict [PackageURL , models . Package ],
303310) -> None :
304311
305312 refs : List [models .ImpactedPackage ] = []
@@ -313,11 +320,11 @@ def _bulk_insert_impacted_and_resolved_packages(
313320 if not p :
314321 p = _package_url_to_package (impacted_purl )
315322 p .save ()
316- impacted_packages [impacted_purl ] = p . id
323+ impacted_packages [impacted_purl ] = p
317324
318325 ip = models .PackageRelatedVulnerability (
319326 vulnerability = vuln ,
320- package_id = p ,
327+ package = p ,
321328 is_vulnerable = True
322329 )
323330 refs .append (ip )
@@ -328,11 +335,11 @@ def _bulk_insert_impacted_and_resolved_packages(
328335 if not p :
329336 p = _package_url_to_package (resolved_purl )
330337 p .save ()
331- resolved_packages [resolved_purl ] = p . id
338+ resolved_packages [resolved_purl ] = p
332339
333340 ip = models .PackageRelatedVulnerability (
334341 vulnerability = vuln ,
335- package_id = p ,
342+ package = p ,
336343 is_vulnerable = False
337344 )
338345 refs .append (ip )
@@ -390,12 +397,12 @@ def _advisory_to_vulnerability(
390397 raise RuntimeError (f'No Vulnerability model object found for this Advisory: { advisory .summary } ' )
391398
392399
393- def _collect_package_urls (batch : Set [Advisory ]) -> Tuple [List [PackageURL ], List [PackageURL ]]:
394- impacted , resolved = [], []
400+ def _collect_package_urls (batch : Set [Advisory ]) -> Tuple [Set [PackageURL ], Set [PackageURL ]]:
401+ impacted , resolved = set (), set ()
395402
396403 for advisory in batch :
397- impacted .extend (advisory .impacted_package_urls )
398- resolved .extend (advisory .resolved_package_urls )
404+ impacted .update (advisory .impacted_package_urls )
405+ resolved .update (advisory .resolved_package_urls )
399406
400407 return impacted , resolved
401408
0 commit comments