Merge ~ilasc/launchpad:add-vulnerability-orm into launchpad:master
- Git
- lp:~ilasc/launchpad
- add-vulnerability-orm
- Merge into master
Proposed by
Ioana Lasc
Status: | Merged |
---|---|
Approved by: | Ioana Lasc |
Approved revision: | bbf4153e065a63d05fa15e0f5fe4d29e01ddc836 |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~ilasc/launchpad:add-vulnerability-orm |
Merge into: | launchpad:master |
Diff against target: |
747 lines (+678/-0) 6 files modified
lib/lp/bugs/configure.zcml (+45/-0) lib/lp/bugs/interfaces/vulnerability.py (+255/-0) lib/lp/bugs/model/tests/test_vulnerability.py (+121/-0) lib/lp/bugs/model/vulnerability.py (+191/-0) lib/lp/security.py (+12/-0) lib/lp/testing/factory.py (+54/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+415966@code.launchpad.net |
Commit message
Add Vulnerability and VulnerabilityAc
Description of the change
To post a comment you must log in.
Revision history for this message
Guruprasad (lgp171188) : | # |
Revision history for this message
Ioana Lasc (ilasc) : | # |
Revision history for this message
Colin Watson (cjwatson) : | # |
Revision history for this message
Ioana Lasc (ilasc) wrote : | # |
Thanks Colin, this might be worth another look now that BugVulnerability was replaced with XRef.
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/bugs/configure.zcml b/lib/lp/bugs/configure.zcml |
2 | index 3064699..5a033b1 100644 |
3 | --- a/lib/lp/bugs/configure.zcml |
4 | +++ b/lib/lp/bugs/configure.zcml |
5 | @@ -593,6 +593,51 @@ |
6 | interface="lp.bugs.interfaces.cve.ICveSet"/> |
7 | </securedutility> |
8 | |
9 | + <!-- Vulnerability --> |
10 | + <class class="lp.bugs.model.vulnerability.Vulnerability"> |
11 | + <require |
12 | + permission="launchpad.View" |
13 | + interface="lp.bugs.interfaces.vulnerability.IVulnerabilityView |
14 | + lp.bugs.interfaces.vulnerability.IVulnerabilityEditableAttributes" /> |
15 | + <require |
16 | + permission="launchpad.Edit" |
17 | + interface="lp.bugs.interfaces.vulnerability.IVulnerabilityEdit" |
18 | + set_schema="lp.bugs.interfaces.vulnerability.IVulnerabilityEditableAttributes" /> |
19 | + |
20 | + <!-- IBugLinkTarget --> |
21 | + <allow |
22 | + attributes=" |
23 | + bugs"/> |
24 | + <require |
25 | + permission="launchpad.Edit" |
26 | + attributes=" |
27 | + linkBug |
28 | + unlinkBug"/> |
29 | + </class> |
30 | + <class class="lp.bugs.model.vulnerability.VulnerabilitySet"> |
31 | + <allow interface="lp.bugs.interfaces.vulnerability.IVulnerabilitySet" /> |
32 | + </class> |
33 | + <securedutility |
34 | + class="lp.bugs.model.vulnerability.VulnerabilitySet" |
35 | + provides="lp.bugs.interfaces.vulnerability.IVulnerabilitySet"> |
36 | + <allow interface="lp.bugs.interfaces.vulnerability.IVulnerabilitySet" /> |
37 | + </securedutility> |
38 | + |
39 | + <!-- VulnerabilityActivity --> |
40 | + <class class="lp.bugs.model.vulnerability.VulnerabilityActivity"> |
41 | + <require |
42 | + permission="launchpad.View" |
43 | + interface="lp.bugs.interfaces.vulnerability.IVulnerabilityActivity" /> |
44 | + </class> |
45 | + <class class="lp.bugs.model.vulnerability.VulnerabilityActivitySet"> |
46 | + <allow interface="lp.bugs.interfaces.vulnerability.IVulnerabilityActivitySet" /> |
47 | + </class> |
48 | + <securedutility |
49 | + class="lp.bugs.model.vulnerability.VulnerabilityActivitySet" |
50 | + provides="lp.bugs.interfaces.vulnerability.IVulnerabilityActivitySet"> |
51 | + <allow interface="lp.bugs.interfaces.vulnerability.IVulnerabilityActivitySet" /> |
52 | + </securedutility> |
53 | + |
54 | <!-- BugSubscription --> |
55 | |
56 | <class |
57 | diff --git a/lib/lp/bugs/interfaces/vulnerability.py b/lib/lp/bugs/interfaces/vulnerability.py |
58 | new file mode 100644 |
59 | index 0000000..8fa01da |
60 | --- /dev/null |
61 | +++ b/lib/lp/bugs/interfaces/vulnerability.py |
62 | @@ -0,0 +1,255 @@ |
63 | +# Copyright 2022 Canonical Ltd. This software is licensed under the |
64 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
65 | + |
66 | +"""Vulnerability interfaces.""" |
67 | + |
68 | +__all__ = [ |
69 | + 'IVulnerability', |
70 | + 'IVulnerabilityActivity', |
71 | + 'IVulnerabilityActivitySet', |
72 | + 'IVulnerabilitySet', |
73 | + 'VulnerabilityChange', |
74 | + 'VulnerabilityStatus' |
75 | + ] |
76 | + |
77 | +from lazr.enum import ( |
78 | + DBEnumeratedType, |
79 | + DBItem, |
80 | + ) |
81 | +from lazr.restful.fields import Reference |
82 | +from zope.interface import Interface |
83 | +from zope.schema import ( |
84 | + Choice, |
85 | + Datetime, |
86 | + Int, |
87 | + TextLine, |
88 | + ) |
89 | + |
90 | +from lp import _ |
91 | +from lp.app.enums import InformationType |
92 | +from lp.app.interfaces.informationtype import IInformationType |
93 | +from lp.bugs.interfaces.bugtask import BugTaskImportance |
94 | +from lp.bugs.interfaces.cve import ICve |
95 | +from lp.registry.interfaces.distribution import IDistribution |
96 | +from lp.registry.interfaces.person import IPerson |
97 | + |
98 | + |
99 | +class VulnerabilityChange(DBEnumeratedType): |
100 | + """Type of change in vulnerability |
101 | + |
102 | + We use this enum to track changes occurring in |
103 | + data stored in the vulnerability table. |
104 | + """ |
105 | + |
106 | + STATUS = DBItem(0, """ |
107 | + Status |
108 | + |
109 | + The status of the vulnerability changed. |
110 | + """) |
111 | + |
112 | + DESCRIPTION = DBItem(1, """ |
113 | + Description |
114 | + |
115 | + The description of the vulnerability changed. |
116 | + """) |
117 | + |
118 | + NOTES = DBItem(2, """ |
119 | + Notes |
120 | + |
121 | + The notes on the vulnerability changed. |
122 | + """) |
123 | + |
124 | + MITIGATION = DBItem(3, """ |
125 | + Mitigation |
126 | + |
127 | + Mitigation for this vulnerability changed. |
128 | + """) |
129 | + |
130 | + IMPORTANCE = DBItem(4, """ |
131 | + Importance |
132 | + |
133 | + The importance assigned for this vulnerability changed. |
134 | + """) |
135 | + |
136 | + IMPORTANCE_EXPLANATION = DBItem(5, """ |
137 | + Importance explanation |
138 | + |
139 | + The importance explanation changed for this vulnerability. |
140 | + """) |
141 | + |
142 | + PRIVACY = DBItem(6, """ |
143 | + Privacy |
144 | + |
145 | + The privacy for this vulnerability changed. |
146 | + """) |
147 | + |
148 | + |
149 | +class VulnerabilityStatus(DBEnumeratedType): |
150 | + """Vulnerability status""" |
151 | + |
152 | + NEEDS_TRIAGE = DBItem(0, """ |
153 | + Needs triage |
154 | + |
155 | + Not looked at yet. |
156 | + """) |
157 | + |
158 | + ACTIVE = DBItem(1, """ |
159 | + Active |
160 | + |
161 | + The vulnerability is active. |
162 | + """) |
163 | + |
164 | + IGNORED = DBItem(2, """ |
165 | + Ignored |
166 | + |
167 | + The vulnerability is currently ignored. |
168 | + """) |
169 | + |
170 | + RETIRED = DBItem(3, """ |
171 | + Retired |
172 | + |
173 | + This vulnerability is now retired. |
174 | + """) |
175 | + |
176 | + |
177 | +class IVulnerabilityView(Interface): |
178 | + """`IVulnerability` attributes that require launchpad.View.""" |
179 | + |
180 | + id = Int(title=_("ID"), required=True, readonly=True) |
181 | + |
182 | + distribution = Reference(IDistribution, title=_("Distribution"), |
183 | + required=True, readonly=True) |
184 | + |
185 | + cve = Reference(ICve, title=_('External CVE reference corresponding' |
186 | + ' to this vulnerability, if any.'), |
187 | + required=False, readonly=True) |
188 | + |
189 | + date_created = Datetime( |
190 | + title=_("The date this vulnerability was made public."), |
191 | + required=True, readonly=True) |
192 | + |
193 | + creator = Reference( |
194 | + title=_('Person'), schema=IPerson, required=True, readonly=True) |
195 | + |
196 | + |
197 | +class IVulnerabilityEditableAttributes(Interface): |
198 | + """`IVulnerability` attributes that can be edited. |
199 | + |
200 | + These attributes need launchpad.View to see, and launchpad.Edit to change. |
201 | + """ |
202 | + |
203 | + status = Choice( |
204 | + title=_('Result of the report'), readonly=True, |
205 | + required=True, vocabulary=VulnerabilityStatus) |
206 | + |
207 | + description = TextLine( |
208 | + title=_("A short description of the vulnerability."), required=False, |
209 | + readonly=False) |
210 | + |
211 | + notes = TextLine( |
212 | + title=_("Free-form notes for this vulnerability."), required=False, |
213 | + readonly=False) |
214 | + |
215 | + mitigation = TextLine( |
216 | + title=_("Explains why we're ignoring a vulnerability."), |
217 | + required=False, readonly=False) |
218 | + |
219 | + importance = Choice(title=_('Importance used to indicate work priority,' |
220 | + ' not severity'), |
221 | + vocabulary=BugTaskImportance, required=True, |
222 | + default=BugTaskImportance.UNDECIDED, readonly=True) |
223 | + |
224 | + importance_explanation = TextLine( |
225 | + title=_("Used to explain why our importance differs " |
226 | + "from somebody else's CVSS score."), |
227 | + required=False, readonly=False) |
228 | + |
229 | + information_type = Choice( |
230 | + title=_("Information type"), vocabulary=InformationType, |
231 | + required=True, readonly=False, default=InformationType.PUBLIC, |
232 | + description=_( |
233 | + "Indicates privacy of the vulnerability.")) |
234 | + |
235 | + date_made_public = Datetime( |
236 | + title=_("The date this vulnerability was made public."), |
237 | + required=False, readonly=False) |
238 | + |
239 | + |
240 | +class IVulnerabilityEdit(Interface): |
241 | + """`IVulnerability` attributes that require launchpad.Edit.""" |
242 | + |
243 | + |
244 | +class IVulnerability(IVulnerabilityView, |
245 | + IVulnerabilityEditableAttributes, |
246 | + IVulnerabilityEdit, IInformationType): |
247 | + """Contract describing a vulnerability.""" |
248 | + |
249 | + |
250 | +class IVulnerabilitySet(Interface): |
251 | + """The set of all vulnerabilities.""" |
252 | + |
253 | + def new(distribution, status, importance, |
254 | + creator, information_type=InformationType.PUBLIC, cve=None, |
255 | + description=None, notes=None, mitigation=None, |
256 | + importance_explanation=None, date_made_public=None): |
257 | + """Return a new vulnerability. |
258 | + |
259 | + :param distribution: The distribution for the vulnerability. |
260 | + :param status: The status of the vulnerability. |
261 | + :param importance: Indicates work priority, not severity. |
262 | + :param creator: The user that created the vulnerability. |
263 | + :param information_type: The privacy of the vulnerability. |
264 | + :param cve: A `Cve` for which the vulnerability is being created. |
265 | + :param description: The description of the vulnerability. |
266 | + :param notes: The notes for the vulnerability. |
267 | + :param mitigation: A short summary of the result. |
268 | + :param importance_explanation: Used to explain why our importance |
269 | + differs from somebody else's CVSS score. |
270 | + :param date_made_public: The date this vulnerability was made public. |
271 | + """ |
272 | + |
273 | + |
274 | +class IVulnerabilityActivity(Interface): |
275 | + """`IVulnerabilityActivity` attributes that require launchpad.View.""" |
276 | + |
277 | + id = Int(title=_("ID"), required=True, readonly=True) |
278 | + |
279 | + vulnerability = Reference(IVulnerability, title=_('Vulnerability'), |
280 | + required=True, readonly=True) |
281 | + |
282 | + date_changed = Datetime( |
283 | + title=_("When activity last changed for this vulnerability."), |
284 | + required=True, readonly=True) |
285 | + |
286 | + changer = Reference(IPerson, title=_("Changer"), required=True, |
287 | + readonly=True, |
288 | + description=_("The person that made the changes.")) |
289 | + |
290 | + what_changed = Choice( |
291 | + title=_('Indicates what field changed for the vulnerability.'), |
292 | + readonly=True, |
293 | + required=True, vocabulary=VulnerabilityChange) |
294 | + |
295 | + old_value = TextLine( |
296 | + title=_("Indicates the value prior to the change."), required=False, |
297 | + readonly=True) |
298 | + |
299 | + new_value = TextLine( |
300 | + title=_("Indicates the current value."), required=False, |
301 | + readonly=True) |
302 | + |
303 | + |
304 | +class IVulnerabilityActivitySet(Interface): |
305 | + """The set of all activities for a certain vulnerability.""" |
306 | + |
307 | + def new(vulnerability, changer, what_changed=None, |
308 | + old_value=None, new_value=None): |
309 | + """Return a new vulnerability activity. |
310 | + |
311 | + :param vulnerability: The vulnerability for this activity. |
312 | + :param changer: The `Person` that performed the activity. |
313 | + :param what_changed: The 'VulnerabilityChange' that occurred |
314 | + for this vulnerability. |
315 | + :param old_value: Indicates the value prior to the change. |
316 | + :param new_value: Indicates the current value. |
317 | + """ |
318 | diff --git a/lib/lp/bugs/model/tests/test_vulnerability.py b/lib/lp/bugs/model/tests/test_vulnerability.py |
319 | new file mode 100644 |
320 | index 0000000..a9d7bc2 |
321 | --- /dev/null |
322 | +++ b/lib/lp/bugs/model/tests/test_vulnerability.py |
323 | @@ -0,0 +1,121 @@ |
324 | +# Copyright 2022 Canonical Ltd. This software is licensed under the |
325 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
326 | + |
327 | +"""Tests for the vulnerability and related models.""" |
328 | +from zope.component import getUtility |
329 | + |
330 | +from lp.bugs.interfaces.vulnerability import ( |
331 | + IVulnerabilitySet, |
332 | + VulnerabilityChange, |
333 | + ) |
334 | +from lp.services.webapp.authorization import check_permission |
335 | +from lp.testing import ( |
336 | + admin_logged_in, |
337 | + anonymous_logged_in, |
338 | + person_logged_in, |
339 | + TestCaseWithFactory, |
340 | + verifyObject, |
341 | + ) |
342 | +from lp.testing.layers import DatabaseFunctionalLayer |
343 | + |
344 | + |
345 | +class TestVulnerability(TestCaseWithFactory): |
346 | + |
347 | + layer = DatabaseFunctionalLayer |
348 | + |
349 | + def setUp(self): |
350 | + super().setUp() |
351 | + self.distribution = self.factory.makeDistribution() |
352 | + self.vulnerability = self.factory.makeVulnerability( |
353 | + distribution=self.distribution) |
354 | + |
355 | + def test_random_user(self): |
356 | + with person_logged_in(self.factory.makePerson()): |
357 | + self.assertTrue( |
358 | + check_permission("launchpad.View", self.vulnerability)) |
359 | + self.assertFalse( |
360 | + check_permission("launchpad.Edit", self.vulnerability)) |
361 | + |
362 | + def test_admin(self): |
363 | + with admin_logged_in(): |
364 | + self.assertTrue( |
365 | + check_permission("launchpad.View", self.vulnerability)) |
366 | + self.assertTrue( |
367 | + check_permission("launchpad.Edit", self.vulnerability)) |
368 | + |
369 | + def test_non_admin(self): |
370 | + with person_logged_in(self.distribution.owner): |
371 | + self.assertTrue( |
372 | + check_permission("launchpad.View", self.vulnerability)) |
373 | + self.assertTrue( |
374 | + check_permission("launchpad.Edit", self.vulnerability)) |
375 | + |
376 | + def test_anonymous(self): |
377 | + with anonymous_logged_in(): |
378 | + self.assertFalse( |
379 | + check_permission("launchpad.View", self.vulnerability)) |
380 | + self.assertFalse( |
381 | + check_permission("launchpad.Edit", self.vulnerability)) |
382 | + |
383 | + |
384 | +class TestVulnerabilityActivity(TestCaseWithFactory): |
385 | + |
386 | + layer = DatabaseFunctionalLayer |
387 | + |
388 | + def test_vulnerability_activity_changes(self): |
389 | + vulnerability = self.factory.makeVulnerability() |
390 | + changer = self.factory.makePerson() |
391 | + activity = self.factory.makeVulnerabilityActivity( |
392 | + vulnerability=vulnerability, changer=None) |
393 | + with person_logged_in(changer): |
394 | + self.assertTrue(VulnerabilityChange.DESCRIPTION, |
395 | + activity.what_changed) |
396 | + |
397 | + |
398 | +class TestVulnerabilitySet(TestCaseWithFactory): |
399 | + |
400 | + layer = DatabaseFunctionalLayer |
401 | + |
402 | + def test_VulnerabilitySet_implements_IVulnerabilitySet(self): |
403 | + vulnerabilitySet = getUtility(IVulnerabilitySet) |
404 | + self.assertTrue(verifyObject(IVulnerabilitySet, vulnerabilitySet)) |
405 | + |
406 | + def test_bugVulnerabilityCount(self): |
407 | + # vulnerability3 linked bugs will not be reflected |
408 | + # in computations of linked bugs on |
409 | + # vulnerability 1 and 2 |
410 | + |
411 | + vulnerability1 = self.factory.makeVulnerability() |
412 | + vulnerability2 = self.factory.makeVulnerability() |
413 | + vulnerability3 = self.factory.makeVulnerability() |
414 | + bug1 = self.factory.makeBug() |
415 | + bug2 = self.factory.makeBug() |
416 | + initial_number = len(vulnerability1.bugs) |
417 | + with admin_logged_in(): |
418 | + vulnerability1.linkBug(bug1) |
419 | + vulnerability3.linkBug(bug1) |
420 | + vulnerability3.linkBug(bug2) |
421 | + |
422 | + self.assertEqual( |
423 | + initial_number + 1, |
424 | + len(vulnerability1.bugs)) |
425 | + |
426 | + with admin_logged_in(): |
427 | + vulnerability2.linkBug(bug2) |
428 | + self.assertEqual( |
429 | + initial_number + 2, |
430 | + (len(vulnerability1.bugs) + len(vulnerability2.bugs))) |
431 | + |
432 | + with admin_logged_in(): |
433 | + vulnerability2.linkBug(bug1) |
434 | + self.assertEqual( |
435 | + initial_number + 3, |
436 | + (len(vulnerability1.bugs) + len(vulnerability2.bugs))) |
437 | + |
438 | + with admin_logged_in(): |
439 | + vulnerability1.unlinkBug(bug1) |
440 | + vulnerability2.unlinkBug(bug2) |
441 | + vulnerability2.unlinkBug(bug1) |
442 | + self.assertEqual( |
443 | + initial_number, |
444 | + (len(vulnerability1.bugs) + len(vulnerability2.bugs))) |
445 | diff --git a/lib/lp/bugs/model/vulnerability.py b/lib/lp/bugs/model/vulnerability.py |
446 | new file mode 100644 |
447 | index 0000000..77ebcb3 |
448 | --- /dev/null |
449 | +++ b/lib/lp/bugs/model/vulnerability.py |
450 | @@ -0,0 +1,191 @@ |
451 | +# Copyright 2022 Canonical Ltd. This software is licensed under the |
452 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
453 | + |
454 | +__all__ = [ |
455 | + 'Vulnerability', |
456 | + 'VulnerabilitySet', |
457 | + ] |
458 | + |
459 | +import operator |
460 | + |
461 | +import pytz |
462 | +from storm.locals import ( |
463 | + DateTime, |
464 | + Int, |
465 | + Reference, |
466 | + Unicode, |
467 | + ) |
468 | +from zope.component import getUtility |
469 | +from zope.interface import implementer |
470 | + |
471 | +from lp.app.enums import InformationType |
472 | +from lp.bugs.interfaces.buglink import IBugLinkTarget |
473 | +from lp.bugs.interfaces.bugtask import BugTaskImportance |
474 | +from lp.bugs.interfaces.vulnerability import ( |
475 | + IVulnerability, |
476 | + IVulnerabilityActivity, |
477 | + IVulnerabilityActivitySet, |
478 | + IVulnerabilitySet, |
479 | + VulnerabilityChange, |
480 | + VulnerabilityStatus, |
481 | + ) |
482 | +from lp.bugs.model.bug import Bug |
483 | +from lp.bugs.model.buglinktarget import BugLinkTargetMixin |
484 | +from lp.services.database import bulk |
485 | +from lp.services.database.constants import UTC_NOW |
486 | +from lp.services.database.enumcol import DBEnum |
487 | +from lp.services.database.interfaces import IStore |
488 | +from lp.services.database.stormbase import StormBase |
489 | +from lp.services.xref.interfaces import IXRefSet |
490 | + |
491 | + |
492 | +@implementer(IVulnerability, IBugLinkTarget) |
493 | +class Vulnerability(StormBase, BugLinkTargetMixin): |
494 | + __storm_table__ = 'Vulnerability' |
495 | + |
496 | + id = Int(primary=True) |
497 | + |
498 | + distribution_id = Int(name="distribution", allow_none=False) |
499 | + distribution = Reference(distribution_id, "Distribution.id") |
500 | + |
501 | + cve_id = Int(name="cve", allow_none=True, default=None) |
502 | + cve = Reference(cve_id, "Cve.id") |
503 | + |
504 | + status = DBEnum(name='status', allow_none=False, |
505 | + enum=VulnerabilityStatus) |
506 | + |
507 | + description = Unicode(name='description', allow_none=True) |
508 | + |
509 | + notes = Unicode(name='notes', allow_none=True) |
510 | + |
511 | + mitigation = Unicode(name='mitigation', allow_none=True) |
512 | + |
513 | + importance = DBEnum( |
514 | + name='importance', allow_none=False, |
515 | + enum=BugTaskImportance, |
516 | + default=BugTaskImportance.UNDECIDED) |
517 | + |
518 | + importance_explanation = Unicode( |
519 | + name='importance_explanation', allow_none=True) |
520 | + |
521 | + information_type = DBEnum( |
522 | + enum=InformationType, default=InformationType.PUBLIC, |
523 | + allow_none=False, name="information_type") |
524 | + |
525 | + date_created = DateTime( |
526 | + name='date_created', tzinfo=pytz.UTC, allow_none=False, |
527 | + default=UTC_NOW) |
528 | + |
529 | + date_made_public = DateTime( |
530 | + name='date_made_public', tzinfo=pytz.UTC, allow_none=True) |
531 | + |
532 | + creator_id = Int(name='creator', allow_none=False) |
533 | + creator = Reference(creator_id, 'Person.id') |
534 | + |
535 | + def __init__(self, distribution, status, importance, |
536 | + creator, information_type=InformationType.PUBLIC, cve=None, |
537 | + description=None, notes=None, mitigation=None, |
538 | + importance_explanation=None, date_made_public=None): |
539 | + super().__init__() |
540 | + self.distribution = distribution |
541 | + self.cve = cve |
542 | + self.status = status |
543 | + self.importance = importance |
544 | + self.information_type = information_type |
545 | + self.creator = creator |
546 | + self.description = description |
547 | + self.notes = notes |
548 | + self.mitigation = mitigation |
549 | + self.importance_explanation = importance_explanation |
550 | + self.date_made_public = date_made_public |
551 | + self.date_created = UTC_NOW |
552 | + |
553 | + @property |
554 | + def bugs(self): |
555 | + bug_ids = [ |
556 | + int(id) for _, id in getUtility(IXRefSet).findFrom( |
557 | + ('vulnerability', str(self.id)), types=['bug'])] |
558 | + return list(sorted( |
559 | + bulk.load(Bug, bug_ids), key=operator.attrgetter('id'))) |
560 | + |
561 | + def createBugLink(self, bug, props=None): |
562 | + """See BugLinkTargetMixin.""" |
563 | + if props is None: |
564 | + props = {} |
565 | + getUtility(IXRefSet).create( |
566 | + {('vulnerability', str(self.id)): {('bug', str(bug.id)): props}}) |
567 | + |
568 | + def deleteBugLink(self, bug): |
569 | + """See BugLinkTargetMixin.""" |
570 | + getUtility(IXRefSet).delete( |
571 | + {('vulnerability', str(self.id)): [('bug', str(bug.id))]}) |
572 | + |
573 | + |
574 | +@implementer(IVulnerabilitySet) |
575 | +class VulnerabilitySet: |
576 | + |
577 | + def new(self, distribution, status, importance, |
578 | + creator, information_type=InformationType.PUBLIC, cve=None, |
579 | + description=None, notes=None, mitigation=None, |
580 | + importance_explanation=None, date_made_public=None): |
581 | + """See `IVulnerabilitySet`.""" |
582 | + store = IStore(Vulnerability) |
583 | + vulnerability = Vulnerability(distribution=distribution, |
584 | + creator=creator, cve=cve, |
585 | + status=status, description=description, |
586 | + notes=notes, mitigation=mitigation, |
587 | + importance=importance, |
588 | + information_type=information_type, |
589 | + importance_explanation= |
590 | + importance_explanation, |
591 | + date_made_public=date_made_public) |
592 | + store.add(vulnerability) |
593 | + return vulnerability |
594 | + |
595 | + |
596 | +@implementer(IVulnerabilityActivity) |
597 | +class VulnerabilityActivity(StormBase): |
598 | + __storm_table__ = 'VulnerabilityActivity' |
599 | + |
600 | + id = Int(primary=True) |
601 | + |
602 | + vulnerability_id = Int(name="vulnerability", allow_none=False) |
603 | + vulnerability = Reference(vulnerability_id, "Vulnerability.id") |
604 | + |
605 | + changer_id = Int(name="changer", allow_none=False) |
606 | + changer = Reference(changer_id, "Person.id") |
607 | + |
608 | + date_changed = DateTime( |
609 | + name='date_changed', tzinfo=pytz.UTC, allow_none=False) |
610 | + |
611 | + what_changed = DBEnum(name='what_changed', allow_none=False, |
612 | + enum=VulnerabilityChange) |
613 | + |
614 | + old_value = Unicode(name='old_value', allow_none=True) |
615 | + |
616 | + new_value = Unicode(name='new_value', allow_none=True) |
617 | + |
618 | + def __init__(self, vulnerability, changer, what_changed=None, |
619 | + old_value=None, new_value=None): |
620 | + super().__init__() |
621 | + self.vulnerability = vulnerability |
622 | + self.changer = changer |
623 | + self.what_changed = what_changed |
624 | + self.old_value = old_value |
625 | + self.new_value = new_value |
626 | + self.date_changed = UTC_NOW |
627 | + |
628 | + |
629 | +@implementer(IVulnerabilityActivitySet) |
630 | +class VulnerabilityActivitySet: |
631 | + |
632 | + def new(self, vulnerability, changer, |
633 | + what_changed, |
634 | + old_value=None, new_value=None): |
635 | + """See `IVulnerabilityActivitySet`.""" |
636 | + store = IStore(VulnerabilityActivity) |
637 | + activity = VulnerabilityActivity(vulnerability, changer, |
638 | + what_changed, |
639 | + old_value, new_value) |
640 | + store.add(activity) |
641 | + return activity |
642 | diff --git a/lib/lp/security.py b/lib/lp/security.py |
643 | index fbada1b..037f0d8 100644 |
644 | --- a/lib/lp/security.py |
645 | +++ b/lib/lp/security.py |
646 | @@ -55,6 +55,7 @@ from lp.blueprints.model.specificationsubscription import ( |
647 | ) |
648 | from lp.bugs.interfaces.bugtarget import IOfficialBugTagTargetRestricted |
649 | from lp.bugs.interfaces.structuralsubscription import IStructuralSubscription |
650 | +from lp.bugs.interfaces.vulnerability import IVulnerability |
651 | from lp.bugs.model.bugsubscription import BugSubscription |
652 | from lp.bugs.model.bugtaskflat import BugTaskFlat |
653 | from lp.bugs.model.bugtasksearch import get_bug_privacy_filter |
654 | @@ -3797,3 +3798,14 @@ class EditCIBuild(AdminByBuilddAdmin): |
655 | if auth_repository.checkAuthenticated(user): |
656 | return True |
657 | return super().checkAuthenticated(user) |
658 | + |
659 | + |
660 | +class EditVulnerability(AuthorizationBase): |
661 | + permission = 'launchpad.Edit' |
662 | + usedfor = IVulnerability |
663 | + |
664 | + def checkAuthenticated(self, user): |
665 | + return (user.in_commercial_admin or user.in_admin or |
666 | + user.isOwner(self.obj.distribution) or |
667 | + user.isDriver(self.obj.distribution) or |
668 | + user.isBugSupervisor(self.obj.distribution)) |
669 | diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py |
670 | index 7c79147..1bbdfe1 100644 |
671 | --- a/lib/lp/testing/factory.py |
672 | +++ b/lib/lp/testing/factory.py |
673 | @@ -83,6 +83,7 @@ from lp.bugs.interfaces.bug import ( |
674 | IBugSet, |
675 | ) |
676 | from lp.bugs.interfaces.bugtask import ( |
677 | + BugTaskImportance, |
678 | BugTaskStatus, |
679 | IBugTaskSet, |
680 | ) |
681 | @@ -95,6 +96,12 @@ from lp.bugs.interfaces.cve import ( |
682 | CveStatus, |
683 | ICveSet, |
684 | ) |
685 | +from lp.bugs.interfaces.vulnerability import ( |
686 | + IVulnerabilityActivitySet, |
687 | + IVulnerabilitySet, |
688 | + VulnerabilityChange, |
689 | + VulnerabilityStatus, |
690 | + ) |
691 | from lp.bugs.model.bug import FileBugData |
692 | from lp.buildmaster.enums import ( |
693 | BuildBaseImageType, |
694 | @@ -5378,6 +5385,53 @@ class BareLaunchpadObjectFactory(ObjectFactory): |
695 | IStore(build).flush() |
696 | return build |
697 | |
698 | + def makeVulnerability(self, distribution=None, status=None, |
699 | + importance=None, creator=None, |
700 | + information_type=InformationType.PUBLIC, cve=None, |
701 | + description=None, notes=None, mitigation=None, |
702 | + importance_explanation=None, date_made_public=None): |
703 | + """Make a new `Vulnerability`.""" |
704 | + if distribution is None: |
705 | + distribution = self.makeDistribution() |
706 | + if status is None: |
707 | + status = VulnerabilityStatus.NEEDS_TRIAGE |
708 | + if importance is None: |
709 | + importance = BugTaskImportance.UNDECIDED |
710 | + if creator is None: |
711 | + creator = self.makePerson() |
712 | + if importance_explanation is None: |
713 | + importance_explanation = self.getUniqueString( |
714 | + "vulnerability-importance-explanation") |
715 | + return getUtility( |
716 | + IVulnerabilitySet).new( |
717 | + distribution=distribution, cve=cve, status=status, |
718 | + importance=importance, creator=creator, |
719 | + information_type=information_type, description=description, |
720 | + notes=notes, mitigation=mitigation, |
721 | + importance_explanation=importance_explanation, |
722 | + date_made_public=date_made_public) |
723 | + |
724 | + def makeVulnerabilityActivity(self, vulnerability=None, changer=None, |
725 | + what_changed=None, old_value=None, |
726 | + new_value=None): |
727 | + """Make a new `VulnerabilityActivity`.""" |
728 | + if vulnerability is None: |
729 | + vulnerability = self.makeVulnerability() |
730 | + if changer is None: |
731 | + changer = self.makePerson() |
732 | + if what_changed is None: |
733 | + what_changed = VulnerabilityChange.DESCRIPTION |
734 | + if old_value is None: |
735 | + old_value = self.getUniqueString("old-value") |
736 | + if new_value is None: |
737 | + new_value = self.getUniqueString("new-value") |
738 | + return getUtility( |
739 | + IVulnerabilityActivitySet).new(vulnerability=vulnerability, |
740 | + changer=changer, |
741 | + what_changed=what_changed, |
742 | + old_value=old_value, |
743 | + new_value=new_value) |
744 | + |
745 | |
746 | # Some factory methods return simple Python types. We don't add |
747 | # security wrappers for them, as well as for objects created by |
Be careful to ensure that non-nullable DB columns have `required=True` in their interface and `allow_none=False` in their model. I think I caught them all, but it might be helpful for you to do another pass after applying my suggestions.
Aside from that, most of my suggestions are relatively small, except for replacing `BugVulnerability` with `XRef` and my comments on an earlier thread about sorting out permissions on `Vulnerability`.