Merge ~andrey-fedoseev/launchpad:uct-import-export-patches into launchpad:master
- Git
- lp:~andrey-fedoseev/launchpad
- uct-import-export-patches
- Merge into 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) |
Related bugs: |
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
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
1 | diff --git a/lib/lp/bugs/scripts/tests/sampledata/CVE-2022-23222 b/lib/lp/bugs/scripts/tests/sampledata/CVE-2022-23222 |
2 | index 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) |
14 | diff --git a/lib/lp/bugs/scripts/tests/test_uct.py b/lib/lp/bugs/scripts/tests/test_uct.py |
15 | index 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( |
216 | diff --git a/lib/lp/bugs/scripts/uct/models.py b/lib/lp/bugs/scripts/uct/models.py |
217 | index 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 | + ) |
369 | diff --git a/lib/lp/bugs/scripts/uct/uctexport.py b/lib/lp/bugs/scripts/uct/uctexport.py |
370 | index 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( |
443 | diff --git a/lib/lp/bugs/scripts/uct/uctimport.py b/lib/lp/bugs/scripts/uct/uctimport.py |
444 | index 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. |
LGTM 👍 I would prefer it if Colin or someone more familiar with this part of the code can review and approve it too.