Merge ~ebarretto/ubuntu-cve-tracker:oval-lsn into ubuntu-cve-tracker:master

Proposed by Eduardo Barretto
Status: Merged
Merged at revision: 3cc230c34f98e9fe6f4128380215a95f6d416928
Proposed branch: ~ebarretto/ubuntu-cve-tracker:oval-lsn
Merge into: ubuntu-cve-tracker:master
Diff against target: 411 lines (+178/-30)
4 files modified
scripts/fetch-lsns.py (+81/-0)
scripts/generate-oval (+14/-12)
scripts/oval_lib.py (+78/-16)
test/test_oval_lib_unit.py (+5/-2)
Reviewer Review Type Date Requested Status
Seth Arnold Approve
Alex Murray Approve
Review via email: mp+429162@code.launchpad.net

Description of the change

This PR adds LSNs to USN OVAL generation, as well as a fetch-lsns script.
Therefore an USN OVAL will contain both USNs and LSNs. The LSNs will be on their own definition, and basically it will check if you canonical-livepatch on your system, and if yes, it will check if you have the module version from the LSN loaded in /proc/modules. That way avoiding false positives if you don't have livepatch on your system.
This doesn't affect OCI USN OVAL, which relies on a manifest file and doesn't scan the actual system.

Currently this change is breaking 3 tests under test/ and that's because I changed the reference URL to USNs and LSNs. We had a hardcoded 'USN-' in the URL. To fix it I can either:
1. Change our infrastructure and USN DB to have keys as 'USN-XXXX-Y' instead of just 'XXXX-Y'
2. Do some logic before generating the URL

The 1 change requires more effort and will implicate in changes to our USN DB.

PS: We decided to go with 2. This PR is now ready for review + merge.

To post a comment you must log in.
Revision history for this message
Seth Arnold (seth-arnold) wrote :

Changing the USN DB from XXXX-Y to USN-XXXX-Y sounds really likely to cause problems in places we may not even know exist; can we add the LSNs with keys LSN-XXXX-Y and keep the USNs with XXXX-Y, and simply tolerate the asymmetry?

I know that adds more gross stuff here, but saves us from learning what needs to change in eg landscape and whoever else might be consuming the database.

I didn't spot anything in the code, but this is all pretty foreign to me. (Grabbing only twenty LSNs at a time, from the web server, seems likely to be brittle some day. We may not be able to do any better if this is what the web team has given us.)

Thanks

Revision history for this message
Eduardo Barretto (ebarretto) wrote :

Thanks Seth!
I added a small hack to make the id as USN-XXXX-YY until we address anything in the USN DB itself.

> (Grabbing only twenty LSNs at a time, from the web server, seems likely to be brittle some day. We may not be able to do any better if this is what the web team has given us.)

The idea is to get 20, as this is the default from the search page, more than it makes the request to take a longer time to get an answer, if any at all, as sometimes it just times out. And in the request I make, it is in newest order, which make us only get the latest one and it stops requesting whenever it hits an LSN that is already in the JSON file.

Revision history for this message
Eduardo Barretto (ebarretto) wrote :

It has been 10 days since last update.
Could someone review this?

Revision history for this message
Seth Arnold (seth-arnold) wrote :

