2424import dataclasses
2525import datetime
2626import logging
27+ from itertools import chain
2728from typing import Dict
2829from typing import List
2930from typing import Set
3031from typing import Tuple
3132
3233import packageurl
3334from django .db import DataError
35+ #remove this
36+ from django .db import connection
3437
3538from vulnerabilities import models
3639from vulnerabilities .data_source import Advisory , DataSource
@@ -58,6 +61,46 @@ class ImportRunner:
5861 def __init__ (self , importer : models .Importer , batch_size : int ):
5962 self .importer = importer
6063 self .batch_size = batch_size
64+ self ._bulk_create_vuln_pkg_refs = []
65+ self ._bulk_update_vuln_pkg_refs = []
66+ self ._bulk_create_vuln_refs = []
67+
68+ def __del__ (self ):
69+ models .Vulnerability_Package_Relation .objects .bulk_create (self ._bulk_create_vuln_pkg_refs , ignore_conflicts = True )
70+ models .Vulnerability_Package_Relation .objects .bulk_update (self ._bulk_update_vuln_pkg_refs ,['is_vulnerable' ])
71+ models .VulnerabilityReference .objects .bulk_create (self ._bulk_create_vuln_refs , ignore_conflicts = True )
72+
73+ # TODO: Currently the implementation may contain duplicate model instances in `_bulk_create_vuln_refs` and `_bulk_create_vuln_pkg_refs`
74+ # to make this work , `ignore_conflicts=True` is used. Find some way to remove duplicates of model instances(before writing them db)
75+ # and remove the `ignore_conflicts=True` flag.
76+ @property
77+ def bulk_create_vuln_pkg_refs (self ):
78+
79+ if len (self ._bulk_create_vuln_pkg_refs ) >= self .batch_size :
80+ models .Vulnerability_Package_Relation .objects .bulk_create (self ._bulk_create_vuln_pkg_refs , ignore_conflicts = True )
81+ self ._bulk_create_vuln_pkg_refs = []
82+
83+ return self ._bulk_create_vuln_pkg_refs
84+
85+
86+ @property
87+ def bulk_update_vuln_pkg_refs (self ):
88+
89+ if len (self ._bulk_update_vuln_pkg_refs ) >= self .batch_size :
90+ models .Vulnerability_Package_Relation .objects .bulk_update (self ._bulk_update_vuln_pkg_refs ,['is_vulnerable' ])
91+ self ._bulk_update_vuln_pkg_refs = []
92+
93+ return self ._bulk_update_vuln_pkg_refs
94+
95+ @property
96+ def bulk_create_vuln_refs (self ):
97+
98+ if len (self ._bulk_create_vuln_refs ) >= self .batch_size :
99+ models .VulnerabilityReference .objects .bulk_create (self ._bulk_create_vuln_refs , ignore_conflicts = True )
100+ self ._bulk_create_vuln_refs = []
101+
102+ return self ._bulk_create_vuln_refs
103+
61104
62105 def run (self , cutoff_date : datetime .datetime = None ) -> None :
63106 """
@@ -72,18 +115,94 @@ def run(self, cutoff_date: datetime.datetime = None) -> None:
72115 """
73116 logger .debug (f'Starting import for { self .importer .name } .' )
74117 data_source = self .importer .make_data_source (self .batch_size , cutoff_date = cutoff_date )
75-
76118 with data_source :
77119 _process_added_advisories (data_source )
78- _process_updated_advisories (data_source )
79-
120+ self ._process_updated_advisories (data_source )
80121 self .importer .last_run = datetime .datetime .now (tz = datetime .timezone .utc )
81122 self .importer .data_source_cfg = dataclasses .asdict (data_source .config )
82123 self .importer .save ()
83124
84125 logger .debug (f'Successfully finished import for { self .importer .name } .' )
126+
85127
86-
128+ def _process_updated_advisories (self ,data_source : DataSource ) -> None :
129+ """
130+ TODO: Break this method into smaller functions
131+ """
132+
133+ for batch in data_source .updated_advisories ():
134+ for advisory in batch :
135+ vuln , vuln_created = _get_or_create_vulnerability (advisory )
136+
137+ if vuln_created :
138+ # This means vulnerability didn't previously exist in the db, so bulk create
139+ # is used without any hesitation
140+ for id_ in set (advisory .reference_ids ):
141+ self .bulk_create_vuln_refs .append (models .VulnerabilityReference (vulnerability = vuln , reference_id = id_ ))
142+
143+ for url in set (advisory .reference_urls ):
144+ self .bulk_create_vuln_refs .append (models .VulnerabilityReference (vulnerability = vuln , url = url ))
145+
146+ else :
147+ vuln_refs_qs = models .VulnerabilityReference .objects .filter (vulnerability = vuln )
148+ # This is to avoid making additional SELECT queries, and do further filtering in python
149+ # Check https://stackoverflow.com/a/5989530
150+ vuln_ids = {ref .reference_id for ref in vuln_refs_qs }
151+ vuln_urls = {ref .url for ref in vuln_refs_qs }
152+
153+ for id_ in advisory .reference_ids :
154+ if id_ not in vuln_ids :
155+ # Delete the item because it will allow the duplicates pass to through if they are not
156+ # present in vuln_ids
157+ vuln_ids .add (id_ )
158+ self .bulk_create_vuln_refs .append (models .VulnerabilityReference (vulnerability = vuln , reference_id = id_ ))
159+
160+ for url in advisory .reference_urls :
161+ # Delete the item because it will allow the duplicates pass to through if they are not
162+ # present in vuln_urls
163+ if url not in vuln_urls :
164+ vuln_urls .add (url )
165+ self .bulk_create_vuln_refs .append (models .VulnerabilityReference (vulnerability = vuln , url = url ))
166+
167+
168+
169+
170+ for ipurl in advisory .impacted_package_urls :
171+ pkg , pkg_created = _get_or_create_package (ipurl )
172+ vuln_pkgs_ref = models .Vulnerability_Package_Relation (package = pkg ,vulnerability = vuln ,is_vulnerable = True )
173+
174+ if pkg_created or vuln_created :
175+ self .bulk_create_vuln_pkg_refs .append (vuln_pkgs_ref )
176+
177+ else :
178+ qs = models .Vulnerability_Package_Relation .objects .filter (package = pkg ,vulnerability = vuln )
179+ if qs :
180+ if not qs [0 ].is_vulnerable :
181+ qs [0 ].is_vulnerable = True
182+ self .bulk_update_vuln_pkg_refs .append (qs [0 ])
183+
184+ else :
185+ self .bulk_create_vuln_pkg_refs .append (vuln_pkgs_ref )
186+
187+ for rpurl in advisory .resolved_package_urls :
188+ pkg , pkg_created = _get_or_create_package (rpurl )
189+ vuln_pkgs_ref = models .Vulnerability_Package_Relation (package = pkg ,vulnerability = vuln ,is_vulnerable = False )
190+
191+ if pkg_created or vuln_created :
192+ # For a `Vulnerability_Package_Relation` tp exist it needs both, the package and
193+ # vulnerability to already exist in the db.
194+ self .bulk_create_vuln_pkg_refs .append (vuln_pkgs_ref )
195+
196+ else :
197+ qs = models .Vulnerability_Package_Relation .objects .filter (package = pkg ,vulnerability = vuln )
198+ if qs :
199+ if qs [0 ].is_vulnerable :
200+ qs [0 ].is_vulnerable = False
201+ self .bulk_update_vuln_pkg_refs .append (qs [0 ])
202+
203+ else :
204+ self .bulk_create_vuln_pkg_refs .append (vuln_pkgs_ref )
205+
87206def _process_added_advisories (data_source : DataSource ) -> None :
88207 for batch in data_source .added_advisories ():
89208 try :
@@ -100,48 +219,6 @@ def _process_added_advisories(data_source: DataSource) -> None:
100219 logger .exception (e )
101220
102221
103- def _process_updated_advisories (data_source : DataSource ) -> None :
104- """
105- TODO: Make efficient; Current implementation needs way too many DB queries.
106- """
107- for batch in data_source .updated_advisories ():
108- for advisory in batch :
109- vuln , _ = _get_or_create_vulnerability (advisory )
110-
111- for id_ in advisory .reference_ids :
112- models .VulnerabilityReference .objects .get_or_create (
113- vulnerability = vuln , reference_id = id_ )
114-
115- for url in advisory .reference_urls :
116- models .VulnerabilityReference .objects .get_or_create (vulnerability = vuln , url = url )
117-
118- for ipkg_url in advisory .impacted_package_urls :
119- pkg , created = _get_or_create_package (ipkg_url )
120-
121- # FIXME Does not work yet due to cascading deletes.
122- # if not created:
123- # qs = models.ResolvedPackage.objects.filter(
124- # vulnerability_id=vuln.id, package_id=pkg.id)
125- # if qs:
126- # qs[0].delete()
127-
128- models .ImpactedPackage .objects .get_or_create (
129- vulnerability_id = vuln .id , package_id = pkg .id )
130-
131- for rpkg_url in advisory .resolved_package_urls :
132- pkg , created = _get_or_create_package (rpkg_url )
133-
134- # FIXME Does not work yet due to cascading deletes.
135- # if not created:
136- # qs = models.ImpactedPackage.objects.filter(
137- # vulnerability_id=vuln.id, package_id=pkg.id)
138- # if qs:
139- # qs[0].delete()
140-
141- models .ResolvedPackage .objects .get_or_create (
142- vulnerability_id = vuln .id , package_id = pkg .id )
143-
144-
145222def _get_or_create_vulnerability (advisory : Advisory ) -> Tuple [models .Vulnerability , bool ]:
146223 if advisory .cve_id :
147224 query_kwargs = {'cve_id' : advisory .cve_id }
@@ -181,36 +258,27 @@ def _get_or_create_package(p: PackageURL) -> Tuple[models.Package, bool]:
181258
182259
183260def _bulk_insert_packages (
184- impacted : Set [PackageURL ],
185- resolved : Set [PackageURL ]
186- ) -> Tuple [Dict [PackageURL , models .Package ], Dict [PackageURL , models .Package ]]:
187-
188- packages = [_package_url_to_package (p ) for p in impacted .union (resolved )]
189- packages = models .Package .objects .bulk_create (packages )
261+ impacted : List [PackageURL ],
262+ resolved : List [PackageURL ]
263+ ) -> Tuple [Dict [PackageURL , int ], Dict [PackageURL , int ]]:
190264
191- impacted_packages , resolved_packages = {}, {}
265+ impacted_packages = models .Package .objects .bulk_create ([_package_url_to_package (p ) for p in impacted ])
266+ resolved_packages = models .Package .objects .bulk_create ([_package_url_to_package (p ) for p in resolved ])
192267
193- for pkg in packages :
194- # Unfortunately, PackageURLMixin.package_url returns a string, not a PackageURL
195- purl = PackageURL .from_string (pkg .package_url )
196-
197- if purl in impacted :
198- impacted_packages [purl ] = pkg
199- elif purl in resolved :
200- resolved_packages [purl ] = pkg
268+ impacted_packages = dict (zip (impacted , [pkg .id for pkg in impacted_packages ]))
269+ resolved_packages = dict (zip (resolved , [pkg .id for pkg in resolved_packages ]))
201270
202271 return impacted_packages , resolved_packages
203272
204273
205274def _bulk_insert_impacted_and_resolved_packages (
206275 batch : Set [Advisory ],
207276 vulnerabilities : Set [models .Vulnerability ],
208- impacted_packages : Dict [PackageURL , models . Package ],
209- resolved_packages : Dict [PackageURL , models . Package ],
277+ impacted_packages : Dict [PackageURL , int ],
278+ resolved_packages : Dict [PackageURL , int ],
210279) -> None :
211280
212- impacted_refs : List [models .ImpactedPackage ] = []
213- resolved_refs : List [models .ResolvedPackage ] = []
281+ refs : List [models .ImpactedPackage ] = []
214282
215283 for advisory in batch :
216284 vuln = _advisory_to_vulnerability (advisory , vulnerabilities )
@@ -221,31 +289,31 @@ def _bulk_insert_impacted_and_resolved_packages(
221289 if not p :
222290 p = _package_url_to_package (impacted_purl )
223291 p .save ()
224- impacted_packages [impacted_purl ] = p
292+ impacted_packages [impacted_purl ] = p . id
225293
226- ip = models .ImpactedPackage (
294+ ip = models .Vulnerability_Package_Relation (
227295 vulnerability = vuln ,
228- package = p ,
296+ package_id = p ,
297+ is_vulnerable = True
229298 )
230- impacted_refs .append (ip )
299+ refs .append (ip )
231300
232301 for resolved_purl in advisory .resolved_package_urls :
233302 # TODO Figure out when/how it happens that a package is missing from the dict and fix it
234303 p = resolved_packages .get (resolved_purl )
235304 if not p :
236305 p = _package_url_to_package (resolved_purl )
237306 p .save ()
238- resolved_packages [resolved_purl ] = p
307+ resolved_packages [resolved_purl ] = p . id
239308
240- ip = models .ResolvedPackage (
309+ ip = models .Vulnerability_Package_Relation (
241310 vulnerability = vuln ,
242- package = p ,
311+ package_id = p ,
312+ is_vulnerable = False
243313 )
244- resolved_refs .append (ip )
245-
246- models .ImpactedPackage .objects .bulk_create (impacted_refs )
247- models .ResolvedPackage .objects .bulk_create (resolved_refs )
314+ refs .append (ip )
248315
316+ models .Vulnerability_Package_Relation .objects .bulk_create (refs )
249317
250318def _insert_vulnerabilities_and_references (batch : Set [Advisory ]) -> Set [models .Vulnerability ]:
251319 """
@@ -294,12 +362,12 @@ def _advisory_to_vulnerability(
294362 raise RuntimeError (f'No Vulnerability model object found for this Advisory: { advisory .summary } ' )
295363
296364
297- def _collect_package_urls (batch : Set [Advisory ]) -> Tuple [Set [PackageURL ], Set [PackageURL ]]:
298- impacted , resolved = set (), set ()
365+ def _collect_package_urls (batch : Set [Advisory ]) -> Tuple [List [PackageURL ], List [PackageURL ]]:
366+ impacted , resolved = [], []
299367
300368 for advisory in batch :
301- impacted .update (advisory .impacted_package_urls )
302- resolved .update (advisory .resolved_package_urls )
369+ impacted .extend (advisory .impacted_package_urls )
370+ resolved .extend (advisory .resolved_package_urls )
303371
304372 return impacted , resolved
305373
0 commit comments