#!/usr/bin/python # gen-DSA -- create a DSA template # Copyright (C) 2011 Florian Weimer # # User interface based on a shell version written by # Raphael Geissert . # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # This script is intended to be run on security-master to get an # unprocessed dump of the contents of the embargoed and unembargoed # queues. # # The script reads .deb and .changes files. A caching database is # written to ~/.cache. import sys import os.path def setup_path(): dirname = os.path.dirname base = dirname(dirname(os.path.realpath(sys.argv[0]))) sys.path.insert(0, os.path.join(base, "lib", "python")) setup_path() from pwd import getpwuid import re import time import bugs import debian_support import secmaster def parsecommand(): args = sys.argv[1:] if not args: usage() global opt_save if args[0] == "--save": opt_save = True del args[0] else: opt_save = False if len(args) < 3: usage() global opt_dsaid opt_dsaid = args[0] if opt_dsaid.upper().startswith("DSA-"): opt_dsaid = opt_dsaid[4:] if "-" not in opt_dsaid: opt_dsaid += "-1" global opt_package opt_package = args[1] if not opt_package: usage("package argument is empty") global opt_vulnerability opt_vulnerability = args[2] if not opt_vulnerability: usage("vulnerability argument is empty") global opt_cve if len(args) >= 4: re_cve = re.compile("(?i)CVE-\d{4}-\d{4,}") opt_cve = set() for cve in args[3].split(): if not cve: continue cve = cve.upper() if not re_cve.match(cve): usage("malformed CVE name: " + repr(cve)) if cve in opt_cve: usage("duplicate CVE: " + repr(cve)) opt_cve.add(cve) opt_cve = tuple(sorted(opt_cve)) else: opt_cve = () global opt_bugs if len(args) >= 5: opt_bugs = set() for bug in args[3].split(): if not bug: continue try: bug = int(bug) if bug <= 0: raise ValueError except: usage("malformed bug number: " + repr(bug)) if bug in opt_bugs: usage("duplicate bug number: " + repr(bug)) opt_bugs.add(cve) opt_bugs = tuple(sorted(opt_bugs)) else: opt_bugs = () if len(args) >= 5: usage() def usage(msg=None): if msg is not None: print >>sys.stderr, "error:", msg print >>sys.stderr, "usage:", sys.argv[0], \ "[--save] DSA package vulnerability [CVE [bug number]]" print >>sys.stderr print >>sys.stderr, \ "Multiple CVE and bug numbers can be separated by spaces" sys.exit(1) def gecos(): gecos = os.getenv("DEBFULLNAME") if gecos is not None: return gecos gecos = getpwuid(os.getuid()).pw_gecos return gecos.split(",")[0] def debemail(): for env in ("DEBEMAIL", "USER"): email = os.getenv(env) if email is not None: return email return "unknown" def filledtemplate(values, re_var=re.compile(r"\$\$?([A-Z_]+)")): template = file(debian_support.findresource("doc", "DSA.template")).read() def repl(match): return values[match.group(1)] return re_var.sub(repl, template) tm = time.gmtime(time.time()) def getdate(months=" January February March April May June July August September October November December".split(" ")): return "{0} {1:02}, {2}".format(months[tm.tm_mon], tm.tm_mday, tm.tm_year) dsa_list_path = debian_support.findresource(*"data DSA list".split()) def checklist(): name = "DSA-" + opt_dsaid for bug in bugs.DSAFile(dsa_list_path): if bug.name == name or (bug.name + "-1") == name: print >>sys.stderr, "DSA already exists:", repr(opt_dsaid) sys.exit(1) def fordsalist(versions, months=" Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" ")): lines = ["[{0:02} {1} {2}] DSA-{3} {4} - {5}\n".format( tm.tm_mday, months[tm.tm_mon], tm.tm_year, opt_dsaid, opt_package, opt_vulnerability)] if opt_cve: lines.append("\t{" + " ".join(opt_cve) + "}\n") for rel, ver in versions.items(): rel = debian_support.releasecodename(rel) if ver: lines.append("\t[{0}] - {1} {2}\n".format( rel, opt_package, ver)) return lines def checkfile(): global dsa_file dsa_file = "DSA-" + opt_dsaid if os.path.exists(dsa_file): print >>sys.stderr, "DSA file already exists:", repr(dsa_file) sys.exit(1) def search_queue(queue, package, codename, missing=None): """Search QUEUE for PACKAGE in distribution CODENAME. Returns MISSING if not found (or raises exception of None). If found, returns the maximum version number encountered. """ cname = debian_support.releasecodename(codename) versions = [debian_support.Version(pkg.source_version) for pkg, dists in queue if pkg.source == package and cname in dists] if versions: versions.sort() return str(versions[-1]) if missing is None: raise IOError("could not find version for package {0!r} in {1!r}". format(package, codename)) return missing def searchlist_bugs(cve): """Returns a list of bug objects pertinent to CVE.""" result = [] for bug in bugs.CVEFile(debian_support.findresource( *"data CVE list".split())): if bug.name in cve: result.append(bug) for bug in bugs.DTSAFile(debian_support.findresource( *"data DTSA list".split())): found = False for xref in bug.xref: if xref in cve: found = True if found: result.append(bug) return result def searchlist(buglist, package, codename): "Returns the highest matching version number from BUGLIST." relcode = debian_support.releasecodename codename = relcode(codename) versions = [] vzero = debian_support.Version("0") # filter out etc. for bug in buglist: for ann in bug.notes: rel = relcode(ann.release or "sid") if isinstance(ann, bugs.PackageNoteParsed): if ann.fixed_version is None: return None # some bug is not fixed if ann.fixed_version != vzero \ and ann.package == package and rel == codename: versions.append(ann.fixed_version) if versions: versions.sort() return str(versions[-1]) return None parsecommand() checklist() checkfile() queue = secmaster.listqueue() stable_version = search_queue(queue, opt_package, "stable") oldstable_version = search_queue(queue, opt_package, "oldstable", False) testing_version = search_queue(queue, opt_package, "testing", False) buglist = searchlist_bugs(opt_cve) if testing_version is False: testing_version = searchlist(buglist, opt_package, "testing") unstable_version = searchlist(buglist, opt_package, "unstable") template = filledtemplate({ "DEBFULLNAME" : gecos(), "DEBEMAIL" : debemail(), "DSAID" : opt_dsaid, "PACKAGE" : opt_package, "SPACEDDATE" : getdate().ljust(22), "VULNERABILITY" : opt_vulnerability, "REMLOCAL" : os.getenv("REMLOCAL", "remote"), "DEBIANSPECIFIC" : os.getenv("DEBIANSPECIFIC", "no"), "CVE" : " ".join(opt_cve), "BUGNUM" : " ".join(opt_bugs), "OLDSTABLE" : debian_support.releasecodename("oldstable"), "STABLE" : debian_support.releasecodename("stable"), "TESTING" : debian_support.releasecodename("testing"), "OLDSTABLE_VERSION" : oldstable_version or "", "STABLE_VERSION" : stable_version, "TESTING_VERSION" : testing_version or "", "UNSTABLE_VERSION" : unstable_version or "", "SPACEDDEBFULLNAME" : gecos().rjust(24), }) updatedlist = fordsalist({ "stable" : stable_version, "oldstable" : oldstable_version }) if opt_save: out = file(dsa_file, "w+") out.write(template) out.close() debian_support.replaceFile(updatedlist + list(file(dsa_list_path)), dsa_list_path) else: print template for line in updatedlist: print "|", line,