← Back to team overview

launchpad-reviewers team mailing list archive

[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