← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~lgp171188/launchpad:vulnerability-ui into launchpad:master

 

Guruprasad has proposed merging ~lgp171188/launchpad:vulnerability-ui into launchpad:master.

Commit message:
Implement a read-only browser page for vulnerability

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~lgp171188/launchpad/+git/launchpad/+merge/428481
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~lgp171188/launchpad:vulnerability-ui into launchpad:master.
diff --git a/lib/lp/bugs/browser/configure.zcml b/lib/lp/bugs/browser/configure.zcml
index d03a00c..ff9e1e4 100644
--- a/lib/lp/bugs/browser/configure.zcml
+++ b/lib/lp/bugs/browser/configure.zcml
@@ -896,6 +896,15 @@
         path_expression="string:+vulnerability/${id}"
         attribute_to_parent="distribution"
         rootsite="bugs"/>
+    <browser:defaultView
+        for="lp.bugs.interfaces.vulnerability.IVulnerability"
+        name="+index" />
+    <browser:page
+        name="+index"
+        for="lp.bugs.interfaces.vulnerability.IVulnerability"
+        class="lp.bugs.browser.vulnerability.VulnerabilityIndexView"
+        permission="zope.Public"
+        template="../templates/vulnerability-index.pt" />
     <browser:url
         for="lp.bugs.interfaces.bugsubscription.IBugSubscription"
         path_expression="string:+subscription/${person/name}"
