summaryrefslogtreecommitdiff
blob: 16a838f6dbd854d50bff030ceb59d52055194f08 (plain)
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
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
#!/usr/bin/python
# Copyright 1999-2007 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2

# Written by Robert Buchholz <rbu@gentoo.org>

import string
import sys
import os
import portage
import portage_versions
import re
import elementtree.ElementTree as ET

genpatcheslist="./output/genpversions.txt"

"""
Bugzilla Kernel Version specification

The whiteboard field on the bug should be used to specify the vulnerable
versions of all kernel sources for this bug. A bug can affect a package in three
ways (and can therefore be fixed in three ways):
    (1) by affecting the kernel.org release ("linux"),
    (2) by affecting a certian set of Gentoo Patchsets ("gp")
    (3) by affecting a specific set of Gentoo kernel sources ("*-sources").

The priorities of these levels override each other with 3 having the highest
priority (2 second and 1 lowest). Note that priority does not mean severity of
the bug. Rather, the priority level is a scale of generality with 1 having the
highest generality. A whiteboard entry of the type [linux] affects all kernels
based off that version until a higher priority entry is added.

Higher levels (2, 3) should normally only mark unaffected versions that are
affected in lower levels. To override this and expand the "affected" interval
over the boundaries giving by lower levels, version specifiers should be
prefixed with a "+".

Intervals specify the affected versions and can, for each level, be specified
open (with upper or lower boundary only), or closed, either inclusive or not.
Spaces are discarded.

The order in which interval are specified is irrelevant.

Examples:
  [linux > 2.6]      -- means all Linux releases since 2.6 are affected
  [linux < 2.6.24.3] -- means all Linux versions prior to 2.6.24.3 are affected.
  [linux >= 2.6.24 < 2.6.24.3] -- means all Linux versions greater than, and
                including, 2.6.24, except if they are equal or greater than .3


Complex examples:
 [linux >= 2.6.18 < 2.6.24.3] [gp < 2.6.23-8]
   This means: affected is every kernel based on a linux release higher/equal than
   2.6.18, but not those based on 2.6.24.3 or later. Kernels using a genpatches
   version 2.6.23-8 or later are also not affected. 2.6.17 or earlier kernels
   using genpatches are not affected.

 [linux >= 2.6.18 < 2.6.24.3] [gp +< 2.6.23-8]
   Same as before, except even 2.6.17 and earlier genpatched kernerls are also
   affected (because of the +).

 [linux >= 2.6.18 < 2.6.24.3] [gp >= 2.6.15 +<= 2.6.23-8]
  Similar to the previous example, except kernels using genpatches are
  affected from versions 2.6.15 (inclusive) up to 2.6.23-8 (inclusive).

 [linux >= 2.6.18] [gp >= 2.6.23 < 2.6.23-8] [gp < 2.6.22-10]
   All Linuxes since 2.6.18, unaffected are all Genpatched kernels between
   2.6.22-10 and (not including) 2.6.23, plus those after 2.6.23-8.

 [linux >= 2.6.18 < 2.6.24.3] [gp < 2.6.23-8] [xen < 2.6.18-r9] [xen >= 2.6.19]
   Same as the first example, except the 2.6.18 series of xen-kernels was fixed in 2.6.18-r9.


"""

class BugError(Exception):
    def __init__(self, message, bug = None):
        self.message = message
        self.bug = bug
    def __str__(self):
        return repr(self.message)

class SourcesError(Exception):
    def __init__(self, message, ebuild):
        self.message = message
        self.ebuild = ebuild
    def __str__(self):
        return repr(self.message)

class KernelAtom:
    def __init__(self, name, version, release, gpver = None, has_base = None, has_extra = None, keywords = ""):
        self.name = name
        self.version = version
        self.release = release
        self.gpver = gpver
        self.has_base = has_base
        self.has_extra = has_extra
        self.keywords = keywords
        
        self.affected_by = []
    
    def __repr__(self):
        if self.gpver:
            return "%s-%s,\tRelease: %s,\tGenpatches: %s\t(Base: %s,\tExtra: %s) " % (self.name, self.version, self.release, self.gpver, self.has_base, self.has_extra)
        else:
            return "%s-%s,\tRelease: %s,\tGenpatches: No" % (self.name, self.version, self.release)

