summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobert Buchholz <rbu@goodpoint.de>2008-08-29 20:40:54 +0200
committerRobert Buchholz <rbu@goodpoint.de>2008-08-29 20:40:54 +0200
commit00ce1e9495bab0d5009b834082235f0f2bc361da (patch)
tree96097bc031ea97295c62ad81d39521c0799b84d9 /kiss2.py
downloadkernel-check-00ce1e9495bab0d5009b834082235f0f2bc361da.tar.gz
kernel-check-00ce1e9495bab0d5009b834082235f0f2bc361da.tar.bz2
kernel-check-00ce1e9495bab0d5009b834082235f0f2bc361da.zip
Initial version of KISS2
Diffstat (limited to 'kiss2.py')
-rwxr-xr-xkiss2.py474
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.'
+