diff --git a/lib/lp/bugs/browser/tests/test_cve.py b/lib/lp/bugs/browser/tests/test_cvereport.py
similarity index 100%
rename from lib/lp/bugs/browser/tests/test_cve.py
rename to lib/lp/bugs/browser/tests/test_cvereport.py
diff --git a/lib/lp/bugs/browser/tests/test_vulnerability.py b/lib/lp/bugs/browser/tests/test_vulnerability.py
new file mode 100644
index 0000000..eac7e06
--- /dev/null
+++ b/lib/lp/bugs/browser/tests/test_vulnerability.py
@@ -0,0 +1,246 @@
+# Copyright 2022 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Vulnerability browser tests"""
+from datetime import datetime, timezone
+
+from soupmatchers import HTMLContains, Tag, Within
+from testtools.matchers import MatchesAll, Not
+
+from lp.services.webapp import canonical_url
+from lp.testing import ANONYMOUS, BrowserTestCase, login, person_logged_in
+from lp.testing.layers import DatabaseFunctionalLayer
+
+
+class TestVulnerabilityPage(BrowserTestCase):
+    """Tests for the vulnerability browser page."""
+
+    layer = DatabaseFunctionalLayer
+
+    def get_vulnerability_field_tag(self, name, text, element="span"):
+        return Tag(
+            name,
+            element,
+            attrs={"id": "-".join(name.lower().split())},
+            text=text,
+        )
+
+    def test_page_title_vulnerability_without_linked_cve(self):
+        vulnerability = self.factory.makeVulnerability()
+        browser = self.getUserBrowser(
+            canonical_url(vulnerability),
+            user=self.factory.makePerson(),
+        )
+        login(ANONYMOUS)
+        self.assertThat(
+            browser.contents,
+            HTMLContains(
+                Tag(
+                    "page title",
+                    "title",
+                    text="Bugs : Vulnerability #{} : {}".format(
+                        vulnerability.id,
+                        vulnerability.distribution.displayname,
+                    ),
+                )
+            ),
+        )
+
+    def test_page_title_vulnerability_with_linked_cve(self):
+        cve = self.factory.makeCVE(sequence="2022-1234")
+        vulnerability = self.factory.makeVulnerability(cve=cve)
+        browser = self.getUserBrowser(
+            canonical_url(vulnerability),
+            user=self.factory.makePerson(),
+        )
+        login(ANONYMOUS)
+        self.assertThat(
+            browser.contents,
+            HTMLContains(
+                Tag(
+                    "page title",
+                    "title",
+                    text="Bugs : Vulnerability CVE-2022-1234 : {}".format(
+                        vulnerability.distribution.displayname
+                    ),
+                )
+            ),
+        )
+
+    def test_vulnerability_page_contains_all_expected_fields(self):
+        vulnerability = self.factory.makeVulnerability()
+        browser = self.getUserBrowser(
+            canonical_url(vulnerability),
+            user=self.factory.makePerson(),
+        )
+        fields = (
+            "Date created",
+            "Date made public",
+            "CVE",
+            "Information type",
+            "Status",
+            "Importance",
+            "Importance explanation",
+            "Creator",
+            "Notes",
+            "Mitigation",
+        )
+        matchers = []
+        for field in fields:
+            matchers.append(
+                HTMLContains(Tag(field, "b", text="{}:".format(field)))
+            )
+        self.assertThat(browser.contents, MatchesAll(*matchers))
+
+    def test_vulnerability_page_default_values(self):
+        vulnerability = self.factory.makeVulnerability()
+        browser = self.getUserBrowser(
+            canonical_url(vulnerability),
+            user=self.factory.makePerson(),
+        )
+        login(ANONYMOUS)
+        self.assertThat(
+            browser.contents,
+            MatchesAll(
+                HTMLContains(
+                    self.get_vulnerability_field_tag(
+                        "Date created",
+                        vulnerability.date_created.strftime("%Y-%m-%d"),
+                    ),
+                    self.get_vulnerability_field_tag(
+                        "Date made public", "None"
+                    ),
+                    self.get_vulnerability_field_tag("CVE", "None"),
+                    self.get_vulnerability_field_tag(
+                        "Information type",
+                        vulnerability.information_type.title,
+                    ),
+                    self.get_vulnerability_field_tag(
+                        "Status", vulnerability.status.title
+                    ),
+                    self.get_vulnerability_field_tag(
+                        "Importance", vulnerability.importance.title
+                    ),
+                    self.get_vulnerability_field_tag(
+                        # importance_explanation defaults to `None` but the
+                        # factory
+                        # method auto-generates a value for it.
+                        "Importance explanation",
+                        vulnerability.importance_explanation,
+                    ),
+                    Within(
+                        Tag("Creator", "span", attrs={"id": "creator"}),
+                        Tag(
+                            "Creator link",
+                            "a",
+                            attrs={
+                                "href": canonical_url(vulnerability.creator),
+                                "class": "sprite person",
+                            },
+                            text=vulnerability.creator.displayname,
+                        ),
+                    ),
+                    self.get_vulnerability_field_tag("Notes", "None"),
+                    self.get_vulnerability_field_tag("Mitigation", "None"),
+                ),
+                Not(
+                    HTMLContains(
+                        Tag(
+                            "Related bugs", "div", attrs={"id": "related-bugs"}
+                        )
+                    )
+                ),
+            ),
+        )
+
+    def test_vulnerability_cve_linked(self):
+        cve = self.factory.makeCVE(sequence="2022-1234")
+        vulnerability = self.factory.makeVulnerability(cve=cve)
+        browser = self.getUserBrowser(
+            canonical_url(vulnerability),
+            user=self.factory.makePerson(),
+        )
+        login(ANONYMOUS)
+        self.assertThat(
+            browser.contents,
+            HTMLContains(
+                Within(
+                    Tag(
+                        "CVE",
+                        "span",
+                        attrs={"id": "cve"},
+                    ),
+                    Tag(
+                        "CVE link",
+                        "a",
+                        attrs={
+                            "href": canonical_url(cve, force_local_path=True)
+                        },
+                    ),
+                )
+            ),
+        )
+
+    def test_vulnerability_optional_parameters_set(self):
+        vulnerability = self.factory.makeVulnerability(
+            date_made_public=datetime(1970, 1, 1, tzinfo=timezone.utc),
+            notes="These are some notes",
+            mitigation="Here is a mitigation",
+        )
+        browser = self.getUserBrowser(
+            canonical_url(vulnerability),
+            user=self.factory.makePerson(),
+        )
+        login(ANONYMOUS)
+        self.assertThat(
+            browser.contents,
+            HTMLContains(
+                self.get_vulnerability_field_tag(
+                    "Date made public",
+                    vulnerability.date_made_public.strftime("%Y-%m-%d"),
+                ),
+                self.get_vulnerability_field_tag(
+                    "Notes", "These are some notes"
+                ),
+                self.get_vulnerability_field_tag(
+                    "Mitigation", "Here is a mitigation"
+                ),
+            ),
+        )
+
+    def test_vulnerability_related_bugs_present(self):
+        vulnerability = self.factory.makeVulnerability()
+        bug1 = self.factory.makeBug()
+        bug2 = self.factory.makeBug()
+        with person_logged_in(vulnerability.distribution.owner):
+            vulnerability.linkBug(bug1)
+            vulnerability.linkBug(bug2)
+        browser = self.getUserBrowser(
+            canonical_url(vulnerability),
+            user=self.factory.makePerson(),
+        )
+        login(ANONYMOUS)
+        self.assertThat(
+            browser.contents,
+            HTMLContains(
+                Tag("Related bugs", "div", attrs={"id": "related-bugs"}),
+                Tag(
+                    "Bug #{}".format(bug1.id),
+                    "a",
+                    attrs={
+                        "class": "sprite bug",
+                        "href": canonical_url(bug1, force_local_path=True),
+                    },
+                    text="Bug #{}: {}".format(bug1.id, bug1.title),
+                ),
+                Tag(
+                    "Bug #{}".format(bug2.id),
+                    "a",
+                    attrs={
+                        "class": "sprite bug",
+                        "href": canonical_url(bug2, force_local_path=True),
+                    },
+                    text="Bug #{}: {}".format(bug2.id, bug2.title),
+                ),
+            ),
+        )
diff --git a/lib/lp/bugs/browser/vulnerability.py b/lib/lp/bugs/browser/vulnerability.py
new file mode 100644
index 0000000..17015cd
--- /dev/null
+++ b/lib/lp/bugs/browser/vulnerability.py
@@ -0,0 +1,35 @@
+# Copyright 2022 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Vulnerability views."""
+
+__all__ = [
+    "VulnerabilityIndexView",
+]
+
+from zope.interface import implementer
+
+from lp.app.interfaces.headings import IHeadingBreadcrumb
+from lp.bugs.browser.buglinktarget import BugLinksListingView
+from lp.services.webapp.breadcrumb import TitleBreadcrumb
+from lp.services.webapp.interfaces import IMultiFacetedBreadcrumb
+
+
+@implementer(IHeadingBreadcrumb, IMultiFacetedBreadcrumb)
+class VulnerabilityBreadcrumb(TitleBreadcrumb):
+    pass
+
+
+class VulnerabilityIndexView(BugLinksListingView):
+    """Vulnerability index page."""
+
+    @property
+    def page_title(self):
+        cve = self.context.cve
+        if cve is not None:
+            displayname = cve.displayname
+        else:
+            displayname = "Vulnerability"
+        return "{} in the {} distribution".format(
+            displayname, self.context.distribution.displayname
+        )
diff --git a/lib/lp/bugs/configure.zcml b/lib/lp/bugs/configure.zcml
index 895b8e9..6eaa58c 100644
--- a/lib/lp/bugs/configure.zcml
+++ b/lib/lp/bugs/configure.zcml
@@ -618,6 +618,12 @@
               linkBug
               unlinkBug"/>
     </class>