class SecurityMatrix:
    def __init__(self):
        self.localtree = portage.portagetree()
        
        self.fill_genpatches()
        self.kernel_atoms = []
        self.fill_kernel_atoms()

    supported_kernels=(  "gentoo-sources",
                        "hardened-sources",
                        "tuxonice-sources",
                        "openvz-sources",
                        "usermode-sources",
                        "xen-sources",
                        "cell-sources",
                        "hppa-sources",
                        "mips-sources",
                        "rsbac-sources",
                        "sparc-sources")
    #                     "vserver-sources",
    
    unsupported_kernels=("git-sources",
                        "mm-sources",
                        "sh-sources",
                        "xbox-sources",
                        "vanilla-sources")




    def fill_genpatches(self):
        """ Read the genpatch'd kernels from a file and prepare KernelAtom objects for them. """
        file = open(genpatcheslist)
        kernelatoms = {}
        for line in file:
            splitline = line.strip().split(':')
            if len(splitline) != 4:
                print "Error reading line: %s" % (line)
            else:
                name      = splitline[0]
                version   = splitline[1]
                release   = release_for_version(splitline[1])
                gpver     = splitline[2]
                has_base  = splitline[3].find("base") != -1
                has_extra = splitline[3].find("extra") != -1
                cpv = "sys-kernel/%s-%s" % (name, version)
                atom = KernelAtom(name, version, release, gpver, has_base, has_extra, self.get_keywords_for(cpv))
                kernelatoms[cpv] = atom
        file.close()
        self.genpatches = kernelatoms

    def fill_kernel_atoms(self):
        """ Fills the kernel atoms list will all kernel CPV's in the tree """
        for source in self.supported_kernels:
            cp = "sys-kernel/%s" % (source)
            all_cpvs = self.localtree.dbapi.cp_list(cp)
            for cpv in all_cpvs:
                self.build_and_add_kernelatom(cpv)
        for source in self.unsupported_kernels:
            cp = "sys-kernel/%s" % (source)
            all_cpvs = self.localtree.dbapi.cp_list(cp)
            for cpv in all_cpvs:
                self.build_and_add_kernelatom(cpv)

    def build_and_add_kernelatom(self, cpv):
        """ Build a KernelAtom object with the given cpv string and add it to our list of atoms """
        if self.genpatches.has_key(cpv):
            self.kernel_atoms.append(self.genpatches[cpv])
        else:
            cpvr = portage_versions.catpkgsplit(cpv)
            if len(cpvr) != 4:
                return
            v = cpvr[2]
            if cpvr[3] != "r0":
                v = "%s-%s" % (cpvr[2], cpvr[3])
            cpv = "%s/%s-%s" % (cpvr[0], cpvr[1], v)
            new_atom = KernelAtom(cpvr[1], v, release_for_version(cpvr[2]), keywords = self.get_keywords_for(cpv))
            self.kernel_atoms.append(new_atom)

    def get_keywords_for(self, cpv):
        try:
            result = self.localtree.dbapi.aux_get(cpv, ("KEYWORDS",))
            if result != None and len(result) == 1:
                return result[0]
        except:
            pass
            #TODO: raise SourcesError here
        return ""

    def check_bug(self, bug):
        if len(bug.affected) == 0:
            raise BugError("No intervals for affected kernel versions were found.", bug)
        for ka in self.kernel_atoms:
            if bug.affects(ka):
                ka.affected_by.append(bug)


def release_for_version(version):
    """ Given an ebuild version, gives the Kernel release it is probably based upon.
        Examples: 2.6.23-r3  -> 2.6.23
                  2.6.23.12  -> 2.6.23.12
                  2.6.23     -> 2.6.23 
                  2.6.25_rc5 -> 2.6.25_rc5
                  moo        -> None      """
    matcher = re.compile("(\d\.\d+\.\d+(:?\.\d+)?(:?_rc\d+)?)")
    match = matcher.match(version)
    if not match:
        # TODO: raise SourcesError in caller
        return None
    else:
        return match.group(1)


