changeset 49:3fd16093536e

merged
author Benoît Allard <benoit.allard@greenbone.net>
date Tue, 30 Dec 2014 12:31:50 +0100
parents 3826f2701ff2 (diff) 2e36289616db (current diff)
children e6b52a8119cd
files CHANGES
diffstat 9 files changed, 997 insertions(+), 31 deletions(-) [+]
line wrap: on
line diff
--- a/CHANGES	Tue Dec 23 09:01:38 2014 +0100
+++ b/CHANGES	Tue Dec 30 12:31:50 2014 +0100
@@ -1,7 +1,17 @@
+FarolLuz next
+=============
+
+Main changes since FarolLuz 1.0:
+--------------------------------
+* Add parsing for CPE.
+* Add parsing for CVEs format from the OpenVAS Greenbone Security Manager.
+* Implement an HTML export format.
+
+
 FarolLuz 1.0 (2014-11-05)
 =========================
 
-THis is the first public release of FarolLuz. FarolLuz is a set of library /
+This is the first public release of FarolLuz. FarolLuz is a set of library /
 utilities to manipulate Security Advisories. It is part of Farol, the Security
 Advisory Management Platform.
 
@@ -13,13 +23,6 @@
 * Add method to rename groupIDs in the whole document.
 * Split the big cvrf.py file into smaller ones
 
-FarolLuz next
-=============
-
-Main changes since FarolLuz 0.1.1
----------------------------------
-* Implement an HTML export format.
-
 
 FarolLuz 0.1.1 (2014-10-17)
 ===========================
--- a/farolluz/document.py	Tue Dec 23 09:01:38 2014 +0100
+++ b/farolluz/document.py	Tue Dec 30 12:31:50 2014 +0100
@@ -82,6 +82,7 @@
 class CVRFTracking(object):
     STATUSES = ('Draft', 'Interim', 'Final')
     def __init__(self, _id, status, version, initial, current):
+        """ version must be a tuple of (max four) ints """
         self._identification = _id
         self._status = status
         self._version = version
@@ -148,6 +149,7 @@
 
 class CVRFRevision(object):
     def __init__(self, number, date, description):
