Restructure system affection detection.
[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 GLSA "+myid+"\n")
209                         if not myglsa.isVulnerable():
210                                 sys.stdout.write(">>> no vulnerable packages installed\n")
211                         else:
212                                 mergelist = myglsa.getMergeList(least_change=least_change)
213                                 if mergelist == []:
214                                         sys.stdout.write(">>> cannot fix GLSA, no unaffected packages available\n")
215                                         sys.exit(2)
216                                 for pkg in mergelist:
217                                         sys.stdout.write(">>> merging "+pkg+"\n")
218                                         # using emerge for the actual merging as it contains the dependency
219                                         # code and we want to be consistent in behaviour. Also this functionality
220                                         # will be integrated in emerge later, so it shouldn't hurt much.
221                                         emergecmd = "emerge --oneshot " + glsaconfig["EMERGE_OPTS"] + " =" + pkg
222                                         if verbose:
223                                                 sys.stderr.write(emergecmd+"\n")
224                                         exitcode = os.system(emergecmd)
225                                         # system() returns the exitcode in the high byte of a 16bit integer
226                                         if exitcode >= 1<<8:
227                                                 exitcode >>= 8
228                                         if exitcode:
229                                                 sys.exit(exitcode)
230                         if len(mergelist):
231                                 sys.stdout.write("\n")
232                         myglsa.inject()
233                 elif mode == "pretend":
234                         sys.stdout.write("Checking GLSA "+myid+"\n")
235                         if not myglsa.isVulnerable():
236                                 sys.stdout.write(">>> no vulnerable packages installed\n")
237                         else:
238                                 mergedict = {}
239                                 for (vuln, update) in myglsa.getAffectionTable(least_change=least_change):
240                                         mergedict.setdefault(update, []).append(vuln)
241                                 
242                                 sys.stdout.write(">>> The following updates will be performed for this GLSA:\n")
243                                 for pkg in mergedict:
244                                         if pkg != "":
245                                                 sys.stdout.write("     " + pkg + " (vulnerable: " + ", ".join(mergedict[pkg]) + ")\n")
246                                 if "" in mergedict:
247                                         sys.stdout.write("\n>>> For the following packages, no upgrade path exists:\n")
248                                         sys.stdout.write("     " + ", ".join(mergedict[""]))
249                 elif mode == "inject":
250                         sys.stdout.write("injecting " + myid + "\n")
251                         myglsa.inject()
252                 sys.stdout.write("\n")
253         sys.exit(0)
254
255 # test is a bit different as Glsa.test() produces no output
256 if mode == "test":
257         outputlist = []
258         for myid in glsalist:
259                 try:
260                         myglsa = Glsa(myid, portage.settings, vardb, portdb)
261                 except (GlsaTypeException, GlsaFormatException) as e:
262                         if verbose:
263                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
264                         continue
265                 if myglsa.isVulnerable():
266                         outputlist.append(str(myglsa.nr))
267         if len(outputlist) > 0:
268                 sys.stderr.write("This system is affected by the following GLSAs:\n")
269                 if verbose:
270                         summarylist(outputlist)
271                 else:
272                         sys.stdout.write("\n".join(outputlist)+"\n")
273         else:
274                 sys.stderr.write("This system is not affected by any of the listed GLSAs\n")
275         sys.exit(0)
276
277 # mail mode as requested by solar
278 if mode == "mail":
279         import portage.mail, socket
280         from io import StringIO
281         from email.mime.text import MIMEText
282
283         # color doesn't make any sense for mail
284         nocolor()
285
286         if "PORTAGE_ELOG_MAILURI" in portage.settings:
287                 myrecipient = portage.settings["PORTAGE_ELOG_MAILURI"].split()[0]
288         else:
289                 myrecipient = "root@localhost"
290
291         if "PORTAGE_ELOG_MAILFROM" in portage.settings:
292                 myfrom = portage.settings["PORTAGE_ELOG_MAILFROM"]
293         else:
294                 myfrom = "glsa-check"
295
296         mysubject = "[glsa-check] Summary for %s" % socket.getfqdn()
297
298         # need a file object for summarylist()
299         myfd = StringIO()
300         myfd.write("GLSA Summary report for host %s\n" % socket.getfqdn())
301         myfd.write("(Command was: %s)\n\n" % " ".join(sys.argv))
302         summarylist(glsalist, fd1=myfd, fd2=myfd)
303         summary = str(myfd.getvalue())
304         myfd.close()
305
306         myattachments = []
307         for myid in glsalist:
308                 try:
309                         myglsa = Glsa(myid, portage.settings, vardb, portdb)
310                 except (GlsaTypeException, GlsaFormatException) as e:
311                         if verbose:
312                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
313                         continue
314                 myfd = StringIO()
315                 myglsa.dump(outstream=myfd)
316                 myattachments.append(MIMEText(str(myfd.getvalue()), _charset="utf8"))
317                 myfd.close()
318
319         mymessage = portage.mail.create_message(myfrom, myrecipient, mysubject, summary, myattachments)
320         portage.mail.send_mail(portage.settings, mymessage)
321
322         sys.exit(0)
323
324 # something wrong here, all valid paths are covered with sys.exit()
325 sys.stderr.write("nothing more to do\n")
326 sys.exit(2)