7fa3688aaaedde7a6e20fc46b777e40de4cd5918
[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 from portage.util._argparse import ArgumentParser
18
19 __program__ = "glsa-check"
20 __author__ = "Marius Mauch <genone@gentoo.org>"
21 __version__ = "1.0"
22
23 # option parsing
24 epilog = "glsa-list can contain an arbitrary number of GLSA ids," \
25                 " filenames containing GLSAs or the special identifiers" \
26                 " 'all', 'new' and 'affected'"
27 parser = ArgumentParser(usage=__program__ + " <option> [glsa-list]",
28         epilog=epilog)
29
30 modes = parser.add_argument_group("Modes")
31 modes.add_argument("-l", "--list", action="store_const",
32                 const="list", dest="mode",
33                 help="List all unapplied GLSA")
34 modes.add_argument("-d", "--dump", action="store_const",
35                 const="dump", dest="mode",
36                 help="Show all information about the given GLSA")
37 modes.add_argument("--print", action="store_const",
38                 const="dump", dest="mode",
39                 help="Alias for --dump")
40 modes.add_argument("-t", "--test", action="store_const",
41                 const="test", dest="mode",
42                 help="Test if this system is affected by the given GLSA")
43 modes.add_argument("-p", "--pretend", action="store_const",
44                 const="pretend", dest="mode",
45                 help="Show the necessary commands to apply this GLSA")
46 modes.add_argument("-f", "--fix", action="store_const",
47                 const="fix", dest="mode",
48                 help="Try to auto-apply this GLSA (experimental)")
49 modes.add_argument("-i", "--inject", action="store_const",
50                 const="inject", dest="mode",
51                 help="inject the given GLSA into the glsa_injected file")
52 modes.add_argument("-m", "--mail", action="store_const",
53                 const="mail", dest="mode",
54                 help="Send a mail with the given GLSAs to the administrator")
55
56 parser.add_argument("-V", "--version", action="store_true",
57                 help="Some information about this tool")
58 parser.add_argument("-v", "--verbose", action="store_true", dest="verbose",
59                 help="Print more information")
60 parser.add_argument("-n", "--nocolor", action="store_true",
61                 help="Disable colors")
62 parser.add_argument("-e", "--emergelike", action="store_false", dest="least_change",
63                 help="Do not use a least-change algorithm")
64 parser.add_argument("-c", "--cve", action="store_true", dest="list_cve",
65                 help="Show CAN ids in listing mode")
66
67 options, params = parser.parse_known_args()
68
69 if options.nocolor:
70         nocolor()
71
72 if options.version:
73         sys.stderr.write("\n"+ __program__ + ", version " + __version__ + "\n")
74         sys.stderr.write("Author: " + __author__ + "\n")
75         sys.stderr.write("This program is licensed under the GPL, version 2\n\n")
76         sys.exit(0)
77
78 mode = options.mode
79 least_change = options.least_change
80 list_cve = options.list_cve
81 verbose = options.verbose
82
83 # Sanity checking
84 if mode is None:
85         sys.stderr.write("No mode given: what should I do?\n")
86         parser.print_help()
87         sys.exit(1)
88 elif mode != "list" and not params:
89         sys.stderr.write("\nno GLSA given, so we'll do nothing for now. \n")
90         sys.stderr.write("If you want to run on all GLSA please tell me so \n")
91         sys.stderr.write("(specify \"all\" as parameter)\n\n")
92         parser.print_help()
93         sys.exit(1)
94 elif mode in ["fix", "inject"] and os.geteuid() != 0:
95         # we need root privileges for write access
96         sys.stderr.write("\nThis tool needs root access to "+options.mode+" this GLSA\n\n")
97         sys.exit(2)
98 elif mode == "list" and not params:
99         params.append("new")
100
101 # delay this for speed increase
102 from portage.glsa import (Glsa, GlsaTypeException, GlsaFormatException,
103         get_applied_glsas, get_glsa_list)
104
105 eroot = portage.settings['EROOT']
106 vardb = portage.db[eroot]["vartree"].dbapi
107 portdb = portage.db[eroot]["porttree"].dbapi
108
109 # build glsa lists
110 completelist = get_glsa_list(portage.settings)
111
112 checklist = get_applied_glsas(portage.settings)
113 todolist = [e for e in completelist if e not in checklist]
114
115 glsalist = []
116 if "new" in params:
117         glsalist = todolist
118         params.remove("new")
119
120 if "all" in params:
121         glsalist = completelist
122         params.remove("all")
123 if "affected" in params:
124         # replaced completelist with todolist on request of wschlich
125         for x in todolist:
126                 try:
127                         myglsa = Glsa(x, portage.settings, vardb, portdb)
128                 except (GlsaTypeException, GlsaFormatException) as e:
129                         if verbose:
130                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (x, e)))
131                         continue
132                 if myglsa.isVulnerable():
133                         glsalist.append(x)
134         params.remove("affected")
135
136 # remove invalid parameters
137 for p in params[:]:
138         if not (p in completelist or os.path.exists(p)):
139                 sys.stderr.write(("(removing %s from parameter list as it isn't a valid GLSA specification)\n" % p))
140                 params.remove(p)
141
142 glsalist.extend([g for g in params if g not in glsalist])
143
144 def summarylist(myglsalist, fd1=sys.stdout, fd2=sys.stderr, encoding="utf-8"):
145         # Get to the raw streams in py3k before wrapping them with an encoded writer
146         # to avoid writing bytes to a text stream (stdout/stderr are text streams
147         # by default in py3k)
148         if hasattr(fd1, "buffer"):
149                 fd1 = fd1.buffer
150         if hasattr(fd2, "buffer"):
151                 fd2 = fd2.buffer
152         fd1 = codecs.getwriter(encoding)(fd1)
153         fd2 = codecs.getwriter(encoding)(fd2)
154         fd2.write(white("[A]")+" means this GLSA was marked as applied (injected),\n")
155         fd2.write(green("[U]")+" means the system is not affected and\n")
156         fd2.write(red("[N]")+" indicates that the system might be affected.\n\n")
157
158         myglsalist.sort()
159         for myid in myglsalist:
160                 try:
161                         myglsa = Glsa(myid, portage.settings, vardb, portdb)
162                 except (GlsaTypeException, GlsaFormatException) as e:
163                         if verbose:
164                                 fd2.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
165                         continue
166                 if myglsa.isInjected():
167                         status = "[A]"
168                         color = white
169                 elif myglsa.isVulnerable():
170                         status = "[N]"
171                         color = red
172                 else:
173                         status = "[U]"
174                         color = green
175
176                 if verbose:
177                         access = ("[%-8s] " % myglsa.access)
178                 else:
179                         access=""
180
181                 fd1.write(color(myglsa.nr) + " " + color(status) + " " + color(access) + myglsa.title + " (")
182                 if not verbose:
183                         for pkg in list(myglsa.packages)[:3]:
184                                 fd1.write(" " + pkg + " ")
185                         if len(myglsa.packages) > 3:
186                                 fd1.write("... ")
187                 else:
188                         for pkg in myglsa.packages:
189                                 mylist = vardb.match(pkg)
190                                 if len(mylist) > 0:
191                                         pkg = color(" ".join(mylist))
192                                 fd1.write(" " + pkg + " ")
193
194                 fd1.write(")")
195                 if list_cve:
196                         fd1.write(" "+(",".join([r[:13] for r in myglsa.references if r[:4] in ["CAN-", "CVE-"]])))
197                 fd1.write("\n")
198         return 0
199
200 if mode == "list":
201         sys.exit(summarylist(glsalist))
202
203 # dump, fix, inject and fix are nearly the same code, only the glsa method call differs
204 if mode in ["dump", "fix", "inject", "pretend"]:
205         for myid in glsalist:
206                 try:
207                         myglsa = Glsa(myid, portage.settings, vardb, portdb)
208                 except (GlsaTypeException, GlsaFormatException) as e:
209                         if verbose:
210                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
211                         continue
212                 if mode == "dump":
213                         myglsa.dump()
214                 elif mode == "fix":
215                         sys.stdout.write("Fixing GLSA "+myid+"\n")
216                         if not myglsa.isVulnerable():
217                                 sys.stdout.write(">>> no vulnerable packages installed\n")
218                         else:
219                                 mergelist = myglsa.getMergeList(least_change=least_change)
220                                 if mergelist == []:
221                                         sys.stdout.write(">>> cannot fix GLSA, no unaffected packages available\n")
222                                         sys.exit(2)
223                                 for pkg in mergelist:
224                                         sys.stdout.write(">>> merging "+pkg+"\n")
225                                         # using emerge for the actual merging as it contains the dependency
226                                         # code and we want to be consistent in behaviour. Also this functionality
227                                         # will be integrated in emerge later, so it shouldn't hurt much.
228                                         emergecmd = "emerge --oneshot " + " =" + pkg
229                                         if verbose:
230                                                 sys.stderr.write(emergecmd+"\n")
231                                         exitcode = os.system(emergecmd)
232                                         # system() returns the exitcode in the high byte of a 16bit integer
233                                         if exitcode >= 1<<8:
234                                                 exitcode >>= 8
235                                         if exitcode:
236                                                 sys.exit(exitcode)
237                         if len(mergelist):
238                                 sys.stdout.write("\n")
239                 elif mode == "pretend":
240                         sys.stdout.write("Checking GLSA "+myid+"\n")
241                         if not myglsa.isVulnerable():
242                                 sys.stdout.write(">>> no vulnerable packages installed\n")
243                         else:
244                                 mergedict = {}
245                                 for (vuln, update) in myglsa.getAffectionTable(least_change=least_change):
246                                         mergedict.setdefault(update, []).append(vuln)
247                                 
248                                 sys.stdout.write(">>> The following updates will be performed for this GLSA:\n")
249                                 for pkg in mergedict:
250                                         if pkg != "":
251                                                 sys.stdout.write("     " + pkg + " (vulnerable: " + ", ".join(mergedict[pkg]) + ")\n")
252                                 if "" in mergedict:
253                                         sys.stdout.write("\n>>> For the following packages, no upgrade path exists:\n")
254                                         sys.stdout.write("     " + ", ".join(mergedict[""]))
255                 elif mode == "inject":
256                         sys.stdout.write("injecting " + myid + "\n")
257                         myglsa.inject()
258                 sys.stdout.write("\n")
259         sys.exit(0)
260
261 # test is a bit different as Glsa.test() produces no output
262 if mode == "test":
263         outputlist = []
264         for myid in glsalist:
265                 try:
266                         myglsa = Glsa(myid, portage.settings, vardb, portdb)
267                 except (GlsaTypeException, GlsaFormatException) as e:
268                         if verbose:
269                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
270                         continue
271                 if myglsa.isVulnerable():
272                         outputlist.append(str(myglsa.nr))
273         if len(outputlist) > 0:
274                 sys.stderr.write("This system is affected by the following GLSAs:\n")
275                 if verbose:
276                         summarylist(outputlist)
277                 else:
278                         sys.stdout.write("\n".join(outputlist)+"\n")
279         else:
280                 sys.stderr.write("This system is not affected by any of the listed GLSAs\n")
281         sys.exit(0)
282
283 # mail mode as requested by solar
284 if mode == "mail":
285         import portage.mail, socket
286         from io import BytesIO
287         from email.mime.text import MIMEText
288
289         # color doesn't make any sense for mail
290         nocolor()
291
292         if "PORTAGE_ELOG_MAILURI" in portage.settings:
293                 myrecipient = portage.settings["PORTAGE_ELOG_MAILURI"].split()[0]
294         else:
295                 myrecipient = "root@localhost"
296
297         if "PORTAGE_ELOG_MAILFROM" in portage.settings:
298                 myfrom = portage.settings["PORTAGE_ELOG_MAILFROM"]
299         else:
300                 myfrom = "glsa-check"
301
302         mysubject = "[glsa-check] Summary for %s" % socket.getfqdn()
303
304         # need a file object for summarylist()
305         myfd = BytesIO()
306         line = "GLSA Summary report for host %s\n" % socket.getfqdn()
307         myfd.write(line.encode("utf-8"))
308         line = "(Command was: %s)\n\n" % " ".join(sys.argv)
309         myfd.write(line.encode("utf-8"))
310         summarylist(glsalist, fd1=myfd, fd2=myfd)
311         summary = myfd.getvalue().decode("utf-8")
312         myfd.close()
313
314         myattachments = []
315         for myid in glsalist:
316                 try:
317                         myglsa = Glsa(myid, portage.settings, vardb, portdb)
318                 except (GlsaTypeException, GlsaFormatException) as e:
319                         if verbose:
320                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
321                         continue
322                 myfd = BytesIO()
323                 myglsa.dump(outstream=myfd)
324                 attachment = myfd.getvalue().decode("utf-8")
325                 myattachments.append(MIMEText(attachment, _charset="utf8"))
326                 myfd.close()
327
328         mymessage = portage.mail.create_message(myfrom, myrecipient, mysubject, summary, myattachments)
329         portage.mail.send_mail(portage.settings, mymessage)
330
331         sys.exit(0)
332
333 # something wrong here, all valid paths are covered with sys.exit()
334 sys.stderr.write("nothing more to do\n")
335 sys.exit(2)