d28383527c3da42b9a3aec7ae42f9eb6c3e7ddb7
[gentoolkit.git] / pym / gentoolkit / glsa / __init__.py
1 # $Header$
2
3 # This program is licensed under the GPL, version 2
4
5 # WARNING: this code is only tested by a few people and should NOT be used
6 # on production systems at this stage. There are possible security holes and probably
7 # bugs in this code. If you test it please report ANY success or failure to
8 # me (genone@gentoo.org).
9
10 # The following planned features are currently on hold:
11 # - getting GLSAs from http/ftp servers (not really useful without the fixed ebuilds)
12 # - GPG signing/verification (until key policy is clear)
13
14 __author__ = "Marius Mauch <genone@gentoo.org>"
15
16 import os
17 import sys
18 import urllib
19 import codecs
20 import re
21 import operator
22 import xml.dom.minidom
23 from StringIO import StringIO
24
25 if sys.version_info[0:2] < (2,3):
26         raise NotImplementedError("Python versions below 2.3 have broken XML code " \
27                                                                 +"and are not supported")
28
29 try:
30         import portage
31 except ImportError:
32         sys.path.insert(0, "/usr/lib/portage/pym")
33         import portage
34
35 # Note: the space for rgt and rlt is important !!
36 opMapping = {"le": "<=", "lt": "<", "eq": "=", "gt": ">", "ge": ">=", 
37                          "rge": ">=~", "rle": "<=~", "rgt": " >~", "rlt": " <~"}
38 NEWLINE_ESCAPE = "!;\\n"        # some random string to mark newlines that should be preserved
39 SPACE_ESCAPE = "!;_"            # some random string to mark spaces that should be preserved
40
41 def center(text, width):
42         """
43         Returns a string containing I{text} that is padded with spaces on both
44         sides. If C{len(text) >= width} I{text} is returned unchanged.
45         
46         @type   text: String
47         @param  text: the text to be embedded
48         @type   width: Integer
49         @param  width: the minimum length of the returned string
50         @rtype:         String
51         @return:        the expanded string or I{text}
52         """
53         if len(text) >= width:
54                 return text
55         margin = (width-len(text))/2
56         rValue = " "*margin
57         rValue += text
58         if 2*margin + len(text) == width:
59                 rValue += " "*margin
60         elif 2*margin + len(text) + 1 == width:
61                 rValue += " "*(margin+1)
62         return rValue
63
64
65 def wrap(text, width, caption=""):
66         """
67         Wraps the given text at column I{width}, optionally indenting
68         it so that no text is under I{caption}. It's possible to encode 
69         hard linebreaks in I{text} with L{NEWLINE_ESCAPE}.
70         
71         @type   text: String
72         @param  text: the text to be wrapped
73         @type   width: Integer
74         @param  width: the column at which the text should be wrapped
75         @type   caption: String
76         @param  caption: this string is inserted at the beginning of the 
77                                          return value and the paragraph is indented up to
78                                          C{len(caption)}.
79         @rtype:         String
80         @return:        the wrapped and indented paragraph
81         """
82         rValue = ""
83         line = caption
84         text = text.replace(2*NEWLINE_ESCAPE, NEWLINE_ESCAPE+" "+NEWLINE_ESCAPE)
85         words = text.split()
86         indentLevel = len(caption)+1
87         
88         for w in words:
89                 if line[-1] == "\n":
90                         rValue += line
91                         line = " "*indentLevel
92                 if len(line)+len(w.replace(NEWLINE_ESCAPE, ""))+1 > width:
93                         rValue += line+"\n"
94                         line = " "*indentLevel+w.replace(NEWLINE_ESCAPE, "\n")
95                 elif w.find(NEWLINE_ESCAPE) >= 0:
96                         if len(line.strip()) > 0:
97                                 rValue += line+" "+w.replace(NEWLINE_ESCAPE, "\n")
98                         else:
99                                 rValue += line+w.replace(NEWLINE_ESCAPE, "\n")
100                         line = " "*indentLevel
101                 else:
102                         if len(line.strip()) > 0:
103                                 line += " "+w
104                         else:
105                                 line += w
106         if len(line) > 0:
107                 rValue += line.replace(NEWLINE_ESCAPE, "\n")
108         rValue = rValue.replace(SPACE_ESCAPE, " ")
109         return rValue
110
111 def checkconfig(myconfig):
112         """
113         takes a portage.config instance and adds GLSA specific keys if
114         they are not present. TO-BE-REMOVED (should end up in make.*)
115         """
116         mysettings = {
117                 "GLSA_DIR": portage.settings["PORTDIR"]+"/metadata/glsa/",
118                 "GLSA_PREFIX": "glsa-",
119                 "GLSA_SUFFIX": ".xml",
120                 "CHECKFILE": "/var/lib/portage/glsa_injected",
121                 "GLSA_SERVER": "www.gentoo.org/security/en/glsa/",      # not completely implemented yet
122                 "CHECKMODE": "local",                                                           # not completely implemented yet
123                 "PRINTWIDTH": "76"
124         }
125         for k in mysettings.keys():
126                 if k not in myconfig:
127                         myconfig[k] = mysettings[k]
128         return myconfig
129
130 def get_glsa_list(repository, myconfig):
131         """
132         Returns a list of all available GLSAs in the given repository
133         by comparing the filelist there with the pattern described in
134         the config.
135         
136         @type   repository: String
137         @param  repository: The directory or an URL that contains GLSA files
138                                                 (Note: not implemented yet)
139         @type   myconfig: portage.config
140         @param  myconfig: a GLSA aware config instance (see L{checkconfig})
141         
142         @rtype:         List of Strings
143         @return:        a list of GLSA IDs in this repository
144         """
145         # TODO: remote fetch code for listing
146
147         rValue = []
148
149         if not os.access(repository, os.R_OK):
150                 return []
151         dirlist = os.listdir(repository)
152         prefix = myconfig["GLSA_PREFIX"]
153         suffix = myconfig["GLSA_SUFFIX"]
154         
155         for f in dirlist:
156                 try:
157                         if f[:len(prefix)] == prefix and f[-1*len(suffix):] == suffix:
158                                 rValue.append(f[len(prefix):-1*len(suffix)])
159                 except IndexError:
160                         pass
161         return rValue
162
163 def getListElements(listnode):
164         """
165         Get all <li> elements for a given <ol> or <ul> node.
166         
167         @type   listnode: xml.dom.Node
168         @param  listnode: <ul> or <ol> list to get the elements for
169         @rtype:         List of Strings
170         @return:        a list that contains the value of the <li> elements
171         """
172         if not listnode.nodeName in ["ul", "ol"]:
173                 raise GlsaFormatException("Invalid function call: listnode is not <ul> or <ol>")
174         rValue = [getText(li, format="strip") \
175                 for li in listnode.childNodes \
176                 if li.nodeType == xml.dom.Node.ELEMENT_NODE]
177         return rValue
178
179 def getText(node, format, textfd = None):
180         """
181         This is the main parser function. It takes a node and traverses
182         recursive over the subnodes, getting the text of each (and the
183         I{link} attribute for <uri> and <mail>). Depending on the I{format}
184         parameter the text might be formatted by adding/removing newlines,
185         tabs and spaces. This function is only useful for the GLSA DTD,
186         it's not applicable for other DTDs.
187         
188         @type   node: xml.dom.Node
189         @param  node: the root node to start with the parsing
190         @type   format: String
191         @param  format: this should be either I{strip}, I{keep} or I{xml}
192                                         I{keep} just gets the text and does no formatting.
193                                         I{strip} replaces newlines and tabs with spaces and
194                                         replaces multiple spaces with one space.
195                                         I{xml} does some more formatting, depending on the
196                                         type of the encountered nodes.
197         @type   textfd: writable file-like object
198         @param  textfd: the file-like object to write the output to
199         @rtype:         String
200         @return:        the (formatted) content of the node and its subnodes
201                         except if textfd was not none
202         """
203         if not textfd:
204                 textfd = StringIO()
205                 returnNone = False
206         else:
207                 returnNone = True
208         if format in ["strip", "keep"]:
209                 if node.nodeName in ["uri", "mail"]:
210                         textfd.write(node.childNodes[0].data+": "+node.getAttribute("link"))
211                 else:
212                         for subnode in node.childNodes:
213                                 if subnode.nodeName == "#text":
214                                         textfd.write(subnode.data)
215                                 else:
216                                         getText(subnode, format, textfd)
217         else: # format = "xml"
218                 for subnode in node.childNodes:
219                         if subnode.nodeName == "p":
220                                 for p_subnode in subnode.childNodes:
221                                         if p_subnode.nodeName == "#text":
222                                                 textfd.write(p_subnode.data.strip())
223                                         elif p_subnode.nodeName in ["uri", "mail"]:
224                                                 textfd.write(p_subnode.childNodes[0].data)
225                                                 textfd.write(" ( "+p_subnode.getAttribute("link")+" )")
226                                 textfd.write(NEWLINE_ESCAPE)
227                         elif subnode.nodeName == "ul":
228                                 for li in getListElements(subnode):
229                                         textfd.write("-"+SPACE_ESCAPE+li+NEWLINE_ESCAPE+" ")
230                         elif subnode.nodeName == "ol":
231                                 i = 0
232                                 for li in getListElements(subnode):
233                                         i = i+1
234                                         textfd.write(str(i)+"."+SPACE_ESCAPE+li+NEWLINE_ESCAPE+" ")
235                         elif subnode.nodeName == "code":
236                                 textfd.write(getText(subnode, format="keep").lstrip().replace("\n", NEWLINE_ESCAPE))
237                                 textfd.write(NEWLINE_ESCAPE)
238                         elif subnode.nodeName == "#text":
239                                 textfd.write(subnode.data)
240                         else:
241                                 raise GlsaFormatException("Invalid Tag found: ", subnode.nodeName)
242         if returnNone:
243                 return None
244         rValue = textfd.getvalue()
245         if format == "strip":
246                 rValue = rValue.strip(" \n\t")
247                 rValue = re.sub("[\s]{2,}", " ", rValue)
248         return rValue
249
250 def getMultiTagsText(rootnode, tagname, format):
251         """
252         Returns a list with the text of all subnodes of type I{tagname}
253         under I{rootnode} (which itself is not parsed) using the given I{format}.
254         
255         @type   rootnode: xml.dom.Node
256         @param  rootnode: the node to search for I{tagname}
257         @type   tagname: String
258         @param  tagname: the name of the tags to search for
259         @type   format: String
260         @param  format: see L{getText}
261         @rtype:         List of Strings
262         @return:        a list containing the text of all I{tagname} childnodes
263         """
264         rValue = [getText(e, format) \
265                 for e in rootnode.getElementsByTagName(tagname)]
266         return rValue
267
268 def makeAtom(pkgname, versionNode):
269         """
270         creates from the given package name and information in the 
271         I{versionNode} a (syntactical) valid portage atom.
272         
273         @type   pkgname: String
274         @param  pkgname: the name of the package for this atom
275         @type   versionNode: xml.dom.Node
276         @param  versionNode: a <vulnerable> or <unaffected> Node that
277                                                  contains the version information for this atom
278         @rtype:         String
279         @return:        the portage atom
280         """
281         rValue = opMapping[versionNode.getAttribute("range")] \
282                                 + pkgname \
283                                 + "-" + getText(versionNode, format="strip")
284         try:
285                 slot = versionNode.getAttribute("slot").strip()
286         except KeyError:
287                 pass
288         else:
289                 if slot and slot != "*":
290                         rValue += ":" + slot
291         return str(rValue)
292
293 def makeVersion(versionNode):
294         """
295         creates from the information in the I{versionNode} a 
296         version string (format <op><version>).
297         
298         @type   versionNode: xml.dom.Node
299         @param  versionNode: a <vulnerable> or <unaffected> Node that
300                                                  contains the version information for this atom
301         @rtype:         String
302         @return:        the version string
303         """
304         rValue = opMapping[versionNode.getAttribute("range")] \
305                         +getText(versionNode, format="strip")
306         try:
307                 slot = versionNode.getAttribute("slot").strip()
308         except KeyError:
309                 pass
310         else:
311                 if slot and slot != "*":
312                         rValue += ":" + slot
313         return rValue
314
315 def match(atom, portdbname, match_type="default"):
316         """
317         wrapper that calls revisionMatch() or portage.dbapi.match() depending on 
318         the given atom.
319         
320         @type   atom: string
321         @param  atom: a <~ or >~ atom or a normal portage atom that contains the atom to match against
322         @type   portdb: portage.dbapi
323         @param  portdb: one of the portage databases to use as information source
324         @type   match_type: string
325         @param  match_type: if != "default" passed as first argument to dbapi.xmatch 
326                                 to apply the wanted visibility filters
327         
328         @rtype:         list of strings
329         @return:        a list with the matching versions
330         """
331         db = portage.db["/"][portdbname].dbapi
332         if atom[2] == "~":
333                 return revisionMatch(atom, db, match_type=match_type)
334         elif match_type == "default" or not hasattr(db, "xmatch"):
335                 return db.match(atom)
336         else:
337                 return db.xmatch(match_type, atom)
338
339 def revisionMatch(revisionAtom, portdb, match_type="default"):
340         """
341         handler for the special >~, >=~, <=~ and <~ atoms that are supposed to behave
342         as > and < except that they are limited to the same version, the range only
343         applies to the revision part.
344         
345         @type   revisionAtom: string
346         @param  revisionAtom: a <~ or >~ atom that contains the atom to match against
347         @type   portdb: portage.dbapi
348         @param  portdb: one of the portage databases to use as information source
349         @type   match_type: string
350         @param  match_type: if != "default" passed as first argument to portdb.xmatch 
351                                 to apply the wanted visibility filters
352         
353         @rtype:         list of strings
354         @return:        a list with the matching versions
355         """
356         if match_type == "default" or not hasattr(portdb, "xmatch"):
357                 if ":" in revisionAtom:
358                         mylist = portdb.match(re.sub(r'-r[0-9]+(:[^ ]+)?$', r'\1', revisionAtom[2:]))
359                 else:
360                         mylist = portdb.match(re.sub("-r[0-9]+$", "", revisionAtom[2:]))
361         else:
362                 if ":" in revisionAtom:
363                         mylist = portdb.xmatch(match_type, re.sub(r'-r[0-9]+(:[^ ]+)?$', r'\1', revisionAtom[2:]))
364                 else:
365                         mylist = portdb.xmatch(match_type, re.sub("-r[0-9]+$", "", revisionAtom[2:]))
366         rValue = []
367         for v in mylist:
368                 r1 = portage.pkgsplit(v)[-1][1:]
369                 r2 = portage.pkgsplit(revisionAtom[3:])[-1][1:]
370                 if eval(r1+" "+revisionAtom[0:2]+" "+r2):
371                         rValue.append(v)
372         return rValue
373                 
374
375 def getMinUpgrade(vulnerableList, unaffectedList, minimize=True):
376         """
377         Checks if the systemstate is matching an atom in
378         I{vulnerableList} and returns string describing
379         the lowest version for the package that matches an atom in 
380         I{unaffectedList} and is greater than the currently installed
381         version. It will return an empty list if the system is affected,
382         and no upgrade is possible or None if the system is not affected.
383         Both I{vulnerableList} and I{unaffectedList} should have the
384         same base package.
385         
386         @type   vulnerableList: List of Strings
387         @param  vulnerableList: atoms matching vulnerable package versions
388         @type   unaffectedList: List of Strings
389         @param  unaffectedList: atoms matching unaffected package versions
390         @type   minimize:       Boolean
391         @param  minimize:       True for a least-change upgrade, False for emerge-like algorithm
392         
393         @rtype:         String | None
394         @return:        the lowest unaffected version that is greater than
395                                 the installed version.
396         """
397         rValue = ""
398         v_installed = reduce(operator.add, [match(v, "vartree") for v in vulnerableList], [])
399         u_installed = reduce(operator.add, [match(u, "vartree") for u in unaffectedList], [])
400         
401         # remove all unaffected atoms from vulnerable list
402         v_installed = list(set(v_installed).difference(set(u_installed)))
403
404         if not v_installed:
405                 return None
406
407         # this tuple holds all vulnerable atoms, and the related upgrade atom
408         vuln_update = []
409         avail_updates = set()
410         for u in unaffectedList:
411                 # TODO: This had match_type="match-all" before. I don't think it should
412                 # since we disregarded masked items later anyway (match(=rValue, "porttree"))
413                 avail_updates.update(match(u, "porttree"))
414         # if an atom is already installed, we should not consider it for upgrades
415         avail_updates.difference_update(u_installed)
416
417         for vuln in v_installed:
418                 update = ""
419                 for c in avail_updates:
420                         c_pv = portage.catpkgsplit(c)
421                         i_pv = portage.catpkgsplit(vuln)
422                         if portage.pkgcmp(c_pv[1:], i_pv[1:]) > 0 \
423                                         and (update == "" \
424                                                 or (minimize ^ (portage.pkgcmp(c_pv[1:], portage.catpkgsplit(rValue)[1:]) > 0))) \
425                                         and portage.db["/"]["porttree"].dbapi.aux_get(c, ["SLOT"]) == portage.db["/"]["vartree"].dbapi.aux_get(vuln, ["SLOT"]):
426                                 update = c_pv[0]+"/"+c_pv[1]+"-"+c_pv[2]
427                                 if c_pv[3] != "r0":             # we don't like -r0 for display
428                                         update += "-"+c_pv[3]
429                 vuln_update.append([vuln, update])
430
431         return vuln_update
432
433 def format_date(datestr):
434         """
435         Takes a date (announced, revised) date from a GLSA and formats
436         it as readable text (i.e. "January 1, 2008").
437         
438         @type   date: String
439         @param  date: the date string to reformat
440         @rtype:         String
441         @return:        a reformatted string, or the original string
442                                 if it cannot be reformatted.
443         """
444         splitdate = datestr.split("-", 2)
445         if len(splitdate) != 3:
446                 return datestr
447         
448         # This cannot raise an error as we use () instead of []
449         splitdate = (int(x) for x in splitdate)
450         
451         from datetime import date
452         try:
453                 d = date(*splitdate)
454         except ValueError:
455                 return datestr
456         
457         # TODO We could format to local date format '%x' here?
458         return d.strftime("%B %d, %Y")
459
460 # simple Exception classes to catch specific errors
461 class GlsaTypeException(Exception):
462         def __init__(self, doctype):
463                 Exception.__init__(self, "wrong DOCTYPE: %s" % doctype)
464
465 class GlsaFormatException(Exception):
466         pass
467                                 
468 class GlsaArgumentException(Exception):
469         pass
470
471 # GLSA xml data wrapper class
472 class Glsa:
473         """
474         This class is a wrapper for the XML data and provides methods to access
475         and display the contained data.
476         """
477         def __init__(self, myid, myconfig):
478                 """
479                 Simple constructor to set the ID, store the config and gets the 
480                 XML data by calling C{self.read()}.
481                 
482                 @type   myid: String
483                 @param  myid: String describing the id for the GLSA object (standard
484                                           GLSAs have an ID of the form YYYYMM-nn) or an existing
485                                           filename containing a GLSA.
486                 @type   myconfig: portage.config
487                 @param  myconfig: the config that should be used for this object.
488                 """
489                 if re.match(r'\d{6}-\d{2}', myid):
490                         self.type = "id"
491                 elif os.path.exists(myid):
492                         self.type = "file"
493                 else:
494                         raise GlsaArgumentException("Given ID "+myid+" isn't a valid GLSA ID or filename.")
495                 self.nr = myid
496                 self.config = myconfig
497                 self.read()
498
499         def read(self):
500                 """
501                 Here we build the filename from the config and the ID and pass
502                 it to urllib to fetch it from the filesystem or a remote server.
503                 
504                 @rtype:         None
505                 @return:        None
506                 """
507                 if self.config["CHECKMODE"] == "local":
508                         repository = "file://" + self.config["GLSA_DIR"]
509                 else:
510                         repository = self.config["GLSA_SERVER"]
511                 if self.type == "file":
512                         myurl = "file://"+self.nr
513                 else:
514                         myurl = repository + self.config["GLSA_PREFIX"] + str(self.nr) + self.config["GLSA_SUFFIX"]
515                 self.parse(urllib.urlopen(myurl))
516                 return None
517
518         def parse(self, myfile):
519                 """
520                 This method parses the XML file and sets up the internal data 
521                 structures by calling the different helper functions in this
522                 module.
523                 
524                 @type   myfile: String
525                 @param  myfile: Filename to grab the XML data from
526                 @rtype:         None
527                 @returns:       None
528                 """
529                 self.DOM = xml.dom.minidom.parse(myfile)
530                 if not self.DOM.doctype:
531                         raise GlsaTypeException(None)
532                 elif self.DOM.doctype.systemId == "http://www.gentoo.org/dtd/glsa.dtd":
533                         self.dtdversion = 0
534                 elif self.DOM.doctype.systemId == "http://www.gentoo.org/dtd/glsa-2.dtd":
535                         self.dtdversion = 2
536                 else:
537                         raise GlsaTypeException(self.DOM.doctype.systemId)
538                 myroot = self.DOM.getElementsByTagName("glsa")[0]
539                 if self.type == "id" and myroot.getAttribute("id") != self.nr:
540                         raise GlsaFormatException("filename and internal id don't match:" + myroot.getAttribute("id") + " != " + self.nr)
541
542                 # the simple (single, required, top-level, #PCDATA) tags first
543                 self.title = getText(myroot.getElementsByTagName("title")[0], format="strip")
544                 self.synopsis = getText(myroot.getElementsByTagName("synopsis")[0], format="strip")
545                 self.announced = format_date(getText(myroot.getElementsByTagName("announced")[0], format="strip"))
546                 
547                 count = 1
548                 # Support both formats of revised:
549                 # <revised>December 30, 2007: 02</revised>
550                 # <revised count="2">2007-12-30</revised>
551                 revisedEl = myroot.getElementsByTagName("revised")[0]
552                 self.revised = getText(revisedEl, format="strip")
553                 if (revisedEl.attributes.has_key("count")):
554                         count = revisedEl.getAttribute("count")
555                 elif (self.revised.find(":") >= 0):
556                         (self.revised, count) = self.revised.split(":")
557                 
558                 self.revised = format_date(self.revised)
559                 
560                 try:
561                         self.count = int(count)
562                 except ValueError:
563                         # TODO should this rais a GlsaFormatException?
564                         self.count = 1
565                 
566                 # now the optional and 0-n toplevel, #PCDATA tags and references
567                 try:
568                         self.access = getText(myroot.getElementsByTagName("access")[0], format="strip")
569                 except IndexError:
570                         self.access = ""
571                 self.bugs = getMultiTagsText(myroot, "bug", format="strip")
572                 self.references = getMultiTagsText(myroot.getElementsByTagName("references")[0], "uri", format="keep")
573                 
574                 # and now the formatted text elements
575                 self.description = getText(myroot.getElementsByTagName("description")[0], format="xml")
576                 self.workaround = getText(myroot.getElementsByTagName("workaround")[0], format="xml")
577                 self.resolution = getText(myroot.getElementsByTagName("resolution")[0], format="xml")
578                 self.impact_text = getText(myroot.getElementsByTagName("impact")[0], format="xml")
579                 self.impact_type = myroot.getElementsByTagName("impact")[0].getAttribute("type")
580                 try:
581                         self.background = getText(myroot.getElementsByTagName("background")[0], format="xml")
582                 except IndexError:
583                         self.background = ""                                    
584
585                 # finally the interesting tags (product, affected, package)
586                 self.glsatype = myroot.getElementsByTagName("product")[0].getAttribute("type")
587                 self.product = getText(myroot.getElementsByTagName("product")[0], format="strip")
588                 self.affected = myroot.getElementsByTagName("affected")[0]
589                 self.packages = {}
590                 for p in self.affected.getElementsByTagName("package"):
591                         name = p.getAttribute("name")
592                         if not self.packages.has_key(name):
593                                 self.packages[name] = []
594                         tmp = {}
595                         tmp["arch"] = p.getAttribute("arch")
596                         tmp["auto"] = (p.getAttribute("auto") == "yes")
597                         tmp["vul_vers"] = [makeVersion(v) for v in p.getElementsByTagName("vulnerable")]
598                         tmp["unaff_vers"] = [makeVersion(v) for v in p.getElementsByTagName("unaffected")]
599                         tmp["vul_atoms"] = [makeAtom(name, v) for v in p.getElementsByTagName("vulnerable")]
600                         tmp["unaff_atoms"] = [makeAtom(name, v) for v in p.getElementsByTagName("unaffected")]
601                         self.packages[name].append(tmp)
602                 # TODO: services aren't really used yet
603                 self.services = self.affected.getElementsByTagName("service")
604                 return None
605
606         def dump(self, outstream=sys.stdout, encoding="utf-8"):
607                 """
608                 Dumps a plaintext representation of this GLSA to I{outfile} or 
609                 B{stdout} if it is ommitted. You can specify an alternate
610                 I{encoding} if needed (default is utf-8).
611                 
612                 @type   outstream: File
613                 @param  outfile: Stream that should be used for writing
614                                                  (defaults to sys.stdout)
615                 """
616                 outstream = codecs.getwriter(encoding)(outstream)
617                 width = int(self.config["PRINTWIDTH"])
618                 outstream.write(center("GLSA %s: \n%s" % (self.nr, self.title), width)+"\n")
619                 outstream.write((width*"=")+"\n")
620                 outstream.write(wrap(self.synopsis, width, caption="Synopsis:         ")+"\n")
621                 outstream.write("Announced on:      %s\n" % self.announced)
622                 outstream.write("Last revised on:   %s : %02d\n\n" % (self.revised, self.count))
623                 if self.glsatype == "ebuild":
624                         for k in self.packages.keys():
625                                 pkg = self.packages[k]
626                                 for path in pkg:
627                                         vul_vers = "".join(path["vul_vers"])
628                                         unaff_vers = "".join(path["unaff_vers"])
629                                         outstream.write("Affected package:  %s\n" % k)
630                                         outstream.write("Affected archs:    ")
631                                         if path["arch"] == "*":
632                                                 outstream.write("All\n")
633                                         else:
634                                                 outstream.write("%s\n" % path["arch"])
635                                         outstream.write("Vulnerable:        %s\n" % vul_vers)
636                                         outstream.write("Unaffected:        %s\n\n" % unaff_vers)
637                 elif self.glsatype == "infrastructure":
638                         pass
639                 if len(self.bugs) > 0:
640                         outstream.write("\nRelated bugs:      ")
641                         outstream.write(", ".join(self.bugs))
642                         outstream.write("\n")
643                 if self.background:
644                         outstream.write("\n"+wrap(self.background, width, caption="Background:       "))
645                 outstream.write("\n"+wrap(self.description, width, caption="Description:      "))
646                 outstream.write("\n"+wrap(self.impact_text, width, caption="Impact:           "))
647                 outstream.write("\n"+wrap(self.workaround, width, caption="Workaround:       "))
648                 outstream.write("\n"+wrap(self.resolution, width, caption="Resolution:       "))
649                 myreferences = " ".join(r.replace(" ", SPACE_ESCAPE)+NEWLINE_ESCAPE for r in self.references)
650                 outstream.write("\n"+wrap(myreferences, width, caption="References:       "))
651                 outstream.write("\n")
652         
653         def isVulnerable(self):
654                 """
655                 Tests if the system is affected by this GLSA by checking if any
656                 vulnerable package versions are installed. Also checks for affected
657                 architectures.
658                 
659                 @rtype:         Boolean
660                 @returns:       True if the system is affected, False if not
661                 """
662                 rValue = False
663                 for k in self.packages.keys():
664                         pkg = self.packages[k]
665                         for path in pkg:
666                                 if path["arch"] == "*" or self.config["ARCH"] in path["arch"].split():
667                                         for v in path["vul_atoms"]:
668                                                 rValue = rValue \
669                                                         or (None != getMinUpgrade([v,], path["unaff_atoms"]))
670                 return rValue
671         
672         def isInjected(self):
673                 """
674                 Looks if the GLSA ID is in the GLSA checkfile to check if this
675                 GLSA should be marked as applied.
676                 
677                 @rtype:         Boolean
678                 @returns:       True if the GLSA is in the inject file, False if not
679                 """
680                 if not os.access(self.config["CHECKFILE"], os.R_OK):
681                         return False
682                 aList = portage.grabfile(self.config["CHECKFILE"])
683                 return (self.nr in aList)
684
685         def inject(self):
686                 """
687                 Puts the ID of this GLSA into the GLSA checkfile, so it won't
688                 show up on future checks. Should be called after a GLSA is 
689                 applied or on explicit user request.
690
691                 @rtype:         None
692                 @returns:       None
693                 """
694                 if not self.isInjected():
695                         checkfile = open(self.config["CHECKFILE"], "a+")
696                         checkfile.write(self.nr+"\n")
697                         checkfile.close()
698                 return None
699         
700         def getMergeList(self, least_change=True):
701                 """
702                 Returns the list of package-versions that have to be merged to
703                 apply this GLSA properly. The versions are as low as possible 
704                 while avoiding downgrades (see L{getMinUpgrade}).
705                 
706                 @type   least_change: Boolean
707                 @param  least_change: True if the smallest possible upgrade should be selected,
708                                         False for an emerge-like algorithm
709                 @rtype:         List of Strings
710                 @return:        list of package-versions that have to be merged
711                 """
712                 return list(set(update for (vuln, update) in self.getAffectionTable(least_change) if update))
713
714         def getAffectionTable(self, least_change=True):
715                 """
716                 Will initialize the self.systemAffection list of
717                 atoms installed on the system that are affected
718                 by this GLSA, and the atoms that are minimal upgrades.
719                 """
720                 systemAffection = []
721                 for pkg in self.packages.keys():
722                         for path in self.packages[pkg]:
723                                 update = getMinUpgrade(path["vul_atoms"], path["unaff_atoms"], minimize=least_change)
724                                 if update:
725                                         systemAffection.extend(update)
726                 return systemAffection