#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Compatibility with vulnerability standards.
"""

__license__ = """
GoLismero 2.0 - The web knife - Copyright (C) 2011-2014

Golismero project site: https://github.com/golismero
Golismero project mail: contact@golismero-project.com

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 Street, Fifth Floor, Boston, MA  02110-1301, USA.
"""

__all__ = [
    "TAXONOMY_NAMES", "extract_vuln_ids",
    "convert_references_to_vuln_ids", "convert_vuln_ids_to_references",
    "get_cpe_version", "parse_cpe", "unparse_cpe23", "cpe22to23",
    "CVSS_Base", "CVSS",
]

import re


#------------------------------------------------------------------------------

# Vulnerability ID types.
TAXONOMY_NAMES = {
    "bid":      "Bugtraq",
    "ca":       "CERT Advisory",
    "capec":    "CAPEC",
    "cisco":    "Cisco Security Advisory",
    "cve":      "CVE",
    "cwe":      "CWE",
    "dsa":      "Debian Security Advisory",
    "edb":      "ExploitDB",
    "glsa":     "Gentoo Linux Security Advisory",
    "mdvsa":    "Mandriva Security Advisory",
    "ms":       "Microsoft Advisory",
    "mskb":     "Microsoft Knowledge Base",
    "nessus":   "Nessus Plugin",
    "osvdb":    "OSVDB",
    "rhsa":     "RedHat Security Advisory",
    "sa":       "Secunia Advisory",
    "sectrack": "Security Tracker",
    "usn":      "Ubuntu Security Notice",
    "vmsa":     "VMWare Security Advisory",
    "vu":       "CERT Vulnerability Note",
    "xf":       "ISS X-Force",
}


#------------------------------------------------------------------------------

# ID extraction from plain text.
_vuln_id_regex = {
    "bid": [
        re.compile(
            r"\b(?:BID\-|BID\: ?|BUGTRAQ\-|BUGTRAQ\: ?|BUGTRAQ ID: ?)"
            r"([0-9]+)\b"),
    ],
    "ca": [
        re.compile(r"\bCA\-([0-9][0-9][0-9][0-9]\-[0-9][0-9])\b"),
    ],
    "capec": [
        re.compile(r"\bCAPEC(?:\-|\: ?)([0-9]+)\b"),
    ],
    "cisco": [
        re.compile(r"\bcisco\-(sa\-[0-9]+\-[a-z]+)\b"),
    ],
    "cve": [
        re.compile(
            r"\bCVE\-([0-9][0-9][0-9][0-9]\-[0-9][0-9][0-9][0-9][0-9]?)\b"),
    ],
    "cwe": [
        re.compile(r"\bCWE(?:\-|\: ?)([0-9]+)\b"),
    ],
    "dsa": [
        re.compile(r"\bDSA\-[0-9][0-9][0-9][0-9]?\b"),
    ],
    "edb": [
        re.compile(r"\bEDB\-ID\: ?([0-9]+)\b"),
        re.compile(r"\bEDB(?:\-|\: ?)([0-9]+)\b"),
    ],
    "glsa": [
        re.compile(r"\b(?:GLSA|glsa)(?:\-|\: ?)([0-9]+\-[0-9]+)\b"),
    ],
    "mdvsa": [
        re.compile(
            r"\bMDVSA\-([0-9][0-9][0-9][0-9]\:[0-9][0-9][0-9][0-9]?)\b"),
    ],
    "ms": [
        re.compile(r"\bMS([0-9][0-9]\-[0-9][0-9][0-9])\b"),
    ],
    "mskb": [
        re.compile(r"\b(?:MS)?KB(?:\-|\: ?)([0-9]+)\b"),
    ],
    "nessus": [
        re.compile(r"\bNESSUS(?:\-|\: ?)([0-9]+)\b"),
    ],
    "osvdb": [
        re.compile(r"\b(?:OSVDB\-|OSVDB\: ?|OSVDB ID\: ?)([0-9]+)\b"),
    ],
    "rhsa": [
        re.compile(r"\bRHSA(?: |\-|\: ?)"
                   r"([0-9][0-9][0-9][0-9][\-\:][0-9][0-9][0-9][0-9]?)\b"),
    ],
    "sa": [
        re.compile(r"\b(?:SECUNIA|SA)(?:\-|\: ?)([0-9]+)\b"),
        re.compile(r"\bSA([0-9]+)\b"),
    ],
    "sectrack": [
        re.compile(r"\b(?:SECTRACK\-|SECTRACK\: ?|SECTRACK ID\: ?)([0-9]+)\b"),
    ],
    "usn": [
        re.compile(r"\bUSN(?:\-|\: ?)([0-9]+\-[0-9]+)\b"),
    ],
    "vmsa": [
        re.compile(r"\bVMSA\-([0-9][0-9][0-9][0-9]\-[0-9][0-9][0-9][0-9])\b"),
    ],
    "vu": [
        re.compile(r"\bVU[\#\-]([0-9]+)\b"),
    ],
    "xf": [
        re.compile(r"\bXF\: ?[a-z0-9\-]* ?\(([0-9]+)\)(?:[^\w]|$)"),
        re.compile(r"\bXF\-([0-9]+)\b"),
    ],
}

# ID extraction from URLs.
_vuln_ref_regex = {
    "bid": [
        re.compile(
            r"^https?\:\/\/(?:www\.)?securityfocus\.com\/bid\/([0-9]+)$"),
    ],
    "ca": [
        re.compile(
            r"^https?\:\/\/(?:www\.)?cert\.org\/advisories\/"
            r"CA\-([0-9][0-9][0-9][0-9]\-[0-9][0-9])\.html$"),
    ],
    "capec": [
        re.compile(
            r"^https?\:\/\/capec\.mitre\.org\/data\/definitions\/"
            r"([0-9]+)\.html$"),
        re.compile(
            r"^https?\:\/\/www\.security\-database\.com\/capec\.php\?"
            r"name\=CAPEC-([0-9]+)$"),
    ],
    "cisco": [
        re.compile(
            r"^https?\:\/\/tools\.cisco\.com\/security\/center\/content\/"
            r"CiscoSecurityAdvisory\/cisco\-(sa\-[^\?]+)$"),
        re.compile(
            r"^https?\:\/\/www\.security\-database\.com\/(?:detail|cvss)\.php"
            r"\?alert\=cisco\-(sa\-[^\?]+)$"),
    ],
    "cve": [
        re.compile(
            r"^https?\:\/\/cve\.mitre\.org\/cgi\-bin\/cvename\.cgi\?name\="
            r"(?:CVE\-)?([0-9][0-9][0-9][0-9]\-[0-9][0-9][0-9][0-9][0-9]?)$"),
        re.compile(
            r"^https?\:\/\/nvd\.nist\.gov\/nvd\.cfm\?cvename\="
            r"(?:CVE\-)?([0-9][0-9][0-9][0-9]\-[0-9][0-9][0-9][0-9][0-9]?)$"),
        re.compile(
            r"^https?\:\/\/web\.nvd\.nist\.gov\/view\/vuln\/detail\?vulnId\="
            r"(?:CVE\-)?([0-9][0-9][0-9][0-9]\-[0-9][0-9][0-9][0-9][0-9]?)$"),
        re.compile(
            r"^https?\:\/\/www\.security\-database\.com\/(?:detail|cvss)\.php"
            r"\?alert\=CVE\-"
            r"([0-9][0-9][0-9][0-9]\-[0-9][0-9][0-9][0-9][0-9]?)$"),
    ],
    "cwe": [
        re.compile(
        r"^https?\:\/\/cwe\.mitre\.org\/data\/definitions\/([0-9]+)\.html$"),
        re.compile(
            r"^https?\:\/\/www\.security\-database\.com\/cwe\.php\?"
            r"name\=CWE-([0-9]+)$"),
    ],
    "dsa": [
        re.compile(
            r"^https?\:\/\/www\.debian\.org\/security\/2000\/"
            r"(2000[0-9][0-9][0-9][0-9][a-z]?)$"),
        re.compile(
            r"^https?\:\/\/www\.debian\.org\/security\/"
            r"dsa\-([0-9][0-9][0-9][0-9]?)$"),
        re.compile(
            r"^https?\:\/\/www\.debian\.org\/security\/20[0-9][0-9]\/"
            r"dsa\-([0-9][0-9][0-9][0-9]?)$"),
        re.compile(
            r"^https?\:\/\/www\.security\-database\.com\/(?:detail|cvss)\.php"
            r"\?alert\=DSA\-([0-9][0-9][0-9][0-9]?)$"),
    ],
    "edb": [
        re.compile(
            r"^https?\:\/\/www\.exploit\-db\.com\/exploits\/([0-9]+)/?$"),
    ],
    "glsa": [
        re.compile(r"^https?\:\/\/security\.gentoo\.org\/glsa\/"
                   r"glsa\-([0-9]+\-[0-9]+)\.xml$"),
        re.compile(r"^https?\:\/\/www\.gentoo\.org\/security\/[a-z]+\/glsa\/"
                   r"glsa\-([0-9]+\-[0-9]+)\.xml$"),
        re.compile(
            r"^https?\:\/\/www\.security\-database\.com\/(?:detail|cvss)\.php"
            r"\?alert\=GLSA\-([0-9]+\-[0-9]+)$"),
    ],
    "mdvsa": [
        re.compile(
            r"^https?\:\/\/www\.mandriva\.com\/security\/advisories\?name\="
            r"MDVSA\-([0-9][0-9][0-9][0-9]\:[0-9][0-9][0-9][0-9]?)$"),
        re.compile(
            r"^https?\:\/\/www\.mandriva\.com\/(?:[a-z][a-z]\/)?support\/"
            r"security\/advisories\/advisory\/"
            r"MDVSA\-([0-9][0-9][0-9][0-9]\:[0-9][0-9][0-9][0-9]?)"
            r"/?(?:\?.*)?$"),
        re.compile(
            r"^https?\:\/\/www\.security\-database\.com\/(?:detail|cvss)\.php"
            r"\?alert\=MDVSA\-([0-9][0-9][0-9][0-9]\:[0-9][0-9][0-9][0-9]?)$"),
    ],
    "ms": [
        re.compile(
            r"^https?\:\/\/technet\.microsoft\.com\/"
            r"[A-Za-z][A-Za-z]\-[A-Za-z][A-Za-z]\/security\/bulletin\/"
            r"[Mm][Ss]([0-9][0-9]\-[0-9][0-9][0-9])$"),
        re.compile(
            r"^https?\:\/\/(?:www\.)?microsoft\.com\/"
            r"technet\/security\/bulletin\/"
            r"[Mm][Ss]([0-9][0-9]\-[0-9][0-9][0-9])\.(?:asp|mspx)$"),
        re.compile(
            r"^https?\:\/\/www\.security\-database\.com\/(?:detail|cvss)\.php"
            r"\?alert\=MS([0-9][0-9]\-[0-9][0-9][0-9])$"),
    ],
    "mskb": [
        re.compile(r"^https?\:\/\/support\.microsoft\.com\/kb\/([0-9]+)"
                   r"(?:\/[A-Za-z][A-Za-z]\-[A-Za-z][A-Za-z])?$"),
        re.compile(r"^https?\:\/\/support\.microsoft\.com\/default\.aspx\?"
                   r"scid\=kb\;[A-Z][A-Z]\-[A-Z][A-Z]\;([0-9]+)$"),
        re.compile(
            r"^https?\:\/\/www\.security\-database\.com\/(?:detail|cvss)\.php"
            r"\?alert\=KB([0-9]+)$"),
    ],
    "nessus": [
        re.compile(
            r"^https?\:\/\/(?:www\.)?tenable.com\/plugins\/index\.php\?"
            r"id\=([0-9]+)\&view\=single$"),
        re.compile(
            r"^https?\:\/\/(?:www\.)?tenable.com\/plugins\/index\.php\?"
            r"view\=single\&id\=([0-9]+)$"),
    ],
    "osvdb": [
        re.compile(
            r"^https?\:\/\/(?:www\.)?osvdb\.org\/show\/osvdb\/([0-9]+)$"),
    ],
    "rhsa": [
        re.compile(r"^https?\:\/\/rhn\.redhat\.com\/errata\/RHSA\-"
                   r"([0-9][0-9][0-9][0-9]\-[0-9][0-9][0-9][0-9]?).html$"),
        re.compile(
            r"^https?\:\/\/www\.security\-database\.com\/(?:detail|cvss)\.php"
            r"\?alert\=RHSA\-([0-9][0-9][0-9][0-9]\:[0-9][0-9][0-9][0-9]?)$"),
    ],
    "sa": [
        re.compile(
            r"^https?\:\/\/(?:www\.)?secunia\.com\/advisories\/([0-9]+)$"),
    ],
    "sectrack": [
        re.compile(
            r"^https?\:\/\/(?:www\.)?securitytracker\.com\/id(?:\?|\/)"
            r"([0-9]+)$"),
        re.compile(
            r"^https?\:\/\/(?:www\.)?securitytracker\.com\/alerts"
            r"\/[0-9]+\/[A-Za-z]+\/([0-9]+)\.html$"),
    ],
    "vmsa": [
        re.compile(
            r"^https?\:\/\/www\.vmware\.com\/security\/advisories\/"
            r"VMSA\-([0-9][0-9][0-9][0-9]\-[0-9][0-9][0-9][0-9])\.html$"),
        re.compile(
            r"^https?\:\/\/www\.security\-database\.com\/(?:detail|cvss)\.php"
            r"\?alert\=VMSA\-([0-9][0-9][0-9][0-9]\-[0-9][0-9][0-9][0-9])$"),
    ],
    "vu": [
        re.compile(
            r"^https?\:\/\/(?:www\.)?kb\.cert\.org\/vuls\/id\/([0-9]+)$"),
        re.compile(
            r"^https?\:\/\/www\.security\-database\.com\/(?:detail|cvss)\.php"
            r"\?alert\=VU([0-9]+)$"),
    ],
    "usn": [
        re.compile(
            r"^https?\:\/\/www\.ubuntu\.com\/usn\/USN\-([0-9]+\-[0-9]+)/?$"),
        re.compile(
            r"^https?\:\/\/www\.security\-database\.com\/(?:detail|cvss)\.php"
            r"\?alert\=USN\-([0-9]+\-[0-9]+)$"),
    ],
    "xf": [
        re.compile(
            r"^https?\:\/\/xforce\.iss\.net\/xforce\/xfdb\/([0-9]+)$"),
    ],
}

# URL templates for references to oficial websites.
_vuln_ref_tpl = {
    "bid":      "http://www.securityfocus.com/bid/%s",
    "ca":       "https://www.cert.org/advisories/CA-%s.html",
    "capec":    "https://capec.mitre.org/data/definitions/%s.html",
    "cisco":    "http://tools.cisco.com/security/center/content/"
                "CiscoSecurityAdvisory/cisco-%s",
    "cve":      "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-%s",
    "cwe":      "https://cwe.mitre.org/data/definitions/%s.html",
    "dsa":      "http://www.debian.org/security/dsa-%s",
    "edb":      "http://www.exploit-db.com/exploits/%s/",
    "glsa":     "https://www.gentoo.org/security/en/glsa/glsa-%s.xml",
    "mdvsa":    "http://www.mandriva.com/security/advisories?name=MDVSA-%s",
    "ms":       "https://technet.microsoft.com/en-us/security/bulletin/ms%s",
    "mskb":     "https://support.microsoft.com/kb/%s",
    "nessus":   "http://www.tenable.com/plugins/index.php?view=single&id=%s",
    "osvdb":    "http://osvdb.org/show/osvdb/%s",
    "rhsa":     "https://rhn.redhat.com/errata/RHSA-%s.html",
    "sa":       "http://www.secunia.com/advisories/%s",
    "sectrack": "http://www.securitytracker.com/id?%s",
    "usn":      "http://www.ubuntu.com/usn/USN-%s/",
    "vmsa":     "https://www.vmware.com/security/advisories/VMSA-%s.html",
    "vu":       "https://www.kb.cert.org/vuls/id/%s",
    "xf":       "http://xforce.iss.net/xforce/xfdb/%s",
}

# Conversion table for Debian security advisories prior to 2001.
# There's no way to infer them from the ID.
_vuln_conversion_dsa = {
    "001": "20001129",
    "002": "20001130",
    "003": "20001201",
    "004": "20001217",
    "005": "20001217a",
    "006": "20001219",
    "007": "20001220",
    "008": "20001225",
    "009": "20001225a",
    "010": "20001225b",
}
_vuln_conversion_dsa_back = {v:k for k,v in _vuln_conversion_dsa.iteritems()}
_vuln_ref_tpl_dsa = "http://www.debian.org/security/2000/%s"

# URL templates for Security Database.
_vuln_ref_tpl_sdb = {
    "capec": "https://www.security-database.com/capec.php?name=CAPEC-%s",
    "cisco": "https://www.security-database.com/detail.php?alert=cisco-%s",
    "cve":   "https://www.security-database.com/detail.php?alert=CVE-%s",
    "cwe":   "https://www.security-database.com/cwe.php?name=CWE-%s",
    "dsa":   "https://www.security-database.com/detail.php?alert=DSA-%s",
    "glsa":  "https://www.security-database.com/detail.php?alert=GLSA-%s",
    "mdvsa": "https://www.security-database.com/detail.php?alert=MDVSA-%s",
    "ms":    "https://www.security-database.com/detail.php?alert=MS%s",
    "mskb":  "https://www.security-database.com/detail.php?alert=KB%s",
    "rhsa":  "https://www.security-database.com/detail.php?alert=RHSA-%s",
    "usn":   "https://www.security-database.com/detail.php?alert=USN-%s",
    "vmsa":  "https://www.security-database.com/detail.php?alert=VMSA-%s",
    "vu":    "https://www.security-database.com/detail.php?alert=VU%s",
}

# Some simple checks to prevent silly bugs when adding new taxonomies.
assert set(_vuln_id_regex.iterkeys()) == \
       set(_vuln_ref_regex.iterkeys()) == \
       set(_vuln_ref_tpl.iterkeys())
assert set(_vuln_ref_tpl.iterkeys()).intersection(
    _vuln_ref_tpl_sdb.iterkeys()) == set(_vuln_ref_tpl_sdb.iterkeys())


#------------------------------------------------------------------------------
def extract_vuln_ids(text):
    """
    Extract vulnerability IDs from plain text using regular expressions.

    Currently the following ID types are supported:%s

    Example:

        >>> extract_vuln_ids(
        ... "Here we have CVE-1234-1234 and CVE-4321-4321.\\n"
        ... "We also have a CWE, namely CWE-1234.\\n"
        ... "However we're only mentioning OSVDB, not using it.\\n"
        ... )
        {'cve': ['CVE-1234-1234', 'CVE-4321-4321'], 'cwe': ['CWE-1234']}
        >>> extract_vuln_ids("There is nothing here!")
        {}

    This can be useful when instancing Vulnerability objects:

        >>> from golismero.api.data.vulnerability import UncategorizedVulnerability
        >>> description = "This vulnerability is CVE-1234-4321."
        >>> kwargs = extract_vuln_ids(description)
        >>> kwargs['description'] = description
        >>> vuln = UncategorizedVulnerability( **kwargs )
        >>> vuln.description
        'This vulnerability is CVE-1234-4321.'
        >>> vuln.cve
        ('CVE-1234-4321',)
        >>> vuln.cwe
        ()

    :param text: Plain text to search.
    :type text: str

    :returns: Map of ID type to lists of one or more strings, each string being
        a vulnerability ID of that type. Vulnerability types not found will not
        be present in the dictionary. If no vulnerability IDs were found at
        all, the dictionary will be empty.
    :rtype: dict( str -> list(str, ...) )
    """ # docstring completed later!
    d = {}
    for vuln_type, vuln_re_list in _vuln_id_regex.iteritems():
        found = set()
        for vuln_re in vuln_re_list:
            found.update(vuln_re.findall(text))
        if found:
            prefix = vuln_type.upper()
            if prefix != "MS":
                prefix += "-"
            d[vuln_type] = sorted(
                prefix + vuln_id
                for vuln_id in found
            )
    return d

# Fix the docstring.
extract_vuln_ids.__doc__ %= "".join(
    "\n     - " + x for x in sorted(TAXONOMY_NAMES.itervalues())
)


#------------------------------------------------------------------------------
def convert_references_to_vuln_ids(urls):
    """
    Convert reference URLs to the vulnerability IDs they point to.

    Currently the following ID types are supported:%s

    :param urls: List of URLs to parse. URLs not pointing to one of the
        supported websites are silently ignored.
    :type urls: list(str)

    """ # docstring completed later!
    d = {}
    for vuln_type, vuln_re_list in _vuln_ref_regex.iteritems():
        found = set()
        for vuln_re in vuln_re_list:
            for url in urls:
                found.update(vuln_re.findall(url))
        if found:
            if vuln_type == "rhsa":
                found = {
                    vuln_id.replace(":", "-")
                    for vuln_id in found
                }
            elif vuln_type == "dsa":
                found = {
                    _vuln_conversion_dsa_back.get(vuln_id, vuln_id)
                    for vuln_id in found
                }
            prefix = vuln_type
            if prefix != "cisco":
                prefix = prefix.upper()
            if prefix != "MS":
                prefix += "-"
            d[vuln_type] = sorted(
                prefix + vuln_id
                for vuln_id in found
            )
    return d

# Fix the docstring.
convert_references_to_vuln_ids.__doc__ %= "".join(
    "\n     - " + x for x in sorted(TAXONOMY_NAMES.itervalues())
)
convert_references_to_vuln_ids.__doc__ += \
    extract_vuln_ids.__doc__[extract_vuln_ids.__doc__.rfind(":returns:"):]


#------------------------------------------------------------------------------
def convert_vuln_ids_to_references(vuln_ids):
    """
    Convert vulnerability IDs to reference URLs.

    Currently the following ID types are supported:%s

    :param vuln_ids: Vulnerability IDs.
    :type vuln_ids: list(str)

    :returns: Reference URLs.
    :rtype: list(str)
    """ # docstring completed later!
    refs = []
    for v_id in vuln_ids:
        vuln_type, vuln_id = v_id.split("-", 1)
        upper = vuln_type.upper()
        if upper.startswith("MS") and not upper.startswith("MSKB"):
            vuln_type, vuln_id = vuln_type[:2], vuln_type[2:] + "-" + vuln_id
        lower = vuln_type.lower()
        if lower == "dsa" and vuln_id in _vuln_conversion_dsa:
            refs.append(_vuln_ref_tpl_dsa % _vuln_conversion_dsa[vuln_id])
        elif lower in _vuln_ref_tpl:
            refs.append(_vuln_ref_tpl[lower] % vuln_id)
        if lower in _vuln_ref_tpl_sdb:
            if lower == "rhsa":
                vuln_id = vuln_id.replace("-", ":")
            refs.append(_vuln_ref_tpl_sdb[lower] % vuln_id)
    return refs

# Fix the docstring.
convert_vuln_ids_to_references.__doc__ %= "".join(
    "\n     - " + x for x in sorted(TAXONOMY_NAMES.itervalues())
)


#------------------------------------------------------------------------------
# CPE parsing from:
# https://github.com/MarioVilas/vuln_tools/blob/master/cpe.py

def get_cpe_version(cpe):
    """
    Determine if the given CPE name is following
    version 2.2 or 2.3 of the standard.

    :param cpe: CPE name.
    :type cpe: str

    :returns: CPE standard version ("2.2" or "2.3").
    :rtype: str
    """
    if not isinstance(cpe, basestring):
        raise TypeError("Expected string, got %r instead" % type(cpe))
    if cpe.startswith("cpe:/"):
        return "2.2"
    elif cpe.startswith("cpe:2.3:"):
        return "2.3"
    else:
        raise ValueError("Not a valid CPE name: %s" % cpe)


#------------------------------------------------------------------------------
def cpe22_unquote(s):
    if not s:
        return s
    r = []
    i = -1
    while i < len(s) - 1:
        i += 1
        c = s[i]
        if c == "\\":
            r.append("\\\\")
            continue
        if c != "%":
            r.append(c)
            continue
        h = s[ i + 1 : i + 2 ]
        if len(h) > 0 and h[0] == "%":
            r.append(c)
            i += 1
            continue
        if len(h) != 2 or \
           h[0] not in "0123456789abcdefABCDEF" or \
           h[1] not in "0123456789abcdefABCDEF":
            r.append(c)
            continue
        r.append("\\")
        r.append( chr( int(h, 16) ) )
    return "".join(r)


#------------------------------------------------------------------------------
_cpe23_split = re.compile(r"(?<!\\)\:")
def parse_cpe(cpe):
    """
    Split the given CPE name into its components.

    :param cpe: CPE name.
    :type cpe: str

    :returns: CPE components.
    :rtype: list(str)
    """
    ver = get_cpe_version(cpe)
    if ver == "2.2":
        parsed = [cpe22_unquote(x.strip()) for x in cpe[5:].split(":")]
        if len(parsed) < 11:
            parsed.extend( "*" * (11 - len(parsed)) )
    elif ver == "2.3":
        parsed = [x.strip() for x in _cpe23_split.split(cpe[8:])]
        if len(parsed) != 11:
            raise ValueError("Not a valid CPE 2.3 name: %s" % cpe)
    else:
        raise ValueError("Not a valid CPE 2.2 or 2.3 name: %s" % cpe)
    return parsed


#------------------------------------------------------------------------------
def unparse_cpe23(parsed):
    """
    Join back the components into a CPE 2.3 name.

    :param parsed: Components parsed by parse_cpe().
    :type parsed: list(str)

    :returns: CPE 2.3 name.
    :rtype: str
    """
    return "cpe:2.3:" + ":".join(x.replace(":", r"\:") for x in parsed)


#------------------------------------------------------------------------------
def cpe22to23(cpe):
    """
    Convert a CPE 2.2 name into a CPE 2.3 name.

    :param cpe: CPE 2.2 name.
    :type cpe: str

    :returns: CPE 2.3 name.
    :rtype: str
    """
    return unparse_cpe23( parse_cpe(cpe) )


#------------------------------------------------------------------------------
# CVSS calculator
# https://github.com/MarioVilas/vuln_tools/blob/master/cvss.py

def _p(metric):
    def _g(self):
        return self.get_metric(metric)
    def _s(self, value):
        self.set_metric(metric, value)
    return property(_g, _s)

class cvss_metaclass(type):

    def __init__(cls, name, bases, namespace):
        super(cvss_metaclass, cls).__init__(name, bases, namespace)
        for _m in cls.METRICS:
            setattr(cls, _m, _p(_m))

class CVSS_Base(object):
    "Base CVSS Calculator."

    __metaclass__ = cvss_metaclass

    METRICS = (
        "AV", "AC", "Au",
        "C", "I", "A",
    )

    ADJACENT_NETWORK = "A"
    COMPLETE = "C"
    HIGH = "H"
    LOCAL = "L"
    LOW = "L"
    MEDIUM = "M"
    NETWORK = "N"
    NONE = "N"
    MULTIPLE = "M"
    PARTIAL = "P"
    SINGLE = "S"

    AV_SCORE = {
        LOCAL: 0.395,
        ADJACENT_NETWORK: 0.646,
        NETWORK: 1.0,
    }

    AC_SCORE = {
        HIGH: 0.35,
        MEDIUM: 0.61,
        LOW: 0.71,
    }

    Au_SCORE = {
        MULTIPLE: 0.45,
        SINGLE: 0.56,
        NONE: 0.704,
    }

    C_SCORE = {
        NONE: 0.0,
        PARTIAL: 0.275,
        COMPLETE: 0.66,
    }
    I_SCORE = C_SCORE
    A_SCORE = C_SCORE

    def get_metric(self, metric):
        return getattr(self, "_CVSS_Base__" + metric)

    def set_metric(self, metric, value):
        try:
            scores = getattr(self, metric + "_SCORE")
        except AttributeError:
            raise ValueError("Invalid metric: %r" % (metric,))
        try:
            score = scores[value]
        except KeyError:
            if value in scores.values():
                score = value
            else:
                raise ValueError("Invalid %s value: %r" % (metric, value))
        setattr(self, "_CVSS_Base__" + metric, score)

    @property
    def vector(self):
        vector = []
        for metric in self.METRICS:
            value = self.get_metric(metric)
            scores = getattr(self, metric + "_SCORE")
            found = False
            for name, candidate in scores.iteritems():
                if value == candidate:
                    vector.append("%s:%s" % (metric, name))
                    found = True
                    break
            assert found, "Internal error while calculating CVSS base vector"
        return "/".join(vector)

    @vector.setter
    def vector(self, vector):
        try:
            old_vector = self.vector
        except Exception:
            old_vector = None
        try:
            for metric_and_value in vector.split("/"):
                metric_and_value = metric_and_value.strip()
                if not metric_and_value:
                    continue
                metric, value = metric_and_value.split(":", 1)
                self.set_metric(metric.strip(), value.strip())
            self.vector # sanity check
        except Exception:
            if old_vector is not None:
                self.vector = old_vector
            raise ValueError("Invalid CVSS base vector: %r" % (vector,))

    base_vector = vector

    @property
    def base_exploitability(self):
        return 20.0 * self.AV * self.AC * self.Au

    @property
    def impact(self):
        return 10.41 * (1.0-(1.0-self.C) * (1.0-self.I) * (1.0-self.A))

    @property
    def f_impact(self):
        return 0.0 if self.impact == 0.0 else 1.176

    @property
    def base_score(self):
        return "%.1f" % (
            self.f_impact * (
                (0.6 * self.impact) + (0.4 * self.base_exploitability) - 1.5
            )
        )

    score = base_score

    @property
    def level(self):
        # https://www.pcisecuritystandards.org/pdfs/asv_program_guide_v1.0.pdf
        score = float(self.score)
        if score == 0.0:
            return "INFORMATIONAL"
        if score == 10.0:
            return "CRITICAL"
        if score < 4.0:
            return "LOW"
        if score < 7.0:
            return "MEDIUM"
        return "HIGH"

    def __init__(self, vector = None):
        self.vector = "AV:N/AC:L/Au:N/C:N/I:N/A:N"
        if vector:
            self.vector = vector

    def __str__(self):
        return "%s: %s [%s]" % (self.score, self.level.title(), self.vector)

    def __repr__(self):
        return "<%s score=%s vector=%s>" % \
               (self.__class__.__name__, self.score, self.vector)

CVSS_Base.access_vector = CVSS_Base.AV
CVSS_Base.access_complexity = CVSS_Base.AC
CVSS_Base.authentication = CVSS_Base.Au
CVSS_Base.confidentiality = CVSS_Base.C
CVSS_Base.integrity = CVSS_Base.I
CVSS_Base.availability = CVSS_Base.A

class CVSS(CVSS_Base):
    "CVSS Calculator."

    METRICS = CVSS_Base.METRICS + (
        "E", "RL", "RC",
        "CDP", "TD", "CR", "IR", "AR",
    )

    HIGH = CVSS_Base.HIGH
    LOW = CVSS_Base.LOW
    MEDIUM = CVSS_Base.MEDIUM
    NONE = CVSS_Base.NONE

    CONFIRMED = "C"
    FUNCTIONAL = "F"
    LOW_MEDIUM = "LM"
    MEDIUM_HIGH = "MH"
    NOT_DEFINED = "ND"
    OFFICIAL_FIX = "OF"
    PROOF_OF_CONCEPT = "POC"
    TEMPORARY_FIX = "TF"
    UNAVAILABLE = "U"
    UNCONFIRMED = "UC"
    UNCORROBORATED = "UR"
    UNPROVEN = "U"
    WORKAROUND = "W"

    E_SCORE = {
        UNPROVEN: 0.85,
        PROOF_OF_CONCEPT: 0.9,
        FUNCTIONAL: 0.95,
        HIGH: 1.0,
        NOT_DEFINED: 1.0,
    }

    RL_SCORE = {
        OFFICIAL_FIX: 0.87,
        TEMPORARY_FIX: 0.9,
        WORKAROUND: 0.95,
        UNAVAILABLE: 1.0,
        NOT_DEFINED: 1.0,
    }

    RC_SCORE = {
        UNCONFIRMED: 0.9,
        UNCORROBORATED: 0.95,
        CONFIRMED: 1.0,
        NOT_DEFINED: 1.0,
    }

    CDP_SCORE = {
        NONE: 0.0,
        LOW: 0.1,
        LOW_MEDIUM: 0.3,
        MEDIUM_HIGH: 0.4,
        HIGH: 0.5,
        NOT_DEFINED: 0.0,
    }

    TD_SCORE = {
        NONE: 0.0,
        LOW: 0.25,
        MEDIUM: 0.75,
        HIGH: 1.0,
        NOT_DEFINED: 1.0,
    }

    CR_SCORE = {
        LOW: 0.5,
        MEDIUM: 1.0,
        HIGH: 1.51,
        NOT_DEFINED: 1.0,
    }
    IR_SCORE = CR_SCORE
    AR_SCORE = CR_SCORE

    def __init__(self, vector = None):
        new_metrics = self.METRICS[len(CVSS_Base.METRICS):]
        for metric in new_metrics:
            self.set_metric(metric, self.NOT_DEFINED)
        super(CVSS, self).__init__(vector)

    @property
    def temporal_score(self):
        return "%.1f" % (
            float(self.base_score) * self.E * self.RL * self.RC
        )

    @property
    def adjusted_impact(self):
        return min(10, 10.41 * (
            1 - (1-self.C*self.CR) * (1-self.I*self.IR) * (1-self.A*self.AR)
        ))

    @property
    def adjusted_base_score(self):
        return "%.1f" % (
            self.f_impact * (
                (0.6 * self.adjusted_impact) +
                (0.4 * self.base_exploitability) -
                1.5
            )
        )

    @property
    def adjusted_temporal_score(self):
        return "%.1f" % (
            float(self.adjusted_base_score) * self.E * self.RL * self.RC
        )

    @property
    def environmental_score(self):
        adjusted_temporal = float(self.adjusted_temporal_score)
        return "%.1f" % (
            (adjusted_temporal + (10 - adjusted_temporal) * self.CDP) * self.TD
        )

    score = environmental_score

    @property
    def base_vector(self):
        return "/".join(self.vector.split("/")[:6])

CVSS.exploitability = CVSS.E
CVSS.remediation_level = CVSS.RL
CVSS.report_confidence = CVSS.RC
CVSS.collateral_damage_potential = CVSS.CDP
CVSS.target_distribution = CVSS.TD
CVSS.confidentiality_requirements = CVSS.CR
CVSS.integrity_requirements = CVSS.IR
CVSS.availability_requirements = CVSS.AR