class IntervalEntry:
    """ Defines """
    def __init__(self, name, lower_inclusive, upper_inclusive, lower, upper, expand, bug):
        if name == "gp":
            name = "genpatches"
        elif name != "linux" and name != "genpatches" and name[-7:] != "sources":
            name = "%s-sources" % (name)
        self.name = name         # string, describing the package
        self.lower_inclusive = lower_inclusive # Defines whether the lower boundary is inclusive
        self.upper_inclusive = upper_inclusive # Defines whether the upper boundary is inclusive
        if name == "genpatches":
            self.lower = dashdot(lower)       # Lower boundary
            self.upper = dashdot(upper)       # Upper boundary
        else:
            self.lower = lower       # Lower boundary
            self.upper = upper       # Upper boundary

        self.expand = expand     # Defines whether the entry is expanding "lower" entries
        self.bug = bug

    def __repr__(self):
        val = "%s " % (self.name)
        if self.expand:
            val += "+"
        if self.lower and self.lower_inclusive:
            val += ">=%s " % (self.lower)
        if self.lower and not self.lower_inclusive:
            val += ">%s " % (self.lower)
        if self.upper and self.upper_inclusive:
            val += "<=%s" % (self.upper)
        if self.upper and not self.upper_inclusive:
            val += "<%s" % (self.upper)
        return val

    def to_xml(self, element = None):
        intnode = ET.Element("interval")
        if element:
            element.append(bugnode)
        
        intnode.source = self.name
        
        
        for item in ("bugno", "title", "arch", "severity", "url"):
            c = ET.SubElement(bugnode, item)
            c.text = self.__getattribute__(item)
        for entry in self.affected:
            entry.to_xml(bugnode)
        for cve in self.cve:
            cve.to_xml(bugnode)
        return bugnode

    def is_in_interval(self, version):
        """ Returns True if the given version is inside our specified interval, False otherwise.
            Note: 'name' is discarded in the comparison. """
        if version == None:
            return True # TODO: why?

        if self.lower: # We actually have a lower boundary set
            result = portage_versions.vercmp(version, self.lower)
            if result == None:
                raise BugError("Could not compare %s and %s, on %s" % (self.lower, version, str(self)), self.bug)

            """" We check the lower boundary. Two things will lead to False:
                    (1) The Result is "equal" and the lower boundary is not inclusive
                        aka: version = 2.6.24 on "> 2.6.24"
                    (2) The Result is "lower":
                        aka: version = 2.6.18 on ">= 2.6.24"  """
            if result == 0 and not self.lower_inclusive:
                return False
            if result == 0 and self.lower_inclusive:
                return True
            if result < 0:
                return False

        if self.upper: # We actually have an upper boundary set
            result = portage_versions.vercmp(version, self.upper)
            if result == None:
                raise BugError("Could not compare %s and %s, on %s" % (self.upper, version, str(self)), self.bug)

            """" We check the upper boundary. Two things will lead to False:
                    (1) The Result is "equal" and the upper boundary is not inclusive
                        aka: version = 2.6.24 on "< 2.6.24"
                    (2) The Result is "lower":
                        aka: version = 2.6.24 on "<= 2.6.18"  """
            if result == 0 and not self.upper_inclusive:
                return False
            if result == 0 and self.upper_inclusive:
                return True
            if result > 0:
                return False

        # Seems we're outa luck, we fell into the vulnerable versions
        return True