+        """ number is a tuple of (max four) ints """
         self._number = number
         self._date = date
         self._description = description
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/farolluz/parsers/cpe.py	Tue Dec 30 12:31:50 2014 +0100
@@ -0,0 +1,370 @@
+# -*- coding: utf-8 -*-
+# Description:
+# Methods for parsing CPEs
+#
+# Authors:
+# Benoît Allard <benoit.allard@greenbone.net>
+#
+# Copyright:
+# Copyright (C) 2014 Greenbone Networks GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""\
+a cpe class to ease the creation of a producttree based on cpe
+
+This is based on:
+
+    NIST Interagency Report 7695
+    Common Platform Enumeration: Naming Specification Version 2.3
+
+CPE is a trademark of The MITRE Corporation.
+
+"""
+
+import re
+
+from ..producttree import CVRFFullProductName, CVRFRelationship
+
+def capitalize(s):
+    """ A custom version of string.capwords that split on _, and join on ' '
+    """
+    s = s.replace('\\', '')
+    return ' '.join(c.capitalize() for c in s.split('_'))
+
+
+PCT_MAP ={'!': "%21", '"': "%22", '#': "%23", '$': "%24", '%': "%25", '&': "%26",
+          "'": "%27", '(': "%28", ')': "%29", '*': "%2a", '+': "%2b", ',': "%2c",
+          '/': "%2f", ':': "%3a", ';': "%3b", '<': "%3c", "=": "%3d", '>': "%3e",
+          '?': "%3f", '@': "%40", '[': "%5b", '\\': "%5c","]": "%5d", '^': "%5e",
+          '`': "%60", '{': "%7b", '|': "%7c", '}': "%7d", "~": "%7e"}
+
+PCT_MAP_i = dict((v, k) for k, v in PCT_MAP.iteritems())
+
+def pct_encode(c):
+    """ Returns the right percent-encoding of c """
+    if c in "-.":
+        return c
+    return PCT_MAP[c]
+    return {'!': "%21", '"': "%22", '#': "%23", '$': "%24", '%': "%25", '&': "%26",
+            "'": "%27", '(': "%28", ')': "%29", '*': "%2a", '+': "%2b", ',': "%2c",
+            "-": c, '.': c, '/': "%2f", ':': "%3a", ';': "%3b", '<': "%3c",
+            "=": "%3d", '>': "%3e", '?': "%3f", '@': "%40", '[': "%5b", '\\': "%5c",
+            "]": "%5d", '^': "%5e", '`': "%60", '{': "%7b", '|': "%7c", '}': "%7d",
+            "~": "%7e"}[c]
+
+def decode(s):
+    if s == '':
+        return ANY
+    if s == '-':
+        return NA
+    s = s.lower()
+    res = ""
+    idx = 0
+    embedded = False
+    while idx < len(s):
+        c = s[idx]
+        if c in ".-~":
+            res += "\\" + c
+            embedded = True
+        elif c != '%':
+            res += c
+            embedded = True
+        else:
+            form = s[idx:idx+3]
+            if form == "%01":
+                if (((idx == 0) or (idx == (len(s) - 3))) or
+                    ( not embedded and (s[idx-4:idx-1] == "%01")) or
+                    (embedded and (len(s) > idx + 6) and (s[idx+3:idx+6] == "%01"))):
+                    res += '?'
+                else:
+                    raise ValueError
+            elif form == "%02":
+                if (idx == 0) or (idx == len(s) - 3):
+                    res += '*'
+                else:
+                    raise ValueError
+            else:
+                res += '\\' + PCT_MAP_i[form]
+            embedded = True
+            idx += 2
+        idx += 1
+    return CPEAttribute(res)
+
+def unbind_value_fs(s):
+    if s == '*':
+        return ANY
+    if s == '-':
+        return NA
+    res = ""
+    idx = 0
+    embedded = False
+    while idx < len(s):
+        c = s[idx]
+        if re.match("[a-zA-Z0-9_]", c) is not None:
+            res += c
+            embedded = True
+        elif c == "\\":
+            res += s[idx:idx+2]
+            embedded = True
+            idx += 1
+        elif c == "*":
+            if (idx == 0) or (idx == (len(s) - 1)):
+                res += c
+                embedded = True
+            else:
+                raise ValueError
+        elif c == "?":
+            if (((idx == 0) or (idx == (len(s) - 1))) or
+                (not embedded and (s[idx - 1] == "?")) or
+                (embedded and (s[idx + 1] == "?"))):
+                res += c
+                embedded = False
+            else:
+                raise ValueError
+        else:
+            res += "\\" + c
+            embedded = True
+        idx += 1
+    return CPEAttribute(res)
+
+class CPEAttribute(object):
+    """ We need a special class to deal with ANY / NA / "string" """
+
+    def __init__(self, value=None, any=False, na=False):
+        self.any = any
+        self.na = na
+        self.value = value
+
+    def bind_for_URI(self):
+#        print self.any, self.na, self.value
+        if self.any:
+            return ""
+        if self.na:
+            return '-'
+        return self.transform_for_uri()
+
+    def transform_for_uri(self):
+        res = ""
+        idx = 0
+        while idx < len(self.value):
+            c = self.value[idx]
+            if re.match("[a-zA-Z0-9_]", c) is not None:
+                res += c
+            elif c == '\\':
+                idx += 1
+                c = self.value[idx]
+                res += pct_encode(c)
+            elif c == '?':
+                res += "%01"
+            elif c == '*':
+                res += "%02"
+            idx += 1
+        return res
+
+    def bind_for_fs(self):
+        if self.any:
+            return "*"
+        if self.na:
+            return "-"
+        return self.process_quoted_chars()
+
+    def process_quoted_chars(self):
+        res = ""
+        idx = 0
+        while idx < len(self.value):
+            c = self.value[idx]
+            if c != '\\':
+                res += c
+            else:
+                idx += 1
+                c = self.value[idx]
+                if c in ".-_":
+                    res += c
+                else:
+                    res += '\\' + c
+            idx += 1
+        return res
+
+ANY = CPEAttribute(any=True)
+NA = CPEAttribute(na=True)
+
+class CPE(object):
+
+    def __init__(self, part=None, vendor=None, product=None, version=None, update=None, edition=None, language=None, sw_edition=None, target_sw=None, target_hw=None, other=None):
+        self.part = part or CPEAttribute(any=True)
+        self.vendor = vendor or CPEAttribute(any=True)
+        self.product = product or CPEAttribute(any=True)
+        self.version = version or CPEAttribute(any=True)
+        self.update = update or CPEAttribute(any=True)
+        self.edition = edition or CPEAttribute(any=True)
+        self.language = language or CPEAttribute(any=True)
+        # Extended attributes:
+        self.sw_edition = sw_edition or CPEAttribute(any=True)
+        self.target_sw = target_sw or CPEAttribute(any=True)
+        self.target_hw = target_hw or CPEAttribute(any=True)
+        self.other = other or CPEAttribute(any=True)
+
+    def bind_to_URI(self):
+        uri = 'cpe:/'
+        uri += ':'.join(a.bind_for_URI() for a in (self.part, self.vendor, self.product, self.version, self.update))
+        # Special handling for edition
+        ed = self.edition.bind_for_URI()
+        sw_ed = self.sw_edition.bind_for_URI()
+        t_sw = self.target_sw.bind_for_URI()
+        t_hw = self.target_hw.bind_for_URI()
+        oth = self.other.bind_for_URI()
+        if sw_ed == "" and t_sw == "" and t_hw == "" and oth == "":
+            uri += ":" + ed
+        else:
+            uri += ":~" + '~'.join([ed, sw_ed, t_sw, t_hw, oth])
+        uri += ':' + self.language.bind_for_URI()
+        return uri.rstrip(':')
+
+    def unbind_URI(self, uri):
+        for idx, comp in enumerate(uri.split(':')):
+            if idx == 0:
+                continue
+            elif idx == 1:
+                self.part = decode(comp[1:])
+            elif idx == 2:
+                self.vendor = decode(comp)
+            elif idx == 3:
+                self.product = decode(comp)
+            elif idx == 4:
+                self.version = decode(comp)
+            elif idx == 5:
+                self.update = decode(comp)
+            elif idx == 6:
+                if comp == "" or comp[0] != '~':
+                    self.edition = decode(comp)
+                else:
+                    ed, sw_ed, t_sw, t_hw, oth = comp[1:].split('~')
+                    self.edition = decode(ed)
+                    self.sw_edition = decode(sw_ed)
+                    self.target_sw = decode(t_sw)
+                    self.target_hw = decode(t_hw)
+                    self.other = decode(oth)
+            elif idx == 7:
+                self.language = decode(comp)
+
+    def bind_to_fs(self):
+        fs = 'cpe:2.3:'
+        fs += ':'.join(a.bind_for_fs() for a in (self.part, self.vendor, self.product, self.version, self.update, self.edition, self.language, self.sw_edition, self.target_sw, self.target_hw, self.other))
+        return fs
+
+    def unbind_fs(self, fs):
+        for idx, v in enumerate(fs.split(':')):
+            v = unbind_value_fs(v)
+            if idx == 2:
+                self.part = v
+            elif idx == 3:
+                self.vendor = v
+            elif idx == 4:
+                self.product = v
+            elif idx == 5:
+                self.version = v
+            elif idx == 6:
+                self.update = v
+            elif idx == 7:
+                self.edition = v
+            elif idx == 8:
+                self.language = v
+            elif idx == 9:
+                self.sw_edition = v
+            elif idx == 10:
+                self.target_sw = v
+            elif idx == 11:
+                self.target_hw = v
+            elif idx == 12:
+                self.other = v
+
+    def addToDoc(self, document, finalProduct=True):
+        """ Add the CPE value as full producttree in the document
+        If finalProduct is false, only the elements leading to the product
+        will be added.
+        """
+        ptree = document._producttree
+        if ptree is None:
+            ptree = document.createProductTree()
+
+        def next_prodid():
+            """ A handy function to generate the next available productid """
+            prods = document._producttree._products
+            if len(prods) > 0:
+                last_prodid = prods[-1]._productid
+                numlen = 0
+                while last_prodid[- (numlen + 1)] in "0123456789":
+                    numlen += 1
+                if numlen != 0:
+                    return last_prodid[:-numlen] + str(int(last_prodid[-numlen:]) + 1)
+            return document.getDocId() + '-P0'
+
+        # Create the main product tree
+        tree = []
+        for value, valtype in [(self.vendor, 'Vendor'),
+                            (self.product, 'Product Name'),
+                            (self.version, 'Product Version'),
+                            (self.update, 'Patch Level'),
+                            (self.language, 'Language'),
+                            (self.target_hw, 'Architecture')]:
+            if value.value is not None:
+                tree.append((valtype, capitalize(value.value)))
+
+        # Import it
+        last_branch = ptree.importTree(tree)
+        # Add a product there
+        if self.target_sw.value is None:
+            if not finalProduct:
+                return last_branch
+            product = CVRFFullProductName(next_prodid(), str(self), last_branch, self.bind_to_fs())
+            ptree.addProduct(product)
+            return product
+        else:
+            product = CVRFFullProductName(next_prodid(), str(self), last_branch)
+            ptree.addProduct(product)
+
+        # We do have a target software, we need to create a relationship !
+        os = CVRFFullProductName(next_prodid(), self.target_sw.value, ptree)
+        ptree.addProduct(os)
+
+        rel = CVRFRelationship(product._productid, 'Installed On', os._productid)
+        ptree.addRelationship(rel)
+        if not finalProduct:
+            return rel
+
+        final_prod = CVRFFullProductName(next_prodid(), ptree.getNameOfRelationship(rel), rel, self.bind_to_fs())
+        ptree.addProduct(final_prod)
+        return final_prod
+
+    def __str__(self):
+        res = []
+        if self.product.value:
+            res.append(capitalize(self.product.value))
+        if self.version.value:
+            res.append(capitalize(self.version.value))
+        if not res:
+            return capitalize(self.vendor.value)
+        return ' '.join(res)
+
+def parse(s):
+    cpe = CPE()
+    if s[:5] == 'cpe:/':
+        cpe.unbind_URI(s)
+    elif s[:8] == 'cpe:2.3:':
+        cpe.unbind_fs(s)
+    else:
+        raise ValueError(s)
+    return cpe
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/farolluz/parsers/cve.py	Tue Dec 30 12:31:50 2014 +0100
@@ -0,0 +1,193 @@
+# -*- coding: utf-8 -*-
+# Description:
+# Methods for parsing CVE XML documents
+#
+# Authors:
+# Benoît Allard <benoit.allard@greenbone.net>
+#
+# Copyright:
+# Copyright (C) 2014 Greenbone Networks GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""\
+Methods for parsing of CVE XML Documents
+
+Ref: http://scap.nist.gov/schema/vulnerability/0.4
+"""
+
+from __future__ import absolute_import
+
+import xml.etree.ElementTree as ET
+
+from .cpe import parse as parseCPE
+from .xml import parseDate
+
+from .. import __version__
+from ..common import CVRFNote, CVRFReference
+from ..document import CVRF, CVRFPublisher, CVRFTracking, CVRFTrackingID, CVRFRevision, CVRFGenerator
+from ..producttree import CVRFFullProductName
+from ..utils import utcnow
+from ..vulnerability import CVRFVulnerability, CVRFCVSSSet, CVRFCWE, CVRFProductStatus
+
+NAMESPACES = {
+    'cve':  "http://scap.nist.gov/schema/feed/vulnerability/2.0",
+    'vuln': "http://scap.nist.gov/schema/vulnerability/0.4",
+    'cvss': "http://scap.nist.gov/schema/cvss-v2/0.2",
+    'xml':  "http://www.w3.org/XML/1998/namespace",
+}
+
+
+def UN(ns, name):
+    """ returns a Universal Name """
+    return "{%s}%s" % (NAMESPACES[ns], name)
+
+def parseCVSS(xmlElem):
+    """ Make a vector out of a list of elements """
+    def get(name):
+        return xmlElem.findtext('/'.join([UN('cvss', 'base_metrics'), UN('cvss', name)]))
+
+    cvss_set = CVRFCVSSSet(float(get('score')))
+    vector = [
+        'AV:%s' % {'LOCAL': 'L',
+                   'ADJACENT_NETWORK': 'A',
+                   'NETWORK': 'N'}[get('access-vector')],
+        'AC:%s' % {'HIGH': 'H',
+                   'MEDIUM': 'M',
+                   'LOW': 'L'}[get('access-complexity')],
+        'Au:%s' % {'MULTIPLE': 'M',
+                   'SINGLE': 'S',
+                   'NONE': 'N'}[get('authentication')],
+        'C:%s' %  {'NONE': 'N',
+                   'PARTIAL': 'P',
+                   'COMPLETE': 'C'}[get('confidentiality-impact')],
+        'I:%s'  % {'NONE': 'N',
+                   'PARTIAL': 'P',
+                   'COMPLETE': 'C'}[get('integrity-impact')],
+        'A:%s'  % {'NONE': 'N',
+                   'PARTIAL': 'P',
+                   'COMPLETE': 'C'}[get('availability-impact')],
+    ]
+    cvss_set.setVector('/'.join(vector))
+    return cvss_set
+
+def parseXML(data):
+    """ returns am ET.Element from the input stuff.
+    input can be:
+      - a string
+      - a file handle
+      - an ET.Element instance
+    """
+    if isinstance(data, ET.Element):
+        return data
+    # To allow passing file handles
+    if hasattr(data, 'read'):
+        data = data.read()
+    # Parse it.
+    return ET.fromstring(data)
+
+def parse_CVE_from_GSA(data):
+    xml = parseXML(data)
+    content = xml.find('/'.join(['get_info', 'get_info_response', 'info', 'cve', 'raw_data', UN('cve', 'entry')]))
+    if content is None:
+        return None
+    return parse(content)
+
+def parse(xml):
+    xml = parseXML(xml)
+
+    # Create an extra-minimal document
+    doc = CVRF(xml.findtext(UN('vuln', 'cve-id')),
+               'Vulnerability Description')
+    pub = CVRFPublisher("Other")
+    doc.setPublisher(pub)
+    now = utcnow()
+    tracking = CVRFTracking(
+        CVRFTrackingID('000000'),
+        "Draft",
+        (0,),
+        now, now
+    )
+    doc.setTracking(tracking)
+    generator = CVRFGenerator()
+    generator.setEngine('FarolLuz ' + __version__)
+    generator.setDate(now)
+    tracking.setGenerator(generator)
+    tracking.addRevision(CVRFRevision((0,), now, 'Document created'))
+
+    # Add the CVE to that document
+    return addToDoc(doc, xml)
+
+def addToDoc(doc, xml):
+    """ Adds the CVE as vulnerability in the document """
+    xml = parseXML(xml)
+
+    vulnid = xml.attrib['id']
+
+    # Get a new ordinal for our new Vulnerability
+    if len(doc._vulnerabilities) == 0:
+        ordinal = 1
+    else:
+        ordinal = doc._vulnerabilities[-1]._ordinal + 1
+
+    # Create a Vulnerability
+    vuln = CVRFVulnerability(ordinal)
+    doc.addVulnerability(vuln)
+
+    vulnerable_products = []
+    # Set the vulnerable products in productTree
+    for i, cpe in enumerate(xml.findall(
+                                '/'.join([UN('vuln', 'vulnerable-software-list'),
+                                          UN('vuln', 'product')]))):
+        prod = parseCPE(cpe.text).addToDoc(doc)
+        vulnerable_products.append(prod)
+
+    if vulnerable_products:
+        status = CVRFProductStatus('Known Affected')
+        for product in vulnerable_products:
+            status.addProductID(product._productid)
+        vuln.addProductStatus(status)
+
+    # Add the CVE-id
+    vuln.setCVE(xml.findtext(UN('vuln', 'cve-id')))
+
+    # The release date
+    vuln.setReleaseDate(parseDate(xml.findtext(UN('vuln', 'published-datetime'))))
+
+    # Add the CVSS
+    xmlcvss = xml.find(UN('vuln', 'cvss'))
+    if xmlcvss is not None:
+        vuln.addCVSSSet(parseCVSS(xmlcvss))
+
+    # Add the CWE id
+    xmlcwe = xml.find(UN('vuln', 'cwe'))
+    if xmlcwe is not None:
+        # XXX: Get a Description for the CWE !
+        vuln.addCWE(CVRFCWE(xmlcwe.attrib['id'], xmlcwe.attrib['id']))
+
+    # Add references
+    for xmlref in xml.findall(UN('vuln', 'references')):
+        vuln.addReference(CVRFReference(xmlref.find(UN('vuln','reference')).attrib['href'],
+                                        xmlref.findtext(UN('vuln', 'reference'))))
+
+    xmlsummary = xml.findtext(UN('vuln', 'summary'))
+    if xmlsummary is not None:
+        vuln.addNote(CVRFNote(
+            'Summary',
+            1,
+            xmlsummary
+        ))
+
+    return doc
--- a/farolluz/parsers/cvrf.py	Tue Dec 23 09:01:38 2014 +0100
+++ b/farolluz/parsers/cvrf.py	Tue Dec 30 12:31:50 2014 +0100
@@ -27,16 +27,13 @@
 """
 
 from __future__ import print_function
+# Allow .xml to be different from xml
+from __future__ import absolute_import
 
-import re
 import textwrap
 import xml.etree.ElementTree as ET
-from datetime import datetime, timedelta
 
-try:
-    from datetime import timezone
-except ImportError:
-    from ..py2 import FixedTimeZone as timezone
+from .xml import parseDate
 
 from ..common import CVRFNote, CVRFAcknowledgment, CVRFReference
 from ..document import (CVRF, CVRFPublisher, CVRFTracking, CVRFRevision,
@@ -63,19 +60,6 @@
     return tuple(int(i) for i in string.split('.'))
 
 
-def parseDate(string):
-    m = re.match('(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:([+-])(\d{2}):(\d{2})|(Z))?', string)
-    if (m.group(7) is None) or (m.group(7) == 'Z'):
-        tzhours = 0
-        tzmin = 0
-    else:
-        tzhours = int(m.group(8))
-        if m.group(7) == '-':
-            tzhours = - tzhours
-        tzmin = int(m.group(9))
-    return datetime(int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4)), int(m.group(5)), int(m.group(6)), tzinfo=timezone(timedelta(hours=tzhours, minutes=tzmin)))
-
-
 def parseNote(elem):
     return CVRFNote(
         elem.attrib['Type'],
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/farolluz/parsers/xml.py	Tue Dec 30 12:31:50 2014 +0100
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+# Description:
+# Methods for parsing CVE XML documents
+#
+# Authors:
+# Benoît Allard <benoit.allard@greenbone.net>
+#
+# Copyright:
+# Copyright (C) 2014 Greenbone Networks GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""\
+Methods for parsing of CVE XML Documents
+
+Ref: http://scap.nist.gov/schema/vulnerability/0.4
+"""
+
+import re
+
+from datetime import datetime, timedelta
+
+try:
+    from datetime import timezone
+except ImportError:
+    from ..py2 import FixedTimeZone as timezone
+
+def parseDate(string):
+    m = re.match('(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:([+-])(\d{2}):(\d{2})|(Z))?', string)
+    if (m.group(7) is None) or (m.group(7) == 'Z'):
+        tzhours = 0
+        tzmin = 0
+    else:
+        tzhours = int(m.group(8))
+        if m.group(7) == '-':
+            tzhours = - tzhours
+        tzmin = int(m.group(9))
+    return datetime(int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4)), int(m.group(5)), int(m.group(6)), tzinfo=timezone(timedelta(hours=tzhours, minutes=tzmin)))
--- a/farolluz/producttree.py	Tue Dec 23 09:01:38 2014 +0100
+++ b/farolluz/producttree.py	Tue Dec 30 12:31:50 2014 +0100
@@ -51,6 +51,12 @@
                 return product
         raise KeyError(productid)
 