+    <adapter
+        provides="lp.services.webapp.interfaces.IBreadcrumb"
+        for="lp.bugs.interfaces.vulnerability.IVulnerability"
+        factory="lp.bugs.browser.vulnerability.VulnerabilityBreadcrumb"
+        permission="zope.Public"/>
+
     <class class="lp.bugs.model.vulnerability.VulnerabilitySet">
       <allow interface="lp.bugs.interfaces.vulnerability.IVulnerabilitySet" />
     </class>
diff --git a/lib/lp/bugs/interfaces/vulnerability.py b/lib/lp/bugs/interfaces/vulnerability.py
index 6fe9abc..966acf2 100644
--- a/lib/lp/bugs/interfaces/vulnerability.py
+++ b/lib/lp/bugs/interfaces/vulnerability.py
@@ -15,7 +15,7 @@ from lazr.enum import DBEnumeratedType, DBItem
 from lazr.restful.declarations import exported, exported_as_webservice_entry
 from lazr.restful.fields import CollectionField, Reference
 from zope.interface import Interface
-from zope.schema import Choice, Datetime, Int, TextLine
+from zope.schema import Bool, Choice, Datetime, Int, TextLine
 
 from lp import _
 from lp.app.enums import InformationType
@@ -105,6 +105,18 @@ class IVulnerabilityView(Interface):
         Int(title=_("ID"), required=True, readonly=True), as_of="devel"
     )
 