class Bug(object):
    def __init__(self, bugno, title = "", arch = "All", severity = "normal", url = "", affected = (), cves = ""):
        self.bugno      = bugno
        self.title      = title
        self.arch       = arch
        self.severity   = severity
        self.url        = url
        self.cves       = cves
        self.affected   = affected #(Entry("linux", "<", "2.6.23"),Entry("gp", "<", "2.6.20-14"),Entry("hardened", ">", "2.6"))

    def affects(self, kernelatom):
        """ Returns True if this bug affects the given KernelAtom, False otherwise. """
        affected = False
        linux_empty = True
        for entry in self.affected:
            if entry.name == "linux":
                linux_empty = False
                # Our linux base version is affected if it falls into any of the intervals
                affected = affected or entry.is_in_interval(dashdot(kernelatom.release))

        if kernelatom.gpver:
            genpatches_affected = False
            genpatches_exist = False
            for entry in self.affected:
                if entry.name == "gp" or entry.name == "genpatches":
                    genpatches_exist = True
                    if entry.is_in_interval(dashdot(kernelatom.gpver)):
                        # Our genpatches version is within the affected Genpatches.
                        genpatches_affected = True
                        if linux_empty:
                            affected = True
                        #elif affected:
                        #    affected = True
                        elif not affected and entry.expand:
                            affected = True
                        #elif not affected and not entry.expand:
                        #    affected = False
            if affected and genpatches_exist and not genpatches_affected:
                # We went through all the genpatches entries, but none marked this affected
                affected = False

        entry_affected = False
        entry_exist    = False
        for entry in self.affected:
            if entry.name == kernelatom.name:
                entry_exist = True
                if entry.is_in_interval(kernelatom.version):
                    # Our entry version is within the affected entry range.
                    entry_affected = True
                    if linux_empty:
                        affected = True
                    #elif affected:
                    #    affected = True
                    elif not affected and entry.expand:
                        affected = True
                    #elif not affected and not entry.expand:
                    #    affected = False
        if affected and entry_exist and not entry_affected:
            # We went through all the entries, but none marked this affected
            affected = False
        return affected

    def to_xml(self, element = None):
        bugnode = ET.Element("bug")
        if element:
            element.append(bugnode)

        for item in ("bugno", "title", "arch", "severity", "url"):
            c = ET.SubElement(bugnode, item)
            c.text = self.__getattribute__(item)

        affnode = bugnode.append("affected")
        for entry in self.affected:
            entry.to_xml(affnode)

        cves = bugnode.append("cves")
        for cve in self.cve:
            cve.to_xml(cves)
        return bugnode


    def set_from_whiteboard(self, whiteboard):
        """ Set the Bug's values given reading a Status Whiteboard string from a Bug. """
        if whiteboard == None:
            raise BugError("Whiteboard empty")
        rest = whiteboard
        affected = []
        matcher = re.compile("\s*\[\s*([^ +<=>]+)\s*(\+?[<=>]{1,2})\s*([^ +<=>\]]+)\s*(?:(\+?[<=>]{1,2})\s*([^ \]]+))?\s*\]\s*(.*)")

        while len(rest.strip()) > 0:
            match = matcher.match(rest)
            if not match:
                raise Exception("Illegal whiteboard: '%s'" % (rest))

            name  = match.group(1)
            comp1 = match.group(2)
            vers1 = match.group(3)
            comp2 = match.group(4)
            vers2 = match.group(5)
            rest  = match.group(6)

            # calculate entry values
            expand          = False
            upper_inclusive = None
            upper           = None
            lower_inclusive = None
            lower           = None

            if comp1[0] == "+":
                comp1 = comp1[1:]
                expand = True
            if comp2 != None and comp2[0] == "+":
                comp2 = comp2[1:]
                expand = True

            if comp1 == "=" or comp1 == "==":
                lower_inclusive = True
                upper_inclusive = True
                lower           = vers1
                upper           = vers1
            for (c, v) in ((comp1, vers1), (comp2, vers2)):
                if c == "<":
                    upper_inclusive = False
                    upper           = v
                elif c == "<=" or c == "=<" :
                    upper_inclusive = True
                    upper           = v
                elif c == ">":
                    lower_inclusive = False
                    lower           = v
                elif c == ">=" or c == "=>" :
                    lower_inclusive = True
                    lower           = v
            affected.append(IntervalEntry(name, lower_inclusive, upper_inclusive, lower, upper, expand, self))
        self.affected = affected


    def __repr__(self):
        return str(self.bugno)

class Bugzilla:
    def __init__(self):
        import bugz
        self.bz = bugz.Bugz(base = "https://bugs.gentoo.org")

        # search bugzilla for kernel bugs
        self.bugs_raw = self.bz.search("", product = ("Gentoo Security",), 
            component = ("Kernel",),
            status = ('NEW', 'ASSIGNED', 'REOPENED'))

        self.bugs = []
        self.failed_bugs = []
        for bug_raw in self.bugs_raw:
            bugid = bug_raw['bugid']

            bug_xml = self.bz.get(bugid)
            bug = Bug(bugid, bug_raw['desc'], bug_raw['arch'], bug_raw['severity'], url = "")
            try:
                bug.set_from_whiteboard(bug_xml.find('//status_whiteboard').text)
                self.bugs.append(bug)
                bug.to_xml()
            except:
                print sys.exc_value
                self.failed_bugs.append(bug)


def dashdot(s):
    if s == None:
        return None
    return s.replace("-",".")



def main():
    m = SecurityMatrix()
    b = Bugzilla()

    bug_errors = []
    for bug in b.failed_bugs:
        bug_errors.append(BugError("No whiteboard status set.", bug))
    succeeded_bugs = []

    for bug in b.bugs:
        try:
            m.check_bug(bug)
            succeeded_bugs.append(bug)
        except BugError, b_ex:
            bug_errors.append(b_ex)

    import kissoutput
    kissoutput.write_xml("./out.xml", m, bug_errors, succeeded_bugs)



if __name__ == "__main__":
    #try:
        main()
    #except KeyboardInterrupt:
        #print '\n ! Exiting.'