Merge ~ilasc/launchpad:add-vulnerability-orm into launchpad: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)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+415966@code.launchpad.net

Commit message

Add Vulnerability and VulnerabilityActivity

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
Colin Watson (cjwatson) wrote :

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`.

review: Approve
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
1diff --git a/lib/lp/bugs/configure.zcml b/lib/lp/bugs/configure.zcml
2index 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
57diff --git a/lib/lp/bugs/interfaces/vulnerability.py b/lib/lp/bugs/interfaces/vulnerability.py
58new file mode 100644
59index 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+ """
318diff --git a/lib/lp/bugs/model/tests/test_vulnerability.py b/lib/lp/bugs/model/tests/test_vulnerability.py
319new file mode 100644
320index 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)))
445diff --git a/lib/lp/bugs/model/vulnerability.py b/lib/lp/bugs/model/vulnerability.py
446new file mode 100644
447index 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
642diff --git a/lib/lp/security.py b/lib/lp/security.py
643index 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))
669diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
670index 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

Subscribers

People subscribed via source and target branches

to status/vote changes: