launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #29011
[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