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