Skip to content

Commit 989a3e5

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 fe1026a commit 989a3e5

4 files changed

Lines changed: 167 additions & 137 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: 149 additions & 85 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}
@@ -150,11 +227,7 @@ def _get_or_create_vulnerability(advisory: Advisory) -> Tuple[models.Vulnerabili
150227
else:
151228
return models.Vulnerability.objects.create(), True
152229

153-
vuln, created = models.Vulnerability.objects.get_or_create(**query_kwargs)
154-
155-
if not created and vuln.summary != advisory.summary:
156-
vuln.summary = advisory.summary
157-
vuln.save()
230+
vuln, created = models.Vulnerability.objects.update_or_create(**query_kwargs)
158231

159232
return vuln, created
160233

@@ -181,36 +254,27 @@ def _get_or_create_package(p: PackageURL) -> Tuple[models.Package, bool]:
181254

182255

183256
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)
257+
impacted: List[PackageURL],
258+
resolved: List[PackageURL]
259+
) -> Tuple[Dict[PackageURL, int], Dict[PackageURL, int]]:
190260

191-
impacted_packages, resolved_packages = {}, {}
261+
impacted_packages = models.Package.objects.bulk_create([_package_url_to_package(p) for p in impacted])
262+
resolved_packages = models.Package.objects.bulk_create([_package_url_to_package(p) for p in resolved])
192263

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
264+
impacted_packages = dict(zip(impacted, [pkg.id for pkg in impacted_packages]))
265+
resolved_packages = dict(zip(resolved, [pkg.id for pkg in resolved_packages]))
201266

202267
return impacted_packages, resolved_packages
203268

204269

205270
def _bulk_insert_impacted_and_resolved_packages(
206271
batch: Set[Advisory],
207272
vulnerabilities: Set[models.Vulnerability],
208-
impacted_packages: Dict[PackageURL, models.Package],
209-
resolved_packages: Dict[PackageURL, models.Package],
273+
impacted_packages: Dict[PackageURL, int],
274+
resolved_packages: Dict[PackageURL, int],
210275
) -> None:
211276

212-
impacted_refs: List[models.ImpactedPackage] = []
213-
resolved_refs: List[models.ResolvedPackage] = []
277+
refs: List[models.ImpactedPackage] = []
214278

215279
for advisory in batch:
216280
vuln = _advisory_to_vulnerability(advisory, vulnerabilities)
@@ -221,31 +285,31 @@ def _bulk_insert_impacted_and_resolved_packages(
221285
if not p:
222286
p = _package_url_to_package(impacted_purl)
223287
p.save()
224-
impacted_packages[impacted_purl] = p
288+
impacted_packages[impacted_purl] = p.id
225289

226-
ip = models.ImpactedPackage(
290+
ip = models.Vulnerability_Package_Relation(
227291
vulnerability=vuln,
228-
package=p,
292+
package_id=p,
293+
is_vulnerable=True
229294
)
230-
impacted_refs.append(ip)
295+
refs.append(ip)
231296

232297
for resolved_purl in advisory.resolved_package_urls:
233298
# TODO Figure out when/how it happens that a package is missing from the dict and fix it
234299
p = resolved_packages.get(resolved_purl)
235300
if not p:
236301
p = _package_url_to_package(resolved_purl)
237302
p.save()
238-
resolved_packages[resolved_purl] = p
303+
resolved_packages[resolved_purl] = p.id
239304

240-
ip = models.ResolvedPackage(
305+
ip = models.Vulnerability_Package_Relation(
241306
vulnerability=vuln,
242-
package=p,
307+
package_id=p,
308+
is_vulnerable=False
243309
)
244-
resolved_refs.append(ip)
245-
246-
models.ImpactedPackage.objects.bulk_create(impacted_refs)
247-
models.ResolvedPackage.objects.bulk_create(resolved_refs)
310+
refs.append(ip)
248311

312+
models.Vulnerability_Package_Relation.objects.bulk_create(refs)
249313

250314
def _insert_vulnerabilities_and_references(batch: Set[Advisory]) -> Set[models.Vulnerability]:
251315
"""
@@ -294,12 +358,12 @@ def _advisory_to_vulnerability(
294358
raise RuntimeError(f'No Vulnerability model object found for this Advisory: {advisory.summary}')
295359

296360

297-
def _collect_package_urls(batch: Set[Advisory]) -> Tuple[Set[PackageURL], Set[PackageURL]]:
298-
impacted, resolved = set(), set()
361+
def _collect_package_urls(batch: Set[Advisory]) -> Tuple[List[PackageURL], List[PackageURL]]:
362+
impacted, resolved = [], []
299363

300364
for advisory in batch:
301-
impacted.update(advisory.impacted_package_urls)
302-
resolved.update(advisory.resolved_package_urls)
365+
impacted.extend(advisory.impacted_package_urls)
366+
resolved.extend(advisory.resolved_package_urls)
303367

304368
return impacted, resolved
305369

0 commit comments

Comments
 (0)