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