~ubuntu-security/ubuntu-cve-tracker/master

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
#!/usr/bin/python3

# Generate CVE OVAL from CVE metadata files
#
# Author: David Ries <ries@jovalcm.com>
# Copyright (C) 2015 Farnam Hall Ventures LLC
#
# This script is distributed under the terms and conditions of the GNU General
# Public License, Version 2 or later. See http://www.gnu.org/copyleft/gpl.html
# for details.
#
# Example usage:
# $ sudo apt-get install libopenscap8
# $ oscap info ./com.ubuntu.trusty.cve.oval.xml
# $ oscap oval generate report ./com.ubuntu.trusty.cve.oval.xml
#
# Requires 5.11.1 in /usr/share/openscap/schemas/oval/ but also openscap to
# support dpkg version comparisons. These will hopefully be part of openscap
# 1.3
# $ oscap oval eval --report /tmp/oval-report.html \
#     ./com.ubuntu.trusty.cve.oval.xml


import argparse
import glob
import os
import sys

import oval_lib

# For now, only LTS releases
supported_releases = {'xenial': {'desc': '16.04 LTS',
                                 'kernel': '^4\.4\.',
                                 'id': 10
                                 },
                      'trusty': {'desc': '14.04 LTS',
                                 'kernel': '^3\.13\.',
                                 'id': 10
                                 },
                      }

# For now, all EOL, ppa overlays and non-LTS releases
ignored_releases = ['dapper', 'edgy', 'feisty', 'gutsy', 'hardy', 'intrepid',
                    'jaunty', 'karmic', 'maverick', 'natty', 'oneiric',
                    'precise', 'precise/esm', 'quantal', 'lucid', 'raring',
                    'saucy', 'utopic', 'vivid', 'vivid/stable-phone-overlay',
                    'vivid/ubuntu-core', 'wily', 'yakkety', 'zesty']

all_releases = list(supported_releases.keys()) + ignored_releases

ignored_package_fields = ['Patches', 'devel', 'upstream', 'Assigned-to',
                          'product']
ignore_indented_package_lines = True

default_cves_to_process = ['./active/CVE-*', './retired/CVE-*']


def main():
    """ parse command line options and iterate through files to be processed
    """

    # parse command line options
    parser = argparse.ArgumentParser(description='Generate CVE OVAL from ' +
                                     'CVE metadata files.')
    parser.add_argument('pathname', nargs='*',
                        help='pathname patterns (globs) specifying CVE ' +
                             'metadata files to be converted into OVAL ' +
                             '(default: "./active/CVE-*" "./retired/CVE-*")')
    parser.add_argument('--output-dir', nargs='?', default='./',
                        help='output directory for reports (default is ./)')
    parser.add_argument('--no-progress', action='store_true',
                        help='do not show progress meter')
    args = parser.parse_args()
    pathnames = args.pathname or default_cves_to_process

    # create oval generators for each supported release
    outdir = './'
    if args.output_dir:
        outdir = args.output_dir
        if not os.path.isdir(outdir):
            raise FileNotFoundError("Could not find '%s'" % outdir)

    ovals = dict()
    for i in supported_releases.keys():
        ovals[i] = oval_lib.OvalGenerator(i, warn, outdir)
        ovals[i].add_release_applicability_definition(
            supported_releases[i]['desc'],
            supported_releases[i]['kernel'],
            supported_releases[i]['id'])

    # loop through all CVE data files
    files = []
    for pathname in pathnames:
        files = files + glob.glob(pathname)
    files.sort()

    files_count = len(files)
    for i_file, filepath in enumerate(files):
        cve_data = parse_cve_file(filepath)

        # skip CVEs without packages for supported releases
        if not cve_data['packages']:
            if not args.no_progress:
                progress_bar(i_file + 1, files_count)
            continue

        for i in ovals:
            ovals[i].generate_cve_definition(cve_data)

        if not args.no_progress:
            progress_bar(i_file + 1, files_count)

    for i in ovals:
        ovals[i].write_to_file()


def parse_package_status(release, package, status_text, filepath):
    """ parse ubuntu package status string format:
          <status code> (<version/notes>)
        outputs dictionary: {
          'status'        : '<not-applicable | unknown | vulnerable | fixed>',
          'note'          : '<description of the status>',
          'fix-version'   : '<version with issue fixed, if applicable>'
        } """

    # break out status code and detail
    status_sections = status_text.strip().split(' ', 1)
    code = status_sections[0].strip().lower()
    detail = status_sections[1].strip('()') if len(status_sections) > 1 else ''

    if code == 'released' and not detail:
        warn('Missing fix version note in {0}_{1} in "{2}". Changing to "unknown".'.format(release, package, filepath))
        code = 'unknown-fix-version'

    status = {}
    note_end = " (note: '{0}').".format(detail) if detail else '.'
    if code == 'dne':
        status['status'] = 'not-applicable'
        status['note'] = \
            "The '{0}' package does not exist in {1}{2}".format(package,
                                                                release,
                                                                note_end)
    elif code == 'ignored':
        status['status'] = 'vulnerable'
        status['note'] = "While related to the CVE in some way, a decision has been made to ignore it{2}".format(package, release, note_end)
    elif code == 'not-affected':
        status['status'] = 'not-vulnerable'
        status['note'] = "While related to the CVE in some way, the '{0}' package in {1} is not affected{2}".format(package, release, note_end)
    elif code == 'needed':
        status['status'] = 'vulnerable'
        status['note'] = \
            "The '{0}' package in {1} is affected and needs fixing{2}".format(
                package, release, note_end)
    elif code == 'active':
        status['status'] = 'vulnerable'
        status['note'] = "The '{0}' package in {1} is affected, needs fixing and is actively being worked on{2}".format(package, release, note_end)
    elif code == 'pending':
        status['status'] = 'vulnerable'
        status['note'] = "The '{0}' package in {1} is affected. An update containing the fix has been completed and is pending publication{2}".format(package, release, note_end)
    elif code == 'deferred':
        status['status'] = 'vulnerable'
        status['note'] = "The '{0}' package in {1} is affected, but a decision has been made to defer addressing it{2}".format(package, release, note_end)
    elif code == 'released':
        status['status'] = 'fixed'
        status['note'] = "The '{0}' package in {1} was vulnerable but has been fixed{2}".format(package, release, note_end)
        status['fix-version'] = detail
    else:
        if code != 'needs-triage' and code != 'unknown-fix-version':
            warn('Unsupported status "{0}" in {1}_{2} in "{3}". Setting to "unknown".'.format(code, release, package, filepath))
        status['status'] = 'unknown'
        status['note'] = "The vulnerability of the '{0}' package in {1} is not known (status: '{2}'). It is pending evaluation{3}".format(package, release, code, note_end)

    return status


