Update to most recent versions and add changes from djanderson
[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 os
7 import sys
8 try:
9         import portage
10 except ImportError:
11         sys.path.insert(0, "/usr/lib/portage/pym")
12         import portage
13
14 try:
15         from portage.output import *
16 except ImportError:
17         from output import *
18
19 from getopt import getopt, GetoptError
20
21 __program__ = "glsa-check"
22 __author__ = "Marius Mauch <genone@gentoo.org>"
23 __version__ = open("/etc/gentoolkit-version").read().strip()
24
25 optionmap = [
26 ["-l", "--list", "list all unapplied GLSA"],
27 ["-d", "--dump", "--print", "show all information about the given GLSA"],
28 ["-t", "--test", "test if this system is affected by the given GLSA"],
29 ["-p", "--pretend", "show the necessary commands to apply this GLSA"],
30 ["-f", "--fix", "try to auto-apply this GLSA (experimental)"],
31 ["-i", "--inject", "inject the given GLSA into the checkfile"],
32 ["-n", "--nocolor", "disable colors (option)"],
33 ["-e", "--emergelike", "do not use a least-change algorithm (option)"],
34 ["-h", "--help", "show this help message"],
35 ["-V", "--version", "some information about this tool"],
36 ["-v", "--verbose", "print more information (option)"],
37 ["-c", "--cve", "show CAN ids in listing mode (option)"],
38 ["-m", "--mail", "send a mail with the given GLSAs to the administrator"]
39 ]
40
41 # print a warning as this is beta code (but proven by now, so no more warning)
42 #sys.stderr.write("WARNING: This tool is completely new and not very tested, so it should not be\n")
43 #sys.stderr.write("used on production systems. It's mainly a test tool for the new GLSA release\n")
44 #sys.stderr.write("and distribution system, it's functionality will later be merged into emerge\n")
45 #sys.stderr.write("and equery.\n")
46 #sys.stderr.write("Please read http://www.gentoo.org/proj/en/portage/glsa-integration.xml\n")
47 #sys.stderr.write("before using this tool AND before reporting a bug.\n\n")
48
49 # option parsing
50 args = []
51 params = []
52 try:
53         args, params = getopt(sys.argv[1:], "".join([o[0][1] for o in optionmap]), \
54                 [x[2:] for x in reduce(lambda x,y: x+y, [z[1:-1] for z in optionmap])])
55 #               ["dump", "print", "list", "pretend", "fix", "inject", "help", "verbose", "version", "test", "nocolor", "cve", "mail"])
56         args = [a for a,b in args]
57         
58         for option in ["--nocolor", "-n"]:
59                 if option in args:
60                         nocolor()
61                         args.remove(option)
62                         
63         verbose = False
64         for option in ["--verbose", "-v"]:
65                 if option in args:
66                         verbose = True
67                         args.remove(option)
68
69         list_cve = False
70         for option in ["--cve", "-c"]:
71                 if option in args:
72                         list_cve = True
73                         args.remove(option)
74         
75         least_change = True
76         for option in ["--emergelike", "-e"]:
77                 if option in args:
78                         least_change = False
79                         args.remove(option)
80
81         # sanity checking
82         if len(args) <= 0:
83                 sys.stderr.write("no option given: what should I do ?\n")
84                 mode = "HELP"
85         elif len(args) > 1:
86                 sys.stderr.write("please use only one command per call\n")
87                 mode = "HELP"
88         else:
89                 # in what mode are we ?
90                 args = args[0]
91                 for m in optionmap:
92                         if args in [o for o in m[:-1]]:
93                                 mode = m[1][2:]
94
95 except GetoptError, e:
96         sys.stderr.write("unknown option given: ")
97         sys.stderr.write(str(e)+"\n")
98         mode = "HELP"
99
100 # we need a set of glsa for most operation modes
101 if len(params) <= 0 and mode in ["fix", "test", "pretend", "dump", "inject", "mail"]:
102         sys.stderr.write("\nno GLSA given, so we'll do nothing for now. \n")
103         sys.stderr.write("If you want to run on all GLSA please tell me so \n")
104         sys.stderr.write("(specify \"all\" as parameter)\n\n")
105         mode = "HELP"
106 elif len(params) <= 0 and mode == "list":
107         params.append("new")
108         
109 # show help message
110 if mode == "help" or mode == "HELP":
111         msg = "Syntax: glsa-check <option> [glsa-list]\n\n"
112         for m in optionmap:
113                 msg += m[0] + "\t" + m[1] + "   \t: " + m[-1] + "\n"
114                 for o in m[2:-1]:
115                         msg += "\t" + o + "\n"
116         msg += "\nglsa-list can contain an arbitrary number of GLSA ids, \n"
117         msg += "filenames containing GLSAs or the special identifiers \n"
118         msg += "'all', 'new' and 'affected'\n"
119         if mode == "help":
120                 sys.stdout.write(msg)
121                 sys.exit(0)
122         else:
123                 sys.stderr.write("\n" + msg)
124                 sys.exit(1)
125
126 # we need root priviledges for write access
127 if mode in ["fix", "inject"] and os.geteuid() != 0:
128         sys.stderr.write(__program__ + ": root access is needed for \""+mode+"\" mode\n")
129         sys.exit(2)
130
131 # show version and copyright information
132 if mode == "version":
133         sys.stderr.write("%(program)s (%(version)s)\n" % {
134                 "program": __program__,
135                 "version": __version__
136         })
137         sys.stderr.write("Author: %s\n" % __author__)
138         sys.stderr.write("This program is licensed under the GPL, version 2\n")
139         sys.exit(0)
140
141 # delay this for speed increase
142 from gentoolkit.glsa import *
143
144 glsaconfig = checkconfig(portage.config(clone=portage.settings))
145
146 vardb = portage.db["/"]["vartree"].dbapi
147 portdb = portage.db["/"]["porttree"].dbapi
148
149 # Check that we really have a glsa dir to work on
150 if not (os.path.exists(glsaconfig["GLSA_DIR"]) and os.path.isdir(glsaconfig["GLSA_DIR"])):
151         sys.stderr.write(red("ERROR")+": GLSA_DIR %s doesn't exist. Please fix this.\n" % glsaconfig["GLSA_DIR"])
152         sys.exit(1)
153
154 # build glsa lists
155 completelist = get_glsa_list(glsaconfig["GLSA_DIR"], glsaconfig)
156
157 if os.access(glsaconfig["CHECKFILE"], os.R_OK):
158         checklist = [line.strip() for line in open(glsaconfig["CHECKFILE"], "r").readlines()]
159 else:
160         checklist = []
161 todolist = [e for e in completelist if e not in checklist]
162
163 glsalist = []
164 if "new" in params:
165         glsalist = todolist
166         params.remove("new")
167         
168 if "all" in params:
169         glsalist = completelist
170         params.remove("all")
171 if "affected" in params:
172         # replaced completelist with todolist on request of wschlich
173         for x in todolist:
174                 try:
175                         myglsa = Glsa(x, glsaconfig)
176                 except (GlsaTypeException, GlsaFormatException), e:
177                         if verbose:
178                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (x, e)))
179                         continue
180                 if myglsa.isVulnerable():
181                         glsalist.append(x)
182         params.remove("affected")
183
184 # remove invalid parameters
185 for p in params[:]:
186         if not (p in completelist or os.path.exists(p)):
187                 sys.stderr.write(("(removing %s from parameter list as it isn't a valid GLSA specification)\n" % p))
188                 params.remove(p)
189
190 glsalist.extend([g for g in params if g not in glsalist])
191
192 def summarylist(myglsalist, fd1=sys.stdout, fd2=sys.stderr):
193         fd2.write(white("[A]")+" means this GLSA was already applied,\n")
194         fd2.write(green("[U]")+" means the system is not affected and\n")
195         fd2.write(red("[N]")+" indicates that the system might be affected.\n\n")
196
197         for myid in myglsalist:
198                 try:
199                         myglsa = Glsa(myid, glsaconfig)
200                 except (GlsaTypeException, GlsaFormatException), e:
201                         if verbose:
202                                 fd2.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
203                         continue
204                 if myglsa.isApplied():
205                         status = "[A]"
206                         color = white
207                 elif myglsa.isVulnerable():
208                         status = "[N]"
209                         color = red
210                 else:
211                         status = "[U]"
212                         color = green
213
214                 if verbose:
215                         access = ("[%-8s] " % myglsa.access)
216                 else:
217                         access=""
218
219                 fd1.write(color(myglsa.nr) + " " + color(status) + " " + color(access) + myglsa.title + " (")
220                 if not verbose:
221                         for pkg in myglsa.packages.keys()[:3]:
222                                 fd1.write(" " + pkg + " ")
223                         if len(myglsa.packages) > 3:
224                                 fd1.write("... ")
225                 else:
226                         for pkg in myglsa.packages.keys():
227                                 mylist = vardb.match(portage.dep_getkey(str(pkg)))
228                                 if len(mylist) > 0:
229                                         pkg = color(" ".join(mylist))
230                                 fd1.write(" " + pkg + " ")
231
232                 fd1.write(")")
233                 if list_cve:
234                         fd1.write(" "+(",".join([r[:13] for r in myglsa.references if r[:4] in ["CAN-", "CVE-"]])))
235                 fd1.write("\n")
236         return 0
237
238 if mode == "list":
239         sys.exit(summarylist(glsalist))
240
241 # dump, fix, inject and fix are nearly the same code, only the glsa method call differs
242 if mode in ["dump", "fix", "inject", "pretend"]:
243         for myid in glsalist:
244                 try:
245                         myglsa = Glsa(myid, glsaconfig)
246                 except (GlsaTypeException, GlsaFormatException), e:
247                         if verbose:
248                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
249                         continue
250                 if mode == "dump":
251                         myglsa.dump()
252                 elif mode == "fix":
253                         sys.stdout.write("fixing "+myid+"\n")
254                         mergelist = myglsa.getMergeList(least_change=least_change)
255                         for pkg in mergelist:
256                                 sys.stdout.write(">>> merging "+pkg+"\n")
257                                 # using emerge for the actual merging as it contains the dependency
258                                 # code and we want to be consistent in behaviour. Also this functionality
259                                 # will be integrated in emerge later, so it shouldn't hurt much.
260                                 emergecmd = "emerge --oneshot " + glsaconfig["EMERGE_OPTS"] + " =" + pkg
261                                 if verbose:
262                                         sys.stderr.write(emergecmd+"\n")
263                                 exitcode = os.system(emergecmd)
264                                 # system() returns the exitcode in the high byte of a 16bit integer
265                                 if exitcode >= 1<<8:
266                                         exitcode >>= 8
267                                 if exitcode:
268                                         sys.exit(exitcode)
269                         if len(mergelist):
270                                 sys.stdout.write("\n")
271                         myglsa.inject()
272                 elif mode == "pretend":
273                         sys.stdout.write("Checking GLSA "+myid+"\n")
274                         mergelist = myglsa.getMergeList(least_change=least_change)
275                         if mergelist:
276                                 sys.stdout.write("The following updates will be performed for this GLSA:\n")
277                                 for pkg in mergelist:
278                                         oldver = None
279                                         for x in vardb.match(portage.dep_getkey(pkg)):
280                                                 if vardb.aux_get(x, ["SLOT"]) == portdb.aux_get(pkg, ["SLOT"]):
281                                                         oldver = x
282                                         if oldver == None:
283                                                 raise ValueError("could not find old version for package %s" % pkg)
284                                         oldver = oldver[len(portage.dep_getkey(oldver))+1:]
285                                         sys.stdout.write("     " + pkg + " (" + oldver + ")\n")
286                         else:
287                                 sys.stdout.write("Nothing to do for this GLSA\n")
288                         sys.stdout.write("\n")
289                 elif mode == "inject":
290                         sys.stdout.write("injecting " + myid + "\n")
291                         myglsa.inject()
292         sys.exit(0)
293
294 # test is a bit different as Glsa.test() produces no output
295 if mode == "test":
296         outputlist = []
297         for myid in glsalist:
298                 try:
299                         myglsa = Glsa(myid, glsaconfig)
300                 except (GlsaTypeException, GlsaFormatException), e:
301                         if verbose:
302                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
303                         continue
304                 if myglsa.isVulnerable():
305                         if verbose:
306                                 outputlist.append(str(myglsa.nr)+" ( "+myglsa.title+" ) ")
307                         else:
308                                 outputlist.append(str(myglsa.nr))
309         if len(outputlist) > 0:
310                 sys.stderr.write("This system is affected by the following GLSAs:\n")
311                 sys.stdout.write("\n".join(outputlist)+"\n")
312         else:
313                 sys.stderr.write("This system is not affected by any of the listed GLSAs\n")
314         sys.exit(0)
315
316 # mail mode as requested by solar
317 if mode == "mail":
318         try:
319                 import portage.mail as portage_mail
320         except ImportError:
321                 import portage_mail
322                 
323         import socket
324         from StringIO import StringIO
325         try:
326                 from email.mime.text import MIMEText
327         except ImportError:
328                 from email.MIMEText import MIMEText
329
330         # color doesn't make any sense for mail
331         nocolor()
332
333         if "PORTAGE_ELOG_MAILURI" in glsaconfig:
334                 myrecipient = glsaconfig["PORTAGE_ELOG_MAILURI"].split()[0]
335         else:
336                 myrecipient = "root@localhost"
337         
338         if "PORTAGE_ELOG_MAILFROM" in glsaconfig:
339                 myfrom = glsaconfig["PORTAGE_ELOG_MAILFROM"]
340         else:
341                 myfrom = "glsa-check"
342
343         mysubject = "[glsa-check] Summary for %s" % socket.getfqdn()
344
345         # need a file object for summarylist()
346         myfd = StringIO()
347         myfd.write("GLSA Summary report for host %s\n" % socket.getfqdn())
348         myfd.write("(Command was: %s)\n\n" % " ".join(sys.argv))
349         summarylist(glsalist, fd1=myfd, fd2=myfd)
350         summary = str(myfd.getvalue())
351         myfd.close()
352
353         myattachments = []
354         for myid in glsalist:
355                 try:
356                         myglsa = Glsa(myid, glsaconfig)
357                 except (GlsaTypeException, GlsaFormatException), e:
358                         if verbose:
359                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
360                         continue
361                 myfd = StringIO()
362                 myglsa.dump(outstream=myfd)
363                 myattachments.append(MIMEText(str(myfd.getvalue()), _charset="utf8"))
364                 myfd.close()
365                 
366         mymessage = portage_mail.create_message(myfrom, myrecipient, mysubject, summary, myattachments)
367         portage_mail.send_mail(glsaconfig, mymessage)
368                 
369         sys.exit(0)
370         
371 # something wrong here, all valid paths are covered with sys.exit()
372 sys.stderr.write("nothing more to do\n")
373 sys.exit(2)