Sorry :( my only thought on the newer changes is that it might be nice to put the hack in a function, so when (if?) we ever get to undo it, we only have to do it in one place.

It's only two lines though, maybe it's not a big deal.

Thanks

Revision history for this message
Alex Murray (alexmurray) wrote :

LGTM - minor comment re adding the USN- prefix to USN IDs.

review: Approve
Revision history for this message
Eduardo Barretto (ebarretto) wrote :

Thanks Alex and Seth,
I took both suggestions into consideration and added a prepend_usn_to_id function using regex, making it easier to adjust or remove it whenever we need.
I've also gave +x to the fetch-lsns script.

I would appreciate a check on both last commits just to make sure I didn't break anything.
Thanks!

Revision history for this message
Alex Murray (alexmurray) wrote :

LGTM!

review: Approve
Revision history for this message
Seth Arnold (seth-arnold) wrote :

a988b5f and a495a0c look good to me! I do kinda vaguely wonder how much time python's going to spend compiling that regex over and over again but lets just pretend it's fine for now and get this landed.

Thanks :)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/scripts/fetch-lsns.py b/scripts/fetch-lsns.py
2new file mode 100755
3index 0000000..109a149
4--- /dev/null
5+++ b/scripts/fetch-lsns.py
6@@ -0,0 +1,81 @@
7+#!/usr/bin/env python3
8+#
9+# Author: Eduardo Barretto
10+# Copyright (C) 2022- Canonical Ltd.
11+#
12+# This script is distributed under the terms and conditions of the GNU General
13+# Public License, Version 3 or later. See http://www.gnu.org/copyleft/gpl.html
14+# for details.
15+
16+import datetime
17+import json
18+import requests
19+import os
20+
21+url = "https://ubuntu.com/security/notices.json?order=newest&details=LSN"
22+data = json.loads(requests.get(url).text)
23+total = data['total_results']
24+filename = 'database-lsn.json'
25+
26+db = {}
27+if os.path.exists(filename):
28+ with open(filename, 'r') as json_file:
29+ try:
30+ db = json.load(json_file)
31+ print('Reading database-lsn.json')
32+ except json.decoder.JSONDecodeError:
33+ print('Creating database-lsn.json')
34+
35+offset = 0
36+with open(filename, 'w+') as json_file:
37+ while offset < total:
38+ data = json.loads(requests.get(url + '&offset=' + str(offset)).text)
39+ for notice in data['notices']:
40+ lsn_id = notice['id']
41+ if lsn_id in db:
42+ print('database is up-to-date')
43+ offset = total
44+ break
45+ else:
46+ print('importing {}'.format(lsn_id))
47+ db[lsn_id] = {}
48+ db[lsn_id]['description'] = notice['description']
49+ db[lsn_id]['releases'] = {}
50+ for release in notice['release_packages']:
51+ db[lsn_id]['releases'][release] = {'sources': {},
52+ 'binaries': {},
53+ 'allbinaries': {}}
54+ for item in notice['release_packages'][release]:
55+ # lsn json have two entries for same source
56+ # one containing the binary as version and
57+ # another with the livepatch module version
58+ if not item['is_source']:
59+ continue
60+ db[lsn_id]['releases'][release]['sources'][item['name']] = {
61+ 'version': item['version'],
62+ 'description': item['description']
63+ }
64+ version = item['version'].replace('.', '_')
65+ module_name = "lkp_Ubuntu_" + version.split('-')[0] + \
66+ r"[_|\d]+_" + item['name'].split('-')[0] + "_(\d+)"
67+ db[lsn_id]['releases'][release]['allbinaries'][item['name']] = {
68+ "pocket": "livepatch",
69+ "module": module_name,
70+ "version": lsn_id.split('-')[1].lstrip('0')
71+ }
72+ db[lsn_id]['title'] = notice['title']
73+ date = datetime.datetime.strptime(notice['published'], "%Y-%m-%dT%H:%M:%S")
74+ db[lsn_id]['timestamp'] = datetime.datetime.timestamp(date)
75+ db[lsn_id]['summary'] = notice['title']
76+ db[lsn_id]['action'] = notice['instructions']
77+ db[lsn_id]['is_hidden'] = 'False'
78+ db[lsn_id]['cves'] = notice['cves_ids']
79+ db[lsn_id]['id'] = notice['id']
80+ db[lsn_id]['isummary'] = notice['summary']
81+ db[lsn_id]['related_notices'] = []
82+ for rn in notice['related_notices']:
83+ db[lsn_id]['related_notices'].append(rn['id'])
84+
85+ offset += 20
86+
87+ json.dump(db, json_file, indent=4)
88diff --git a/scripts/generate-oval b/scripts/generate-oval
89index 0b91d9a..ddcd74b 100755
90--- a/scripts/generate-oval
91+++ b/scripts/generate-oval
92@@ -466,7 +466,6 @@ def debug(message):
93 if debug_level > 0:
94 sys.stdout.write('\rDEBUG: {0}\n'.format(message))
95
96-
97 def progress_bar(current, total, size=20):
98 """ show a simple progress bar on the CLI """
99 current_percent = float(current) / total
100@@ -478,6 +477,10 @@ def progress_bar(current, total, size=20):
101
102 sys.stdout.flush()
103
104+def prepend_usn_to_id(usn_database, usn_id):
105+ if re.search(r'^[0-9]+-[0-9]$', usn_id):
106+ usn_database[usn_id]['id'] = 'USN-' + usn_id
107+
108
109 # Class to contain the binary package cache
110 class PackageCache():
111@@ -634,15 +637,12 @@ class PackageCache():
112 # loads usn database.json based given a path to it.
113 # To get the database proceed as: $UCT/scripts/fetch-db database.json.bz2
114 def get_usn_database(usn_db_dir):
115- data = None
116- default_usn_database = os.path.join(usn_db_dir, 'database.json')
117- if not os.path.exists(default_usn_database):
118- error('{} must exists'.format(default_usn_database))
119+ data = {}
120+ for filename in glob.glob(os.path.join(usn_db_dir, 'database*.json')):
121+ with open(filename, 'r') as f:
122+ data.update(json.load(f))
123
124- with open(default_usn_database, 'r') as database:
125- data = json.load(database)
126- return data
127- return None
128+ return data
129
130 # Usage:
131 # for a given release only:
132@@ -693,18 +693,20 @@ def generate_oval_usn(outdir, usn, usn_release, cve_dir, usn_db_dir, ociprefix=N
133
134 # Generate OVAL USN data
135 if usn:
136+ prepend_usn_to_id(usn_database, usn)
137 for oval in ovals:
138- oval.generate_usn_oval(usn_database[usn], usn, cve_dir)
139+ oval.generate_usn_oval(usn_database[usn], usn_database[usn]['id'], cve_dir)
140 else:
141 for usn in usn_database.keys():
142+ prepend_usn_to_id(usn_database, usn)
143 for oval in ovals:
144- oval.generate_usn_oval(usn_database[usn], usn, cve_dir)
145+ oval.generate_usn_oval(usn_database[usn], usn_database[usn]['id'], cve_dir)
146
147 for oval in ovals:
148 oval.write_oval_elements()
149
150-
151 return True
152
153+
154 if __name__ == '__main__':
155 main()
156diff --git a/scripts/oval_lib.py b/scripts/oval_lib.py
157index 95c3856..2d86175 100644
158--- a/scripts/oval_lib.py
159+++ b/scripts/oval_lib.py
160@@ -53,6 +53,12 @@ def _open(fn, mode, encoding='utf-8'):
161 return fd
162
163 def prepare_instructions(instruction, cve, product_description, package):
164+ if "LSN" in cve:
165+ instruction = """\n
166+To check your kernel type and Livepatch version, enter this command:
167+
168+canonical-livepatch status"""
169+
170 if not instruction:
171 instruction = """\n
172 Update Instructions:
173@@ -64,7 +70,9 @@ by updating your system to the following package versions:""".format(cve)
174 for binary in package["binaries"]:
175 instruction += """{0} - {1}\n""".format(binary, package["fix-version"])
176
177- if "Long Term" in product_description or "Interim" in product_description:
178+ if "LSN" in cve:
179+ instruction += "Livepatch subscription required"
180+ elif "Long Term" in product_description or "Interim" in product_description:
181 instruction += "No subscription required"
182 else:
183 instruction += product_description
184@@ -737,7 +745,7 @@ class OvalGeneratorUSN():
185 'variable')
186 cve_base_url = 'https://ubuntu.com/security/{}'
187 mitre_base_url = 'https://cve.mitre.org/cgi-bin/cvename.cgi?name={}'
188- usn_base_url = 'https://ubuntu.com/security/notices/USN-{}'
189+ usn_base_url = 'https://ubuntu.com/security/notices/{}'
190 lookup_cve_path = ['./active', './retired']
191 generator_version = '1'
192 oval_schema_version = '5.11.1'
193@@ -746,6 +754,7 @@ class OvalGeneratorUSN():
194 def __init__(self, release_codename, release_name, outdir='./', cve_dir=None, prefix='', oval_format='dpkg'):
195 self.release_codename = release_codename.replace('/', '_')
196 self.release_name = release_name
197+ self.pocket = "security"
198 self.product_description = None
199 self.current_oval = None
200 self.tmpdir = tempfile.mkdtemp(prefix='oval_lib-')
201@@ -940,7 +949,7 @@ class OvalGeneratorUSN():
202 'usn_id': usn_object['id'],
203 'ns': self.ns,
204 'title': "{} -- {}".format(usn_object['id'], usn_object['title']),
205- 'plataform': "{}".format(self.release_name),
206+ 'platform': "{}".format(self.release_name),
207 'usn_url': self.usn_base_url.format(usn_object['id']),
208 'description': escape(' '.join((usn_object['description'].strip() + instructions).split('\n'))),
209 'cves_references': cve_references,
210@@ -954,7 +963,12 @@ class OvalGeneratorUSN():
211 criteria = []
212 kernel = False
213 for test_ref in test_refs:
214- if 'kernel' in test_ref and self.oval_format == 'dpkg':
215+ if self.pocket == 'livepatch' and self.oval_format == 'dpkg':
216+ criteria.append('<criteria operator="AND">')
217+ criteria.append(' <criterion test_ref="{0}:tst:{1}" comment="{2}" />'.format(self.ns, str(int(test_ref['testref_id']) + 1), self.product_description))
218+ criteria.append(' <criterion test_ref="{0}:tst:{1}" comment="{2}" />'.format(self.ns, test_ref['testref_id'], self.product_description))
219+ criteria.append('</criteria>')
220+ elif 'kernel' in test_ref and self.oval_format == 'dpkg':
221 kernel = True
222 criteria.append('<criteria operator="AND">')
223 criteria.append(' <criterion test_ref="{0}:tst:{1}" comment="{2}" />'.format(self.ns, test_ref['testref_id'], self.product_description))
224@@ -972,9 +986,9 @@ class OvalGeneratorUSN():
225 <metadata>
226 <title>{title}</title>
227 <affected family="unix">
228- <platform>{plataform}</platform>
229+ <platform>{platform}</platform>
230 </affected>
231- <reference source="USN" ref_url="{usn_url}" ref_id="USN-{usn_id}"/>
232+ <reference source="USN" ref_url="{usn_url}" ref_id="{usn_id}"/>
233 {cves_references}
234 <description>{description}</description>
235 <advisory from="security@ubuntu.com">
236@@ -1015,6 +1029,19 @@ class OvalGeneratorUSN():
237 <ind:state state_ref="{ns}:ste:{id}"/>
238 </ind:variable_test>""".format(**mapping)
239
240+ elif self.pocket == 'livepatch':
241+ mapping['liv-id'] = str(int(test_ref['testref_id']) + 1)
242+ test = \
243+ """
244+ <unix:file_test id="{ns}:tst:{liv-id}" version="1" check="all" check_existence="all_exist" comment="canonical-livepatch installed">
245+ <unix:object object_ref="{ns}:obj:{liv-id}" />
246+ <unix:state state_ref="{ns}:ste:{liv-id}" />
247+ </unix:file_test>
248+ <ind:textfilecontent54_test id="{ns}:tst:{id}" version="1" check="all" check_existence="all_exist" comment="livepatch testing">
249+ <ind:object object_ref="{ns}:obj:{id}"/>
250+ <ind:state state_ref="{ns}:ste:{id}"/>
251+ </ind:textfilecontent54_test>""".format(**mapping)
252+
253 else:
254 test = \
255 """
256@@ -1053,6 +1080,21 @@ class OvalGeneratorUSN():
257 <ind:var_ref>{ns}:var:{id}</ind:var_ref>
258 </ind:variable_object>""".format(**mapping)
259
260+ elif self.pocket == "livepatch":
261+ mapping['liv-id'] = str(int(test_ref['testref_id']) + 1)
262+ mapping['module'] = test_ref['pkgs']
263+ _object = \
264+ """
265+ <unix:file_object id="{ns}:obj:{liv-id}" version="1" comment="{product}">
266+ <unix:filepath>/snap/bin/canonical-livepatch</unix:filepath>
267+ </unix:file_object>
268+ <ind:textfilecontent54_object id="{ns}:obj:{id}" version="1" comment="{product}">
269+ <ind:filepath datatype="string">/proc/modules</ind:filepath>
270+ <!-- <ind:pattern operation="pattern match">^{module}\s.*$</ind:pattern> -->
271+ <ind:pattern operation="pattern match" var_ref="{ns}:var:{id}" var_check="at least one" />
272+ <ind:instance datatype="int">1</ind:instance>
273+ </ind:textfilecontent54_object>""".format(**mapping)
274+
275 else:
276 _object = \
277 """
278@@ -1100,6 +1142,18 @@ class OvalGeneratorUSN():
279 <ind:value operation="greater than" datatype="debian_evr_string" var_ref="{ns}:var:{varid}" var_check="at least one" />
280 </ind:variable_state>""".format(**mapping)
281
282+ elif self.pocket == "livepatch":
283+ mapping['liv-id'] = str(int(test_ref['testref_id']) + 1)
284+ mapping['bversion'] = binary_version
285+ state = \
286+ """
287+ <unix:file_state id="{ns}:ste:{liv-id}" version="1">
288+ <unix:size datatype="int" operation="greater than">0</unix:size>
289+ </unix:file_state>
290+ <ind:textfilecontent54_state id="{ns}:ste:{id}" version="1">
291+ <ind:subexpression datatype="int" operation="less than">{bversion}</ind:subexpression>
292+ </ind:textfilecontent54_state>""".format(**mapping)
293+
294 else:
295 if binary_version.find(':') != -1:
296 mapping['bversion'] = binary_version
297@@ -1250,7 +1304,12 @@ class OvalGeneratorUSN():
298 def get_version_from_binaries(self, usn_allbinaries):
299 version_map = collections.defaultdict(list)
300 for k, v in usn_allbinaries.items():
301- version_map[v['version']].append(k)
302+ if 'module' in v:
303+ self.pocket = 'livepatch'
304+ version_map[v['version']].append(v['module'])
305+ else:
306+ self.pocket = 'security'
307+ version_map[v['version']].append(k)
308
309 return version_map
310
311@@ -1289,20 +1348,19 @@ class OvalGeneratorUSN():
312 return usn_allbinaries
313
314 def update_release_name_from_pocket_or_stamp(self, binaries, stamp):
315- pocket = "security"
316 for b in binaries:
317 try:
318- pocket = binaries[b]['pocket']
319+ self.pocket = binaries[b]['pocket']
320 break
321 except KeyError:
322 # trusty usns don't have pocket, so try to check on timestamp
323 if self.release_codename == 'trusty' and stamp >= release_stamp('esm/trusty'):
324- pocket = 'esm'
325+ self.pocket = 'esm'
326 else:
327- pocket = 'security'
328+ self.pocket = 'security'
329 break
330
331- if pocket in ['security', 'updates']:
332+ if self.pocket in ['security', 'updates', 'livepatch']:
333 self.release_name = release_name(self.release_codename)
334 self.product_description = get_subproject_description(self.release_codename)
335 else:
336@@ -1311,8 +1369,8 @@ class OvalGeneratorUSN():
337 self.release_name = release_name('esm/' + self.release_codename)
338 self.product_description = get_subproject_description('esm/' + self.release_codename)
339 else:
340- self.release_name = release_name(pocket + '/' + self.release_codename)
341- self.product_description = get_subproject_description(pocket + '/' + self.release_codename)
342+ self.release_name = release_name(self.pocket + '/' + self.release_codename)
343+ self.product_description = get_subproject_description(self.pocket + '/' + self.release_codename)
344
345 def generate_usn_oval(self, usn_object, usn_number, cve_dir):
346 if self.release_codename not in usn_object['releases'].keys():
347@@ -1327,6 +1385,11 @@ class OvalGeneratorUSN():
348
349 binary_versions = self.get_version_from_binaries(usn_allbinaries)
350
351+ # OCI OVAL does not check running system, therefore it can
352+ # skip LSNs
353+ if self.oval_format == "oci" and self.pocket == 'livepatch':
354+ return
355+
356 # group binaries with same version (most likely from same source)
357 # and create a test_ref for the group to be used when creating
358 # the oval def, test, state and var.
359@@ -1343,12 +1406,11 @@ class OvalGeneratorUSN():
360 # prepare update instructions
361 pkg['binaries'] = binary_versions[key]
362 pkg['fix-version'] = key
363- instructions = prepare_instructions(instructions, "USN-" + usn_number, self.product_description, pkg)
364+ instructions = prepare_instructions(instructions, usn_object['id'], self.product_description, pkg)
365
366 # Create the oval objects
367 # Only need one definition, but if multiple versions of binary pkgs,
368 # then may need several test, object, state and var
369-
370 usn_def = self.create_usn_definition(usn_object, usn_number, id_base, test_refs, cve_dir, instructions)
371 self.oval_structure['definition'].write(usn_def)
372
373diff --git a/test/test_oval_lib_unit.py b/test/test_oval_lib_unit.py
374index c99a41b..f603210 100644
375--- a/test/test_oval_lib_unit.py
376+++ b/test/test_oval_lib_unit.py
377@@ -52,6 +52,8 @@ class TestOvalLibUnit:
378 usn_mock = "4388-1"
379 id_base_mock = 43881000000
380 test_cve_file = "CVE-TEST"
381+ usn_object_mock['id'] = "USN-" + usn_mock
382+
383
384 bin_dict_mock = collections.defaultdict(list)
385 bin_dict_mock = {'5.0.0.1042.27': ['linux-image-gke-5.0'], '5.0.0-1059.64':
386@@ -83,7 +85,7 @@ class TestOvalLibUnit:
387 definition_mock = """
388 <definition id="oval:com.ubuntu.bionic:def:43881000000" version="1" class="patch">
389 <metadata>
390- <title>4388-1 -- Linux kernel vulnerabilities</title>
391+ <title>USN-4388-1 -- Linux kernel vulnerabilities</title>
392 <affected family="unix">
393 <platform>Ubuntu 18.04 LTS</platform>
394 </affected>
395@@ -276,7 +278,7 @@ class TestOvalLibUnit:
396 invalid_priority_ret = """
397 <definition id="oval:com.ubuntu.bionic:def:43881000000" version="1" class="patch">
398 <metadata>
399- <title>4388-1 -- Linux kernel vulnerabilities</title>
400+ <title>USN-4388-1 -- Linux kernel vulnerabilities</title>
401 <affected family="unix">
402 <platform>Ubuntu 18.04 LTS</platform>
403 </affected>
404@@ -497,6 +499,7 @@ No subscription required"""
405 create_bug_ref_mock.return_value = self.url_ref_mock
406 get_usn_severity_mock.return_value = self.avg_severity_mock
407
408+ print(self.usn_object_mock)
409 definition_ret = oval_lib.OvalGeneratorUSN.create_usn_definition(
410 self.oval_gen_mock, self.usn_object_mock, self.usn_mock,
411 self.id_base_mock, self.test_refs_mock, rel_test_path,

Subscribers

People subscribed via source and target branches