Merge ~andrey-fedoseev/launchpad:uct-import-export-patches into launchpad:master

Proposed by Andrey Fedoseev
Status: Merged
Approved by: Andrey Fedoseev
Approved revision: d04a846ab1638ece2e5f5916ed18f2746e71c4ac
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~andrey-fedoseev/launchpad:uct-import-export-patches
Merge into: launchpad:master
Prerequisite: ~andrey-fedoseev/launchpad:bug-attachment-url
Diff against target: 500 lines (+267/-4)
5 files modified
lib/lp/bugs/scripts/tests/sampledata/CVE-2022-23222 (+2/-0)
lib/lp/bugs/scripts/tests/test_uct.py (+125/-3)
lib/lp/bugs/scripts/uct/models.py (+79/-1)
lib/lp/bugs/scripts/uct/uctexport.py (+35/-0)
lib/lp/bugs/scripts/uct/uctimport.py (+26/-0)
Reviewer Review Type Date Requested Status
Guruprasad Approve
Review via email: mp+431263@code.launchpad.net

This proposal supersedes a proposal from 2022-10-10.

Commit message

UCT import/export: handle patch URLs

Description of the change

Patch URLs are imported as `BugAttachments` with URLs

Attachment title consists of:

1. package name, e.g. "linux"
2. patch type, e.g. "upstream" or "other"
3. optional notes, e.g. "1.5.4"

To post a comment you must log in.
Revision history for this message
Guruprasad (lgp171188) wrote :

LGTM 👍 I would prefer it if Colin or someone more familiar with this part of the code can review and approve it too.

review: Approve
Revision history for this message
Otto Co-Pilot (otto-copilot) wrote :
d04a846... by Andrey Fedoseev

Merge remote-tracking branch 'origin/master' into uct-import-export-patches

