4 # This program is licensed under the GPL, version 2
9 from functools import reduce
12 from portage.output import *
14 from getopt import getopt, GetoptError
16 __program__ = "glsa-check"
17 __author__ = "Marius Mauch <genone@gentoo.org>"
21 ["-l", "--list", "list the GLSAs"],
22 ["-d", "--dump", "--print", "show all information about the GLSAs"],
23 ["-t", "--test", "test if this system is affected by the GLSAs"],
24 ["-p", "--pretend", "show the necessary steps to apply the GLSAs"],
25 ["-f", "--fix", "try to auto-apply the GLSAs (experimental)"],
26 ["-i", "--inject", "inject the given GLSA into the glsa_injected file"],
27 ["-n", "--nocolor", "disable colors (option)"],
28 ["-e", "--emergelike", "upgrade to latest version (not least-change, option)"],
29 ["-h", "--help", "show this help message"],
30 ["-V", "--version", "some information about this tool"],
31 ["-v", "--verbose", "print more information (option)"],
32 ["-c", "--cve", "show CVE ids in listing mode (option)"],
33 ["-q", "--quiet", "be less verbose and do not send empty mail (option)"],
34 ["-m", "--mail", "send a mail with the given GLSAs to the administrator"],
37 # print a warning as this is beta code (but proven by now, so no more warning)
38 #sys.stderr.write("WARNING: This tool is completely new and not very tested, so it should not be\n")
39 #sys.stderr.write("used on production systems. It's mainly a test tool for the new GLSA release\n")
40 #sys.stderr.write("and distribution system, it's functionality will later be merged into emerge\n")
41 #sys.stderr.write("and equery.\n")
42 #sys.stderr.write("Please read http://www.gentoo.org/proj/en/portage/glsa-integration.xml\n")
43 #sys.stderr.write("before using this tool AND before reporting a bug.\n\n")
49 args, params = getopt(sys.argv[1:], "".join([o[0][1] for o in optionmap]), \
50 [x[2:] for x in reduce(lambda x,y: x+y, [z[1:-1] for z in optionmap])])
51 args = [a for a,b in args]
53 for option in ["--nocolor", "-n"]:
59 for option in ["--verbose", "-v"]:
65 for option in ["--cve", "-c"]:
71 for option in ["--emergelike", "-e"]:
77 for option in ["--quiet", "-q"]:
85 sys.stderr.write("no option given: what should I do ?\n")
88 sys.stderr.write("please use only one command per call\n")
91 # in what mode are we ?
94 if args in [o for o in m[:-1]]:
97 except GetoptError as e:
98 sys.stderr.write("unknown option given: ")
99 sys.stderr.write(str(e)+"\n")
102 # we need a set of glsa for most operation modes
103 if len(params) <= 0 and mode in ["fix", "test", "pretend", "dump", "inject", "mail"]:
104 sys.stderr.write("\nno GLSA given, so we'll do nothing for now. \n")
105 sys.stderr.write("If you want to run on all GLSA please tell me so \n")
106 sys.stderr.write("(specify \"all\" as parameter)\n\n")
108 elif len(params) <= 0 and mode == "list":
109 params.append("affected")
112 if mode == "help" or mode == "HELP":
113 msg = "Syntax: glsa-check <option> [glsa-list]\n\n"
115 msg += m[0] + "\t" + m[1] + " \t: " + m[-1] + "\n"
117 msg += "\t" + o + "\n"
118 msg += "\nglsa-list can contain an arbitrary number of GLSA ids, \n"
119 msg += "filenames containing GLSAs or the special identifiers \n"
120 msg += "'all' and 'affected'\n"
122 sys.stdout.write(msg)
125 sys.stderr.write("\n" + msg)
128 # we need root privileges for write access
129 if mode in ["fix", "inject"] and os.geteuid() != 0:
130 sys.stderr.write(__program__ + ": root access is needed for \""+mode+"\" mode\n")
133 # show version and copyright information
134 if mode == "version":
135 sys.stderr.write("%(program)s (%(version)s)\n" % {
136 "program": __program__,
137 "version": __version__
139 sys.stderr.write("Author: %s\n" % __author__)
140 sys.stderr.write("This program is licensed under the GPL, version 2\n")
143 # delay this for speed increase
144 from gentoolkit.glsa import *
146 glsaconfig = checkconfig(portage.config(clone=portage.settings))
149 glsaconfig["EMERGE_OPTS"] += " --quiet"
151 vardb = portage.db[portage.root]["vartree"].dbapi
152 portdb = portage.db[portage.root]["porttree"].dbapi
154 # Check that we really have a glsa dir to work on
155 if not (os.path.exists(glsaconfig["GLSA_DIR"]) and os.path.isdir(glsaconfig["GLSA_DIR"])):
156 sys.stderr.write(red("ERROR")+": GLSA_DIR %s doesn't exist. Please fix this.\n" % glsaconfig["GLSA_DIR"])
160 completelist = get_glsa_list(glsaconfig["GLSA_DIR"], glsaconfig)
162 if os.access(glsaconfig["CHECKFILE"], os.R_OK):
163 checklist = [line.strip() for line in open(glsaconfig["CHECKFILE"], "r").readlines()]
166 todolist = [e for e in completelist if e not in checklist]
171 sys.stderr.write("Warning: The 'new' glsa-list target has been removed, using 'affected'.\n")
172 params.append("affected")
175 glsalist = completelist
178 if "affected" in params:
181 myglsa = Glsa(x, glsaconfig)
182 except (GlsaTypeException, GlsaFormatException) as e:
184 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (x, e)))
186 if myglsa.isVulnerable():
188 params.remove("affected")
190 # remove invalid parameters
192 if not (p in completelist or os.path.exists(p)):
193 sys.stderr.write(("(removing %s from parameter list as it isn't a valid GLSA specification)\n" % p))
196 glsalist.extend([g for g in params if g not in glsalist])
198 def summarylist(myglsalist, fd1=sys.stdout, fd2=sys.stderr, encoding="utf-8"):
199 # Get to the raw streams in py3k before wrapping them with an encoded writer
200 # to avoid writing bytes to a text stream (stdout/stderr are text streams
201 # by default in py3k)
202 if hasattr(fd1, "buffer"):
204 if hasattr(fd2, "buffer"):
206 fd1 = codecs.getwriter(encoding)(fd1)
207 fd2 = codecs.getwriter(encoding)(fd2)
209 fd2.write(white("[A]")+" means this GLSA was marked as applied (injected),\n")
210 fd2.write(green("[U]")+" means the system is not affected and\n")
211 fd2.write(red("[N]")+" indicates that the system might be affected.\n\n")
214 for myid in myglsalist:
216 myglsa = Glsa(myid, glsaconfig)
217 except (GlsaTypeException, GlsaFormatException) as e:
219 fd2.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
221 if myglsa.isInjected():
224 elif myglsa.isVulnerable():
232 access = ("[%-8s] " % myglsa.access)
236 fd1.write(color(myglsa.nr) + " " + color(status) + " " + color(access) + myglsa.title + " (")
238 for pkg in list(myglsa.packages.keys())[:3]:
239 fd1.write(" " + pkg + " ")
240 if len(myglsa.packages) > 3:
243 for pkg in myglsa.packages.keys():
244 mylist = vardb.match(portage.dep_getkey(str(pkg)))
246 pkg = color(" ".join(mylist))
247 fd1.write(" " + pkg + " ")
251 fd1.write(" "+(",".join([r[:13] for r in myglsa.references if r[:4] in ["CAN-", "CVE-"]])))
256 sys.exit(summarylist(glsalist))
258 # dump, fix, inject and fix are nearly the same code, only the glsa method call differs
259 if mode in ["dump", "fix", "inject", "pretend"]:
260 for myid in glsalist:
262 myglsa = Glsa(myid, glsaconfig)
263 except (GlsaTypeException, GlsaFormatException) as e:
265 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
271 sys.stdout.write("Fixing GLSA "+myid+"\n")
272 if not myglsa.isVulnerable():
274 sys.stdout.write(">>> no vulnerable packages installed\n")
277 sys.stdout.write("Fixing GLSA "+myid+"\n")
278 mergelist = myglsa.getMergeList(least_change=least_change)
280 sys.stdout.write(">>> cannot fix GLSA, no unaffected packages available\n")
282 for pkg in mergelist:
283 sys.stdout.write(">>> merging "+pkg+"\n")
284 # using emerge for the actual merging as it contains the dependency
285 # code and we want to be consistent in behaviour. Also this functionality
286 # will be integrated in emerge later, so it shouldn't hurt much.
287 emergecmd = "emerge --oneshot " + glsaconfig["EMERGE_OPTS"] + " =" + pkg
289 sys.stderr.write(emergecmd+"\n")
290 exitcode = os.system(emergecmd)
291 # system() returns the exitcode in the high byte of a 16bit integer
297 sys.stdout.write("\n")
298 elif mode == "pretend":
300 sys.stdout.write("Checking GLSA "+myid+"\n")
301 if not myglsa.isVulnerable():
303 sys.stdout.write(">>> no vulnerable packages installed\n")
306 sys.stdout.write("Checking GLSA "+myid+"\n")
308 for (vuln, update) in myglsa.getAffectionTable(least_change=least_change):
309 mergedict.setdefault(update, []).append(vuln)
311 # first, extract the atoms that cannot be upgraded (where key == "")
314 no_upgrades = mergedict[""]
317 # see if anything is left that can be upgraded
319 sys.stdout.write(">>> Updates that will be performed:\n")
320 for (upd, vuln) in mergedict.items():
321 sys.stdout.write(" " + green(upd) + " (vulnerable: " + red(", ".join(vuln)) + ")\n")
324 sys.stdout.write(">>> No upgrade path exists for these packages:\n")
325 sys.stdout.write(" " + red(", ".join(no_upgrades)) + "\n")
326 elif mode == "inject":
327 sys.stdout.write("injecting " + myid + "\n")
331 # test is a bit different as Glsa.test() produces no output
334 for myid in glsalist:
336 myglsa = Glsa(myid, glsaconfig)
337 except (GlsaTypeException, GlsaFormatException) as e:
339 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
341 if myglsa.isVulnerable():
342 outputlist.append(str(myglsa.nr))
343 if len(outputlist) > 0:
344 sys.stderr.write("This system is affected by the following GLSAs:\n")
346 summarylist(outputlist)
348 sys.stdout.write("\n".join(outputlist)+"\n")
350 sys.stderr.write("This system is not affected by any of the listed GLSAs\n")
353 # mail mode as requested by solar
356 import portage.mail as portage_mail
361 from io import StringIO
363 from email.mime.text import MIMEText
365 from email.MIMEText import MIMEText
367 # color doesn't make any sense for mail
370 if "PORTAGE_ELOG_MAILURI" in glsaconfig:
371 myrecipient = glsaconfig["PORTAGE_ELOG_MAILURI"].split()[0]
373 myrecipient = "root@localhost"
375 if "PORTAGE_ELOG_MAILFROM" in glsaconfig:
376 myfrom = glsaconfig["PORTAGE_ELOG_MAILFROM"]
378 myfrom = "glsa-check"
380 mysubject = "[glsa-check] Summary for %s" % socket.getfqdn()
382 # need a file object for summarylist()
384 myfd.write("GLSA Summary report for host %s\n" % socket.getfqdn())
385 myfd.write("(Command was: %s)\n\n" % " ".join(sys.argv))
386 summarylist(glsalist, fd1=myfd, fd2=myfd)
387 summary = str(myfd.getvalue())
391 for myid in glsalist:
393 myglsa = Glsa(myid, glsaconfig)
394 except (GlsaTypeException, GlsaFormatException) as e:
396 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
399 myglsa.dump(outstream=myfd)
400 myattachments.append(MIMEText(str(myfd.getvalue()), _charset="utf8"))
403 if glsalist or not quiet:
404 mymessage = portage_mail.create_message(myfrom, myrecipient, mysubject, summary, myattachments)
405 portage_mail.send_mail(glsaconfig, mymessage)
409 # something wrong here, all valid paths are covered with sys.exit()
410 sys.stderr.write("nothing more to do\n")