# $Header: /home/cvsroot/gentoo-projects/gentoo-security/GLSA/user-tools/glsa.py,v 1.5 2003/11/04 23:00:35 genone Exp $ # This program is licensed under the GPL, version 2 # WARNING: this code is only tested by a few people and should NOT be used # on production systems at this stage. There are possible security holes and probably # bugs in this code. If you test it please report ANY success or failure to # me (genone@gentoo.org). # This is the main class for GLSA support in portage. It's currently designed # to interface with portage through emerge and portageq, this will be changed # to internal portage versions over time. It's the backend code for the glsa-check.py # tool, which will go into portage.py / emerge at some point. # The following planned features are currently on hold: # - getting GLSAs from http/ftp servers (not really useful without the fixed ebuilds) # - GPG signing/verification (until key policy is clear and reasons are outlined) __author__ = "Marius Mauch " import os, sys, urllib # if you get a traceback on this with python-2.3 try to unmerge pyxml, worked here import xml.dom.minidom sys.path += ["/usr/lib/portage/pym"] # to find portage.py from time import strftime, strptime from string import split from portage import pkgcmp # most/all of the commented code is for a "between" version range which is not # implemented in portage yet, so this is a nasty hack I don't want to support, # it was just a proof-of-concept thing. # This class encapsulates the tag from the glsa.dtd class version: # package = cat/name of the package # version = version part # rangetype = equal | above | below | above_equal | below_equal def __init__(self, package, version, rangetype): self.package = package self.rangetype = rangetype #if rangetype == "between": # self.version = split(version) #else: self.version = version # returns a textual represenation of this version def getTextVersion(self): #if self.rangetype == "between": # return "between " + self.version[0] + " and " + self.version[1] if self.rangetype == "equal": prefix = "=" elif self.rangetype == "below_equal": prefix = "<=" elif self.rangetype == "above_equal": prefix = ">=" elif self.rangetype == "below": prefix = "<" elif self.rangetype == "above": prefix = ">" else: raise Exception("unknown range type: " + self.rangetype) return prefix + self.version # returns a representation of this version that can be passed to emerge def getCliVersion(self): rValue = self.getPortageVersion() rValue = rValue.replace("<", "\<") rValue = rValue.replace(">", "\>") return rValue # returns a version string that can be passed to portage functions def getPortageVersion(self): #if self.rangetype == "between": # return (self.package+"-"+self.version[0], "\<="+self.package+"-"+self.version[1]) if self.rangetype == "equal": prefix = "=" elif self.rangetype == "below_equal": prefix = "<=" elif self.rangetype == "above_equal": prefix = ">=" elif self.rangetype == "below": prefix = "<" elif self.rangetype == "above": prefix = ">" else: raise Exception("unknown range type: " + self.rangetype) return prefix + self.package + "-" + self.version # read config file and provide default config def getconfig(configfile): rValue = dict([ ("GLSA_DIR", "/usr/portage/glsa/"), ("GLSA_PREFIX", "glsa-"), ("GLSA_SUFFIX", ".xml"), ("CHECKFILE", "/var/cache/edb/glsa"), ("GLSA_SERVER", "www.gentoo.org/security/en/glsa/"), # not completely implemented yet ("CHECKMODE", "local") # not completely implemented yet ]) if not os.access(configfile, os.R_OK): return rValue filecontent = [line for line in open(configfile).readlines() if line[1] != '#'] for line in filecontent: if '=' in line: name, value = line.split('=') value = value.strip("\n") rValue[name] = value return rValue # get a list of all available GLSA in the given repository def get_glsa_list(p_repository): # TODO: remote fetch code for listing if not os.access(p_repository, os.R_OK): return [] dirlist = os.listdir(p_repository) prefix = config["GLSA_PREFIX"] suffix = config["GLSA_SUFFIX"] return [f[len(prefix):-1*len(suffix)] for f in dirlist if f[:len(prefix)] == prefix] config = getconfig("/etc/portage/glsa.conf") # helper function for xml parser, copied from api doc def getText(node): rc = "" for node in node.childNodes: if node.nodeType == node.TEXT_NODE: rc = rc + node.data return rc # GLSA xml data wrapper class class glsa: # set the id and read the xml file def __init__(self, p_id): self.nr = p_id self.read() # read the xml file for this glsa using GLSA_DIR if CHECKMODE=local or GLSA_SERVER otherwise def read(self): if config["CHECKMODE"] == "local": repository = "file://" + config["GLSA_DIR"] else: repository = config["GLSA_SERVER"] myurl = repository + config["GLSA_PREFIX"] + str(self.nr) + config["GLSA_SUFFIX"] self.parse(urllib.urlopen(myurl)) # helper function for xml parser: create a list of version instances def parseVersions(self, node): versions = node.getElementsByTagName("version") rValue = [] for v in versions: mytype = v.getAttribute("range") rValue.append(version(self.package, getText(v), mytype)) return rValue # parse the XML document for this glsa (using xml.dom.minidom) def parse(self, p_file): myroot = xml.dom.minidom.parse(p_file).getElementsByTagName("glsa")[0] if myroot.getAttribute("id") != self.nr: raise Exception("filename and internal id don't match:" + myroot.getAttribute("id") + " != " + self.nr) self.package = getText(myroot.getElementsByTagName("package")[0]) self.date = strptime(getText(myroot.getElementsByTagName("date")[0]), "%Y-%m-%d") self.summary = getText(myroot.getElementsByTagName("summary")[0]) self.bug = getText(myroot.getElementsByTagName("bug")[0]) self.severity = myroot.getAttribute("severity") tmp = myroot.getElementsByTagName("exploit") self.exploits = [] for e in tmp: self.exploits.append(getText(e)) self.affected = self.parseVersions(myroot.getElementsByTagName("affected")[0]) self.fixed = self.parseVersions(myroot.getElementsByTagName("fixed")[0]) tmp = myroot.getElementsByTagName("cve") self.cve = [] for c in tmp: self.cve.append({"id": getText(c), "url":c.getAttribute("url")}) self.description = getText(myroot.getElementsByTagName("description")[0]) self.solution_description = \ getText(myroot.getElementsByTagName("solution")[0].getElementsByTagName("description")[0]) tmp = myroot.getElementsByTagName("solution")[0].getElementsByTagName("command") self.precommands = [] self.postcommands = [] for c in tmp: if c.getAttribute("phase") == "before": self.precommands.append(getText(c).strip()) else: self.postcommands.append(getText(c).strip()) # check if this glsa is already applied self.status = "not applied" if os.access(config["CHECKFILE"], os.R_OK): checkfile = open(config["CHECKFILE"], "r") for line in checkfile.readlines(): if line.strip() == self.nr: self.status = "applied" checkfile.close() # print a info page about this glsa (like the old announcements) def dump(self): print "GLSA: ", self.nr print "package: ", self.package print "summary: ", self.summary print "severity: ", self.severity print "bug: ", self.bug print "date: ", strftime("%x", self.date) print "status: ", self.status for cve in self.cve: if cve["url"]: print "CVE: ", cve["id"], "(" , cve["url"] , ")" else: print "CVE: ", cve["id"] for e in self.exploits: print "exploit: ", e for v in self.affected: print "affected: ", v.getTextVersion() for v in self.fixed: print "fixed: ", v.getTextVersion() print print 30*'=' + " DESCRIPTION " + 30*'=' print self.description if self.solution_description: print 32*'=' + " SOLUTION " + 31*'=' print self.solution_description # put this glsa into the checkfile, so it is not checked on later runs def check_in(self): already_in = False checkfile = open(config["CHECKFILE"], "r+") for line in checkfile.readlines(): if line.strip() == self.nr: already_in = True if not already_in: checkfile.write(self.nr) checkfile.close() # do all steps to apply this glsa def fix(self): if self.test(): myversion = os.popen("portageq match / "+self.package, "r") if len(myversion) < 1: raise Exception("Error: test is positive but no package version found") print "this system is affected" print print "running pre-emerge commands now" for c in self.precommands: os.system(c) newversion = "" # compare the actual version to all fixed versions, use the lowest fixed version # that is newer than the actual version for v in self.fixed: # I assume the list is sorted in the GLSA nextversion = os.popen("portageq best_visibe / "+v.getCliVersion(), "r") if pkgcmp(myversion, nextversion) > 0: newversion = nextversion break print "installing " + newversion if os.system("emerge =" + newversion) == 0: print "update was successful, adding this glsa to the checklist" self.check_in() else: print "update failed, NOT adding this glsa to the ckecklist" print print "running post-emerge commands now" for c in self.postcommands: os.system(c) else: print "this system is not affected, adding to checklist" self.check_in() # check if the system is affected def test(self): rValue = False for v in self.affected: #if v.rangetype == "between": # output = os.popen("portageq match / " + v.getCliVersion()[1], "r") # if len(output.read().strip()) > 0: # rValue = (output.read().strip() >= v.getCliVersion()[0]) #else: output = os.popen("portageq match / " + v.getCliVersion(), "r") if len(output.read().strip()) > 0: rValue = True return rValue # show all steps necessary to apply this glsa def pretend(self): print "to apply this GLSA run the following commands:\n" print print "# check if the system is affected:" print "# (if the commands return nothing you're not affected)" for v in self.affected: #if v.rangetype == "between": # print "portageq match / " + v.getCliVersion()[1] # print "# check if that version is equal to or greater than " + v.getPortageVersion()[0] #else: print "portageq match / " + v.getCliVersion() print print "# pre-emerge commands:" for c in self.precommands: print c print print "# emerge commands:" print "emerge --sync" if len(self.fixed) > 1: print "# (only one of the following is needed):" for v in self.fixed: #if v.rangetype == "between": # print "emerge " + v.getCliVersion() #else: print "emerge " + v.getCliVersion() print print "# post-emerge commands:" for c in self.postcommands: print c