blob: 84481d8495934a819f02b9efb962668d1c6ee674 [file] [log] [blame]
#!/usr/bin/python -b
# Copyright 2008-2014 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
from __future__ import print_function
import argparse
import sys
import codecs
from os import path as osp
if osp.isfile(osp.join(osp.dirname(osp.dirname(osp.realpath(__file__))), ".portage_not_installed")):
sys.path.insert(0, osp.join(osp.dirname(osp.dirname(osp.realpath(__file__))), "pym"))
import portage
portage._internal_caller = True
from portage import os
from portage.output import green, red, nocolor, white
__program__ = "glsa-check"
__author__ = "Marius Mauch <genone@gentoo.org>"
__version__ = "1.0"
# option parsing
epilog = "glsa-list can contain an arbitrary number of GLSA ids," \
" filenames containing GLSAs or the special identifiers" \
" 'all', 'new' and 'affected'"
parser = argparse.ArgumentParser(usage=__program__ + " <option> [glsa-list]",
epilog=epilog)
modes = parser.add_argument_group("Modes")
modes.add_argument("-l", "--list", action="store_const",
const="list", dest="mode",
help="List all unapplied GLSA")
modes.add_argument("-d", "--dump", action="store_const",
const="dump", dest="mode",
help="Show all information about the given GLSA")
modes.add_argument("--print", action="store_const",
const="dump", dest="mode",
help="Alias for --dump")
modes.add_argument("-t", "--test", action="store_const",
const="test", dest="mode",
help="Test if this system is affected by the given GLSA")
modes.add_argument("-p", "--pretend", action="store_const",
const="pretend", dest="mode",
help="Show the necessary commands to apply this GLSA")
modes.add_argument("-f", "--fix", action="store_const",
const="fix", dest="mode",
help="Try to auto-apply this GLSA (experimental)")
modes.add_argument("-i", "--inject", action="store_const",
const="inject", dest="mode",
help="inject the given GLSA into the glsa_injected file")
modes.add_argument("-m", "--mail", action="store_const",
const="mail", dest="mode",
help="Send a mail with the given GLSAs to the administrator")
parser.add_argument("-V", "--version", action="store_true",
help="Some information about this tool")
parser.add_argument("-v", "--verbose", action="store_true", dest="verbose",
help="Print more information")
parser.add_argument("-n", "--nocolor", action="store_true",
help="Disable colors")
parser.add_argument("-e", "--emergelike", action="store_false", dest="least_change",
help="Do not use a least-change algorithm")
parser.add_argument("-c", "--cve", action="store_true", dest="list_cve",
help="Show CAN ids in listing mode")
options, params = parser.parse_known_args()
if options.nocolor:
nocolor()
if options.version:
sys.stderr.write("\n"+ __program__ + ", version " + __version__ + "\n")
sys.stderr.write("Author: " + __author__ + "\n")
sys.stderr.write("This program is licensed under the GPL, version 2\n\n")
sys.exit(0)
mode = options.mode
least_change = options.least_change
list_cve = options.list_cve
verbose = options.verbose
# Sanity checking
if mode is None:
sys.stderr.write("No mode given: what should I do?\n")
parser.print_help()
sys.exit(1)
elif mode != "list" and not params:
sys.stderr.write("\nno GLSA given, so we'll do nothing for now. \n")
sys.stderr.write("If you want to run on all GLSA please tell me so \n")
sys.stderr.write("(specify \"all\" as parameter)\n\n")
parser.print_help()
sys.exit(1)
elif mode in ["fix", "inject"] and os.geteuid() != 0:
# we need root privileges for write access
sys.stderr.write("\nThis tool needs root access to "+options.mode+" this GLSA\n\n")
sys.exit(2)
elif mode == "list" and not params:
params.append("new")
# delay this for speed increase
from portage.glsa import (Glsa, GlsaTypeException, GlsaFormatException,
get_applied_glsas, get_glsa_list)
eroot = portage.settings['EROOT']
vardb = portage.db[eroot]["vartree"].dbapi
portdb = portage.db[eroot]["porttree"].dbapi
# build glsa lists
completelist = get_glsa_list(portage.settings)
checklist = get_applied_glsas(portage.settings)
todolist = [e for e in completelist if e not in checklist]
glsalist = []
if "new" in params:
glsalist = todolist
params.remove("new")
if "all" in params:
glsalist = completelist
params.remove("all")
if "affected" in params:
# replaced completelist with todolist on request of wschlich
for x in todolist:
try:
myglsa = Glsa(x, portage.settings, vardb, portdb)
except (GlsaTypeException, GlsaFormatException) as e:
if verbose:
sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (x, e)))
continue
if myglsa.isVulnerable():
glsalist.append(x)
params.remove("affected")
# remove invalid parameters
for p in params[:]:
if not (p in completelist or os.path.exists(p)):
sys.stderr.write(("(removing %s from parameter list as it isn't a valid GLSA specification)\n" % p))
params.remove(p)
glsalist.extend([g for g in params if g not in glsalist])
def summarylist(myglsalist, fd1=sys.stdout, fd2=sys.stderr, encoding="utf-8"):
# Get to the raw streams in py3k before wrapping them with an encoded writer
# to avoid writing bytes to a text stream (stdout/stderr are text streams
# by default in py3k)
if hasattr(fd1, "buffer"):
fd1 = fd1.buffer
if hasattr(fd2, "buffer"):
fd2 = fd2.buffer
fd1 = codecs.getwriter(encoding)(fd1)
fd2 = codecs.getwriter(encoding)(fd2)
fd2.write(white("[A]")+" means this GLSA was marked as applied (injected),\n")
fd2.write(green("[U]")+" means the system is not affected and\n")
fd2.write(red("[N]")+" indicates that the system might be affected.\n\n")
myglsalist.sort()
for myid in myglsalist:
try:
myglsa = Glsa(myid, portage.settings, vardb, portdb)
except (GlsaTypeException, GlsaFormatException) as e:
if verbose:
fd2.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
continue
if myglsa.isInjected():
status = "[A]"
color = white
elif myglsa.isVulnerable():
status = "[N]"
color = red
else:
status = "[U]"
color = green
if verbose:
access = ("[%-8s] " % myglsa.access)
else:
access=""
fd1.write(color(myglsa.nr) + " " + color(status) + " " + color(access) + myglsa.title + " (")
if not verbose:
for pkg in list(myglsa.packages)[:3]:
fd1.write(" " + pkg + " ")
if len(myglsa.packages) > 3:
fd1.write("... ")
else:
for pkg in myglsa.packages:
mylist = vardb.match(pkg)
if len(mylist) > 0:
pkg = color(" ".join(mylist))
fd1.write(" " + pkg + " ")
fd1.write(")")
if list_cve:
fd1.write(" "+(",".join([r[:13] for r in myglsa.references if r[:4] in ["CAN-", "CVE-"]])))
fd1.write("\n")
return 0
if mode == "list":
sys.exit(summarylist(glsalist))
# dump, fix, inject and fix are nearly the same code, only the glsa method call differs
if mode in ["dump", "fix", "inject", "pretend"]:
for myid in glsalist:
try:
myglsa = Glsa(myid, portage.settings, vardb, portdb)
except (GlsaTypeException, GlsaFormatException) as e:
if verbose:
sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
continue
if mode == "dump":
myglsa.dump()
elif mode == "fix":
sys.stdout.write("Fixing GLSA "+myid+"\n")
if not myglsa.isVulnerable():
sys.stdout.write(">>> no vulnerable packages installed\n")
else:
mergelist = myglsa.getMergeList(least_change=least_change)
if mergelist == []:
sys.stdout.write(">>> cannot fix GLSA, no unaffected packages available\n")
sys.exit(2)
for pkg in mergelist:
sys.stdout.write(">>> merging "+pkg+"\n")
# using emerge for the actual merging as it contains the dependency
# code and we want to be consistent in behaviour. Also this functionality
# will be integrated in emerge later, so it shouldn't hurt much.
emergecmd = "emerge --oneshot " + " =" + pkg
if verbose:
sys.stderr.write(emergecmd+"\n")
exitcode = os.system(emergecmd)
# system() returns the exitcode in the high byte of a 16bit integer
if exitcode >= 1<<8:
exitcode >>= 8
if exitcode:
sys.exit(exitcode)
if len(mergelist):
sys.stdout.write("\n")
elif mode == "pretend":
sys.stdout.write("Checking GLSA "+myid+"\n")
if not myglsa.isVulnerable():
sys.stdout.write(">>> no vulnerable packages installed\n")
else:
mergedict = {}
for (vuln, update) in myglsa.getAffectionTable(least_change=least_change):
mergedict.setdefault(update, []).append(vuln)
sys.stdout.write(">>> The following updates will be performed for this GLSA:\n")
for pkg in mergedict:
if pkg != "":
sys.stdout.write(" " + pkg + " (vulnerable: " + ", ".join(mergedict[pkg]) + ")\n")
if "" in mergedict:
sys.stdout.write("\n>>> For the following packages, no upgrade path exists:\n")
sys.stdout.write(" " + ", ".join(mergedict[""]))
elif mode == "inject":
sys.stdout.write("injecting " + myid + "\n")
myglsa.inject()
sys.stdout.write("\n")
sys.exit(0)
# test is a bit different as Glsa.test() produces no output
if mode == "test":
outputlist = []
for myid in glsalist:
try:
myglsa = Glsa(myid, portage.settings, vardb, portdb)
except (GlsaTypeException, GlsaFormatException) as e:
if verbose:
sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
continue
if myglsa.isVulnerable():
outputlist.append(str(myglsa.nr))
if len(outputlist) > 0:
sys.stderr.write("This system is affected by the following GLSAs:\n")
if verbose:
summarylist(outputlist)
else:
sys.stdout.write("\n".join(outputlist)+"\n")
else:
sys.stderr.write("This system is not affected by any of the listed GLSAs\n")
sys.exit(0)
# mail mode as requested by solar
if mode == "mail":
import portage.mail, socket
from io import BytesIO
from email.mime.text import MIMEText
# color doesn't make any sense for mail
nocolor()
if "PORTAGE_ELOG_MAILURI" in portage.settings:
myrecipient = portage.settings["PORTAGE_ELOG_MAILURI"].split()[0]
else:
myrecipient = "root@localhost"
if "PORTAGE_ELOG_MAILFROM" in portage.settings:
myfrom = portage.settings["PORTAGE_ELOG_MAILFROM"]
else:
myfrom = "glsa-check"
mysubject = "[glsa-check] Summary for %s" % socket.getfqdn()
# need a file object for summarylist()
myfd = BytesIO()
line = "GLSA Summary report for host %s\n" % socket.getfqdn()
myfd.write(line.encode("utf-8"))
line = "(Command was: %s)\n\n" % " ".join(sys.argv)
myfd.write(line.encode("utf-8"))
summarylist(glsalist, fd1=myfd, fd2=myfd)
summary = myfd.getvalue().decode("utf-8")
myfd.close()
myattachments = []
for myid in glsalist:
try:
myglsa = Glsa(myid, portage.settings, vardb, portdb)
except (GlsaTypeException, GlsaFormatException) as e:
if verbose:
sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
continue
myfd = BytesIO()
myglsa.dump(outstream=myfd)
attachment = myfd.getvalue().decode("utf-8")
myattachments.append(MIMEText(attachment, _charset="utf8"))
myfd.close()
mymessage = portage.mail.create_message(myfrom, myrecipient, mysubject, summary, myattachments)
portage.mail.send_mail(portage.settings, mymessage)
sys.exit(0)
# something wrong here, all valid paths are covered with sys.exit()
sys.stderr.write("nothing more to do\n")
sys.exit(2)