Skip to content

Commit

Permalink
make UUID constant whenever possible (OSIDB-392)
Browse files Browse the repository at this point in the history
basically whenever the Bugzilla and CVE ID mapping
is unambiguous we consider a potential CVE(s) change
as not resulting in a new flaw (and UUID)

for multi-CVE flaws however this is not always possible
  • Loading branch information
osoukup authored and kgrant-rh committed Feb 13, 2023
1 parent 34fda96 commit 8f38fe1
Show file tree
Hide file tree
Showing 3 changed files with 238 additions and 20 deletions.
75 changes: 55 additions & 20 deletions collectors/bzimport/convertors.py
Original file line number Diff line number Diff line change
Expand Up @@ -773,10 +773,11 @@ def get_comments(self, flaw):
for comment in self.flaw_comments
]

def get_flaw(self, cve_id):
def get_flaw(self, cve_id, full_match=False):
"""get Flaw Django model"""
flaw = Flaw.objects.create_flaw(
bz_id=self.bz_id,
full_match=full_match,
cve_id=cve_id,
type=FlawType.VULNERABILITY,
meta_attr=self.get_meta_attr(cve_id),
Expand Down Expand Up @@ -947,21 +948,31 @@ def get_all_meta(self, flaw):
return meta

def bug2flaws(self):
"""perform flaw bug to flaw models conversion"""
"""
perform flaw bug to flaw models conversion
the tricky part here is the possible CVE change as we do not want to change
the UUID willy-nilly but rather consider it as CVE-only change when possible
the exception is the case of multi-CVE flaw where there are no
guarantees whenever the mapping is not completely unambiguous
"""
logger.debug(f"{self.__class__}: processing flaw bug {self.bz_id}")

# there might be between zero and infinity existing flaws with this BZ ID
existing_flaws = Flaw.objects.filter(meta_attr__bz_id=self.bz_id)

#################
# CVE-less flaw #
#################

if not self.cve_ids:
# remove all flaws with this BZ ID and any CVE ID in case there were
# some CVEs before which got removed as they might have been multiple
# and matching them to a single CVE-less flaw would be nontrivial
# - see the next comment for more details
Flaw.objects.filter(meta_attr__bz_id=self.bz_id).exclude(
cve_id__isnull=True
).delete()
logger.debug(f"{self.__class__}: processing CVE-less flaw")

# remove all flaws with this BZ ID in case there were multiple CVE flaws before
# and got removed as matching them to a single CVE-less flaw would be ambiguous
if existing_flaws.count() > 1:
existing_flaws.delete()

flaw = self.get_flaw(cve_id=None)
return [
Expand All @@ -976,26 +987,50 @@ def bug2flaws(self):
)
]

#############
# CVE flaws #
#############
###################
# single-CVE flaw #
###################

if len(self.cve_ids) == 1:
logger.debug(f"{self.__class__}: processing {self.cve_ids[0]}")

# if there was multiple flaws but now it is only one
# we remove all the previous as the mapping is ambiguous
# except the CVE itself where the mapping is straightforward
if existing_flaws.count() > 1:
existing_flaws.exclude(cve_id=self.cve_ids[0]).delete()

flaw = self.get_flaw(self.cve_ids[0])
return [
FlawSaver(
flaw,
self.get_affects(flaw),
self.get_comments(flaw),
self.get_history(),
self.get_all_meta(flaw),
self.get_trackers(),
self.package_versions,
)
]

##################
# multi-CVE flaw #
##################

flaws = []

# CVE as Bugzilla alias is not persistent unlike BZ ID which is the identifier
# so if it changes or gets removed (both looks the same from OSIDB point of view)
# during the sync there will be that CVE missing in the fetched data
# and to reflect it correctly we should remove corresponding flaw
Flaw.objects.filter(meta_attr__bz_id=self.bz_id).exclude(
cve_id__in=self.cve_ids
).delete()
# as we have multiple flaws now the mapping is always ambiguous
# except the exact CVE match where the mapping is straightforward
existing_flaws.exclude(cve_id__in=self.cve_ids).delete()

