Merge ~lgp171188/launchpad:security-tracker-cve-changes into launchpad:master

Proposed by Guruprasad
Status: Merged
Approved by: Guruprasad
Approved revision: e77cdf5529be8f1c81252b78516fc921ae4feac3
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~lgp171188/launchpad:security-tracker-cve-changes
Merge into: launchpad:master
Diff against target: 374 lines (+256/-8)
4 files modified
lib/lp/bugs/interfaces/cve.py (+38/-1)
lib/lp/bugs/model/cve.py (+38/-4)
lib/lp/bugs/tests/test_cve.py (+168/-1)
lib/lp/testing/factory.py (+12/-2)
Reviewer Review Type Date Requested Status
Guruprasad Approve
Review via email: mp+416733@code.launchpad.net

Commit message

Add new fields and methods to ICve and Cve

Add the 'date_made_public', 'discoverer', and the 'cvss' fields. Also
add the 'setCVSSVectorForAuthority()' method.

To post a comment you must log in.
Revision history for this message
Guruprasad (lgp171188) wrote :

Self-approving this MP as its dependency DB changes have been merged and this just reinstates a previously approved, merged, and reverted change.

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/interfaces/cve.py b/lib/lp/bugs/interfaces/cve.py
2index 2ba5a53..3a860da 100644
3--- a/lib/lp/bugs/interfaces/cve.py
4+++ b/lib/lp/bugs/interfaces/cve.py
5@@ -30,12 +30,15 @@ from zope.interface import (
6 from zope.schema import (
7 Choice,
8 Datetime,
9+ Dict,
10 Int,
11+ Text,
12 TextLine,
13 )
14
15 from lp import _
16 from lp.app.validators.validation import valid_cve_sequence
17+from lp.services.fields import PersonChoice
18
19
20 class CveStatus(DBEnumeratedType):
21@@ -121,12 +124,45 @@ class ICve(Interface):
22 description=_("A title for the CVE")))
23 references = Attribute("The set of CVE References for this CVE.")
24
25+ date_made_public = exported(
26+ Datetime(title=_('Date Made Public'), required=False, readonly=True),
27+ as_of='devel'
28+ )
29+
30+ discoverer = exported(
31+ PersonChoice(
32+ title=_('Discoverer'),
33+ required=False,
34+ readonly=True,
35+ vocabulary='ValidPerson'
36+ ),
37+ as_of='devel'
38+ )
39+
40+ cvss = exported(
41+ Dict(
42+ title=_('CVSS'),
43+ description=_(
44+ 'The CVSS vector strings from various authorities '
45+ 'that publish it.'
46+ ),
47+ key_type=Text(title=_('The authority that published the score.')),
48+ value_type=Text(title=_('The CVSS vector string.')),
49+ required=False,
50+ readonly=True,
51+ ),
52+ as_of='devel'
53+ )
54+
55 def createReference(source, content, url=None):
56 """Create a new CveReference for this CVE."""
57
58 def removeReference(ref):
59 """Remove a CveReference."""
60
61+ def setCVSSVectorForAuthority(authority, vector_string):
62+ """Set the CVSS vector string from an authority."""
63+
64
65 @exported_as_webservice_collection(ICve)
66 class ICveSet(Interface):
67@@ -140,7 +176,8 @@ class ICveSet(Interface):
68 def __iter__():
69 """Iterate through all the Cve records."""
70
71- def new(sequence, description, cvestate=CveStatus.CANDIDATE):
72+ def new(sequence, description, cvestate=CveStatus.CANDIDATE,
73+ date_made_public=None, discoverer=None, cvss=None):
74 """Create a new ICve."""
75
76 @collection_default_content()
77diff --git a/lib/lp/bugs/model/cve.py b/lib/lp/bugs/model/cve.py
78index 27a40b5..f49ce00 100644
79--- a/lib/lp/bugs/model/cve.py
80+++ b/lib/lp/bugs/model/cve.py
81@@ -9,10 +9,12 @@ __all__ = [
82 import operator
83
84 import pytz
85+from storm.databases.postgres import JSON
86 from storm.locals import (
87 DateTime,
88 Desc,
89 Int,
90+ Reference,
91 ReferenceSet,
92 Store,
93 Unicode,
94@@ -60,11 +62,29 @@ class Cve(StormBase, BugLinkTargetMixin):
95 references = ReferenceSet(
96 id, 'CveReference.cve_id', order_by='CveReference.id')
97
98- def __init__(self, sequence, status, description):
99+ date_made_public = DateTime(tzinfo=pytz.UTC, allow_none=True)
100+ discoverer_id = Int(name='discoverer', allow_none=True)
101+ discoverer = Reference(discoverer_id, 'Person.id')
102+ _cvss = JSON(name='cvss', allow_none=True)
103+
104+ @property
105+ def cvss(self):
106+ return self._cvss or {}
107+
108+ @cvss.setter
109+ def cvss(self, value):
110+ assert value is None or isinstance(value, dict)
111+ self._cvss = value
112+
113+ def __init__(self, sequence, status, description,
114+ date_made_public=None, discoverer=None, cvss=None):
115 super().__init__()
116 self.sequence = sequence
117 self.status = status
118 self.description = description
119+ self.date_made_public = date_made_public
120+ self.discoverer = discoverer
121+ self._cvss = cvss
122
123 @property
124 def url(self):
125@@ -111,6 +131,12 @@ class Cve(StormBase, BugLinkTargetMixin):
126 getUtility(IXRefSet).delete(
127 {('cve', self.sequence): [('bug', str(bug.id))]})
128
129+ def setCVSSVectorForAuthority(self, authority, vector_string):
130+ """See ICveReference."""
131+ if self._cvss is None:
132+ self._cvss = {}
133+ self._cvss[authority] = vector_string
134+
135
136 @implementer(ICveSet)
137 class CveSet:
138@@ -136,10 +162,18 @@ class CveSet:
139 """See ICveSet."""
140 return iter(IStore(Cve).find(Cve))
141
142- def new(self, sequence, description, status=CveStatus.CANDIDATE):
143+ def new(self, sequence, description, status=CveStatus.CANDIDATE,
144+ date_made_public=None, discoverer=None, cvss=None):
145 """See ICveSet."""
146- cve = Cve(sequence=sequence, status=status,
147- description=description)
148+ cve = Cve(
149+ sequence=sequence,
150+ status=status,
151+ description=description,
152+ date_made_public=date_made_public,
153+ discoverer=discoverer,
154+ cvss=cvss
155+ )
156+
157 IStore(Cve).add(cve)
158 return cve
159
160diff --git a/lib/lp/bugs/tests/test_cve.py b/lib/lp/bugs/tests/test_cve.py
161index 90bffc7..6882b34 100644
162--- a/lib/lp/bugs/tests/test_cve.py
163+++ b/lib/lp/bugs/tests/test_cve.py
164@@ -3,10 +3,19 @@
165
166 """CVE related tests."""
167
168+from datetime import datetime
169+
170+import pytz
171+from testtools.matchers import MatchesStructure
172+from testtools.testcase import ExpectedException
173 from zope.component import getUtility
174+from zope.security.proxy import removeSecurityProxy
175
176 from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams
177-from lp.bugs.interfaces.cve import ICveSet
178+from lp.bugs.interfaces.cve import (
179+ CveStatus,
180+ ICveSet,
181+ )
182 from lp.testing import (
183 login_person,
184 person_logged_in,
185@@ -133,3 +142,161 @@ class TestBugLinks(TestCaseWithFactory):
186 self.assertContentEqual([bug1], cve2.bugs)
187 self.assertContentEqual([cve2], bug1.cves)
188 self.assertContentEqual([], bug2.cves)
189+
190+
191+class TestCve(TestCaseWithFactory):
192+ """Tests for Cve fields and methods."""
193+
194+ layer = DatabaseFunctionalLayer
195+
196+ def test_cveset_new_method_optional_parameters(self):
197+ cve = getUtility(ICveSet).new(
198+ sequence='2099-1234',
199+ description='A critical vulnerability',
200+ status=CveStatus.CANDIDATE
201+ )
202+ self.assertThat(cve, MatchesStructure.byEquality(
203+ sequence='2099-1234',
204+ status=CveStatus.CANDIDATE,
205+ description='A critical vulnerability',
206+ date_made_public=None,
207+ discoverer=None,
208+ cvss={}
209+ ))
210+
211+ def test_cveset_new_method_parameters(self):
212+ person = self.factory.makePerson()
213+ today = datetime.now(tz=pytz.UTC)
214+ cvss = {
215+ 'nvd': 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H'
216+ }
217+ cve = getUtility(ICveSet).new(
218+ sequence='2099-1234',
219+ description='A critical vulnerability',
220+ status=CveStatus.CANDIDATE,
221+ date_made_public=today,
222+ discoverer=person,
223+ cvss=cvss
224+ )
225+ self.assertThat(cve, MatchesStructure.byEquality(
226+ sequence='2099-1234',
227+ status=CveStatus.CANDIDATE,
228+ description='A critical vulnerability',
229+ date_made_public=today,
230+ discoverer=person,
231+ cvss=cvss
232+ ))
233+
234+ def test_cve_date_made_public_invalid_values(self):
235+ invalid_values = ['', 'abcd', {'a': 1},
236+ [1, 'a', '2', 'b'], '2022-01-01']
237+ cve = self.factory.makeCVE(
238+ sequence='2099-1234',
239+ description='A critical vulnerability',
240+ cvestate=CveStatus.CANDIDATE,
241+ )
242+ for invalid_value in invalid_values:
243+ with ExpectedException(TypeError, 'Expected datetime,.*'):
244+ removeSecurityProxy(cve).date_made_public = invalid_value
245+
246+ def test_cve_discoverer_id_invalid_values(self):
247+ invalid_values = ['', 'abcd', '2022-01-01', datetime.now()]
248+
249+ cve = self.factory.makeCVE(
250+ sequence='2099-1234',
251+ description='A critical vulnerability',
252+ cvestate=CveStatus.CANDIDATE,
253+ )
254+ for invalid_value in invalid_values:
255+ with ExpectedException(TypeError, 'Expected int,.*'):
256+ removeSecurityProxy(cve).discoverer_id = invalid_value
257+
258+ def test_cve_cvss_invalid_values(self):
259+ invalid_values = ['', 'abcd', '2022-01-01', datetime.now()]
260+ cve = self.factory.makeCVE(
261+ sequence='2099-1234',
262+ description='A critical vulnerability',
263+ cvestate=CveStatus.CANDIDATE,
264+ )
265+ for invalid_value in invalid_values:
266+ with ExpectedException(AssertionError):
267+ removeSecurityProxy(cve).cvss = invalid_value
268+
269+ def test_cvss_value_returned_when_null(self):
270+ cve = self.factory.makeCVE(
271+ sequence='2099-1234',
272+ description='A critical vulnerability',
273+ cvestate=CveStatus.CANDIDATE,
274+ )
275+ cve = removeSecurityProxy(cve)
276+ self.assertIsNone(cve._cvss)
277+ self.assertEqual({}, cve.cvss)
278+
279+ def test_setCVSSVectorForAuthority_initially_unset(self):
280+ cve = self.factory.makeCVE(
281+ sequence='2099-1234',
282+ description='A critical vulnerability',
283+ cvestate=CveStatus.CANDIDATE,
284+ )
285+ unproxied_cve = removeSecurityProxy(cve)
286+ self.assertIsNone(unproxied_cve._cvss)
287+ self.assertEqual({}, unproxied_cve.cvss)
288+
289+ cve.setCVSSVectorForAuthority(
290+ authority="nvd",
291+ vector_string="CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
292+ )
293+
294+ self.assertEqual(
295+ {"nvd": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"},
296+ unproxied_cve.cvss
297+ )
298+
299+ def test_setCVSSVectorForAuthority_overwrite_existing_key_value(self):
300+ cve = self.factory.makeCVE(
301+ sequence='2099-1234',
302+ description='A critical vulnerability',
303+ cvestate=CveStatus.CANDIDATE,
304+ cvss={"nvd": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"}
305+ )
306+ unproxied_cve = removeSecurityProxy(cve)
307+ self.assertEqual(
308+ {"nvd": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"},
309+ unproxied_cve.cvss
310+ )
311+
312+ cve.setCVSSVectorForAuthority(
313+ authority="nvd",
314+ vector_string="CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N"
315+ )
316+
317+ self.assertEqual(
318+ {"nvd": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N"},
319+ unproxied_cve.cvss
320+ )
321+
322+ def test_setCVSSVectorForAuthority_add_new_when_initial_value_set(self):
323+ cve = self.factory.makeCVE(
324+ sequence='2099-1234',
325+ description='A critical vulnerability',
326+ cvestate=CveStatus.CANDIDATE,
327+ cvss={"nvd": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"}
328+ )
329+ unproxied_cve = removeSecurityProxy(cve)
330+ self.assertEqual(
331+ {"nvd": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"},
332+ unproxied_cve.cvss
333+ )
334+
335+ cve.setCVSSVectorForAuthority(
336+ authority="nist",
337+ vector_string="CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N"
338+ )
339+
340+ self.assertEqual(
341+ {
342+ "nvd": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
343+ "nist": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N"
344+ },
345+ unproxied_cve.cvss
346+ )
347diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
348index 041cb89..9e37780 100644
349--- a/lib/lp/testing/factory.py
350+++ b/lib/lp/testing/factory.py
351@@ -4582,11 +4582,21 @@ class BareLaunchpadObjectFactory(ObjectFactory):
352 return secret, token
353
354 def makeCVE(self, sequence, description=None,
355- cvestate=CveStatus.CANDIDATE):
356+ cvestate=CveStatus.CANDIDATE,
357+ date_made_public=None, discoverer=None,
358+ cvss=None):
359 """Create a new CVE record."""
360 if description is None:
361 description = self.getUniqueUnicode()
362- return getUtility(ICveSet).new(sequence, description, cvestate)
363+
364+ return getUtility(ICveSet).new(
365+ sequence,
366+ description,
367+ cvestate,
368+ date_made_public,
369+ discoverer,
370+ cvss
371+ )
372
373 def makePublisherConfig(self, distribution=None, root_dir=None,
374 base_url=None, copy_base_url=None):

Subscribers

People subscribed via source and target branches

to status/vote changes: