Remove 'new' target from glsa-list
[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 the GLSAs"],
28 ["-d", "--dump", "--print", "show all information about the GLSAs"],
29 ["-t", "--test", "test if this system is affected by the GLSAs"],
30 ["-p", "--pretend", "show the necessary steps to apply the GLSAs"],
31 ["-f", "--fix", "try to auto-apply the GLSAs (experimental)"],
32 ["-i", "--inject", "inject the given GLSA into the glsa_injected file"],
33 ["-n", "--nocolor", "disable colors (option)"],
34 ["-e", "--emergelike", "upgrade to latest version (not least-change, 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("affected")
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' 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         params.remove("new")
177         sys.stderr.write("Warning: The 'new' glsa-list target has been removed, using 'affected'.\n")
178         params.append("affected")
179         
180 if "all" in params:
181         glsalist = completelist
182         params.remove("all")
183
184 if "affected" in params:
185         for x in todolist:
186                 try:
187                         myglsa = Glsa(x, glsaconfig)
188                 except (GlsaTypeException, GlsaFormatException), e:
189                         if verbose:
190                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (x, e)))
191                         continue
192                 if myglsa.isVulnerable():
193                         glsalist.append(x)
194         params.remove("affected")
195
196 # remove invalid parameters
197 for p in params[:]:
198         if not (p in completelist or os.path.exists(p)):
199                 sys.stderr.write(("(removing %s from parameter list as it isn't a valid GLSA specification)\n" % p))
200                 params.remove(p)
201
202 glsalist.extend([g for g in params if g not in glsalist])
203
204 def summarylist(myglsalist, fd1=sys.stdout, fd2=sys.stderr, encoding="utf-8"):
205         fd1 = codecs.getwriter(encoding)(fd1)
206         fd2 = codecs.getwriter(encoding)(fd2)
207         if not quiet:
208                 fd2.write(white("[A]")+" means this GLSA was marked as applied (injected),\n")
209                 fd2.write(green("[U]")+" means the system is not affected and\n")
210                 fd2.write(red("[N]")+" indicates that the system might be affected.\n\n")
211
212         myglsalist.sort()
213         for myid in myglsalist:
214                 try:
215                         myglsa = Glsa(myid, glsaconfig)
216                 except (GlsaTypeException, GlsaFormatException), e:
217                         if verbose:
218                                 fd2.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
219                         continue
220                 if myglsa.isInjected():
221                         status = "[A]"
222                         color = white
223                 elif myglsa.isVulnerable():
224                         status = "[N]"
225                         color = red
226                 else:
227                         status = "[U]"
228                         color = green
229
230                 if verbose:
231                         access = ("[%-8s] " % myglsa.access)
232                 else:
233                         access=""
234
235                 fd1.write(color(myglsa.nr) + " " + color(status) + " " + color(access) + myglsa.title + " (")
236                 if not verbose:
237                         for pkg in myglsa.packages.keys()[:3]:
238                                 fd1.write(" " + pkg + " ")
239                         if len(myglsa.packages) > 3:
240                                 fd1.write("... ")
241                 else:
242                         for pkg in myglsa.packages.keys():
243                                 mylist = vardb.match(portage.dep_getkey(str(pkg)))
244                                 if len(mylist) > 0:
245                                         pkg = color(" ".join(mylist))
246                                 fd1.write(" " + pkg + " ")
247
248                 fd1.write(")")
249                 if list_cve:
250                         fd1.write(" "+(",".join([r[:13] for r in myglsa.references if r[:4] in ["CAN-", "CVE-"]])))
251                 fd1.write("\n")
252         return 0
253
254 if mode == "list":
255         sys.exit(summarylist(glsalist))
256
257 # dump, fix, inject and fix are nearly the same code, only the glsa method call differs
258 if mode in ["dump", "fix", "inject", "pretend"]:
259         for myid in glsalist:
260                 try:
261                         myglsa = Glsa(myid, glsaconfig)
262                 except (GlsaTypeException, GlsaFormatException), e:
263                         if verbose:
264                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
265                         continue
266                 if mode == "dump":
267                         myglsa.dump()
268                 elif mode == "fix":
269                         sys.stdout.write("Fixing GLSA "+myid+"\n")
270                         if not myglsa.isVulnerable():
271                                 sys.stdout.write(">>> no vulnerable packages installed\n")
272                         else:
273                                 mergelist = myglsa.getMergeList(least_change=least_change)
274                                 if mergelist == []:
275                                         sys.stdout.write(">>> cannot fix GLSA, no unaffected packages available\n")
276                                         sys.exit(2)
277                                 for pkg in mergelist:
278                                         sys.stdout.write(">>> merging "+pkg+"\n")
279                                         # using emerge for the actual merging as it contains the dependency
280                                         # code and we want to be consistent in behaviour. Also this functionality
281                                         # will be integrated in emerge later, so it shouldn't hurt much.
282                                         emergecmd = "emerge --oneshot " + glsaconfig["EMERGE_OPTS"] + " =" + pkg
283                                         if verbose:
284                                                 sys.stderr.write(emergecmd+"\n")
285                                         exitcode = os.system(emergecmd)
286                                         # system() returns the exitcode in the high byte of a 16bit integer
287                                         if exitcode >= 1<<8:
288                                                 exitcode >>= 8
289                                         if exitcode:
290                                                 sys.exit(exitcode)
291                                 if len(mergelist):
292                                         sys.stdout.write("\n")
293                 elif mode == "pretend":
294                         sys.stdout.write("Checking GLSA "+myid+"\n")
295                         if not myglsa.isVulnerable():
296                                 sys.stdout.write(">>> no vulnerable packages installed\n")
297                         else:
298                                 mergedict = {}
299                                 for (vuln, update) in myglsa.getAffectionTable(least_change=least_change):
300                                         mergedict.setdefault(update, []).append(vuln)
301                                 
302                                 # first, extract the atoms that cannot be upgraded (where key == "")
303                                 no_upgrades = []
304                                 if "" in mergedict:
305                                         no_upgrades = mergedict[""]
306                                         del mergedict[""]
307
308                                 # see if anything is left that can be upgraded
309                                 if mergedict:
310                                         sys.stdout.write(">>> Updates that will be performed:\n")
311                                         for (upd, vuln) in mergedict.iteritems():
312                                                 sys.stdout.write("     " + green(upd) + " (vulnerable: " + red(", ".join(vuln)) + ")\n")
313
314                                 if no_upgrades:
315                                         sys.stdout.write(">>> No upgrade path exists for these packages:\n")
316                                         sys.stdout.write("     " + red(", ".join(no_upgrades)) + "\n")
317                         sys.stdout.write("\n")
318                 elif mode == "inject":
319                         sys.stdout.write("injecting " + myid + "\n")
320                         myglsa.inject()
321         sys.exit(0)
322
323 # test is a bit different as Glsa.test() produces no output
324 if mode == "test":
325         outputlist = []
326         for myid in glsalist:
327                 try:
328                         myglsa = Glsa(myid, glsaconfig)
329                 except (GlsaTypeException, GlsaFormatException), e:
330                         if verbose:
331                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
332                         continue
333                 if myglsa.isVulnerable():
334                         outputlist.append(str(myglsa.nr))
335         if len(outputlist) > 0:
336                 sys.stderr.write("This system is affected by the following GLSAs:\n")
337                 if verbose:
338                         summarylist(outputlist)
339                 else:
340                         sys.stdout.write("\n".join(outputlist)+"\n")
341         else:
342                 sys.stderr.write("This system is not affected by any of the listed GLSAs\n")
343         sys.exit(0)
344
345 # mail mode as requested by solar
346 if mode == "mail":
347         try:
348                 import portage.mail as portage_mail
349         except ImportError:
350                 import portage_mail
351                 
352         import socket
353         from StringIO import StringIO
354         try:
355                 from email.mime.text import MIMEText
356         except ImportError:
357                 from email.MIMEText import MIMEText
358
359         # color doesn't make any sense for mail
360         nocolor()
361
362         if "PORTAGE_ELOG_MAILURI" in glsaconfig:
363                 myrecipient = glsaconfig["PORTAGE_ELOG_MAILURI"].split()[0]
364         else:
365                 myrecipient = "root@localhost"
366         
367         if "PORTAGE_ELOG_MAILFROM" in glsaconfig:
368                 myfrom = glsaconfig["PORTAGE_ELOG_MAILFROM"]
369         else:
370                 myfrom = "glsa-check"
371
372         mysubject = "[glsa-check] Summary for %s" % socket.getfqdn()
373
374         # need a file object for summarylist()
375         myfd = StringIO()
376         myfd.write("GLSA Summary report for host %s\n" % socket.getfqdn())
377         myfd.write("(Command was: %s)\n\n" % " ".join(sys.argv))
378         summarylist(glsalist, fd1=myfd, fd2=myfd)
379         summary = str(myfd.getvalue())
380         myfd.close()
381
382         myattachments = []
383         for myid in glsalist:
384                 try:
385                         myglsa = Glsa(myid, glsaconfig)
386                 except (GlsaTypeException, GlsaFormatException), e:
387                         if verbose:
388                                 sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
389                         continue
390                 myfd = StringIO()
391                 myglsa.dump(outstream=myfd)
392                 myattachments.append(MIMEText(str(myfd.getvalue()), _charset="utf8"))
393                 myfd.close()
394
395         if glsalist or not quiet:
396                 mymessage = portage_mail.create_message(myfrom, myrecipient, mysubject, summary, myattachments)
397                 portage_mail.send_mail(glsaconfig, mymessage)
398                 
399         sys.exit(0)
400         
401 # something wrong here, all valid paths are covered with sys.exit()
402 sys.stderr.write("nothing more to do\n")
403 sys.exit(2)