2 # Copyright 2008-2013 Gentoo Foundation
3 # Distributed under the terms of the GNU General Public License v2
5 from __future__ import print_function
10 from os import path as osp
11 pym_path = osp.join(osp.dirname(osp.dirname(osp.realpath(__file__))), "pym")
12 sys.path.insert(0, pym_path)
14 portage._internal_caller = True
15 from portage import os
16 from portage.output import green, red, nocolor, white
18 from optparse import OptionGroup, OptionParser
20 __program__ = "glsa-check"
21 __author__ = "Marius Mauch <genone@gentoo.org>"
24 def cb_version(*args, **kwargs):
25 """Callback for --version"""
26 sys.stderr.write("\n"+ __program__ + ", version " + __version__ + "\n")
27 sys.stderr.write("Author: " + __author__ + "\n")
28 sys.stderr.write("This program is licensed under the GPL, version 2\n\n")
32 parser = OptionParser(usage="%prog <option> [glsa-list]",
33 version="%prog "+ __version__)
34 parser.epilog = "glsa-list can contain an arbitrary number of GLSA ids," \
35 " filenames containing GLSAs or the special identifiers" \
36 " 'all', 'new' and 'affected'"
38 modes = OptionGroup(parser, "Modes")
39 modes.add_option("-l", "--list", action="store_const",
40 const="list", dest="mode",
41 help="List all unapplied GLSA")
42 modes.add_option("-d", "--dump", action="store_const",
43 const="dump", dest="mode",
44 help="Show all information about the given GLSA")
45 modes.add_option("", "--print", action="store_const",
46 const="dump", dest="mode",
47 help="Alias for --dump")
48 modes.add_option("-t", "--test", action="store_const",
49 const="test", dest="mode",
50 help="Test if this system is affected by the given GLSA")
51 modes.add_option("-p", "--pretend", action="store_const",
52 const="pretend", dest="mode",
53 help="Show the necessary commands to apply this GLSA")
54 modes.add_option("-f", "--fix", action="store_const",
55 const="fix", dest="mode",
56 help="Try to auto-apply this GLSA (experimental)")
57 modes.add_option("-i", "--inject", action="store_const", dest="mode",
58 help="inject the given GLSA into the glsa_injected file")
59 modes.add_option("-m", "--mail", action="store_const",
60 const="mail", dest="mode",
61 help="Send a mail with the given GLSAs to the administrator")
62 parser.add_option_group(modes)
64 parser.remove_option("--version")
65 parser.add_option("-V", "--version", action="callback",
66 callback=cb_version, help="Some information about this tool")
67 parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
68 help="Print more information")
69 parser.add_option("-n", "--nocolor", action="callback",
70 callback=lambda *args, **kwargs: nocolor(),
71 help="Disable colors")
72 parser.add_option("-e", "--emergelike", action="store_false", dest="least_change",
73 help="Do not use a least-change algorithm")
74 parser.add_option("-c", "--cve", action="store_true", dest="list_cve",
75 help="Show CAN ids in listing mode")
77 options, params = parser.parse_args()
80 least_change = options.least_change
81 list_cve = options.list_cve
82 verbose = options.verbose
86 sys.stderr.write("No mode given: what should I do?\n")
89 elif mode != "list" and not params:
90 sys.stderr.write("\nno GLSA given, so we'll do nothing for now. \n")
91 sys.stderr.write("If you want to run on all GLSA please tell me so \n")
92 sys.stderr.write("(specify \"all\" as parameter)\n\n")
95 elif mode in ["fix", "inject"] and os.geteuid() != 0:
96 # we need root privileges for write access
97 sys.stderr.write("\nThis tool needs root access to "+options.mode+" this GLSA\n\n")
99 elif mode == "list" and not params:
102 # delay this for speed increase
103 from portage.glsa import (Glsa, GlsaTypeException, GlsaFormatException,
104 get_applied_glsas, get_glsa_list)
106 eroot = portage.settings['EROOT']
107 vardb = portage.db[eroot]["vartree"].dbapi
108 portdb = portage.db[eroot]["porttree"].dbapi
111 completelist = get_glsa_list(portage.settings)
113 checklist = get_applied_glsas(portage.settings)
114 todolist = [e for e in completelist if e not in checklist]
122 glsalist = completelist
124 if "affected" in params:
125 # replaced completelist with todolist on request of wschlich
128 myglsa = Glsa(x, portage.settings, vardb, portdb)
129 except (GlsaTypeException, GlsaFormatException) as e:
131 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (x, e)))
133 if myglsa.isVulnerable():
135 params.remove("affected")
137 # remove invalid parameters
139 if not (p in completelist or os.path.exists(p)):
140 sys.stderr.write(("(removing %s from parameter list as it isn't a valid GLSA specification)\n" % p))
143 glsalist.extend([g for g in params if g not in glsalist])
145 def summarylist(myglsalist, fd1=sys.stdout, fd2=sys.stderr, encoding="utf-8"):
146 # Get to the raw streams in py3k before wrapping them with an encoded writer
147 # to avoid writing bytes to a text stream (stdout/stderr are text streams
148 # by default in py3k)
149 if hasattr(fd1, "buffer"):
151 if hasattr(fd2, "buffer"):
153 fd1 = codecs.getwriter(encoding)(fd1)
154 fd2 = codecs.getwriter(encoding)(fd2)
155 fd2.write(white("[A]")+" means this GLSA was marked as applied (injected),\n")
156 fd2.write(green("[U]")+" means the system is not affected and\n")
157 fd2.write(red("[N]")+" indicates that the system might be affected.\n\n")
160 for myid in myglsalist:
162 myglsa = Glsa(myid, portage.settings, vardb, portdb)
163 except (GlsaTypeException, GlsaFormatException) as e:
165 fd2.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
167 if myglsa.isInjected():
170 elif myglsa.isVulnerable():
178 access = ("[%-8s] " % myglsa.access)
182 fd1.write(color(myglsa.nr) + " " + color(status) + " " + color(access) + myglsa.title + " (")
184 for pkg in list(myglsa.packages)[:3]:
185 fd1.write(" " + pkg + " ")
186 if len(myglsa.packages) > 3:
189 for pkg in myglsa.packages:
190 mylist = vardb.match(pkg)
192 pkg = color(" ".join(mylist))
193 fd1.write(" " + pkg + " ")
197 fd1.write(" "+(",".join([r[:13] for r in myglsa.references if r[:4] in ["CAN-", "CVE-"]])))
202 sys.exit(summarylist(glsalist))
204 # dump, fix, inject and fix are nearly the same code, only the glsa method call differs
205 if mode in ["dump", "fix", "inject", "pretend"]:
206 for myid in glsalist:
208 myglsa = Glsa(myid, portage.settings, vardb, portdb)
209 except (GlsaTypeException, GlsaFormatException) as e:
211 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
216 sys.stdout.write("Fixing GLSA "+myid+"\n")
217 if not myglsa.isVulnerable():
218 sys.stdout.write(">>> no vulnerable packages installed\n")
220 mergelist = myglsa.getMergeList(least_change=least_change)
222 sys.stdout.write(">>> cannot fix GLSA, no unaffected packages available\n")
224 for pkg in mergelist:
225 sys.stdout.write(">>> merging "+pkg+"\n")
226 # using emerge for the actual merging as it contains the dependency
227 # code and we want to be consistent in behaviour. Also this functionality
228 # will be integrated in emerge later, so it shouldn't hurt much.
229 emergecmd = "emerge --oneshot " + " =" + pkg
231 sys.stderr.write(emergecmd+"\n")
232 exitcode = os.system(emergecmd)
233 # system() returns the exitcode in the high byte of a 16bit integer
239 sys.stdout.write("\n")
240 elif mode == "pretend":
241 sys.stdout.write("Checking GLSA "+myid+"\n")
242 if not myglsa.isVulnerable():
243 sys.stdout.write(">>> no vulnerable packages installed\n")
246 for (vuln, update) in myglsa.getAffectionTable(least_change=least_change):
247 mergedict.setdefault(update, []).append(vuln)
249 sys.stdout.write(">>> The following updates will be performed for this GLSA:\n")
250 for pkg in mergedict:
252 sys.stdout.write(" " + pkg + " (vulnerable: " + ", ".join(mergedict[pkg]) + ")\n")
254 sys.stdout.write("\n>>> For the following packages, no upgrade path exists:\n")
255 sys.stdout.write(" " + ", ".join(mergedict[""]))
256 elif mode == "inject":
257 sys.stdout.write("injecting " + myid + "\n")
259 sys.stdout.write("\n")
262 # test is a bit different as Glsa.test() produces no output
265 for myid in glsalist:
267 myglsa = Glsa(myid, portage.settings, vardb, portdb)
268 except (GlsaTypeException, GlsaFormatException) as e:
270 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
272 if myglsa.isVulnerable():
273 outputlist.append(str(myglsa.nr))
274 if len(outputlist) > 0:
275 sys.stderr.write("This system is affected by the following GLSAs:\n")
277 summarylist(outputlist)
279 sys.stdout.write("\n".join(outputlist)+"\n")
281 sys.stderr.write("This system is not affected by any of the listed GLSAs\n")
284 # mail mode as requested by solar
286 import portage.mail, socket
287 from io import BytesIO
288 from email.mime.text import MIMEText
290 # color doesn't make any sense for mail
293 if "PORTAGE_ELOG_MAILURI" in portage.settings:
294 myrecipient = portage.settings["PORTAGE_ELOG_MAILURI"].split()[0]
296 myrecipient = "root@localhost"
298 if "PORTAGE_ELOG_MAILFROM" in portage.settings:
299 myfrom = portage.settings["PORTAGE_ELOG_MAILFROM"]
301 myfrom = "glsa-check"
303 mysubject = "[glsa-check] Summary for %s" % socket.getfqdn()
305 # need a file object for summarylist()
307 line = "GLSA Summary report for host %s\n" % socket.getfqdn()
308 myfd.write(line.encode("utf-8"))
309 line = "(Command was: %s)\n\n" % " ".join(sys.argv)
310 myfd.write(line.encode("utf-8"))
311 summarylist(glsalist, fd1=myfd, fd2=myfd)
312 summary = myfd.getvalue().decode("utf-8")
316 for myid in glsalist:
318 myglsa = Glsa(myid, portage.settings, vardb, portdb)
319 except (GlsaTypeException, GlsaFormatException) as e:
321 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
324 myglsa.dump(outstream=myfd)
325 attachment = myfd.getvalue().decode("utf-8")
326 myattachments.append(MIMEText(attachment, _charset="utf8"))
329 mymessage = portage.mail.create_message(myfrom, myrecipient, mysubject, summary, myattachments)
330 portage.mail.send_mail(portage.settings, mymessage)
334 # something wrong here, all valid paths are covered with sys.exit()
335 sys.stderr.write("nothing more to do\n")