+    def getProductForCPE(self, cpe):
+        for product in self._products:
+            if product._cpe == cpe:
+                return product
+        raise KeyError(cpe)
+
     def getGroupForID(self, groupid):
         for group in self._groups:
             if group._groupid == groupid:
@@ -75,6 +81,7 @@
         )
 
     def getBranch(self, path):
+        """ path is a tuple of indexes """
         if len(path) == 0:
             return self
         branches = self._branches
@@ -114,9 +121,10 @@
         The branches that could accept `b2` as new sub-branches
         Note that b2 and all its sub-branches cannot be listed
         """
-        black_list = []
+        black_list = set()
         if b2 is not None:
-            black_list = [b2] + list(b2.getBranches())
+            black_list.add(b2)
+            black_list.update(b2.getBranches())
         for branch in self.getBranches():
             if branch in black_list:
                 continue
@@ -136,6 +144,19 @@
         """ Amount of 'raw' Products """
         return len([p for p in self._products if p._parent is self])
 
+    def importTree(self, tree):
+        """ tree is a list of tuple (type, value) like the one generated by
+        getTree() """
+        if len(tree) == 0:
+            return self
+        found = None
+        for branch in self._branches:
+            if branch.getTree() == tree[:1]:
+                found = branch
+        if found is None:
+            found = CVRFProductBranch(tree[0][0], tree[0][1], self)
+        return found.importTree(tree, 1)
+
     def validate(self):
         for branch in self._branches:
             branch.validate()
@@ -181,7 +202,8 @@
         return self._parentbranch
 
     def getPath(self, string=False):
-        """ return the path to that branch element as a tuple """
+        """ return the path to that branch element as a tuple or as '/'
+        separated string """
         if self.isRoot():
             for i, b in enumerate(self._parentbranch._branches):
                 if b is self:
@@ -233,13 +255,25 @@
         self._parentbranch = None
 
     def link(self, parent):
-        """ Actually, only set the parent """
+        """ Set the parent, and add ourself to our parent's childs """
         self._parentbranch = parent
         if self.isRoot():
             parent._branches.append(self)
         else:
             parent._childs.append(self)
 