# in the past there was possible to have multiple CVEs for a flaw
# but it is no more desired and we create a flaw for every CVE
for cve_id in self.cve_ids:
logger.debug(f"{self.__class__}: processing {cve_id}")

flaw = self.get_flaw(cve_id)
# for multi-CVE flaw we have to perform
# the full match to make it unambiguous
flaw = self.get_flaw(cve_id, full_match=True)
flaws.append(
FlawSaver(
flaw,
Expand Down
182 changes: 182 additions & 0 deletions collectors/bzimport/tests/test_convertors.py
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,7 @@ def test_cve_changed(self):

# changing CVE - bug ID is unchanged
flaw_bug["alias"] = ["CVE-2000-9999"]
flaw_uuid = flaw.uuid

fbc = FlawBugConvertor(
flaw_bug,
Expand All @@ -640,6 +641,59 @@ def test_cve_changed(self):
assert Flaw.objects.count() == 1
flaw = Flaw.objects.first()
assert flaw.cve_id == "CVE-2000-9999"
assert flaw.uuid == flaw_uuid

def test_cve_preserved_and_changed(self):
"""
test that flaw CVE change is correctly reflected
while there is additionally another CVE preserved
"""
flaw_bug = self.get_flaw_bug()
flaw_bug["alias"] = ["CVE-2000-0001", "CVE-2000-0002"]

fbc = FlawBugConvertor(
flaw_bug,
[],
self.get_flaw_history(),
None,
[],
[],
{},
)
flaws = fbc.bug2flaws()
assert not fbc.errors
assert len(flaws) == 2
for flaw in flaws:
flaw.save()

assert Flaw.objects.count() == 2
cve_ids = [flaw.cve_id for flaw in Flaw.objects.all()]
assert sorted(cve_ids) == ["CVE-2000-0001", "CVE-2000-0002"]
# store UUID for later comparison
flaw_uuid = Flaw.objects.get(cve_id="CVE-2000-0001").uuid

# changing CVE - bug ID is unchanged
flaw_bug["alias"] = ["CVE-2000-0001", "CVE-2000-9999"]

fbc = FlawBugConvertor(
flaw_bug,
[],
self.get_flaw_history(),
None,
[],
[],
{},
)
flaws = fbc.bug2flaws()
assert not fbc.errors
assert len(flaws) == 2
for flaw in flaws:
flaw.save()

assert Flaw.objects.count() == 2
cve_ids = [flaw.cve_id for flaw in Flaw.objects.all()]
assert sorted(cve_ids) == ["CVE-2000-0001", "CVE-2000-9999"]
assert flaw_uuid == Flaw.objects.get(cve_id="CVE-2000-0001").uuid

def test_cve_removed(self):
"""
Expand All @@ -666,6 +720,8 @@ def test_cve_removed(self):
assert Flaw.objects.count() == 2
cve_ids = [flaw.cve_id for flaw in Flaw.objects.all()]
assert sorted(cve_ids) == ["CVE-2000-0001", "CVE-2000-0002"]
# store UUID for later comparison
flaw_uuid = Flaw.objects.get(cve_id="CVE-2000-0001").uuid

# removing one of the CVEs
flaw_bug["alias"] = ["CVE-2000-0001"]
Expand All @@ -688,6 +744,7 @@ def test_cve_removed(self):
assert Flaw.objects.count() == 1
flaw = Flaw.objects.first()
assert flaw.cve_id == "CVE-2000-0001"
assert flaw.uuid == flaw_uuid

# removing all CVEs
flaw_bug["alias"] = []
Expand All @@ -710,6 +767,58 @@ def test_cve_removed(self):
assert Flaw.objects.count() == 1
flaw = Flaw.objects.first()
assert flaw.cve_id is None
assert flaw.uuid == flaw_uuid

def test_cves_removed(self):
"""
test that removal of multiple CVEs at once is correctly reflected
"""
flaw_bug = self.get_flaw_bug()
flaw_bug["alias"] = ["CVE-2000-0001", "CVE-2000-0002", "CVE-2000-0003"]

fbc = FlawBugConvertor(
flaw_bug,
[],
self.get_flaw_history(),
None,
[],
[],
{},
)
flaws = fbc.bug2flaws()
assert not fbc.errors
assert len(flaws) == 3
for flaw in flaws:
flaw.save()

assert Flaw.objects.count() == 3
cve_ids = [flaw.cve_id for flaw in Flaw.objects.all()]
assert sorted(cve_ids) == ["CVE-2000-0001", "CVE-2000-0002", "CVE-2000-0003"]
# store UUID for later comparison
flaw_uuid = Flaw.objects.get(cve_id="CVE-2000-0001").uuid

# removing two CVEs at once
flaw_bug["alias"] = ["CVE-2000-0001"]

fbc = FlawBugConvertor(
flaw_bug,
[],
self.get_flaw_history(),
None,
[],
[],
{},
)
flaws = fbc.bug2flaws()
assert not fbc.errors
assert len(flaws) == 1
flaw = flaws[0]
flaw.save()

assert Flaw.objects.count() == 1
flaw = Flaw.objects.first()
assert flaw.cve_id == "CVE-2000-0001"
assert flaw.uuid == flaw_uuid

def test_no_cve(self):
"""
Expand Down Expand Up @@ -737,6 +846,79 @@ def test_no_cve(self):
flaw = Flaw.objects.first()
assert flaw.cve_id is None

# test that repeated sync preserves UUID
flaw_uuid = flaw.uuid

fbc = FlawBugConvertor(
flaw_bug,
[],
self.get_flaw_history(),
None,
[],
[],
{},
)
flaws = fbc.bug2flaws()
assert not fbc.errors
assert len(flaws) == 1
flaw = flaws[0]
flaw.save()

assert Flaw.objects.count() == 1
flaw = Flaw.objects.first()
assert flaw.cve_id is None
assert flaw.uuid == flaw_uuid

def test_cve_assign(self):
"""
test that CVE assignment to a CVE-less flaw is correctly processed
"""
flaw_bug = self.get_flaw_bug()
flaw_bug["alias"] = []

fbc = FlawBugConvertor(
flaw_bug,
[],
self.get_flaw_history(),
None,
[],
[],
{},
)
flaws = fbc.bug2flaws()
assert not fbc.errors
assert len(flaws) == 1
flaw = flaws[0]
flaw.save()

assert Flaw.objects.count() == 1
flaw = Flaw.objects.first()
assert flaw.cve_id is None
flaw_uuid = flaw.uuid

# assign CVE to the flaw
flaw_bug["alias"] = ["CVE-2000-0001"]

fbc = FlawBugConvertor(
flaw_bug,
[],
self.get_flaw_history(),
None,
[],
[],
{},
)
flaws = fbc.bug2flaws()
assert not fbc.errors
assert len(flaws) == 1
flaw = flaws[0]
flaw.save()

assert Flaw.objects.count() == 1
flaw = Flaw.objects.first()
assert flaw.cve_id == "CVE-2000-0001"
assert flaw.uuid == flaw_uuid

def test_major_incident_flag_order(self):
"""
test reproducer for OSIDB-416 where the erroneous condition logic led to unsetting
Expand Down
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix Jira tracker created and updated timestamps (OSIDB-14)
- Fix errata created and updated timestamps (OSIDB-453)
- Restrict write operations on placeholder flaws (OSIDB-388)
- Avoid recreating flaws on CVE ID changes whenever possible (OSIDB-392)

## [2.3.4] - 2022-12-15
### Changed
Expand Down

0 comments on commit 8f38fe1

Please sign in to comment.