; Conflicts:
; lib/lp/bugs/scripts/tests/test_uct.py

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/bugs/scripts/tests/sampledata/CVE-2022-23222 b/lib/lp/bugs/scripts/tests/sampledata/CVE-2022-23222
2index 30574d6..95a74f0 100644
3--- a/lib/lp/bugs/scripts/tests/sampledata/CVE-2022-23222
4+++ b/lib/lp/bugs/scripts/tests/sampledata/CVE-2022-23222
5@@ -30,6 +30,8 @@ CVSS:
6
7 Patches_linux:
8 break-fix: 457f44363a8894135c85b7a9afd2bd8196db24ab c25b2ae136039ffa820c26138ed4a5e5f3ab3841|local-CVE-2022-23222-fix
9+ upstream: https://github.com/389ds/389-ds-base/commit/58dbf084a63e6dbbd999bf6a70475fad8255f26a (1.4.4)
10+ upstream: https://github.com/389ds/389-ds-base/commit/2e5b526012612d1d6ccace46398bee679a730271
11 upstream_linux: released (5.17~rc1)
12 impish_linux: released (5.13.0-37.42)
13 devel_linux: not-affected (5.15.0-25.25)
14diff --git a/lib/lp/bugs/scripts/tests/test_uct.py b/lib/lp/bugs/scripts/tests/test_uct.py
15index 2f27ef0..20ae931 100644
16--- a/lib/lp/bugs/scripts/tests/test_uct.py
17+++ b/lib/lp/bugs/scripts/tests/test_uct.py
18@@ -120,7 +120,19 @@ class TestUCTRecord(TestCase):
19 "c25b2ae136039ffa820c26138ed4a5e5f3ab3841|"
20 "local-CVE-2022-23222-fix"
21 ),
22- )
23+ ),
24+ UCTRecord.Patch(
25+ patch_type="upstream",
26+ entry=(
27+ "https://github.com/389ds/389-ds-base/commit/58dbf084a63e6dbbd999bf6a70475fad8255f26a (1.4.4)" # noqa: 501
28+ ),
29+ ),
30+ UCTRecord.Patch(
31+ patch_type="upstream",
32+ entry=(
33+ "https://github.com/389ds/389-ds-base/commit/2e5b526012612d1d6ccace46398bee679a730271" # noqa: 501
34+ ),
35+ ),
36 ],
37 ),
38 UCTRecord.Package(
39@@ -260,7 +272,22 @@ class TestCVE(TestCaseWithFactory):
40 ],
41 priority=None,
42 tags=set(),
43- patches=[],
44+ patches=[
45+ UCTRecord.Patch(
46+ patch_type="upstream",
47+ entry=(
48+ "https://github.com/389ds/389-ds-base/"
49+ "commit/123 (1.4.4)"
50+ ),
51+ ),
52+ UCTRecord.Patch(
53+ patch_type="upstream",
54+ entry=(
55+ "https://github.com/389ds/389-ds-base/"
56+ "commit/456"
57+ ),
58+ ),
59+ ],
60 ),
61 UCTRecord.Package(
62 name=dsp2.sourcepackagename.name,
63@@ -417,6 +444,20 @@ class TestCVE(TestCaseWithFactory):
64 ),
65 ),
66 ],
67+ patch_urls=[
68+ CVE.PatchURL(
69+ package_name=dsp1.sourcepackagename,
70+ type="upstream",
71+ url="https://github.com/389ds/389-ds-base/" "commit/123",
72+ notes="1.4.4",
73+ ),
74+ CVE.PatchURL(
75+ package_name=dsp1.sourcepackagename,
76+ type="upstream",
77+ url="https://github.com/389ds/389-ds-base/" "commit/456",
78+ notes=None,
79+ ),
80+ ],
81 )
82
83 def test_make_from_uct_record(self):
84@@ -439,6 +480,40 @@ class TestCVE(TestCaseWithFactory):
85 self.assertEqual(xenial, CVE.get_distro_series("esm-infra/xenial"))
86 self.assertEqual(precise, CVE.get_distro_series("precise/esm"))
87
88+ def test_get_patches(self):
89+ spn = self.factory.makeSourcePackageName()
90+ self.assertListEqual(
91+ [
92+ CVE.PatchURL(
93+ package_name=spn,
94+ url="https://github.com/repo/1",
95+ type="upstream",
96+ notes=None,
97+ ),
98+ CVE.PatchURL(
99+ package_name=spn,
100+ url="https://github.com/repo/2",
101+ type="upstream",
102+ notes="1.2.3",
103+ ),
104+ ],
105+ list(
106+ CVE.get_patch_urls(
107+ spn,
108+ [
109+ UCTRecord.Patch("break-fix", "- -"),
110+ UCTRecord.Patch(
111+ "upstream", "https://github.com/repo/1"
112+ ),
113+ UCTRecord.Patch(
114+ "upstream", "https://github.com/repo/2 (1.2.3)"
115+ ),
116+ UCTRecord.Patch("other", "foo"),
117+ ],
118+ )
119+ ),
120+ )
121+
122
123 class TestUCTImporterExporter(TestCaseWithFactory):
124
125@@ -621,6 +696,20 @@ class TestUCTImporterExporter(TestCaseWithFactory):
126 ),
127 ),
128 ],
129+ patch_urls=[
130+ CVE.PatchURL(
131+ package_name=self.ubuntu_package.sourcepackagename,
132+ type="upstream",
133+ url="https://github.com/389ds/389-ds-base/" "commit/123",
134+ notes="1.4.4",
135+ ),
136+ CVE.PatchURL(
137+ package_name=self.ubuntu_package.sourcepackagename,
138+ type="upstream",
139+ url="https://github.com/389ds/389-ds-base/" "commit/456",
140+ notes=None,
141+ ),
142+ ],
143 )
144 self.importer = UCTImporter()
145 self.exporter = UCTExporter()
146@@ -641,6 +730,8 @@ class TestUCTImporterExporter(TestCaseWithFactory):
147 self.assertEqual(len(cve.bug_urls), len(watches))
148 self.assertEqual(sorted(cve.bug_urls), sorted(w.url for w in watches))
149
150+ self.checkBugAttachments(bug, cve)
151+
152 def checkBugTasks(self, bug: Bug, cve: CVE):
153 bug_tasks = bug.bugtasks # type: List[BugTask]
154
155@@ -701,6 +792,20 @@ class TestUCTImporterExporter(TestCaseWithFactory):
156 for t in bug_tasks:
157 self.assertEqual(cve.assignee, t.assignee)
158
159+ def checkBugAttachments(self, bug: Bug, cve: CVE):
160+ attachments_by_url = {a.url: a for a in bug.attachments if a.url}
161+ for patch_url in cve.patch_urls:
162+ self.assertIn(patch_url.url, attachments_by_url)
163+ attachment = attachments_by_url[patch_url.url]
164+ expected_title = "{}/{}".format(
165+ patch_url.package_name.name, patch_url.type
166+ )
167+ if patch_url.notes:
168+ expected_title = "{}/{}".format(
169+ expected_title, patch_url.notes
170+ )
171+ self.assertEqual(expected_title, attachment.title)
172+
173 def checkVulnerabilities(self, bug: Bug, cve: CVE):
174 vulnerabilities = bug.vulnerabilities
175
176@@ -769,6 +874,7 @@ class TestUCTImporterExporter(TestCaseWithFactory):
177 self.assertEqual(expected.notes, actual.notes)
178 self.assertEqual(expected.mitigation, actual.mitigation)
179 self.assertListEqual(expected.cvss, actual.cvss)
180+ self.assertListEqual(expected.patch_urls, actual.patch_urls)
181
182 def test_create_bug(self):
183 bug = self.importer.create_bug(self.cve, self.lp_cve)
184@@ -780,7 +886,7 @@ class TestUCTImporterExporter(TestCaseWithFactory):
185 self.assertEqual([self.lp_cve], bug.cves)
186
187 activities = list(bug.activity)
188- self.assertEqual(4, len(activities))
189+ self.assertEqual(6, len(activities))
190 import_bug_activity = activities[-1]
191 self.assertEqual(self.bug_importer, import_bug_activity.person)
192 self.assertEqual("bug", import_bug_activity.whatchanged)
193@@ -1016,6 +1122,22 @@ class TestUCTImporterExporter(TestCaseWithFactory):
194 self.importer.update_bug(bug, cve, self.lp_cve)
195 self.checkBug(bug, cve)
196
197+ def test_update_patch_urls(self):
198+ bug = self.importer.create_bug(self.cve, self.lp_cve)
199+ cve = self.cve
200+
201+ # Add new patch URL
202+ cve.patch_urls.append(
203+ CVE.PatchURL(
204+ package_name=cve.distro_packages[0].package_name,
205+ type="upstream",
206+ url="https://github.com/123",
207+ notes=None,
208+ )
209+ )
210+ self.importer.update_bug(bug, cve, self.lp_cve)
211+ self.checkBug(bug, cve)
212+
213 def test_import_cve(self):
214 self.importer.import_cve(self.cve)
215 self.assertIsNotNone(
216diff --git a/lib/lp/bugs/scripts/uct/models.py b/lib/lp/bugs/scripts/uct/models.py
217index e5405b5..fd77ea5 100644
218--- a/lib/lp/bugs/scripts/uct/models.py
219+++ b/lib/lp/bugs/scripts/uct/models.py
220@@ -2,16 +2,29 @@
221 # GNU Affero General Public License version 3 (see the file LICENSE).
222
223 import logging
224+import re
225 from collections import OrderedDict, defaultdict
226 from datetime import datetime
227 from enum import Enum
228 from pathlib import Path
229-from typing import Any, Dict, List, NamedTuple, Optional, Set, Tuple, Union
230+from typing import (
231+ Any,
232+ Dict,
233+ Iterable,
234+ List,
235+ NamedTuple,
236+ Optional,
237+ Set,
238+ Tuple,
239+ Union,
240+)
241 from typing.io import TextIO
242
243 import dateutil.parser
244 from contrib.cve_lib import load_cve
245 from zope.component import getUtility
246+from zope.schema import URI
247+from zope.schema.interfaces import InvalidURI
248
249 from lp.bugs.enums import VulnerabilityStatus
250 from lp.bugs.interfaces.bugtask import BugTaskImportance, BugTaskStatus
251@@ -448,6 +461,21 @@ class CVE:
252 ),
253 )
254
255+ PatchURL = NamedTuple(
256+ "PatchURL",
257+ (
258+ ("package_name", SourcePackageName),
259+ ("type", str),
260+ ("url", str),
261+ ("notes", Optional[str]),
262+ ),
263+ )
264+
265+ # Example:
266+ # https://github.com/389ds/389-ds-base/commit/123 (1.4.4)
267+ # https://github.com/389ds/389-ds-base/commit/345
268+ PATCH_URL_RE = re.compile(r"^(?P<url>.+?)(\s+\((?P<notes>.+)\))?$")
269+
270 PRIORITY_MAP = {
271 UCTRecord.Priority.CRITICAL: BugTaskImportance.CRITICAL,
272 UCTRecord.Priority.HIGH: BugTaskImportance.HIGH,
273@@ -502,6 +530,7 @@ class CVE:
274 notes: str,
275 mitigation: str,
276 cvss: List[CVSS],
277+ patch_urls: Optional[List[PatchURL]] = None,
278 ):
279 self.sequence = sequence
280 self.date_made_public = date_made_public
281@@ -521,6 +550,7 @@ class CVE:
282 self.notes = notes
283 self.mitigation = mitigation
284 self.cvss = cvss
285+ self.patch_urls = patch_urls or [] # type: List[CVE.PatchURL]
286
287 @classmethod
288 def make_from_uct_record(cls, uct_record: UCTRecord) -> "CVE":
289@@ -532,6 +562,7 @@ class CVE:
290
291 distro_packages = []
292 series_packages = []
293+ patch_urls = []
294
295 spn_set = getUtility(ISourcePackageNameSet)
296
297@@ -541,6 +572,11 @@ class CVE:
298
299 for uct_package in uct_record.packages:
300 source_package_name = spn_set.getOrCreateByName(uct_package.name)
301+
302+ patch_urls.extend(
303+ cls.get_patch_urls(source_package_name, uct_package.patches)
304+ )
305+
306 package_importance = (
307 cls.PRIORITY_MAP[uct_package.priority]
308 if uct_package.priority
309@@ -660,6 +696,7 @@ class CVE:
310 notes=uct_record.notes,
311 mitigation=uct_record.mitigation,
312 cvss=uct_record.cvss,
313+ patch_urls=patch_urls,
314 )
315
316 def to_uct_record(self) -> UCTRecord:
317@@ -749,6 +786,17 @@ class CVE:
318 patches=[],
319 )
320
321+ for patch_url in self.patch_urls:
322+ entry = patch_url.url
323+ if patch_url.notes:
324+ entry = "{} ({})".format(entry, patch_url.notes)
325+ packages_by_name[patch_url.package_name.name].patches.append(
326+ UCTRecord.Patch(
327+ patch_type=patch_url.type,
328+ entry=entry,
329+ )
330+ )
331+
332 return UCTRecord(
333 parent_dir=self.VULNERABILITY_STATUS_MAP_REVERSE.get(
334 self.status, ""
335@@ -831,3 +879,33 @@ class CVE:
336 "Could not find the distro series: %s", distro_series_name
337 )
338 return distro_series
339+
340+ @classmethod
341+ def get_patch_urls(
342+ cls,
343+ source_package_name: SourcePackageName,
344+ patches: List[UCTRecord.Patch],
345+ ) -> Iterable[PatchURL]:
346+ for patch in patches:
347+ if patch.patch_type == "break-fix":
348+ continue
349+ match = cls.PATCH_URL_RE.match(patch.entry)
350+ if not match:
351+ logger.warning(
352+ "Could not parse the patch entry: %s", patch.entry
353+ )
354+ continue
355+
356+ try:
357+ url = URI().fromUnicode(match.groupdict()["url"])
358+ except InvalidURI:
359+ logger.error("Invalid patch URL: %s", patch.entry)
360+ continue
361+
362+ notes = match.groupdict().get("notes")
363+ yield cls.PatchURL(
364+ package_name=source_package_name,
365+ type=patch.patch_type,
366+ url=url,
367+ notes=notes,
368+ )
369diff --git a/lib/lp/bugs/scripts/uct/uctexport.py b/lib/lp/bugs/scripts/uct/uctexport.py
370index d4b5b45..8a1dd27 100644
371--- a/lib/lp/bugs/scripts/uct/uctexport.py
372+++ b/lib/lp/bugs/scripts/uct/uctexport.py
373@@ -2,6 +2,7 @@
374 # GNU Affero General Public License version 3 (see the file LICENSE).
375
376 import logging
377+import re
378 from collections import defaultdict
379 from pathlib import Path
380 from typing import Dict, List, NamedTuple, Optional
381@@ -10,6 +11,7 @@ from zope.component import getUtility
382 from zope.security.proxy import removeSecurityProxy
383
384 from lp.bugs.interfaces.bug import IBugSet
385+from lp.bugs.interfaces.bugattachment import BugAttachmentType
386 from lp.bugs.model.bug import Bug as BugModel
387 from lp.bugs.model.bugtask import BugTask
388 from lp.bugs.model.cve import Cve as CveModel
389@@ -44,6 +46,13 @@ class UCTExporter:
390 ),
391 )
392
393+ # Example:
394+ # linux/upstream
395+ # linux/upstream/5.19.1
396+ BUG_ATTACHMENT_TITLE_RE = re.compile(
397+ "^(?P<package_name>.+?)/(?P<patch_type>.+?)(/(?P<notes>.+?))?$"
398+ )
399+
400 def export_bug_to_uct_file(
401 self, bug_id: int, output_dir: Path
402 ) -> Optional[Path]:
403@@ -192,6 +201,31 @@ class UCTExporter:
404 )
405 )
406
407+ packages_by_name = {
408+ p.name: p for p in package_name_by_product.values()
409+ }
410+ patch_urls = []
411+ for attachment in bug.attachments:
412+ if (
413+ not attachment.url
414+ or not attachment.type == BugAttachmentType.PATCH
415+ ):
416+ continue
417+ title_match = self.BUG_ATTACHMENT_TITLE_RE.match(attachment.title)
418+ if not title_match:
419+ continue
420+ spn = packages_by_name.get(title_match.groupdict()["package_name"])
421+ if not spn:
422+ continue
423+ patch_urls.append(
424+ CVE.PatchURL(
425+ package_name=spn,
426+ type=title_match.groupdict()["patch_type"],
427+ url=attachment.url,
428+ notes=title_match.groupdict().get("notes"),
429+ )
430+ )
431+
432 return CVE(
433 sequence="CVE-{}".format(lp_cve.sequence),
434 date_made_public=vulnerability.date_made_public,
435@@ -217,6 +251,7 @@ class UCTExporter:
436 )
437 for authority, vector_string in lp_cve.cvss.items()
438 ],
439+ patch_urls=patch_urls,
440 )
441
442 def _parse_bug_description(
443diff --git a/lib/lp/bugs/scripts/uct/uctimport.py b/lib/lp/bugs/scripts/uct/uctimport.py
444index 12bc601..5bc8411 100644
445--- a/lib/lp/bugs/scripts/uct/uctimport.py
446+++ b/lib/lp/bugs/scripts/uct/uctimport.py
447@@ -38,6 +38,7 @@ from lp.app.enums import InformationType
448 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
449 from lp.bugs.interfaces.bug import CreateBugParams, IBugSet
450 from lp.bugs.interfaces.bugactivity import IBugActivitySet
451+from lp.bugs.interfaces.bugattachment import BugAttachmentType
452 from lp.bugs.interfaces.bugtask import BugTaskImportance, IBugTaskSet
453 from lp.bugs.interfaces.bugwatch import IBugWatchSet
454 from lp.bugs.interfaces.cve import ICveSet
455@@ -166,6 +167,7 @@ class UCTImporter:
456 ) # type: BugModel
457
458 self._update_external_bug_urls(bug, cve.bug_urls)
459+ self._update_patches(bug, cve.patch_urls)
460
461 self._create_bug_tasks(
462 bug,
463@@ -222,6 +224,7 @@ class UCTImporter:
464 )
465 self._assign_bug_tasks(bug, cve.assignee)
466 self._update_external_bug_urls(bug, cve.bug_urls)
467+ self._update_patches(bug, cve.patch_urls)
468
469 # Update or add new Vulnerabilities
470 vulnerabilities_by_distro = {
471@@ -429,6 +432,29 @@ class UCTImporter:
472 for external_bug_url in bug_urls:
473 bug_watch_set.fromText(external_bug_url, bug, self.bug_importer)
474
475+ def _update_patches(self, bug: BugModel, patch_urls: List[CVE.PatchURL]):
476+ attachments_by_url = {a.url: a for a in bug.attachments if a.url}
477+ for patch_url in patch_urls:
478+ title = "{}/{}".format(patch_url.package_name.name, patch_url.type)
479+ if patch_url.notes:
480+ title = "{}/{}".format(title, patch_url.notes)
481+ if patch_url in attachments_by_url:
482+ attachment = removeSecurityProxy(
483+ attachments_by_url[patch_url.url]
484+ )
485+ attachment.title = title
486+ attachment.type = BugAttachmentType.PATCH
487+ else:
488+ bug.addAttachment(
489+ owner=bug.owner,
490+ data=None,
491+ comment=None,
492+ filename=None,
493+ url=patch_url.url,
494+ is_patch=True,
495+ description=title,
496+ )
497+
498 def _make_bug_description(self, cve: CVE) -> str:
499 """
500 Some `CVE` fields can't be mapped to Launchpad models.

Subscribers

People subscribed via source and target branches

to status/vote changes: