egencache: portage.util._argparse
[portage.git] / bin / glsa-check
1 #!/usr/bin/python
2 # Copyright 2008-2013 Gentoo Foundation
3 # Distributed under the terms of the GNU General Public License v2
4
5 from __future__ import print_function
6
7 import sys
8 import codecs
9
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)
13 import portage
14 portage._internal_caller = True
15 from portage import os
16 from portage.output import green, red, nocolor, white
17
18 from optparse import OptionGroup, OptionParser
19
20 __program__ = "glsa-check"
21 __author__ = "Marius Mauch <genone@gentoo.org>"
22 __version__ = "1.0"
23
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")
29         sys.exit(0)
30
31 # option parsing
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'"
37
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)
63
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")
76
77 options, params = parser.parse_args()
78
79 mode = options.mode
80 least_change = options.least_change
81 list_cve = options.list_cve
82 verbose = options.verbose
83
84 # Sanity checking
85 if mode is None:
86         sys.stderr.write("No mode given: what should I do?\n")
87         parser.print_help()
88         sys.exit(1)
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")
93         parser.print_help()
94         sys.exit(1)
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")
98         sys.exit(2)
99 elif mode == "list" and not params:
100         params.append("new")
101
102 # delay this for speed increase
103 from portage.glsa import (Glsa, GlsaTypeException, GlsaFormatException,
104         get_applied_glsas, get_glsa_list)
105
106 eroot = portage.settings['EROOT']
107 vardb = portage.db[eroot]["vartree"].dbapi
108 portdb = portage.db[eroot]["porttree"].dbapi
109
110 # build glsa lists
111 completelist = get_glsa_list(portage.settings)
112
113 checklist = get_applied_glsas(portage.settings)
114 todolist = [e for e in completelist if e not in checklist]
115
116 glsalist = []
117 if "new" in params:
118         glsalist = todolist
119         params.remove("new")
120
121 if "all" in params:
122         glsalist = completelist
123         params.remove("all")
124 if "affected" in params:
125         # replaced completelist with todolist on request of wschlich
126         for x in todolist:
127                 try:
128                         myglsa = Glsa(x, portage.settings, vardb, portdb)
129                 except (GlsaTypeException, GlsaFormatException) as e:
130                         if verbose:
131                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (x, e)))
132                         continue
133                 if myglsa.isVulnerable():
134                         glsalist.append(x)
135         params.remove("affected")
136
137 # remove invalid parameters
138 for p in params[:]:
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))
141                 params.remove(p)
142
143 glsalist.extend([g for g in params if g not in glsalist])
144
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"):
150                 fd1 = fd1.buffer
151         if hasattr(fd2, "buffer"):
152                 fd2 = 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")
158
159         myglsalist.sort()
160         for myid in myglsalist:
161                 try:
162                         myglsa = Glsa(myid, portage.settings, vardb, portdb)
163                 except (GlsaTypeException, GlsaFormatException) as e:
164                         if verbose:
165                                 fd2.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
166                         continue
167                 if myglsa.isInjected():
168                         status = "[A]"
169                         color = white
170                 elif myglsa.isVulnerable():
171                         status = "[N]"
172                         color = red
173                 else:
174                         status = "[U]"
175                         color = green
176
177                 if verbose:
178                         access = ("[%-8s] " % myglsa.access)
179                 else:
180                         access=""
181
182                 fd1.write(color(myglsa.nr) + " " + color(status) + " " + color(access) + myglsa.title + " (")
183                 if not verbose:
184                         for pkg in list(myglsa.packages)[:3]:
185                                 fd1.write(" " + pkg + " ")
186                         if len(myglsa.packages) > 3:
187                                 fd1.write("... ")
188                 else:
189                         for pkg in myglsa.packages:
190                                 mylist = vardb.match(pkg)
191                                 if len(mylist) > 0:
192                                         pkg = color(" ".join(mylist))
193                                 fd1.write(" " + pkg + " ")
194
195                 fd1.write(")")
196                 if list_cve:
197                         fd1.write(" "+(",".join([r[:13] for r in myglsa.references if r[:4] in ["CAN-", "CVE-"]])))
198                 fd1.write("\n")
199         return 0
200
201 if mode == "list":
202         sys.exit(summarylist(glsalist))
203
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:
207                 try:
208                         myglsa = Glsa(myid, portage.settings, vardb, portdb)
209                 except (GlsaTypeException, GlsaFormatException) as e:
210                         if verbose:
211                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
212                         continue
213                 if mode == "dump":
214                         myglsa.dump()
215                 elif mode == "fix":
216                         sys.stdout.write("Fixing GLSA "+myid+"\n")
217                         if not myglsa.isVulnerable():
218                                 sys.stdout.write(">>> no vulnerable packages installed\n")
219                         else:
220                                 mergelist = myglsa.getMergeList(least_change=least_change)
221                                 if mergelist == []:
222                                         sys.stdout.write(">>> cannot fix GLSA, no unaffected packages available\n")
223                                         sys.exit(2)
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
230                                         if verbose:
231                                                 sys.stderr.write(emergecmd+"\n")
232                                         exitcode = os.system(emergecmd)
233                                         # system() returns the exitcode in the high byte of a 16bit integer
234                                         if exitcode >= 1<<8:
235                                                 exitcode >>= 8
236                                         if exitcode:
237                                                 sys.exit(exitcode)
238                         if len(mergelist):
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")
244                         else:
245                                 mergedict = {}
246                                 for (vuln, update) in myglsa.getAffectionTable(least_change=least_change):
247                                         mergedict.setdefault(update, []).append(vuln)
248                                 
249                                 sys.stdout.write(">>> The following updates will be performed for this GLSA:\n")
250                                 for pkg in mergedict:
251                                         if pkg != "":
252                                                 sys.stdout.write("     " + pkg + " (vulnerable: " + ", ".join(mergedict[pkg]) + ")\n")
253                                 if "" in mergedict:
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")
258                         myglsa.inject()
259                 sys.stdout.write("\n")
260         sys.exit(0)
261
262 # test is a bit different as Glsa.test() produces no output
263 if mode == "test":
264         outputlist = []
265         for myid in glsalist:
266                 try:
267                         myglsa = Glsa(myid, portage.settings, vardb, portdb)
268                 except (GlsaTypeException, GlsaFormatException) as e:
269                         if verbose:
270                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
271                         continue
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")
276                 if verbose:
277                         summarylist(outputlist)
278                 else:
279                         sys.stdout.write("\n".join(outputlist)+"\n")
280         else:
281                 sys.stderr.write("This system is not affected by any of the listed GLSAs\n")
282         sys.exit(0)
283
284 # mail mode as requested by solar
285 if mode == "mail":
286         import portage.mail, socket
287         from io import BytesIO
288         from email.mime.text import MIMEText
289
290         # color doesn't make any sense for mail
291         nocolor()
292
293         if "PORTAGE_ELOG_MAILURI" in portage.settings:
294                 myrecipient = portage.settings["PORTAGE_ELOG_MAILURI"].split()[0]
295         else:
296                 myrecipient = "root@localhost"
297
298         if "PORTAGE_ELOG_MAILFROM" in portage.settings:
299                 myfrom = portage.settings["PORTAGE_ELOG_MAILFROM"]
300         else:
301                 myfrom = "glsa-check"
302
303         mysubject = "[glsa-check] Summary for %s" % socket.getfqdn()
304
305         # need a file object for summarylist()
306         myfd = BytesIO()
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")
313         myfd.close()
314
315         myattachments = []
316         for myid in glsalist:
317                 try:
318                         myglsa = Glsa(myid, portage.settings, vardb, portdb)
319                 except (GlsaTypeException, GlsaFormatException) as e:
320                         if verbose:
321                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
322                         continue
323                 myfd = BytesIO()
324                 myglsa.dump(outstream=myfd)
325                 attachment = myfd.getvalue().decode("utf-8")
326                 myattachments.append(MIMEText(attachment, _charset="utf8"))
327                 myfd.close()
328
329         mymessage = portage.mail.create_message(myfrom, myrecipient, mysubject, summary, myattachments)
330         portage.mail.send_mail(portage.settings, mymessage)
331
332         sys.exit(0)
333
334 # something wrong here, all valid paths are covered with sys.exit()
335 sys.stderr.write("nothing more to do\n")
336 sys.exit(2)