Handle unicode encoding when dumping to stdout and start migration to using StringIO
[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 *
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 checkfile")
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 *
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         fd1 = codecs.getwriter(encoding)(fd1)
146         fd2 = codecs.getwriter(encoding)(fd2)
147         fd2.write(white("[A]")+" means this GLSA was already applied,\n")
148         fd2.write(green("[U]")+" means the system is not affected and\n")
149         fd2.write(red("[N]")+" indicates that the system might be affected.\n\n")
150
151         myglsalist.sort()
152         for myid in myglsalist:
153                 try:
154                         myglsa = Glsa(myid, portage.settings, vardb, portdb)
155                 except (GlsaTypeException, GlsaFormatException) as e:
156                         if verbose:
157                                 fd2.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
158                         continue
159                 if myglsa.isApplied():
160                         status = "[A]"
161                         color = white
162                 elif myglsa.isVulnerable():
163                         status = "[N]"
164                         color = red
165                 else:
166                         status = "[U]"
167                         color = green
168
169                 if verbose:
170                         access = ("[%-8s] " % myglsa.access)
171                 else:
172                         access=""
173
174                 fd1.write(color(myglsa.nr) + " " + color(status) + " " + color(access) + myglsa.title + " (")
175                 if not verbose:
176                         for pkg in list(myglsa.packages)[:3]:
177                                 fd1.write(" " + pkg + " ")
178                         if len(myglsa.packages) > 3:
179                                 fd1.write("... ")
180                 else:
181                         for pkg in myglsa.packages:
182                                 mylist = vardb.match(pkg)
183                                 if len(mylist) > 0:
184                                         pkg = color(" ".join(mylist))
185                                 fd1.write(" " + pkg + " ")
186
187                 fd1.write(")")
188                 if list_cve:
189                         fd1.write(" "+(",".join([r[:13] for r in myglsa.references if r[:4] in ["CAN-", "CVE-"]])))
190                 fd1.write("\n")
191         return 0
192
193 if mode == "list":
194         sys.exit(summarylist(glsalist))
195
196 # dump, fix, inject and fix are nearly the same code, only the glsa method call differs
197 if mode in ["dump", "fix", "inject", "pretend"]:
198         for myid in glsalist:
199                 try:
200                         myglsa = Glsa(myid, portage.settings, vardb, portdb)
201                 except (GlsaTypeException, GlsaFormatException) as e:
202                         if verbose:
203                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
204                         continue
205                 if mode == "dump":
206                         myglsa.dump()
207                 elif mode == "fix":
208                         sys.stdout.write("fixing "+myid+"\n")
209                         mergelist = myglsa.getMergeList(least_change=least_change)
210                         for pkg in mergelist:
211                                 sys.stdout.write(">>> merging "+pkg+"\n")
212                                 # using emerge for the actual merging as it contains the dependency
213                                 # code and we want to be consistent in behaviour. Also this functionality
214                                 # will be integrated in emerge later, so it shouldn't hurt much.
215                                 emergecmd = "emerge --oneshot " + portage.settings["EMERGE_OPTS"] + " =" + pkg
216                                 if verbose:
217                                         sys.stderr.write(emergecmd+"\n")
218                                 exitcode = os.system(emergecmd)
219                                 # system() returns the exitcode in the high byte of a 16bit integer
220                                 if exitcode >= 1<<8:
221                                         exitcode >>= 8
222                                 if exitcode:
223                                         sys.exit(exitcode)
224                         myglsa.inject()
225                 elif mode == "pretend":
226                         sys.stdout.write("Checking GLSA "+myid+"\n")
227                         mergelist = myglsa.getMergeList(least_change=least_change)
228                         if mergelist:
229                                 sys.stdout.write("The following updates will be performed for this GLSA:\n")
230                                 for pkg in mergelist:
231                                         oldver = None
232                                         for x in vardb.match(portage.cpv_getkey(pkg)):
233                                                 if vardb._pkg_str(x, None).slot == portdb._pkg_str(pkg, None).slot:
234                                                         oldver = x
235                                         if oldver == None:
236                                                 raise ValueError("could not find old version for package %s" % pkg)
237                                         oldver = oldver[len(portage.cpv_getkey(oldver))+1:]
238                                         sys.stdout.write("     " + pkg + " (" + oldver + ")\n")
239                         else:
240                                 sys.stdout.write("Nothing to do for this GLSA\n")
241                 elif mode == "inject":
242                         sys.stdout.write("injecting " + myid + "\n")
243                         myglsa.inject()
244                 sys.stdout.write("\n")
245         sys.exit(0)
246
247 # test is a bit different as Glsa.test() produces no output
248 if mode == "test":
249         outputlist = []
250         for myid in glsalist:
251                 try:
252                         myglsa = Glsa(myid, portage.settings, vardb, portdb)
253                 except (GlsaTypeException, GlsaFormatException) as e:
254                         if verbose:
255                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
256                         continue
257                 if myglsa.isVulnerable():
258                         outputlist.append(str(myglsa.nr))
259         if len(outputlist) > 0:
260                 sys.stderr.write("This system is affected by the following GLSAs:\n")
261                 if verbose:
262                         summarylist(outputlist)
263                 else:
264                         sys.stdout.write("\n".join(outputlist)+"\n")
265         else:
266                 sys.stderr.write("This system is not affected by any of the listed GLSAs\n")
267         sys.exit(0)
268
269 # mail mode as requested by solar
270 if mode == "mail":
271         import portage.mail, socket
272         from io import StringIO
273         from email.mime.text import MIMEText
274
275         # color doesn't make any sense for mail
276         nocolor()
277
278         if "PORTAGE_ELOG_MAILURI" in portage.settings:
279                 myrecipient = portage.settings["PORTAGE_ELOG_MAILURI"].split()[0]
280         else:
281                 myrecipient = "root@localhost"
282
283         if "PORTAGE_ELOG_MAILFROM" in portage.settings:
284                 myfrom = portage.settings["PORTAGE_ELOG_MAILFROM"]
285         else:
286                 myfrom = "glsa-check"
287
288         mysubject = "[glsa-check] Summary for %s" % socket.getfqdn()
289
290         # need a file object for summarylist()
291         myfd = StringIO()
292         myfd.write("GLSA Summary report for host %s\n" % socket.getfqdn())
293         myfd.write("(Command was: %s)\n\n" % " ".join(sys.argv))
294         summarylist(glsalist, fd1=myfd, fd2=myfd)
295         summary = str(myfd.getvalue())
296         myfd.close()
297
298         myattachments = []
299         for myid in glsalist:
300                 try:
301                         myglsa = Glsa(myid, portage.settings, vardb, portdb)
302                 except (GlsaTypeException, GlsaFormatException) as e:
303                         if verbose:
304                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
305                         continue
306                 myfd = StringIO()
307                 myglsa.dump(outstream=myfd)
308                 myattachments.append(MIMEText(str(myfd.getvalue()), _charset="utf8"))
309                 myfd.close()
310
311         mymessage = portage.mail.create_message(myfrom, myrecipient, mysubject, summary, myattachments)
312         portage.mail.send_mail(portage.settings, mymessage)
313
314         sys.exit(0)
315
316 # something wrong here, all valid paths are covered with sys.exit()
317 sys.stderr.write("nothing more to do\n")
318 sys.exit(2)