Enrique Sánchez has proposed merging ~enriqueesanchz/launchpad:add-bugpresence into launchpad:master.
Commit message: Add SVT `BugPresence` model Add `Bug` and `BugPresence` integration Add SVT scripts `BugPresence` integration Requested reviews: Launchpad code reviewers (launchpad-reviewers) For more details, see: https://code.launchpad.net/~enriqueesanchz/launchpad/+git/launchpad/+merge/485525 -- Your team Launchpad code reviewers is requested to review the proposed merge of ~enriqueesanchz/launchpad:add-bugpresence into launchpad:master.
diff --git a/lib/lp/bugs/adapters/bugchange.py b/lib/lp/bugs/adapters/bugchange.py index 1532bfc..80afe09 100644 --- a/lib/lp/bugs/adapters/bugchange.py +++ b/lib/lp/bugs/adapters/bugchange.py @@ -19,6 +19,8 @@ __all__ = [ "BugInformationTypeChange", "BugLocked", "BugLockReasonSet", + "BugPresenceAdded", + "BugPresenceRemoved", "BugTagsChange", "BugTaskAdded", "BugTaskAssigneeChange", @@ -72,6 +74,8 @@ ATTACHMENT_ADDED = "attachment added" ATTACHMENT_REMOVED = "attachment removed" BRANCH_LINKED = "branch linked" BRANCH_UNLINKED = "branch unlinked" +BUG_PRESENCE_ADDED = "bug presence added" +BUG_PRESENCE_REMOVED = "bug presence removed" BUG_WATCH_ADDED = "bug watch added" BUG_WATCH_REMOVED = "bug watch removed" CHANGED_DUPLICATE_MARKER = "changed duplicate marker" @@ -328,6 +332,58 @@ class SeriesNominated(BugChangeBase): return None +class BugPresenceAdded(BugChangeBase): + """A bug presence was added to the bug.""" + + def __init__(self, when, person, bug_presence): + super().__init__(when, person) + self.bug_presence = bug_presence + + def getBugActivity(self): + """See `IBugChange`.""" + return dict( + whatchanged=BUG_PRESENCE_ADDED, + newvalue=str(self.bug_presence.break_fix_data), + ) + + def getBugNotification(self): + """See `IBugChange`.""" + return { + "text": "** Bug presence added: " + f"project: {self.bug_presence.project}" + f"distribution: {self.bug_presence.distribution}" + f"source_package_name: {self.bug_presence.source_package_name}" + f"gitrepository: {self.bug_presence.git_repository}" + f"break_fix_data: {self.bug_presence.break_fix_data}\n" + } + + +class BugPresenceRemoved(BugChangeBase): + """A bug presence was removed from the bug.""" + + def __init__(self, when, person, bug_presence): + super().__init__(when, person) + self.bug_presence = bug_presence + + def getBugActivity(self): + """See `IBugChange`.""" + return dict( + whatchanged=BUG_PRESENCE_REMOVED, + newvalue=str(self.bug_presence.break_fix_data), + ) + + def getBugNotification(self): + """See `IBugChange`.""" + return { + "text": "** Bug presence removed: " + f"project: {self.bug_presence.project}" + f"distribution: {self.bug_presence.distribution}" + f"source_package_name: {self.bug_presence.source_package_name}" + f"gitrepository: {self.bug_presence.git_repository}" + f"break_fix_data: {self.bug_presence.break_fix_data}\n" + } + + class BugWatchAdded(BugChangeBase): """A bug watch was added to the bug.""" diff --git a/lib/lp/bugs/configure.zcml b/lib/lp/bugs/configure.zcml index 8ac1fcd..057cfdf 100644 --- a/lib/lp/bugs/configure.zcml +++ b/lib/lp/bugs/configure.zcml @@ -807,6 +807,7 @@ permission="launchpad.Append" interface="lp.bugs.interfaces.bug.IBugAppend" attributes=" + presences linkBranch unlinkBranch"/> <require @@ -933,6 +934,42 @@ for="lp.bugs.interfaces.bugsubscription.IBugSubscription lazr.lifecycle.interfaces.IObjectModifiedEvent" handler="lp.bugs.subscribers.bugactivity.record_bugsubscription_edited"/> + <!-- BugPresence --> + + <class + class="lp.bugs.model.bugpresence.BugPresence"> + <implements + interface="lp.bugs.interfaces.bugpresence.IBugPresence"/> + <require + permission="launchpad.View" + set_schema="lp.bugs.interfaces.bugpresence.IBugPresence" + attributes=" + id + bug + project + distribution + source_package_name + git_repository + break_fix_data"/> + <require + permission="launchpad.Edit" + attributes="destroySelf"/> + </class> + + <!-- BugPresenceSet --> + + <class + class="lp.bugs.model.bugpresence.BugPresenceSet"> + <allow + interface="lp.bugs.interfaces.bugpresence.IBugPresenceSet"/> + </class> + <lp:securedutility + class="lp.bugs.model.bugpresence.BugPresenceSet" + provides="lp.bugs.interfaces.bugpresence.IBugPresenceSet"> + <allow + interface="lp.bugs.interfaces.bugpresence.IBugPresenceSet"/> + </lp:securedutility> + <!-- BugWatch --> <class diff --git a/lib/lp/bugs/interfaces/bug.py b/lib/lp/bugs/interfaces/bug.py index 5135dee..390dd0a 100644 --- a/lib/lp/bugs/interfaces/bug.py +++ b/lib/lp/bugs/interfaces/bug.py @@ -912,6 +912,34 @@ class IBugAppend(Interface): :is_patch: A boolean. """ + def addPresence( + project, + distribution, + source_package_name, + git_repository, + break_fix_data, + user, + ): + """Add a bug presence to this bug. + + :project: The project where this BugPresence is related to. + :distribution: The distribution where this BugPresence is related to. + :source_package_name: The source_package_name where this BugPresence is + related to. + :git_repository: The git_repository where this BugPresence is related + to. + :break_fix_data: The dict of commits that caused the issue (break) and + commits that solved it (fix). + :user: The user that adds this bug presence. + """ + + def removePresence(bug_presence, user): + """Remove a bug presence from the bug. + + :bug_presence: The bug presence to be removed. + :user: The user that removes the bug presence. + """ + def addCommentNotification( message, recipients=None, diff --git a/lib/lp/bugs/interfaces/bugpresence.py b/lib/lp/bugs/interfaces/bugpresence.py new file mode 100644 index 0000000..bbba90d --- /dev/null +++ b/lib/lp/bugs/interfaces/bugpresence.py @@ -0,0 +1,48 @@ +# Copyright 2009-2025 Canonical Ltd. This software is licensed under the +# GNU Affero General Public License version 3 (see the file LICENSE). + +"""BugPresence interfaces""" + +__all__ = [ + "IBugPresence", + "IBugPresenceSet", +] + +from zope.interface import Interface +from zope.schema import Dict, Int + +from lp import _ +from lp.services.fields import BugField + + +class IBugPresence(Interface): + """A single `BugPresence` database entry.""" + + id = Int(title=_("ID"), required=True, readonly=True) + bug = BugField(title=_("Bug"), readonly=True) + project = Int(title=_("Project")) + distribution = Int(title=_("Distribution")) + source_package_name = Int(title=_("Source Package Name")) + git_repository = Int(title=_("Git Repository")) + break_fix_data = Dict(title=_("Break-Fix")) + + def destroySelf(self): + """Destroy this `IBugPresence` object.""" + + +class IBugPresenceSet(Interface): + """The set of `IBugPresence` objects.""" + + def __getitem__(id): + """Get a `IBugPresence` by id.""" + + def create( + id, + bug, + project, + distribution, + source_package_name, + git_repository, + break_fix_data, + ): + """Create a new `IBugPresence`.""" diff --git a/lib/lp/bugs/model/bug.py b/lib/lp/bugs/model/bug.py index 989abe8..d5af37d 100644 --- a/lib/lp/bugs/model/bug.py +++ b/lib/lp/bugs/model/bug.py @@ -86,6 +86,8 @@ from lp.bugs.adapters.bugchange import ( BugDuplicateChange, BugLocked, BugLockReasonSet, + BugPresenceAdded, + BugPresenceRemoved, BugUnlocked, BugWatchAdded, BugWatchRemoved, @@ -114,6 +116,7 @@ from lp.bugs.interfaces.bugnomination import ( NominationSeriesObsoleteError, ) from lp.bugs.interfaces.bugnotification import IBugNotificationSet +from lp.bugs.interfaces.bugpresence import IBugPresenceSet from lp.bugs.interfaces.bugtarget import ISeriesBugTarget from lp.bugs.interfaces.bugtask import ( UNRESOLVED_BUGTASK_STATUSES, @@ -135,6 +138,7 @@ from lp.bugs.model.buglinktarget import ObjectLinkedEvent, ObjectUnlinkedEvent from lp.bugs.model.bugmessage import BugMessage from lp.bugs.model.bugnomination import BugNomination from lp.bugs.model.bugnotification import BugNotification +from lp.bugs.model.bugpresence import BugPresence from lp.bugs.model.bugsubscription import BugSubscription from lp.bugs.model.bugtarget import OfficialBugTag from lp.bugs.model.bugtask import BugTask, bugtask_sort_key @@ -836,6 +840,15 @@ class Bug(StormBase, InformationTypeMixin): return sorted(tasks, key=bugtask_sort_key) @property + def presences(self): + """See `IBug`.""" + store = Store.of(self) + presences = list( + store.find(BugPresence, BugPresence.bug_id == self.id) + ) + return presences + + @property def default_bugtask(self): """See `IBug`.""" from lp.registry.model.product import Product @@ -1719,6 +1732,32 @@ class Bug(StormBase, InformationTypeMixin): send_notifications=send_notifications, ) + def addPresence( + self, + project, + distribution, + source_package_name, + git_repository, + break_fix_data, + user, + ): + """See `IBug`.""" + bug_presence = getUtility(IBugPresenceSet).create( + bug=self, + project=project, + distribution=distribution, + source_package_name=source_package_name, + git_repository=git_repository, + break_fix_data=break_fix_data, + ) + self.addChange(BugPresenceAdded(UTC_NOW, user, bug_presence)) + return bug_presence + + def removePresence(self, bug_presence, user): + """See `IBug`.""" + self.addChange(BugPresenceRemoved(UTC_NOW, user, bug_presence)) + bug_presence.destroySelf() + def linkBranch(self, branch, registrant): """See `IBug`.""" if branch in self.linked_branches: diff --git a/lib/lp/bugs/model/bugpresence.py b/lib/lp/bugs/model/bugpresence.py new file mode 100644 index 0000000..32f87d3 --- /dev/null +++ b/lib/lp/bugs/model/bugpresence.py @@ -0,0 +1,114 @@ +# Copyright 2009-2025 Canonical Ltd. This software is licensed under the +# GNU Affero General Public License version 3 (see the file LICENSE). + +__all__ = [ + "BugPresence", + "BugPresenceSet", +] + +from storm.databases.postgres import JSON +from storm.locals import Int +from storm.references import Reference +from storm.store import Store +from zope.interface import implementer + +from lp.bugs.interfaces.bugpresence import IBugPresence, IBugPresenceSet +from lp.services.database.interfaces import IStore +from lp.services.database.stormbase import StormBase + + +@implementer(IBugPresence) +class BugPresence(StormBase): + """ + Points in the code history of various entities like a project, a + distribution, or a distribution source package when something was broken + and/or when it was fixed. + """ + + __storm_table__ = "bugpresence" + + id = Int(primary=True) + + bug_id = Int(name="bug", allow_none=False) + bug = Reference(bug_id, "Bug.id") + + project_id = Int(name="project", allow_none=True) + # Is this product or project: dbmodels says project but we dont have the + # project entity + project = Reference(project_id, "Product.id") + + distribution_id = Int(name="distribution", allow_none=True) + distribution = Reference(distribution_id, "Distribution.id") + + source_package_name_id = Int(name="source_package_name", allow_none=True) + source_package_name = Reference( + source_package_name_id, "SourcePackageName.id" + ) + + git_repository_id = Int(name="git_repository", allow_none=True) + git_repository = Reference(git_repository_id, "GitRepository.id") + + _break_fix_data = JSON(name="break_fix_data", allow_none=False) + + def __init__( + self, + bug, + project, + distribution, + source_package_name, + git_repository, + break_fix_data, + ): + super().__init__() + self.bug = bug + self.project = project + self.distribution = distribution + self.source_package_name = source_package_name + self.git_repository = git_repository + self._break_fix_data = break_fix_data + + @property + def break_fix_data(self): + """See `IBugPresence`.""" + return self._break_fix_data or {} + + @break_fix_data.setter + def break_fix_data(self, value): + """See `IBugPresence`.""" + assert value is None or isinstance(value, list) + self._break_fix_data = value + + def destroySelf(self): + """See `IBugPresence`.""" + Store.of(self).remove(self) + + +@implementer(IBugPresenceSet) +class BugPresenceSet: + """The set of `IBugPresence` objects.""" + + def __getitem__(id): + """See IBugPresenceSet.""" + return IStore(BugPresence).find(BugPresence, id=id).one() + + def create( + id, + bug, + project, + distribution, + source_package_name, + git_repository, + break_fix_data, + ): + """See IBugPresenceSet.""" + bug_presence = BugPresence( + bug=bug, + project=project, + distribution=distribution, + source_package_name=source_package_name, + git_repository=git_repository, + break_fix_data=break_fix_data, + ) + + IStore(BugPresence).add(bug_presence) + return bug_presence diff --git a/lib/lp/bugs/scripts/tests/test_uct.py b/lib/lp/bugs/scripts/tests/test_uct.py index a3b35dd..bf43d4d 100644 --- a/lib/lp/bugs/scripts/tests/test_uct.py +++ b/lib/lp/bugs/scripts/tests/test_uct.py @@ -373,6 +373,14 @@ class TestCVE(TestCaseWithFactory): "commit/456" ), ), + UCTRecord.Patch( + patch_type="break-fix", + entry=( + "457f44363a8894135c85b7a9afd2bd8196db24ab " + "c25b2ae136039ffa820c26138ed4a5e5f3ab3841|" + "local-CVE-2022-23222-fix" + ), + ), ], ), UCTRecord.Package( @@ -546,6 +554,16 @@ class TestCVE(TestCaseWithFactory): notes=None, ), ], + break_fix_data=[ + CVE.BreakFix( + package_name=dsp1.sourcepackagename, + break_="457f44363a8894135c85b7a9afd2bd8196db24ab", + fix=( + "c25b2ae136039ffa820c26138ed4a5e5f3ab3841|" + "local-CVE-2022-23222-fix" + ), + ), + ], global_tags={"cisa-kev"}, ) @@ -603,6 +621,43 @@ class TestCVE(TestCaseWithFactory): ), ) + def test_get_break_fix(self): + spn = self.factory.makeSourcePackageName() + self.assertListEqual( + [ + CVE.BreakFix( + package_name=spn, + break_="d2406291483775ecddaee929231a39c70c08fda2", + fix="f64e67e5d3a45a4a04286c47afade4b518acd47b", + ), + CVE.BreakFix( + package_name=spn, + break_="-", + fix="f2ef6f7539c68c6bd6c32323d8845ee102b7c450", + ), + ], + list( + CVE.get_break_fix( + spn, + [ + UCTRecord.Patch( + "break-fix", + "d2406291483775ecddaee929231a39c70c08fda2 " + "f64e67e5d3a45a4a04286c47afade4b518acd47b", + ), + UCTRecord.Patch( + "break-fix", + "- f2ef6f7539c68c6bd6c32323d8845ee102b7c450", + ), + UCTRecord.Patch( + "upstream", "https://github.com/repo/2 (1.2.3)" + ), + UCTRecord.Patch("other", "foo"), + ], + ) + ), + ) + class TestUCTImporterExporter(TestCaseWithFactory): maxDiff = None @@ -791,6 +846,16 @@ class TestUCTImporterExporter(TestCaseWithFactory): notes=None, ), ], + break_fix_data=[ + CVE.BreakFix( + package_name=self.ubuntu_package.sourcepackagename, + break_="457f44363a8894135c85b7a9afd2bd8196db24ab", + fix=( + "c25b2ae136039ffa820c26138ed4a5e5f3ab3841|" + "local-CVE-2022-23222-fix" + ), + ), + ], global_tags={"cisa-kev"}, ) self.importer = UCTImporter() @@ -814,6 +879,7 @@ class TestUCTImporterExporter(TestCaseWithFactory): self.checkBugTags(bug, cve) self.checkBugAttachments(bug, cve) + self.checkBugPresences(bug, cve) def checkBugTags(self, bug: Bug, cve: CVE): tags = cve.global_tags.copy() @@ -893,6 +959,51 @@ class TestUCTImporterExporter(TestCaseWithFactory): for t in bug_tasks: self.assertEqual(cve.assignee, t.assignee) + def checkBugPresences(self, bug: Bug, cve: CVE): + presences_by_pkg = { + presence.source_package_name: presence + for presence in bug.presences + } + break_fix_by_pkg = defaultdict(list) + for break_fix in cve.break_fix_data: + break_fix_by_pkg[break_fix.package_name].append(break_fix) + + self.assertEqual( + len(bug.presences), + len(break_fix_by_pkg), + "Mismatch in presences count and break_fix count", + ) + + for package, break_fix_data in break_fix_by_pkg.items(): + presence = presences_by_pkg.get(package) + + self.assertIsNotNone( + presence, f"Presence for package '{package}' not found" + ) + + self.assertEqual(package, presence.source_package_name) + self.assertEqual( + len(break_fix_data), + len(presence.break_fix_data), + "Number of break_fix_data don't match for package " + f"'{package}'", + ) + + # Check content and its order + for break_fix, presence_break_fix in zip( + break_fix_data, presence.break_fix_data + ): + self.assertEqual( + break_fix.break_, + presence_break_fix["break"], + f"Break mismatch for patch in package '{package}'", + ) + self.assertEqual( + break_fix.fix, + presence_break_fix["fix"], + f"Fix mismatch for patch in package '{package}'", + ) + def checkBugAttachments(self, bug: Bug, cve: CVE): attachments_by_url = {a.url: a for a in bug.attachments if a.url} for patch_url in cve.patch_urls: @@ -981,6 +1092,7 @@ class TestUCTImporterExporter(TestCaseWithFactory): self.assertEqual(expected.mitigation, actual.mitigation) self.assertListEqual(expected.cvss, actual.cvss) self.assertListEqual(expected.patch_urls, actual.patch_urls) + self.assertListEqual(expected.break_fix_data, actual.break_fix_data) self.assertEqual(expected.global_tags, actual.global_tags) def test_create_bug(self): @@ -993,7 +1105,8 @@ class TestUCTImporterExporter(TestCaseWithFactory): self.assertEqual([self.lp_cve], bug.cves) activities = list(bug.activity) - self.assertEqual(6, len(activities)) + # We are adding a bug presence so we add 1 activity + self.assertEqual(7, len(activities)) import_bug_activity = activities[-1] self.assertEqual(self.bug_importer, import_bug_activity.person) self.assertEqual("bug", import_bug_activity.whatchanged) @@ -1079,6 +1192,7 @@ class TestUCTImporterExporter(TestCaseWithFactory): ), ], patch_urls=[], + break_fix_data=[], global_tags={"cisa-kev"}, ) lp_cve = self.factory.makeCVE(sequence="2022-1234") @@ -1341,6 +1455,37 @@ class TestUCTImporterExporter(TestCaseWithFactory): self.importer.update_bug(bug, cve, self.lp_cve) self.checkBug(bug, cve) + def test_update_break_fix(self): + bug = self.importer.create_bug(self.cve, self.lp_cve) + cve = self.cve + + # Add new patch URL + cve.break_fix_data.append( + CVE.BreakFix( + package_name=cve.distro_packages[0].package_name, + break_="d2406291483775ecddaee929231a39c70c08fda2", + fix=( + "f64e67e5d3a45a4a04286c47afade4b518acd47b" + "|cc8c837cf1b2f714dda723541c04acd1b8922d92" + ), + ), + ) + cve.break_fix_data.append( + CVE.BreakFix( + package_name=cve.distro_packages[1].package_name, + break_="-", + fix="cffe487026be13eaf37ea28b783d9638ab147204", + ), + ) + self.importer.update_bug(bug, cve, self.lp_cve) + self.checkBug(bug, cve) + + # Remove break_fix and check if it removes from bug + cve.break_fix_data.pop() + cve.break_fix_data.pop() + self.importer.update_bug(bug, cve, self.lp_cve) + self.checkBug(bug, cve) + def test_update_tags(self): bug = self.importer.create_bug(self.cve, self.lp_cve) cve = self.cve diff --git a/lib/lp/bugs/scripts/uct/models.py b/lib/lp/bugs/scripts/uct/models.py index daceec2..209d44e 100644 --- a/lib/lp/bugs/scripts/uct/models.py +++ b/lib/lp/bugs/scripts/uct/models.py @@ -466,6 +466,11 @@ class CVE: url: str notes: Optional[str] + class BreakFix(NamedTuple): + package_name: SourcePackageName + break_: str + fix: str + # Example: # https://github.com/389ds/389-ds-base/commit/123 (1.4.4) # https://github.com/389ds/389-ds-base/commit/345 @@ -525,6 +530,7 @@ class CVE: mitigation: str, cvss: List[CVSS], global_tags: Set[str], + break_fix_data: List[BreakFix], patch_urls: Optional[List[PatchURL]] = None, importance_explanation: str = "", ): @@ -549,6 +555,7 @@ class CVE: self.cvss = cvss self.global_tags = global_tags self.patch_urls: List[CVE.PatchURL] = patch_urls or [] + self.break_fix_data: List[CVE.BreakFix] = break_fix_data or [] @classmethod def make_from_uct_record(cls, uct_record: UCTRecord) -> "CVE": @@ -561,6 +568,7 @@ class CVE: distro_packages = [] series_packages = [] patch_urls = [] + break_fix_data = [] spn_set = getUtility(ISourcePackageNameSet) @@ -575,6 +583,10 @@ class CVE: cls.get_patch_urls(source_package_name, uct_package.patches) ) + break_fix_data.extend( + cls.get_break_fix(source_package_name, uct_package.patches) + ) + package_importance = ( cls.PRIORITY_MAP[uct_package.priority] if uct_package.priority @@ -697,6 +709,7 @@ class CVE: cvss=uct_record.cvss, global_tags=uct_record.global_tags, patch_urls=patch_urls, + break_fix_data=break_fix_data, ) def to_uct_record(self) -> UCTRecord: @@ -797,6 +810,14 @@ class CVE: ) ) + for break_fix in self.break_fix_data: + packages_by_name[patch_url.package_name.name].patches.append( + UCTRecord.Patch( + patch_type="break-fix", + entry=f"{break_fix.break_} {break_fix.fix}", + ) + ) + return UCTRecord( parent_dir=self.VULNERABILITY_STATUS_MAP_REVERSE.get( self.status, "" @@ -911,3 +932,27 @@ class CVE: url=url, notes=notes, ) + + @classmethod + def get_break_fix( + cls, + source_package_name: SourcePackageName, + patches: List[UCTRecord.Patch], + ) -> Iterable[PatchURL]: + for patch in patches: + if patch.patch_type != "break-fix": + continue + + if " " not in patch.entry: + logger.warning( + "Could not parse the break-fix patch entry: %s", + patch.entry, + ) + continue + + break_, fix = patch.entry.split(maxsplit=1) + yield cls.BreakFix( + package_name=source_package_name, + break_=break_, + fix=fix, + ) diff --git a/lib/lp/bugs/scripts/uct/uctexport.py b/lib/lp/bugs/scripts/uct/uctexport.py index fd7df39..80462e4 100644 --- a/lib/lp/bugs/scripts/uct/uctexport.py +++ b/lib/lp/bugs/scripts/uct/uctexport.py @@ -239,6 +239,17 @@ class UCTExporter: ) ) + break_fix_data = [] + for bugpresence in bug.presences: + for break_fix in bugpresence.break_fix_data: + break_fix_data.append( + CVE.BreakFix( + package_name=bugpresence.source_package_name, + break_=break_fix.get("break"), + fix=break_fix.get("fix"), + ) + ) + return CVE( sequence=f"CVE-{lp_cve.sequence}", date_made_public=vulnerability.date_made_public, @@ -268,6 +279,7 @@ class UCTExporter: ], global_tags=global_tags, patch_urls=patch_urls, + break_fix_data=break_fix_data, ) def _parse_bug_description( diff --git a/lib/lp/bugs/scripts/uct/uctimport.py b/lib/lp/bugs/scripts/uct/uctimport.py index a4abb5e..a6172fd 100644 --- a/lib/lp/bugs/scripts/uct/uctimport.py +++ b/lib/lp/bugs/scripts/uct/uctimport.py @@ -25,6 +25,7 @@ Three types of bug tasks are created: status of the package in upstream. """ import logging +from collections import defaultdict from datetime import timezone from itertools import chain from pathlib import Path @@ -178,6 +179,7 @@ class UCTImporter: self._update_external_bug_urls(bug, cve.bug_urls) self._update_patches(bug, cve.patch_urls) + self._update_break_fix(bug, cve.break_fix_data) self._update_tags(bug, cve.global_tags, cve.distro_packages) self._create_bug_tasks( @@ -236,6 +238,7 @@ class UCTImporter: self._assign_bug_tasks(bug, cve.assignee) self._update_external_bug_urls(bug, cve.bug_urls) self._update_patches(bug, cve.patch_urls) + self._update_break_fix(bug, cve.break_fix_data) self._update_tags(bug, cve.global_tags, cve.distro_packages) # Update or add new Vulnerabilities @@ -482,6 +485,35 @@ class UCTImporter: description=title, ) + def _update_break_fix( + self, bug: BugModel, break_fix_data: List[CVE.BreakFix] + ): + break_fix_by_pkg = defaultdict(list) + for break_fix in break_fix_data: + break_fix_by_pkg[break_fix.package_name].append( + {"break": break_fix.break_, "fix": break_fix.fix} + ) + + for presence in bug.presences: + if break_fix := break_fix_by_pkg.pop( + presence.source_package_name, None + ): + presence.break_fix_data = break_fix + else: + # Remove non existing + bug.removePresence(presence, self.bug_importer) + + for package in break_fix_by_pkg: + # Create new presence + bug.addPresence( + project=None, + distribution=None, + source_package_name=package, + git_repository=None, + break_fix_data=break_fix_by_pkg[package], + user=self.bug_importer, + ) + def _make_bug_description(self, cve: CVE) -> str: """ Some `CVE` fields can't be mapped to Launchpad models.
_______________________________________________ Mailing list: https://launchpad.net/~launchpad-reviewers Post to : launchpad-reviewers@lists.launchpad.net Unsubscribe : https://launchpad.net/~launchpad-reviewers More help : https://help.launchpad.net/ListHelp