# Copyright 1999-2008 Gentoo Foundation # Distributed under the terms of the GNU General Public License v2 import os import re import assign class CVEData: CVEID_RE = re.compile("(CVE-\d+-\d+)") """This class handles the CVE database""" def __init__(self): self.create_cvedata() def create_cvedata(self): import datetime import cPickle files = [] year = datetime.date.today().year # read all nvdcve-200*.xml files for yr in range(2002, year+1): files.append("./cache/nvdcve-%s.xml" % (yr)) files.append("./cache/nvdcve-modified.xml") if os.path.exists("./cache/cvedata.pickle"): ptime = os.path.getmtime("./cache/cvedata.pickle") newer_files = [xml for xml in files if os.path.getmtime(xml) > ptime] if len(newer_files) == 0: try: pickle = open("./cache/cvedata.pickle") self.cvedata = cPickle.load(pickle) return except Exception, e: print "Error opening Hash-cache, recreating: %s" % (e) import nvd self.cvedata = nvd.parseAll(files) pickle = open("./cache/cvedata.pickle", "w") cPickle.dump(self.cvedata, pickle, protocol=-1) def get_cve_from(self, entry): """ returns the CVE-YYYY-IIII name for an entry or None. """ if len(entry) == 0: return None match = CVEData.CVEID_RE.match(entry[0]) if not match: return None cve = match.group(1) if not cve in self.cvedata: return None return cve def get_refs_for(self, cve): return self.cvedata[cve]['refs'] def guess_name_for(self, cve): try: refs = self.cvedata[cve]['refs'] except KeyError: return {} SAs = [] names = {} for source, url in refs: if source == u"SECUNIA" or url.startswith("http://secunia.com/advisories/") or url.startswith("http://www.secunia.com/advisories/"): SAs.append(re.sub(r".*advisories/(\d+)", r"\1", url)) import urllib2 for SAid in SAs: html = urllib2.urlopen("http://secunia.com/advisories/%s/2/" % SAid).read() match = re.findall(r'Provided and/or discovered by:(.+?)', html, flags = re.S) if match: text = re.sub('<.*?>', '', match[0]) names[SAid] = [re.sub(r'(in|via) a .* bug report', '', re.sub(r'^[\d,) -]*(.* credits|Reported by)?', '', line)).strip(' \r\n.') for line in text.split('\n')] return names def print_all_about(self, entry): """ Prints all info about a list entry. Returns the product name if possible or None. """ cve = self.get_cve_from(entry) if not cve: print "CVE name not found in CVE database: %s" % (entry) return None self.print_cveinfo(cve) query = "" if self.cvedata[cve]['product_vendor']: print "Vendor: %s" % (self.cvedata[cve]['product_vendor']) query = "%s" % (self.cvedata[cve]['product_vendor']) if (self.cvedata[cve]['product_name']): productname = self.cvedata[cve]['product_name'] print "Product: %s" % (productname) if productname.lower().find(query.lower()) > -1: # productname already contains vendor (or vendor is empty) query = "%s" % (productname) else: query += " %s" % (productname) if not query: query = self.guess_product(self.cvedata[cve]['desc']) if query: print "Product (guessed): %s" % (query) else: print "Product name unknown." return (cve, query) def print_cveinfo(self, cvename): """ Print all info we have about a given CVE id """ cveinfo = self.cvedata[cvename] os.spawnlp(os.P_WAIT, 'clear', 'clear') print "="*80 print "Name: %s" % (cvename) print "URL: http://cve.mitre.org/cgi-bin/cvename.cgi?name=%s" % (cvename) print "Published: %s" % (cveinfo['published']) print "Severity: %s" % (cveinfo['severity']) print "Description: \n" print self.get_cve_desc(cvename) print def get_cve_desc(self, cvename, indentation = 0, linelength = 72): """ returns the cve description, wrapped to specified line length and given space indentation""" cveinfo = self.cvedata[cvename] linelength = linelength - indentation spaces = "".join(" " for x in range(0, indentation)) text = "" start = 0 end = 0 leng = len(cveinfo['desc']) while leng - start > linelength: end = cveinfo['desc'].rfind(' ', start, min(start + linelength, leng)) if end == -1: # in case we do not find a space, use up as much as we can end = start + linelength - 1 text += spaces + cveinfo['desc'][start:end] + "\n" start = end + 1 text += spaces + cveinfo['desc'][start:] return text def guess_product(self, desc): """ Guess a product name from a description. Returns a string or None. """ # ... in vulnfile.c in (the) VulApp Application 1.0 matcher = re.compile(" in (\S+\.\S+) in (?:the )?(?:a )?(\D+) \d+") match = matcher.search(desc) if match: if match.group(2)[-6:] == "before": return match.group(2)[:-7] else: return match.group(2) # ... in (the) VulnApp Application 1.0 matcher = re.compile(" in (?:the )?(?:a )?(\D+) \d+") match = matcher.search(desc) if match: if match.group(1)[-6:] == "before": return match.group(1)[:-7] else: return match.group(1) matcher = re.compile(" in (\S+\.\S+) in (?:the )?(?:a )?(\S+) ") match = matcher.search(desc) if match: return match.group(2) # ... in (the) VulnApp matcher = re.compile(" in (?:the )?(?:a )?(\S+) ") match = matcher.search(desc) if match: return match.group(1) # (The) VulnApp matcher = re.compile("(?:The )?(\S+) ") match = matcher.search(desc) if match: return match.group(1) return None class BugReporter: CVEGROUPALL = re.compile(r'[ (]*CVE-(\d{4})([-,(){}|, \d]+)') CVEGROUPSPLIT = re.compile(r'(?<=\D)(\d{4})(?=\D|$)') def __init__(self, username = None, password = None): try: import bugz except: return # edit bugz config defaults for us bugz.config.params['post'] = { 'product': 'Gentoo Security', 'version': 'unspecified', 'rep_platform': 'All', 'op_sys': 'Linux', 'priority': 'P2', 'bug_severity': 'normal', 'bug_status': 'NEW', 'assigned_to': '', 'keywords': '', 'dependson':'', 'blocked':'', 'component': 'Vulnerabilities', # needs to be filled in 'bug_file_loc': '', 'short_desc': '', 'comment': '', } self.bugz_auth = bugz.Bugz(base = "https://bugs.gentoo.org", user = username, password = password, forget = False) def post_bug(self, title, description, component="", whiteboard="", url=""): """ Posts a security bug, returning the Bug number or 0 """ bugno = 0 ccs = assign.get_cc_from_string(title) ccs = ",".join(ccs) severity = 'normal' try: try: bugno = self.bugz_auth.post(title = title, description = description, cc = ccs, url = url) print "Ignoring Bug component, please upgrade pybugz." except TypeError: # pybugz since 0.7.4 requires to specify product and component bugno = self.bugz_auth.post(title = title, product="Gentoo Security", component=component, description = description, cc = ccs, url = url) except Exception, e: print "An error occurred posting a bug: %s" % (e) if bugno and whiteboard: severity = self.severity_from_whiteboard(whiteboard) self.bugz_auth.modify(bugid = bugno, whiteboard = whiteboard, severity = severity) return bugno def modify_bug(self, bugid, title, comment): """ Modifies bug and adds comment """ try: bugno = self.bugz_auth.modify(bugid, comment = comment, title = title) except Exception, e: print "An error occurred modifying a bug: %s" % (e) def get_bug_title(self, bugno): """ Get the title of a bug number """ try: return self.bugz_auth.get(bugno).find('//short_desc').text except Exception, e: pass return None def get_bug_cves(self, bugno, title = ""): """ Get a list of CVEs on a bug number """ bug_cves = [] try: title = title or self.get_bug_title(bugno) if not title: return bug_cves for (year, split_cves) in BugReporter.CVEGROUPALL.findall(title): for cve in BugReporter.CVEGROUPSPLIT.findall(split_cves): bug_cves.append('CVE-%s-%s' % (year, cve)) except Exception, e: pass return bug_cves def severity_from_whiteboard(self, whiteboard): if (len(whiteboard)) < 2: return 'normal' evaluation = whiteboard[0:2] if evaluation in ['A0', 'B0']: return 'blocker' if evaluation in ['A1', 'C0']: return 'critical' if evaluation in ['A2', 'B1', 'C1']: return 'major' if evaluation in ['A3', 'B2', 'C2']: return 'normal' if evaluation in ['A4', 'B3', 'B4', 'C3']: return 'minor' if evaluation in ['C4', '~0', '~1', '~2', '~3', '~4']: return 'trivial' return 'normal'