launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #28157
[Merge] ~ilasc/launchpad:add-vulnerability-orm into launchpad:master
Ioana Lasc has proposed merging ~ilasc/launchpad:add-vulnerability-orm into launchpad:master.
Commit message:
Add Vulnerability, VulnerabilityActivity and BugVulnerability
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~ilasc/launchpad/+git/launchpad/+merge/415966
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~ilasc/launchpad:add-vulnerability-orm into launchpad:master.
diff --git a/lib/lp/bugs/configure.zcml b/lib/lp/bugs/configure.zcml
index 3064699..82eb6ee 100644
--- a/lib/lp/bugs/configure.zcml
+++ b/lib/lp/bugs/configure.zcml
@@ -593,6 +593,55 @@
interface="lp.bugs.interfaces.cve.ICveSet"/>
</securedutility>
+ <!-- Vulnerability -->
+ <class class="lp.bugs.model.vulnerability.Vulnerability">
+ <require
+ permission="launchpad.View"
+ interface="lp.bugs.interfaces.vulnerability.IVulnerabilityView
+ lp.bugs.interfaces.vulnerability.IVulnerabilityEditableAttributes" />
+ <require
+ permission="launchpad.Edit"
+ interface="lp.bugs.interfaces.vulnerability.IVulnerabilityEdit"
+ set_schema="lp.bugs.interfaces.vulnerability.IVulnerabilityEditableAttributes" />
+ </class>
+ <class class="lp.bugs.model.vulnerability.VulnerabilitySet">
+ <allow interface="lp.bugs.interfaces.vulnerability.IVulnerabilitySet" />
+ </class>
+ <securedutility
+ class="lp.bugs.model.vulnerability.VulnerabilitySet"
+ provides="lp.bugs.interfaces.vulnerability.IVulnerabilitySet">
+ <allow interface="lp.bugs.interfaces.vulnerability.IVulnerabilitySet" />
+ </securedutility>
+
+ <!-- Vulnerability Activity-->
+ <class class="lp.bugs.model.vulnerability.VulnerabilityActivity">
+ <require
+ permission="launchpad.View"
+ interface="lp.bugs.interfaces.vulnerability.IVulnerabilityActivityView
+ lp.bugs.interfaces.vulnerability.IVulnerabilityActivityEditableAttributes" />
+ <require
+ permission="launchpad.Edit"
+ interface="lp.bugs.interfaces.vulnerability.IVulnerabilityActivityEdit"
+ set_schema="lp.bugs.interfaces.vulnerability.IVulnerabilityActivityEditableAttributes" />
+ </class>
+ <class class="lp.bugs.model.vulnerability.VulnerabilityActivitySet">
+ <allow interface="lp.bugs.interfaces.vulnerability.IVulnerabilityActivitySet" />
+ </class>
+ <securedutility
+ class="lp.bugs.model.vulnerability.VulnerabilityActivitySet"
+ provides="lp.bugs.interfaces.vulnerability.IVulnerabilityActivitySet">
+ <allow interface="lp.bugs.interfaces.vulnerability.IVulnerabilityActivitySet" />
+ </securedutility>
+
+ <class class="lp.bugs.model.vulnerability.BugVulnerabilitySet">
+ <allow interface="lp.bugs.interfaces.vulnerability.IBugVulnerabilitySet" />
+ </class>
+ <securedutility
+ class="lp.bugs.model.vulnerability.BugVulnerabilitySet"
+ provides="lp.bugs.interfaces.vulnerability.IBugVulnerabilitySet">
+ <allow interface="lp.bugs.interfaces.vulnerability.IBugVulnerabilitySet" />
+ </securedutility>
+
<!-- BugSubscription -->
<class
diff --git a/lib/lp/bugs/interfaces/vulnerability.py b/lib/lp/bugs/interfaces/vulnerability.py
new file mode 100644
index 0000000..bdbb609
--- /dev/null
+++ b/lib/lp/bugs/interfaces/vulnerability.py
@@ -0,0 +1,252 @@
+# Copyright 2022 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Vulnerability interfaces."""
+
+__all__ = [
+ 'IBugVulnerabilitySet',
+ 'IVulnerability',
+ 'IVulnerabilityActivity',
+ 'IVulnerabilityActivitySet',
+ 'IVulnerabilitySet',
+ 'VulnerabilityChange',
+ 'VulnerabilityStatus'
+ ]
+
+from lazr.enum import (
+ DBEnumeratedType,
+ DBItem,
+ )
+from zope.interface import Interface
+from zope.schema import (
+ Bool,
+ Choice,
+ Datetime,
+ Int,
+ TextLine,
+ )
+
+from lp import _
+from lp.bugs.interfaces.bugtask import BugTaskImportance
+
+
+class VulnerabilityChange(DBEnumeratedType):
+ """Type of change in vulnerability
+
+ We use this enum to track changes occurring in
+ data stored in the vulnerability table.
+ """
+
+ STATUS = DBItem(0, """
+ Status
+
+ The status of the vulnerability changed.
+ """)
+
+ DESCRIPTION = DBItem(1, """
+ Description
+
+ The description of the vulnerability changed.
+ """)
+
+ NOTES = DBItem(2, """
+ Notes
+
+ The notes on the vulnerability changed.
+ """)
+
+ MITIGATION = DBItem(3, """
+ Mitigation
+
+ Mitigation for this vulnerability changed.
+ """)
+
+ IMPORTANCE = DBItem(4, """
+ Importance
+
+ The importance assigned for this vulnerability changed.
+ """)
+
+ IMPORTANCE_EXPLANATION = DBItem(5, """
+ Importance explanation
+
+ The importance explanation changed for this vulnerability.
+ """)
+
+ PRIVACY = DBItem(6, """
+ Privacy
+
+ The privacy for this vulnerability changed.
+ """)
+
+
+class VulnerabilityStatus(DBEnumeratedType):
+ """Vulnerability status"""
+
+ NEEDS_TRIAGE = DBItem(0, """
+ Needs triage
+
+ Not looked at yet.
+ """)
+
+ ACTIVE = DBItem(1, """
+ Active
+
+ The vulnerability is active.
+ """)
+
+ IGNORED = DBItem(2, """
+ Ignored
+
+ The vulnerability is currently ignored.
+ """)
+
+ RETIRED = DBItem(3, """
+ Retired
+
+ This vulnerability is now retired.
+ """)
+
+
+class IVulnerabilityView(Interface):
+ """`IVulnerability` attributes that require launchpad.View."""
+
+ id = Int(title=_("ID"), required=True, readonly=True)
+
+
+class IVulnerabilityEditableAttributes(Interface):
+ """`IVulnerability` attributes that can be edited.
+
+ These attributes need launchpad.View to see, and launchpad.Edit to change.
+ """
+
+ status = Choice(
+ title=_('Result of the report'), readonly=True,
+ required=False, vocabulary=VulnerabilityStatus)
+
+ description = TextLine(
+ title=_("A short description of the vulnerability."), required=False,
+ readonly=False)
+
+ notes = TextLine(
+ title=_("Free-form notes for this vulnerability."), required=False,
+ readonly=False)
+
+ mitigation = TextLine(
+ title=_("Explains why we're ignoring a vulnerability."),
+ required=False, readonly=False)
+
+ importance = Choice(title=_('Importance used to indicate work priority,'
+ ' not severity'),
+ vocabulary=BugTaskImportance,
+ default=BugTaskImportance.UNDECIDED, readonly=True)
+
+ importance_explanation = TextLine(
+ title=_("Used to explain why our importance differs "
+ "from somebody else's CVSS score."),
+ required=False, readonly=False)
+
+ private = Bool(
+ title=_("Indicates privacy of the vulnerability."), required=False,
+ readonly=True, default=False)
+
+
+class IVulnerabilityEdit(Interface):
+ """`IVulnerability` attributes that require launchpad.Edit."""
+
+
+class IVulnerability(IVulnerabilityView,
+ IVulnerabilityEditableAttributes,
+ IVulnerabilityEdit):
+ """Contract describing a vulnerability."""
+
+
+class IVulnerabilitySet(Interface):
+ """The set of all vulnerabilities."""
+
+ def new(distribution, cve, status, description,
+ notes, mitigation, importance, importance_explanation,
+ private):
+ """Return a new vulnerability.
+
+ :param distribution: The distribution for the vulnerability.
+ :param cve: A `Cve` for which the vulnerability is being created.
+ :param status: The status of the vulnerability.
+ :param description: The description of the vulnerability.
+ :param notes: The notes for the vulnerability.
+ :param mitigation: A short summary of the result.
+ :param importance: Indicates work priority, not severity.
+ :param importance_explanation: Used to explain why our importance
+ differs from somebody else''s CVSS score.
+ :param private: The privacy of the vulnerability.
+ """
+
+ def getByID(id):
+ """Returns the IVulnerability for a given ID."""
+
+
+class IVulnerabilityActivityView(Interface):
+ """`IVulnerabilityActivity` attributes that require launchpad.View."""
+
+ id = Int(title=_("ID"), required=True, readonly=True)
+
+ date_changed = Datetime(
+ title=_("When activity last changed for this vulnerability."),
+ required=True, readonly=True)
+
+
+class IVulnerabilityActivityEditableAttributes(Interface):
+ """`IVulnerabilityActivity` attributes that can be edited.
+
+ These attributes need launchpad.View to see, and launchpad.Edit to change.
+ """
+
+ what_changed = Choice(
+ title=_('Indicates what field changed for the vulnerability.'),
+ readonly=True,
+ required=True, vocabulary=VulnerabilityChange)
+
+ old_value = TextLine(
+ title=_("Indicates the value prior to the change."), required=False,
+ readonly=False)
+
+ new_value = TextLine(
+ title=_("Indicates the current value."), required=False,
+ readonly=False)
+
+
+class IVulnerabilityActivityEdit(Interface):
+ """`IVulnerabilityActivity` attributes that require launchpad.Edit."""
+
+
+class IVulnerabilityActivity(IVulnerabilityActivityView,
+ IVulnerabilityActivityEditableAttributes,
+ IVulnerabilityActivityEdit):
+ """Contract describing the actions taken for a vulnerability."""
+
+
+class IVulnerabilityActivitySet(Interface):
+ """The set of all activities for a certain vulnerability."""
+
+ def new(vulnerability, changer, what_changed=None,
+ old_value=None, new_value=None):
+ """Return a new vulnerability activity.
+
+ :param vulnerability: The vulnerability for this activity.
+ :param changer: The `Person` that performed the activity.
+ :param what_changed: The 'VulnerabilityChange' that occurred
+ for this vulnerability.
+ :param old_value: Indicates the value prior to the change.
+ :param new_value: Indicates the current value.
+ """
+
+
+class IBugVulnerabilitySet(Interface):
+ """The set of all activities for a certain vulnerability."""
+
+ def new(vulnerability, bug):
+ """Return a new bug-vulnerability link.
+
+ :param vulnerability: The `Vulnerability`.
+ :param bug: The `Bug`.
+ """
diff --git a/lib/lp/bugs/model/tests/test_vulnerability.py b/lib/lp/bugs/model/tests/test_vulnerability.py
new file mode 100644
index 0000000..09e1a39
--- /dev/null
+++ b/lib/lp/bugs/model/tests/test_vulnerability.py
@@ -0,0 +1,61 @@
+# Copyright 2022 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for the vulnerability and related models."""
+from zope.security.proxy import removeSecurityProxy
+
+from lp.bugs.interfaces.vulnerability import VulnerabilityChange
+from lp.services.webapp.authorization import check_permission
+from lp.testing import (
+ admin_logged_in,
+ anonymous_logged_in,
+ person_logged_in,
+ TestCaseWithFactory,
+ )
+from lp.testing.layers import DatabaseFunctionalLayer
+
+
+class TestVulnerability(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_vulnerability_permissions(self):
+ vulnerability = self.factory.makeVulnerability()
+
+ with person_logged_in(self.factory.makePerson()):
+ self.assertTrue(check_permission("launchpad.View", vulnerability))
+ self.assertFalse(check_permission("launchpad.Edit", vulnerability))
+
+ with admin_logged_in():
+ self.assertTrue(check_permission("launchpad.View", vulnerability))
+ self.assertTrue(check_permission("launchpad.View", vulnerability))
+
+ with anonymous_logged_in():
+ self.assertFalse(check_permission("launchpad.View", vulnerability))
+ self.assertFalse(check_permission("launchpad.View", vulnerability))
+
+
+class TestVulnerabilityActivity(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_vulnerability_activity_changes(self):
+ vulnerability = self.factory.makeVulnerability()
+ changer = self.factory.makePerson()
+ activity = self.factory.makeVulnerabilityActivity(
+ vulnerability=vulnerability, changer=None)
+ with person_logged_in(changer):
+ self.assertTrue(VulnerabilityChange.DESCRIPTION,
+ activity.what_changed)
+
+
+class TestBugVulnerability(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_bug_vulnerability(self):
+ vulnerability = self.factory.makeVulnerability()
+ bug = self.factory.makeBug()
+ bugVulnerability = removeSecurityProxy(
+ self.factory.makeBugVulnerability(vulnerability, bug))
+ self.assertEqual(bug, bugVulnerability.bug)
diff --git a/lib/lp/bugs/model/vulnerability.py b/lib/lp/bugs/model/vulnerability.py
new file mode 100644
index 0000000..8b70480
--- /dev/null
+++ b/lib/lp/bugs/model/vulnerability.py
@@ -0,0 +1,172 @@
+# Copyright 2022 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__all__ = [
+ 'Vulnerability',
+ 'VulnerabilitySet',
+ ]
+
+import pytz
+from storm.locals import (
+ Bool,
+ DateTime,
+ Int,
+ Reference,
+ Unicode,
+ )
+from zope.interface import implementer
+
+from lp.bugs.interfaces.bugtask import BugTaskImportance
+from lp.bugs.interfaces.vulnerability import (
+ IBugVulnerabilitySet,
+ IVulnerability,
+ IVulnerabilityActivity,
+ IVulnerabilityActivitySet,
+ VulnerabilityChange,
+ VulnerabilityStatus,
+ )
+from lp.services.database.constants import UTC_NOW
+from lp.services.database.enumcol import DBEnum
+from lp.services.database.interfaces import IStore
+from lp.services.database.stormbase import StormBase
+
+
+@implementer(IVulnerability)
+class Vulnerability(StormBase):
+ __storm_table__ = 'Vulnerability'
+
+ id = Int(primary=True)
+
+ distribution_id = Int(name="distribution", allow_none=True, default=None)
+ distribution = Reference(distribution_id, "Distribution.id")
+
+ cve_id = Int(name="cve", allow_none=True, default=None)
+ cve = Reference(cve_id, "Cve.id")
+
+ status = DBEnum(name='status', allow_none=True,
+ enum=VulnerabilityStatus)
+
+ description = Unicode(name='description', allow_none=True)
+
+ notes = Unicode(name='notes', allow_none=True)
+
+ mitigation = Unicode(name='mitigation', allow_none=True)
+
+ importance = DBEnum(
+ name='importance', allow_none=False,
+ enum=BugTaskImportance,
+ default=BugTaskImportance.UNDECIDED)
+
+ importance_explanation = Unicode(
+ name='importance_explanation', allow_none=True)
+
+ private = Bool(name='private', allow_none=False, default=False)
+
+ def __init__(self, distribution, cve, status, description,
+ notes, mitigation, importance, importance_explanation,
+ private):
+ super().__init__()
+ self.distribution = distribution
+ self.cve = cve
+ self.status = status
+ self.description = description
+ self.notes = notes
+ self.mitigation = mitigation
+ self.importance = importance
+ self.importance_explanation = importance_explanation
+ self.private = private
+
+
+@implementer(IVulnerability)
+class VulnerabilitySet:
+
+ def new(self, distribution, cve, status, description,
+ notes, mitigation, importance, importance_explanation,
+ private):
+ """See `VulnerabilitySet`."""
+ store = IStore(Vulnerability)
+ vulnerability = Vulnerability(distribution, cve, status, description,
+ notes, mitigation, importance,
+ importance_explanation, private)
+ store.add(vulnerability)
+ return vulnerability
+
+ def getByID(self, id):
+ return IStore(
+ Vulnerability).find(Vulnerability, id=id).one()
+
+
+@implementer(IVulnerabilityActivity)
+class VulnerabilityActivity(StormBase):
+ __storm_table__ = 'VulnerabilityActivity'
+
+ id = Int(primary=True)
+
+ vulnerability_id = Int(name="vulnerability", allow_none=True, default=None)
+ vulnerability = Reference(vulnerability_id, "Vulnerability.id")
+
+ changer_id = Int(name="changer", allow_none=True, default=None)
+ changer = Reference(changer_id, "Person.id")
+
+ date_changed = DateTime(
+ name='date_changed', tzinfo=pytz.UTC, allow_none=False)
+
+ what_changed = DBEnum(name='what_changed', allow_none=True,
+ enum=VulnerabilityChange)
+
+ old_value = Unicode(name='old_value', allow_none=True)
+
+ new_value = Unicode(name='new_value', allow_none=True)
+
+ def __init__(self, vulnerability, changer, what_changed=None,
+ old_value=None, new_value=None):
+ super().__init__()
+ self.vulnerability = vulnerability
+ self.changer = changer
+ self.what_changed = what_changed
+ self.old_value = old_value
+ self.new_value = new_value
+ self.date_changed = UTC_NOW
+
+
+@implementer(IVulnerabilityActivitySet)
+class VulnerabilityActivitySet:
+
+ def new(self, vulnerability, changer,
+ what_changed,
+ old_value=None, new_value=None):
+ """See `VulnerabilitySet`."""
+ store = IStore(VulnerabilityActivity)
+ activity = VulnerabilityActivity(vulnerability, changer,
+ what_changed,
+ old_value, new_value)
+ store.add(activity)
+ return activity
+
+
+class BugVulnerability(StormBase):
+
+ __storm_table__ = 'BugVulnerability'
+ __storm_primary__ = 'bug_id', 'vulnerability_id'
+
+ bug_id = Int(name="bug", allow_none=True, default=None)
+ bug = Reference(bug_id, "Bug.id")
+
+ vulnerability_id = Int(name="vulnerability", allow_none=True, default=None)
+ vulnerability = Reference(vulnerability_id, "Vulnerability.id")
+
+ def __init__(self, vulnerability, bug):
+ super().__init__()
+ self.vulnerability = vulnerability
+ self.bug = bug
+
+
+@implementer(IBugVulnerabilitySet)
+class BugVulnerabilitySet:
+
+ def new(self, vulnerability, bug):
+ """See `BugVulnerability`."""
+ store = IStore(BugVulnerability)
+ bugVulnerability = BugVulnerability(vulnerability, bug)
+ store.add(bugVulnerability)
+ return bugVulnerability
diff --git a/lib/lp/security.py b/lib/lp/security.py
index 22bc140..98e6f26 100644
--- a/lib/lp/security.py
+++ b/lib/lp/security.py
@@ -55,6 +55,7 @@ from lp.blueprints.model.specificationsubscription import (
)
from lp.bugs.interfaces.bugtarget import IOfficialBugTagTargetRestricted
from lp.bugs.interfaces.structuralsubscription import IStructuralSubscription
+from lp.bugs.interfaces.vulnerability import IVulnerability
from lp.bugs.model.bugsubscription import BugSubscription
from lp.bugs.model.bugtaskflat import BugTaskFlat
from lp.bugs.model.bugtasksearch import get_bug_privacy_filter
@@ -3781,3 +3782,12 @@ class EditCIBuild(AdminByBuilddAdmin):
if auth_repository.checkAuthenticated(user):
return True
return super().checkAuthenticated(user)
+
+
+class EditVulnerability(AuthorizationBase):
+ permission = 'launchpad.Edit'
+ usedfor = IVulnerability
+
+ def checkAuthenticated(self, user):
+ return (
+ user.in_commercial_admin or user.in_admin)
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index 492f57b..d0c4e39 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -83,6 +83,7 @@ from lp.bugs.interfaces.bug import (
IBugSet,
)
from lp.bugs.interfaces.bugtask import (
+ BugTaskImportance,
BugTaskStatus,
IBugTaskSet,
)
@@ -95,6 +96,13 @@ from lp.bugs.interfaces.cve import (
CveStatus,
ICveSet,
)
+from lp.bugs.interfaces.vulnerability import (
+ IBugVulnerabilitySet,
+ IVulnerabilityActivitySet,
+ IVulnerabilitySet,
+ VulnerabilityChange,
+ VulnerabilityStatus,
+ )
from lp.bugs.model.bug import FileBugData
from lp.buildmaster.enums import (
BuilderResetProtocol,
@@ -5313,6 +5321,62 @@ class BareLaunchpadObjectFactory(ObjectFactory):
IStore(build).flush()
return build
+ def makeVulnerability(self, distribution=None, cve=None, status=None,
+ description=None, notes=None, mitigation=None,
+ importance=None, importance_explanation=None,
+ private=None):
+ """Make a new `Vulnerability`."""
+ if distribution is None:
+ distribution = self.makeDistribution()
+ if cve is None:
+ cve = self.makeCVE('2022-1234')
+ if status is None:
+ status = VulnerabilityStatus.NEEDS_TRIAGE
+ if description is None:
+ description = self.getUniqueString("vulnerability-description")
+ if notes is None:
+ notes = self.getUniqueString("vulnerability-notes")
+ if mitigation is None:
+ mitigation = self.getUniqueString("vulnerability-mitigation")
+ if importance is None:
+ importance = BugTaskImportance.UNDECIDED
+ if importance_explanation is None:
+ importance_explanation = self.getUniqueString(
+ "vulnerability-importance-explanation")
+ if private is None:
+ private = False
+ return getUtility(
+ IVulnerabilitySet).new(distribution, cve, status,
+ description, notes, mitigation,
+ importance, importance_explanation,
+ private)
+
+ def makeVulnerabilityActivity(self, vulnerability=None, changer=None,
+ what_changed=None, old_value=None,
+ new_value=None):
+ """Make a new `VulnerabilityActivity`."""
+ if vulnerability is None:
+ vulnerability = self.makeVulnerability()
+ if changer is None:
+ changer = self.makePerson()
+ if what_changed is None:
+ what_changed = VulnerabilityChange.DESCRIPTION
+ if old_value is None:
+ old_value = self.getUniqueString("old-value")
+ if new_value is None:
+ new_value = self.getUniqueString("new-description")
+ return getUtility(
+ IVulnerabilityActivitySet).new(vulnerability, changer,
+ what_changed, old_value, new_value)
+
+ def makeBugVulnerability(self, vulnerability=None, bug=None):
+ """Make a new `BugVulnerability`."""
+ if vulnerability is None:
+ vulnerability = self.makeVulnerability()
+ if bug is None:
+ bug = self.makeBug()
+ return getUtility(
+ IBugVulnerabilitySet).new(vulnerability, bug)
# Some factory methods return simple Python types. We don't add
# security wrappers for them, as well as for objects created by
Follow ups