diff options
author | Robert Buchholz <rbu@goodpoint.de> | 2008-08-29 20:40:54 +0200 |
---|---|---|
committer | Robert Buchholz <rbu@goodpoint.de> | 2008-08-29 20:40:54 +0200 |
commit | 00ce1e9495bab0d5009b834082235f0f2bc361da (patch) | |
tree | 96097bc031ea97295c62ad81d39521c0799b84d9 /kiss2.py | |
download | kernel-check-00ce1e9495bab0d5009b834082235f0f2bc361da.tar.gz kernel-check-00ce1e9495bab0d5009b834082235f0f2bc361da.tar.bz2 kernel-check-00ce1e9495bab0d5009b834082235f0f2bc361da.zip |
Initial version of KISS2
Diffstat (limited to 'kiss2.py')
-rwxr-xr-x | kiss2.py | 474 |
1 files changed, 474 insertions, 0 deletions
diff --git a/kiss2.py b/kiss2.py new file mode 100755 index 0000000..9ede7b6 --- /dev/null +++ b/kiss2.py @@ -0,0 +1,474 @@ +#!/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 + +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 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: + def __init__(self, bugno, title = "", severity = "normal", affected = ()): + self.bugno = bugno + self.title = title + self.severity = severity + 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 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['severity']) + try: + bug.set_from_whiteboard(bug_xml.find('//status_whiteboard').text) + self.bugs.append(bug) + 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.' + |