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