Merge branch 'gentoolkit' into euse
[gentoolkit.git] / bin / glsa-check
1 #!/usr/bin/python
2
3 # $Header: $
4 # This program is licensed under the GPL, version 2
5
6 import sys
7 import os
8 import codecs
9 from functools import reduce
10
11 import portage
12 from portage.output import *
13
14 from getopt import getopt, GetoptError
15
16 __program__ = "glsa-check"
17 __author__ = "Marius Mauch <genone@gentoo.org>"
18 __version__ = "svn"
19
20 optionmap = [
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"],
35 ]
36
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")
44
45 # option parsing
46 args = []
47 params = []
48 try:
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]
52
53         for option in ["--nocolor", "-n"]:
54                 if option in args:
55                         nocolor()
56                         args.remove(option)
57
58         verbose = False
59         for option in ["--verbose", "-v"]:
60                 if option in args:
61                         verbose = True
62                         args.remove(option)
63
64         list_cve = False
65         for option in ["--cve", "-c"]:
66                 if option in args:
67                         list_cve = True
68                         args.remove(option)
69
70         least_change = True
71         for option in ["--emergelike", "-e"]:
72                 if option in args:
73                         least_change = False
74                         args.remove(option)
75
76         quiet = False
77         for option in ["--quiet", "-q"]:
78                 if option in args:
79                         quiet = True
80                         args.remove(option)
81
82
83         # sanity checking
84         if len(args) <= 0:
85                 sys.stderr.write("no option given: what should I do ?\n")
86                 mode = "HELP"
87         elif len(args) > 1:
88                 sys.stderr.write("please use only one command per call\n")
89                 mode = "HELP"
90         else:
91                 # in what mode are we ?
92                 args = args[0]
93                 for m in optionmap:
94                         if args in [o for o in m[:-1]]:
95                                 mode = m[1][2:]
96
97 except GetoptError as e:
98         sys.stderr.write("unknown option given: ")
99         sys.stderr.write(str(e)+"\n")
100         mode = "HELP"
101
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")
107         mode = "HELP"
108 elif len(params) <= 0 and mode == "list":
109         params.append("affected")
110
111 # show help message
112 if mode == "help" or mode == "HELP":
113         msg = "Syntax: glsa-check <option> [glsa-list]\n\n"
114         for m in optionmap:
115                 msg += m[0] + "\t" + m[1] + "   \t: " + m[-1] + "\n"
116                 for o in m[2:-1]:
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"
121         if mode == "help":
122                 sys.stdout.write(msg)
123                 sys.exit(0)
124         else:
125                 sys.stderr.write("\n" + msg)
126                 sys.exit(1)
127
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")
131         sys.exit(2)
132
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__
138         })
139         sys.stderr.write("Author: %s\n" % __author__)
140         sys.stderr.write("This program is licensed under the GPL, version 2\n")
141         sys.exit(0)
142
143 # delay this for speed increase
144 from gentoolkit.glsa import *
145
146 glsaconfig = checkconfig(portage.config(clone=portage.settings))
147
148 if quiet:
149     glsaconfig["EMERGE_OPTS"] += " --quiet"
150
151 vardb = portage.db[portage.root]["vartree"].dbapi
152 portdb = portage.db[portage.root]["porttree"].dbapi
153
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"])
157         sys.exit(1)
158
159 # build glsa lists
160 completelist = get_glsa_list(glsaconfig["GLSA_DIR"], glsaconfig)
161
162 if os.access(glsaconfig["CHECKFILE"], os.R_OK):
163         checklist = [line.strip() for line in open(glsaconfig["CHECKFILE"], "r").readlines()]
164 else:
165         checklist = []
166 todolist = [e for e in completelist if e not in checklist]
167
168 glsalist = []
169 if "new" in params:
170         params.remove("new")
171         sys.stderr.write("Warning: The 'new' glsa-list target has been removed, using 'affected'.\n")
172         params.append("affected")
173
174 if "all" in params:
175         glsalist = completelist
176         params.remove("all")
177
178 if "affected" in params:
179         for x in todolist:
180                 try:
181                         myglsa = Glsa(x, glsaconfig)
182                 except (GlsaTypeException, GlsaFormatException) as e:
183                         if verbose:
184                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (x, e)))
185                         continue
186                 if myglsa.isVulnerable():
187                         glsalist.append(x)
188         params.remove("affected")
189
190 # remove invalid parameters
191 for p in params[:]:
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))
194                 params.remove(p)
195
196 glsalist.extend([g for g in params if g not in glsalist])
197
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"):
203                 fd1 = fd1.buffer
204         if hasattr(fd2, "buffer"):
205                 fd2 = fd2.buffer
206         fd1 = codecs.getwriter(encoding)(fd1)
207         fd2 = codecs.getwriter(encoding)(fd2)
208         if not quiet:
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")
212
213         myglsalist.sort()
214         for myid in myglsalist:
215                 try:
216                         myglsa = Glsa(myid, glsaconfig)
217                 except (GlsaTypeException, GlsaFormatException) as e:
218                         if verbose:
219                                 fd2.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
220                         continue
221                 if myglsa.isInjected():
222                         status = "[A]"
223                         color = white
224                 elif myglsa.isVulnerable():
225                         status = "[N]"
226                         color = red
227                 else:
228                         status = "[U]"
229                         color = green
230
231                 if verbose:
232                         access = ("[%-8s] " % myglsa.access)
233                 else:
234                         access=""
235
236                 fd1.write(color(myglsa.nr) + " " + color(status) + " " + color(access) + myglsa.title + " (")
237                 if not verbose:
238                         for pkg in list(myglsa.packages.keys())[:3]:
239                                 fd1.write(" " + pkg + " ")
240                         if len(myglsa.packages) > 3:
241                                 fd1.write("... ")
242                 else:
243                         for pkg in myglsa.packages.keys():
244                                 mylist = vardb.match(portage.dep_getkey(str(pkg)))
245                                 if len(mylist) > 0:
246                                         pkg = color(" ".join(mylist))
247                                 fd1.write(" " + pkg + " ")
248
249                 fd1.write(")")
250                 if list_cve:
251                         fd1.write(" "+(",".join([r[:13] for r in myglsa.references if r[:4] in ["CAN-", "CVE-"]])))
252                 fd1.write("\n")
253         return 0
254
255 if mode == "list":
256         sys.exit(summarylist(glsalist))
257
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:
261                 try:
262                         myglsa = Glsa(myid, glsaconfig)
263                 except (GlsaTypeException, GlsaFormatException) as e:
264                         if verbose:
265                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
266                         continue
267                 if mode == "dump":
268                         myglsa.dump()
269                 elif mode == "fix":
270                         if not quiet:
271                                 sys.stdout.write("Fixing GLSA "+myid+"\n")
272                         if not myglsa.isVulnerable():
273                                 if not quiet:
274                                         sys.stdout.write(">>> no vulnerable packages installed\n")
275                         else:
276                                 if quiet:
277                                         sys.stdout.write("Fixing GLSA "+myid+"\n")
278                                 mergelist = myglsa.getMergeList(least_change=least_change)
279                                 if mergelist == []:
280                                         sys.stdout.write(">>> cannot fix GLSA, no unaffected packages available\n")
281                                         sys.exit(2)
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
288                                         if verbose:
289                                                 sys.stderr.write(emergecmd+"\n")
290                                         exitcode = os.system(emergecmd)
291                                         # system() returns the exitcode in the high byte of a 16bit integer
292                                         if exitcode >= 1<<8:
293                                                 exitcode >>= 8
294                                         if exitcode:
295                                                 sys.exit(exitcode)
296                                 if len(mergelist):
297                                         sys.stdout.write("\n")
298                 elif mode == "pretend":
299                         if not quiet:
300                                 sys.stdout.write("Checking GLSA "+myid+"\n")
301                         if not myglsa.isVulnerable():
302                                 if not quiet:
303                                         sys.stdout.write(">>> no vulnerable packages installed\n")
304                         else:
305                                 if quiet:
306                                         sys.stdout.write("Checking GLSA "+myid+"\n")
307                                 mergedict = {}
308                                 for (vuln, update) in myglsa.getAffectionTable(least_change=least_change):
309                                         mergedict.setdefault(update, []).append(vuln)
310
311                                 # first, extract the atoms that cannot be upgraded (where key == "")
312                                 no_upgrades = []
313                                 if "" in mergedict:
314                                         no_upgrades = mergedict[""]
315                                         del mergedict[""]
316
317                                 # see if anything is left that can be upgraded
318                                 if mergedict:
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")
322
323                                 if no_upgrades:
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")
328                         myglsa.inject()
329         sys.exit(0)
330
331 # test is a bit different as Glsa.test() produces no output
332 if mode == "test":
333         outputlist = []
334         for myid in glsalist:
335                 try:
336                         myglsa = Glsa(myid, glsaconfig)
337                 except (GlsaTypeException, GlsaFormatException) as e:
338                         if verbose:
339                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
340                         continue
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")
345                 if verbose:
346                         summarylist(outputlist)
347                 else:
348                         sys.stdout.write("\n".join(outputlist)+"\n")
349         else:
350                 sys.stderr.write("This system is not affected by any of the listed GLSAs\n")
351         sys.exit(0)
352
353 # mail mode as requested by solar
354 if mode == "mail":
355         try:
356                 import portage.mail as portage_mail
357         except ImportError:
358                 import portage_mail
359
360         import socket
361         from io import StringIO
362         try:
363                 from email.mime.text import MIMEText
364         except ImportError:
365                 from email.MIMEText import MIMEText
366
367         # color doesn't make any sense for mail
368         nocolor()
369
370         if "PORTAGE_ELOG_MAILURI" in glsaconfig:
371                 myrecipient = glsaconfig["PORTAGE_ELOG_MAILURI"].split()[0]
372         else:
373                 myrecipient = "root@localhost"
374
375         if "PORTAGE_ELOG_MAILFROM" in glsaconfig:
376                 myfrom = glsaconfig["PORTAGE_ELOG_MAILFROM"]
377         else:
378                 myfrom = "glsa-check"
379
380         mysubject = "[glsa-check] Summary for %s" % socket.getfqdn()
381
382         # need a file object for summarylist()
383         myfd = StringIO()
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())
388         myfd.close()
389
390         myattachments = []
391         for myid in glsalist:
392                 try:
393                         myglsa = Glsa(myid, glsaconfig)
394                 except (GlsaTypeException, GlsaFormatException) as e:
395                         if verbose:
396                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
397                         continue
398                 myfd = StringIO()
399                 myglsa.dump(outstream=myfd)
400                 myattachments.append(MIMEText(str(myfd.getvalue()), _charset="utf8"))
401                 myfd.close()
402
403         if glsalist or not quiet:
404                 mymessage = portage_mail.create_message(myfrom, myrecipient, mysubject, summary, myattachments)
405                 portage_mail.send_mail(glsaconfig, mymessage)
406
407         sys.exit(0)
408
409 # something wrong here, all valid paths are covered with sys.exit()
410 sys.stderr.write("nothing more to do\n")
411 sys.exit(2)