Skip to content

Commit c421fe1

Browse files
committed
Refactor ImpactedPackage and ResolvedPackage into one Vulnerability_Package_Relation
Changes are made in import_runner.py to refactore the models. Other major changes in import_runner.py is to change the _process_updated_advisories method to use less db queries and increase performance by several times. Signed-off-by: Shivam Sandbhor <shivam.sandbhor@gmail.com>
1 parent 58d0376 commit c421fe1

4 files changed

Lines changed: 166 additions & 132 deletions

File tree

vulnerabilities/admin.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,9 @@
2424
from django.contrib import admin
2525

2626
from vulnerabilities.models import (
27-
ImpactedPackage,
27+
Vulnerability_Package_Relation,
2828
Importer,
2929
Package,
30-
ResolvedPackage,
3130
Vulnerability,
3231
VulnerabilityReference,
3332
)
@@ -48,13 +47,8 @@ class PackageAdmin(admin.ModelAdmin):
4847
pass
4948

5049

51-
@admin.register(ImpactedPackage)
52-
class ImpactedPackageAdmin(admin.ModelAdmin):
53-
pass
54-
55-
56-
@admin.register(ResolvedPackage)
57-
class ResolvedPackageAdmin(admin.ModelAdmin):
50+
@admin.register(Vulnerability_Package_Relation)
51+
class Vulnerability_Package_RelationAdmin(admin.ModelAdmin):
5852
pass
5953

6054

vulnerabilities/import_runner.py

Lines changed: 148 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,16 @@
2424
import dataclasses
2525
import datetime
2626
import logging
27+
from itertools import chain
2728
from typing import Dict
2829
from typing import List
2930
from typing import Set
3031
from typing import Tuple
3132

3233
import packageurl
3334
from django.db import DataError
35+
#remove this
36+
from django.db import connection
3437

3538
from vulnerabilities import models
3639
from 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+
87206
def _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-
145222
def _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

183260
def _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

205274
def _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

250318
def _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

Comments
 (0)