Change behaviour of getMinUpgrade
[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                         if mergelist == None:
271                                 sys.stdout.write(">>> no vulnerable packages installed\n")
272                         elif mergelist == []:
273                                 sys.stdout.write(">>> cannot fix GLSA, no unaffected packages available\n")
274                                 sys.exit(2)
275                         for pkg in mergelist:
276                                 sys.stdout.write(">>> merging "+pkg+"\n")
277                                 # using emerge for the actual merging as it contains the dependency
278                                 # code and we want to be consistent in behaviour. Also this functionality
279                                 # will be integrated in emerge later, so it shouldn't hurt much.
280                                 emergecmd = "emerge --oneshot " + glsaconfig["EMERGE_OPTS"] + " =" + pkg
281                                 if verbose:
282                                         sys.stderr.write(emergecmd+"\n")
283                                 exitcode = os.system(emergecmd)
284                                 # system() returns the exitcode in the high byte of a 16bit integer
285                                 if exitcode >= 1<<8:
286                                         exitcode >>= 8
287                                 if exitcode:
288                                         sys.exit(exitcode)
289                         if len(mergelist):
290                                 sys.stdout.write("\n")
291                         myglsa.inject()
292                 elif mode == "pretend":
293                         sys.stdout.write("Checking GLSA "+myid+"\n")
294                         mergelist = myglsa.getMergeList(least_change=least_change)
295                         if mergelist == None:
296                                 sys.stdout.write(">>> no vulnerable packages installed\n")
297                         elif mergelist == []:
298                                 sys.stdout.write(">>> cannot fix GLSA, no unaffected packages available\n")
299                                 sys.exit(2)
300                         if mergelist:
301                                 sys.stdout.write("The following updates will be performed for this GLSA:\n")
302                                 for pkg in mergelist:
303                                         oldver = None
304                                         for x in vardb.match(portage.dep_getkey(pkg)):
305                                                 if vardb.aux_get(x, ["SLOT"]) == portdb.aux_get(pkg, ["SLOT"]):
306                                                         oldver = x
307                                         if oldver == None:
308                                                 raise ValueError("could not find old version for package %s" % pkg)
309                                         oldver = oldver[len(portage.dep_getkey(oldver))+1:]
310                                         sys.stdout.write("     " + pkg + " (" + oldver + ")\n")
311                         else:
312                                 sys.stdout.write("Nothing to do for this GLSA\n")
313                         sys.stdout.write("\n")
314                 elif mode == "inject":
315                         sys.stdout.write("injecting " + myid + "\n")
316                         myglsa.inject()
317         sys.exit(0)
318
319 # test is a bit different as Glsa.test() produces no output
320 if mode == "test":
321         outputlist = []
322         for myid in glsalist:
323                 try:
324                         myglsa = Glsa(myid, glsaconfig)
325                 except (GlsaTypeException, GlsaFormatException), e:
326                         if verbose:
327                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
328                         continue
329                 if myglsa.isVulnerable():
330                         outputlist.append(str(myglsa.nr))
331         if len(outputlist) > 0:
332                 sys.stderr.write("This system is affected by the following GLSAs:\n")
333                 if verbose:
334                         summarylist(outputlist)
335                 else:
336                         sys.stdout.write("\n".join(outputlist)+"\n")
337         else:
338                 sys.stderr.write("This system is not affected by any of the listed GLSAs\n")
339         sys.exit(0)
340
341 # mail mode as requested by solar
342 if mode == "mail":
343         try:
344                 import portage.mail as portage_mail
345         except ImportError:
346                 import portage_mail
347                 
348         import socket
349         from StringIO import StringIO
350         try:
351                 from email.mime.text import MIMEText
352         except ImportError:
353                 from email.MIMEText import MIMEText
354
355         # color doesn't make any sense for mail
356         nocolor()
357
358         if "PORTAGE_ELOG_MAILURI" in glsaconfig:
359                 myrecipient = glsaconfig["PORTAGE_ELOG_MAILURI"].split()[0]
360         else:
361                 myrecipient = "root@localhost"
362         
363         if "PORTAGE_ELOG_MAILFROM" in glsaconfig:
364                 myfrom = glsaconfig["PORTAGE_ELOG_MAILFROM"]
365         else:
366                 myfrom = "glsa-check"
367
368         mysubject = "[glsa-check] Summary for %s" % socket.getfqdn()
369
370         # need a file object for summarylist()
371         myfd = StringIO()
372         myfd.write("GLSA Summary report for host %s\n" % socket.getfqdn())
373         myfd.write("(Command was: %s)\n\n" % " ".join(sys.argv))
374         summarylist(glsalist, fd1=myfd, fd2=myfd)
375         summary = str(myfd.getvalue())
376         myfd.close()
377
378         myattachments = []
379         for myid in glsalist:
380                 try:
381                         myglsa = Glsa(myid, glsaconfig)
382                 except (GlsaTypeException, GlsaFormatException), e:
383                         if verbose:
384                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
385                         continue
386                 myfd = StringIO()
387                 myglsa.dump(outstream=myfd)
388                 myattachments.append(MIMEText(str(myfd.getvalue()), _charset="utf8"))
389                 myfd.close()
390
391         if glsalist or not quiet:
392                 mymessage = portage_mail.create_message(myfrom, myrecipient, mysubject, summary, myattachments)
393                 portage_mail.send_mail(glsaconfig, mymessage)
394                 
395         sys.exit(0)
396         
397 # something wrong here, all valid paths are covered with sys.exit()
398 sys.stderr.write("nothing more to do\n")
399 sys.exit(2)