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