def parse_cve_file(filepath):
    """ parse CVE data file into a dictionary """

    cve_header_data = {
        'Candidate': '',
        'CRD': '',
        'PublicDate': '',
        'PublicDateAtUSN': '',
        'References': [get_cve_url(filepath)],
        'Description': '',
        'Ubuntu-Description': '',
        'Notes': '',
        'Bugs': [],
        'Priority': '',
        'Discovered-by': '',
        'Assigned-to': '',
        'Unknown-Fields': [],
        'Source-note': filepath
    }

    f = open(filepath, 'r')
    key = ''
    values = []
    in_header = True
    packages = {}
    current_package = ''
    packages_section_keys = all_releases + ['Patches', 'Tags', 'upstream']

    for line in f:
        if line.strip().startswith('#') or line.strip().startswith('--'):
            continue

        if in_header and line.split('_', 1)[0] in packages_section_keys:
            in_header = False

        # Note: some older cves include Priority_package in header section
        if in_header and not line.startswith('Priority_'):
            if line.startswith(' '):
                values.append(line.strip())
            else:
                if key and key in cve_header_data and \
                        type(cve_header_data[key]) is str:
                    if cve_header_data[key]:
                        cve_header_data[key] = cve_header_data[key] + ' ' + \
                            ' '.join(values)
                    else:
                        cve_header_data[key] = ' '.join(values)
                elif key and key in cve_header_data and \
                        type(cve_header_data[key]) is list:
                    cve_header_data[key] = cve_header_data[key] + values
                elif key:
                    warn('Unknown header field "{0}" found in {1} '.format(key,
                         filepath))
                    cve_header_data['Unknown-Fields'].append(
                        {key: ' '.join(values)})

                if line.strip() == '':
                    continue

                key, value = line.split(':', 1)
                key = key.strip()
                value = value.strip()
                values = [value] if value else []

        else:
            # we're in the packages section
            if ignore_indented_package_lines and line.startswith(' '):
                continue

            line = line.strip()
            if not line:
                current_package = ''
                continue

            keys, value = line.split(':', 1)
            value = value.strip()
            keys = keys.split('_', 1)
            key = keys[0]
            if len(keys) == 2:
                package = keys[1]
                current_package = package
            else:
                package = current_package

            if (package not in packages):
                packages[package] = {
                    'Priority': '',
                    'Tags': [],
                    'Releases': {}
                }

            if key in ignored_package_fields or key in ignored_releases:
                continue

            if key in supported_releases:
                if key in packages[package]['Releases']:
                    warn('Duplicate package field key "{0}" found in "{1}" package in {2}'.format(key, package, filepath))
                packages[package]['Releases'][key] = \
                    parse_package_status(key, package, value, filepath)
            elif key == 'Priority':
                if packages[package][key]:
                    warn('Duplicate package field key "{0}" found in "{1}" package in {2}'.format(key, package, filepath))
                packages[package][key] = value
            elif key == 'Tags':
                packages[package][key].append(value)
            else:
                warn('Unknown package field "{0}" in {0}_{1} in "{2}"'.format(key, package, filepath))

    f.close()

    # remove packages with no supported releases
    packages = {name: package for name, package in packages.items()
                if package['Releases']}

    return {'header': cve_header_data, 'packages': packages}


def get_cve_url(filepath):
    """ returns a url to CVE data from a filepath """
    path = os.path.realpath(filepath).split(os.sep)
    url = "http://people.canonical.com/~ubuntu-security/cve"
    cve = path[-1]
    year = cve.split('-')[1]
    return "%s/%s/%s.html" % (url, year, cve)


def warn(message):
    """ print a warning message """
    sys.stdout.write('\rWARNING: {0}\n'.format(message))


def progress_bar(current, total, size=20):
    """ show a simple progress bar on the CLI """
    current_percent = float(current) / total
    hashes = '#' * int(round(current_percent * size))
    spaces = ' ' * (size - len(hashes))
    sys.stdout.write('\rProgress: [{0}] {1}% ({2} of {3} CVEs processed)'.format(hashes + spaces, int(round(current_percent * 100)), current, total))
    if (current == total):
        sys.stdout.write('\n')

    sys.stdout.flush()


if __name__ == '__main__':
    main()