+    def importTree(self, tree, index):
+        """ tree is a list of tuple (type, value) like the one generated by
+        getTree() """
+        if len(tree) == index:
+            return self
+        found = None
+        for branch in self._childs:
+            if branch.getTree() == tree[:index + 1]:
+                found = branch
+        if found is None:
+            found = CVRFProductBranch(tree[index][0], tree[index][1], self)
+        return found.importTree(tree, index + 1)
 
     def validate(self):
         if not self._type:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/testCPE.py	Tue Dec 30 12:31:50 2014 +0100
@@ -0,0 +1,249 @@
+# -*- coding: utf-8 -*-
+# Description:
+# Tests for the CPE parsing methods
+#
+# Authors:
+# Benoît Allard <benoit.allard@greenbone.net>
+#
+# Copyright:
+# Copyright (C) 2014 Greenbone Networks GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import unittest
+
+from farolluz.parsers.cpe import CPE, CPEAttribute, parse
+
+class testbindToURI(unittest.TestCase):
+    
+    def test_example1(self):
+        cpe = CPE(part=CPEAttribute('a'), vendor=CPEAttribute('microsoft'), product=CPEAttribute('internet_explorer'), version=CPEAttribute(r'8\.0\.6001'), update=CPEAttribute('beta'), edition=CPEAttribute(any=True))
+
+        self.assertEqual(cpe.bind_to_URI(), 'cpe:/a:microsoft:internet_explorer:8.0.6001:beta')
+    
+    def test_example2(self):
+        cpe = CPE(part=CPEAttribute('a'), vendor=CPEAttribute('microsoft'), product=CPEAttribute('internet_explorer'), version=CPEAttribute(r'8\.*'), update=CPEAttribute('sp?'))
+
+        self.assertEqual(cpe.bind_to_URI(), 'cpe:/a:microsoft:internet_explorer:8.%02:sp%01')
+
+    def test_example3(self):
+        cpe = CPE(part=CPEAttribute('a'), vendor=CPEAttribute('hp'), product=CPEAttribute('insight_diagnostics'), version=CPEAttribute(r'7\.4\.0\.1570'), update=CPEAttribute(na=True), sw_edition=CPEAttribute('online'), target_sw=CPEAttribute('win2003'), target_hw=CPEAttribute("x64"))
+
+        self.assertEqual(cpe.bind_to_URI(), 'cpe:/a:hp:insight_diagnostics:7.4.0.1570:-:~~online~win2003~x64~')
+
+    def test_example4(self):
+        cpe = CPE(part=CPEAttribute('a'), vendor=CPEAttribute('hp'), product=CPEAttribute('openview_network_manager'), version=CPEAttribute(r'7\.51'), target_sw=CPEAttribute('linux'))
+
+        self.assertEqual(cpe.bind_to_URI(), 'cpe:/a:hp:openview_network_manager:7.51::~~~linux~~')
+
+    def test_example5(self):
+        cpe = CPE(part=CPEAttribute('a'), vendor=CPEAttribute(r'foo\\bar'), product=CPEAttribute(r'big\$money_manager_2010'), sw_edition=CPEAttribute('special'), target_sw=CPEAttribute('ipod_touch'), target_hw=CPEAttribute("80gb"))
+
+        self.assertEqual(cpe.bind_to_URI(), 'cpe:/a:foo%5cbar:big%24money_manager_2010:::~~special~ipod_touch~80gb~')
+
+class testunbindURI(unittest.TestCase):
+
+    def test_example1(self):
+        cpe = CPE()
+        cpe.unbind_URI('cpe:/a:microsoft:internet_explorer:8.0.6001:beta')
+        self.assertEqual(cpe.part.value, 'a')
+        self.assertEqual(cpe.vendor.value, 'microsoft')
+        self.assertEqual(cpe.product.value, 'internet_explorer')
+        self.assertEqual(cpe.version.value, r'8\.0\.6001')
+        self.assertEqual(cpe.update.value, 'beta')
+        self.assertTrue(cpe.edition.any)
+        self.assertTrue(cpe.language.any)
+
+    def test_example2(self):
+        cpe = CPE()
+        cpe.unbind_URI('cpe:/a:microsoft:internet_explorer:8.%2a:sp%3f')
+        self.assertEqual(cpe.part.value, 'a')
+        self.assertEqual(cpe.vendor.value, 'microsoft')
+        self.assertEqual(cpe.product.value, 'internet_explorer')
+        self.assertEqual(cpe.version.value, r'8\.\*')
+        self.assertEqual(cpe.update.value, 'sp\?')
+        self.assertTrue(cpe.edition.any)
+        self.assertTrue(cpe.language.any)
+
+    def test_example3(self):
+        cpe = CPE()
+        cpe.unbind_URI('cpe:/a:microsoft:internet_explorer:8.%02:sp%01')
+        self.assertEqual(cpe.part.value, 'a')
+        self.assertEqual(cpe.vendor.value, 'microsoft')
+        self.assertEqual(cpe.product.value, 'internet_explorer')
+        self.assertEqual(cpe.version.value, r'8\.*')
+        self.assertEqual(cpe.update.value, 'sp?')
+        self.assertTrue(cpe.edition.any)
+        self.assertTrue(cpe.language.any)
+
+    def test_example4(self):
+        cpe = CPE()
+        cpe.unbind_URI('cpe:/a:hp:insight_diagnostics:7.4.0.1570::~~online~win2003~x64~')
+        self.assertEqual(cpe.part.value, 'a')
+        self.assertEqual(cpe.vendor.value, 'hp')
+        self.assertEqual(cpe.product.value, 'insight_diagnostics')
+        self.assertEqual(cpe.version.value, '7\.4\.0\.1570')
+        self.assertTrue(cpe.update.any)
+        self.assertTrue(cpe.edition.any)
+        self.assertEqual(cpe.sw_edition.value, 'online')
+        self.assertEqual(cpe.target_sw.value, 'win2003')
+        self.assertEqual(cpe.target_hw.value, 'x64')
+        self.assertTrue(cpe.other.any)
+        self.assertTrue(cpe.language.any)
+
+    def test_example5(self):
+        cpe = CPE()
+        cpe.unbind_URI('cpe:/a:hp:openview_network_manager:7.51:-:~~~linux~~')
+        self.assertEqual(cpe.part.value, 'a')
+        self.assertEqual(cpe.vendor.value, 'hp')
+        self.assertEqual(cpe.product.value, 'openview_network_manager')
+        self.assertEqual(cpe.version.value, '7\.51')
+        self.assertTrue(cpe.update.na)
+        self.assertTrue(cpe.edition.any)
+        self.assertTrue(cpe.sw_edition.any)
+        self.assertEqual(cpe.target_sw.value, 'linux')
+        self.assertTrue(cpe.target_hw.any)
+        self.assertTrue(cpe.other.any)
+        self.assertTrue(cpe.language.any)
+
+
+    def test_example6(self):
+        cpe = CPE()
+        self.assertRaises(KeyError, cpe.unbind_URI, 'cpe:/a:foo%5cbar:big%24money_2010%07:::~~special~ipod_touch~80gb~')
+
+    
+    def test_example7(self):
+        cpe = CPE()
+        cpe.unbind_URI('cpe:/a:foo~bar:big%7emoney_2010')
+        self.assertEqual(cpe.part.value, 'a')
+        self.assertEqual(cpe.vendor.value, 'foo\~bar')
+        self.assertEqual(cpe.product.value, 'big\~money_2010')
+        self.assertTrue(cpe.version.any)
+        self.assertTrue(cpe.update.any)
+        self.assertTrue(cpe.edition.any)
+        self.assertTrue(cpe.language.any)
+
+    def test_example8(self):
+        cpe = CPE()
+        self.assertRaises(ValueError, cpe.unbind_URI, 'cpe:/a:foo:bar:12.%02.1234')
+
+class testbindFS(unittest.TestCase):
+
+    def test_example1(self):
+        cpe = CPE(part=CPEAttribute('a'), vendor=CPEAttribute('microsoft'), product=CPEAttribute('internet_explorer'), version=CPEAttribute(r'8\.0\.6001'), update=CPEAttribute('beta'), edition=CPEAttribute(any=True))
+
+        self.assertEqual(cpe.bind_to_fs(), 'cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:*:*:*:*:*')
+
+    def test_example2(self):
+        cpe = CPE(part=CPEAttribute('a'), vendor=CPEAttribute('microsoft'), product=CPEAttribute('internet_explorer'), version=CPEAttribute(r'8\.*'), update=CPEAttribute('sp?'), edition=CPEAttribute(any=True))
+
+        self.assertEqual(cpe.bind_to_fs(), 'cpe:2.3:a:microsoft:internet_explorer:8.*:sp?:*:*:*:*:*:*')
+
+        cpe.version = CPEAttribute(r'8\.\*')
+
+        self.assertEqual(cpe.bind_to_fs(), 'cpe:2.3:a:microsoft:internet_explorer:8.\*:sp?:*:*:*:*:*:*')
+
+    def test_example3(self):
+        cpe = CPE(part=CPEAttribute('a'), vendor=CPEAttribute('hp'), product=CPEAttribute('insight'), version=CPEAttribute(r'7\.4\.0\.1570'), update=CPEAttribute(na=True), sw_edition=CPEAttribute("online"), target_sw=CPEAttribute("win2003"), target_hw=CPEAttribute("x64"))
+
+        self.assertEqual(cpe.bind_to_fs(), 'cpe:2.3:a:hp:insight:7.4.0.1570:-:*:*:online:win2003:x64:*')
+    
+    def test_example4(self):
+        cpe = CPE(part=CPEAttribute('a'), vendor=CPEAttribute('hp'), product=CPEAttribute('openview_network_manager'), version=CPEAttribute(r'7\.51'), target_sw=CPEAttribute('linux'))
+
+        self.assertEqual(cpe.bind_to_fs(), 'cpe:2.3:a:hp:openview_network_manager:7.51:*:*:*:*:linux:*:*')
+
+    def test_example5(self):
+        cpe = CPE(part=CPEAttribute('a'), vendor=CPEAttribute(r'foo\\bar'), product=CPEAttribute(r'big\$money_2010'), sw_edition=CPEAttribute('special'), target_sw=CPEAttribute('ipod_touch'), target_hw=CPEAttribute("80gb"))
+
+        self.assertEqual(cpe.bind_to_fs(), r'cpe:2.3:a:foo\\bar:big\$money_2010:*:*:*:*:special:ipod_touch:80gb:*')
+
+class testunbind_fs(unittest.TestCase):
+
+    def test_example1(self):
+        cpe = CPE()
+        cpe.unbind_fs('cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:*:*:*:*:*')
+        self.assertEqual(cpe.part.value, 'a')
+        self.assertEqual(cpe.vendor.value, 'microsoft')
+        self.assertEqual(cpe.product.value, 'internet_explorer')
+        self.assertEqual(cpe.version.value, '8\.0\.6001')
+        self.assertEqual(cpe.update.value, "beta")
+        self.assertTrue(cpe.edition.any)
+        self.assertTrue(cpe.sw_edition.any)
+        self.assertTrue(cpe.target_sw.any)
+        self.assertTrue(cpe.target_hw.any)
+        self.assertTrue(cpe.other.any)
+        self.assertTrue(cpe.language.any)
+
+    def test_example2(self):
+        cpe = CPE()
+        cpe.unbind_fs('cpe:2.3:a:microsoft:internet_explorer:8.*:sp?:*:*:*:*:*:*')
+        self.assertEqual(cpe.part.value, 'a')
+        self.assertEqual(cpe.vendor.value, 'microsoft')
+        self.assertEqual(cpe.product.value, 'internet_explorer')
+        self.assertEqual(cpe.version.value, '8\.*')
+        self.assertEqual(cpe.update.value, "sp?")
+        self.assertTrue(cpe.edition.any)
+        self.assertTrue(cpe.sw_edition.any)
+        self.assertTrue(cpe.target_sw.any)
+        self.assertTrue(cpe.target_hw.any)
+        self.assertTrue(cpe.other.any)
+        self.assertTrue(cpe.language.any)
+
+    def test_example3(self):
+        cpe = CPE()
+        cpe.unbind_fs('cpe:2.3:a:hp:insight_diagnostics:7.4.0.1570:-:*:*:online:win2003:x64:*')
+        self.assertEqual(cpe.part.value, 'a')
+        self.assertEqual(cpe.vendor.value, 'hp')
+        self.assertEqual(cpe.product.value, 'insight_diagnostics')
+        self.assertEqual(cpe.version.value, '7\.4\.0\.1570')
+        self.assertTrue(cpe.update.na)
+        self.assertTrue(cpe.edition.any)
+        self.assertEqual(cpe.sw_edition.value, "online")
+        self.assertEqual(cpe.target_sw.value, "win2003")
+        self.assertEqual(cpe.target_hw.value, "x64")
+        self.assertTrue(cpe.other.any)
+        self.assertTrue(cpe.language.any)
+
+        self.assertRaises(ValueError, cpe.unbind_fs, 'cpe:2.3:a:hp:insight_diagnostics:7.4.*.1570:*:*:*:*:*:*')
+
+
+    def test_example4(self):
+        cpe = CPE()
+        cpe.unbind_fs(r'cpe:2.3:a:foo\\bar:big\$money:2010:*:*:*:special:ipod_touch:80gb:*')
+        self.assertEqual(cpe.part.value, 'a')
+        self.assertEqual(cpe.vendor.value, r'foo\\bar')
+        self.assertEqual(cpe.product.value, 'big\$money')
+        self.assertEqual(cpe.version.value, '2010')
+        self.assertTrue(cpe.update.any)
+        self.assertTrue(cpe.edition.any)
+        self.assertEqual(cpe.sw_edition.value, "special")
+        self.assertEqual(cpe.target_sw.value, "ipod_touch")
+        self.assertEqual(cpe.target_hw.value, "80gb")
+        self.assertTrue(cpe.other.any)
+        self.assertTrue(cpe.language.any)
+
+class testParse(unittest.TestCase):
+
+    def testURI(self):
+        cpe = parse('cpe:/a:fogproject:fog:0.31')
+        self.assertEqual(cpe.part.value, 'a')
+
+    def testFS(self):
+        cpe = parse('cpe:2.3:a:tenable:web_ui:2.3.3:*:*:*:*:*:*')
+        self.assertEqual(cpe.vendor.value, 'tenable')
+
+    def testGarbage(self):
+        self.assertRaises(ValueError, parse, 'garbage')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/testParseCVE.py	Tue Dec 30 12:31:50 2014 +0100
@@ -0,0 +1,81 @@
+import utils
+
+from farolluz.parsers.cve import parse
+
+FULL_CVE = """\
+<entry xmlns:scap-core="http://scap.nist.gov/schema/scap-core/0.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:patch="http://scap.nist.gov/schema/patch/0.1" xmlns:vuln="http://scap.nist.gov/schema/vulnerability/0.4" xmlns:cvss="http://scap.nist.gov/schema/cvss-v2/0.2" xmlns:cpe-lang="http://cpe.mitre.org/language/2.0" xmlns="http://scap.nist.gov/schema/feed/vulnerability/2.0" id="CVE-2014-7088">
+<vuln:vulnerable-configuration id="http://nvd.nist.gov/">
+<cpe-lang:logical-test operator="OR" negate="false">
+<cpe-lang:fact-ref name="cpe:/a:jdm_lifestyle_project:jdm_lifestyle:6.4::~~~android~~"/>
+</cpe-lang:logical-test>
+</vuln:vulnerable-configuration>
+<vuln:vulnerable-software-list>
+<vuln:product>
+cpe:/a:jdm_lifestyle_project:jdm_lifestyle:6.4::~~~android~~
+</vuln:product>
+</vuln:vulnerable-software-list>
+<vuln:cve-id>CVE-2014-7088</vuln:cve-id>
+<vuln:published-datetime>2014-10-18T21:55:17.027-04:00</vuln:published-datetime>
+<vuln:last-modified-datetime>2014-11-14T09:07:51.650-05:00</vuln:last-modified-datetime>
+<vuln:cvss>
+<cvss:base_metrics>
+<cvss:score>5.4</cvss:score>
+<cvss:access-vector>ADJACENT_NETWORK</cvss:access-vector>
+<cvss:access-complexity>MEDIUM</cvss:access-complexity>
+<cvss:authentication>NONE</cvss:authentication>
+<cvss:confidentiality-impact>PARTIAL</cvss:confidentiality-impact>
+<cvss:integrity-impact>PARTIAL</cvss:integrity-impact>
+<cvss:availability-impact>PARTIAL</cvss:availability-impact>
+<cvss:source>http://nvd.nist.gov</cvss:source>
+<cvss:generated-on-datetime>2014-11-14T09:07:51.290-05:00</cvss:generated-on-datetime>
+</cvss:base_metrics>
+</vuln:cvss>
+<vuln:cwe id="CWE-310"/>
+<vuln:references reference_type="UNKNOWN" xml:lang="en">
+<vuln:source>CERT-VN</vuln:source>
+<vuln:reference href="http://www.kb.cert.org/vuls/id/582497" xml:lang="en">VU#582497</vuln:reference>
+</vuln:references>
+<vuln:references reference_type="UNKNOWN" xml:lang="en">
+<vuln:source>MISC</vuln:source>
+<vuln:reference href="https://docs.google.com/spreadsheets/d/1t5GXwjw82SyunALVJb2w0zi3FoLRIkfGPc7AMjRF0r4/edit?usp=sharing" xml:lang="en">
+https://docs.google.com/spreadsheets/d/1t5GXwjw82SyunALVJb2w0zi3FoLRIkfGPc7AMjRF0r4/edit?usp=sharing
+</vuln:reference>
+</vuln:references>
+<vuln:summary>
+The JDM Lifestyle (aka com.hondatech) application 6.4 for Android does not verify X.509 certificates from SSL servers, which allows man-in-the-middle attackers to spoof servers and obtain sensitive information via a crafted certificate.
+</vuln:summary>
+</entry>"""
+
+CVE_NO_CVSS = """\
+<entry xmlns:scap-core="http://scap.nist.gov/schema/scap-core/0.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:patch="http://scap.nist.gov/schema/patch/0.1" xmlns:vuln="http://scap.nist.gov/schema/vulnerability/0.4" xmlns:cvss="http://scap.nist.gov/schema/cvss-v2/0.2" xmlns:cpe-lang="http://cpe.mitre.org/language/2.0" xmlns="http://scap.nist.gov/schema/feed/vulnerability/2.0" id="CVE-2014-9388">
+<vuln:cve-id>CVE-2014-9388</vuln:cve-id>
+<vuln:published-datetime>2014-12-17T14:59:08.587-05:00</vuln:published-datetime>
+<vuln:last-modified-datetime>2014-12-17T14:59:09.620-05:00</vuln:last-modified-datetime>
+<vuln:references reference_type="UNKNOWN" xml:lang="en">
+<vuln:source>CONFIRM</vuln:source>
+<vuln:reference href="https://www.mantisbt.org/bugs/view.php?id=17878" xml:lang="en">https://www.mantisbt.org/bugs/view.php?id=17878</vuln:reference>
+</vuln:references>
+<vuln:references reference_type="UNKNOWN" xml:lang="en">
+<vuln:source>CONFIRM</vuln:source>
+<vuln:reference href="https://www.mantisbt.org/bugs/changelog_page.php?version_id=191" xml:lang="en">
+https://www.mantisbt.org/bugs/changelog_page.php?version_id=191
+</vuln:reference>
+</vuln:references>
+<vuln:references reference_type="UNKNOWN" xml:lang="en">
+<vuln:source>MLIST</vuln:source>
+<vuln:reference href="http://seclists.org/oss-sec/2014/q4/955" xml:lang="en">[oss-security] 20141207 MantisBT 1.2.18 Released</vuln:reference>
+</vuln:references>
+<vuln:summary>
+bug_report.php in MantisBT before 1.2.18 allows remote attackers to assign arbitrary issues via the handler_id parameter.
+</vuln:summary>
+</entry>"""
+
+class testCVEParsing(utils.TestCase):
+
+    def test_Full(self):
+        self.doc = parse(FULL_CVE)
+        self._validate()
+
+    def test_no_CVSS(self):
+        self.doc = parse(CVE_NO_CVSS)
+        self._validate()
This site is hosted by Intevation GmbH (Datenschutzerklärung und Impressum | Privacy Policy and Imprint)