+    private = exported(
+        Bool(
+            title=_("This vulnerability should be private"),
+            required=False,
+            description=_(
+                "Private vulnerabilities are visible only "
+                "to their subscribers."
+            ),
+            readonly=True,
+        )
+    )
+
     distribution = exported(
         Reference(
             IDistribution,
@@ -115,6 +127,12 @@ class IVulnerabilityView(Interface):
         as_of="devel",
     )
 
+    title = TextLine(
+        title=_("Title"),
+        required=True,
+        description=_("The title for this vulnerability"),
+    )
+
     date_created = exported(
         Datetime(
             title=_("The date this vulnerability was created."),
diff --git a/lib/lp/bugs/model/vulnerability.py b/lib/lp/bugs/model/vulnerability.py
index 63b6e78..d8d00a7 100644
--- a/lib/lp/bugs/model/vulnerability.py
+++ b/lib/lp/bugs/model/vulnerability.py
@@ -135,6 +135,18 @@ class Vulnerability(StormBase, BugLinkTargetMixin, InformationTypeMixin):
         self.date_created = UTC_NOW
 
     @property
+    def private(self):
+        return self.information_type not in PUBLIC_INFORMATION_TYPES
+
+    @property
+    def title(self):
+        if self.cve:
+            displayname = self.cve.displayname
+        else:
+            displayname = "#{}".format(self.id)
+        return "Vulnerability {}".format(displayname)
+
+    @property
     def bugs(self):
         bug_ids = [
             int(id)
diff --git a/lib/lp/bugs/templates/vulnerability-index.pt b/lib/lp/bugs/templates/vulnerability-index.pt
new file mode 100644
index 0000000..2151686
--- /dev/null
+++ b/lib/lp/bugs/templates/vulnerability-index.pt
@@ -0,0 +1,89 @@
+<html
+  xmlns="http://www.w3.org/1999/xhtml";
+  xmlns:tal="http://xml.zope.org/namespaces/tal";
+  xmlns:metal="http://xml.zope.org/namespaces/metal";
+  xml:lang="en"
+  lang="en"
+  dir="ltr"
+  metal:use-macro="view/macro:page/main_side">
+  <body>
+    <div metal:fill-slot="main">
+      <div tal:define="global linked_cve context/cve"><a href="#">Launchpad vulnerabilities</a></div>
+      <h1 tal:condition="linked_cve">
+        Vulnerability CVE-<tal:num replace="context/cve/sequence">1234-5678</tal:num> in <tal:distribution replace="context/distribution/displayname">ubuntu</tal:distribution>
+      </h1>
+      <h1 tal:condition="not:linked_cve">
+        Vulnerability in <tal:distribution replace="context/distribution/displayname">ubuntu</tal:distribution>
+      </h1>
+      <div tal:condition="linked_cve">
+      </div>
+      <tal:desc replace="structure context/description/fmt:text-to-html">foo</tal:desc>
+      <ul id="vulnerability-fields">
+        <li>
+          <b>Date created:</b>
+          <span id="date-created" tal:content="structure context/date_created/fmt:date">date created</span>
+        </li>
+        <li>
+          <b>Date made public:</b>
+          <span id="date-made-public"
+                tal:condition="context/date_made_public"
+                tal:content="structure context/date_made_public/fmt:date">date made public</span>
+          <span id="date-made-public" tal:condition="not:context/date_made_public">None</span>
+        </li>
+        <li>
+          <b>CVE:</b>
+          <span id="cve" tal:condition="linked_cve">
+            <img src="/@@/cve"/>
+            <a tal:attributes="href context/cve/fmt:url"
+                tal:content="string: CVE-${context/cve/sequence}"
+                href="/bugs/cve/2014-6271">CVE-1234-5678
+            </a>
+          </span>
+          <span id="cve" tal:condition="not:linked_cve">None</span>
+        </li>
+        <li>
+          <b>Information type:</b>
+          <span id="information-type" tal:content="context/information_type/title">information type</span>
+        </li>
+        <li>
+          <b>Status:</b>
+          <span id="status" tal:content="context/status/title">status</span>
+        </li>
+        <li>
+          <b>Importance:</b>
+          <span id="importance" tal:content="context/importance/title">importance</span>
+        </li>
+        <li>
+          <b>Importance explanation:</b>
+          <span id="importance-explanation" tal:content="python: context.importance_explanation or 'None'">
+            importance explanation
+          </span>
+        </li>
+        <li>
+          <b>Creator:</b>
+          <span id="creator"><tal:creator replace="structure context/creator/fmt:link" /></span>
+        </li>
+        <li>
+          <b>Notes:</b>
+          <span id="notes" tal:content="python: context.notes or 'None'">notes</span>
+        </li>
+        <li>
+          <b>Mitigation:</b>
+          <span id="mitigation" tal:content="python: context.mitigation or 'None'">mitigation</span>
+        </li>
+      </ul>
+      <div id="related-bugs" tal:condition="view/buglinks">
+        <h2>Related bugs and status</h2>
+        <p tal:condition="view/buglinks">
+          This vulnerability is related to these bugs:
+        </p>
+        <div tal:repeat="link view/buglinks">
+          <strong>
+            <a tal:replace="structure link/bug/fmt:link" />
+          </strong>
+          <div tal:replace="structure link/batch_navigator/@@+table-view-without-navlinks" />
+        </div>
+      </div>
+    </div>
+  </body>
+</html>

Follow ups