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

"""
Vulnerability types.
"""

__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__ = [
    "Vulnerability",
    "WebVulnerability",
]

from .vuln_utils import convert_vuln_ids_to_references, \
     convert_references_to_vuln_ids, _vuln_ref_regex, CVSS, TAXONOMY_NAMES
from .. import Data, identity, merge, keep_newer, keep_true, LocalDataCache
from ..resource import Resource
from ..resource.url import BaseURL, FolderURL, URL
from ...config import Config
from ...plugin import get_plugin_info
from ...text.text_utils import to_utf8

from collections import defaultdict
from inspect import getmro
from textwrap import dedent
from warnings import warn


#------------------------------------------------------------------------------
# Merge strategy for taxonomy IDs.

class merge_vuln_ids(merge):


    #--------------------------------------------------------------------------
    @staticmethod
    def do_merge(old_data, new_data, key):

        # Get the original value.
        my_value = getattr(old_data, key, None)

        # Get the new value.
        their_value = getattr(new_data, key, None)

        # Concatenate the tuples.
        new_value = my_value + their_value

        # Sanitize the vulnerability IDs.
        new_value = sanitize_vuln_ids(new_value)

        # Return the merged value.
        return new_value


#------------------------------------------------------------------------------
def sanitize_vuln_ids(vid):
    if vid:
        if isinstance(vid, basestring):
            return (str(to_utf8(vid)),)
        else:
            return tuple(sorted(set(str(to_utf8(x)) for x in vid)))
    else:
        return ()


#------------------------------------------------------------------------------
# Base class for all vulnerabilities.

class Vulnerability(Data):
    """
    Vulnerability Title.

    After the title comes the vulnerability description. You can make it as
    long as you wish, as long as you don't leave blank lines in between. If
    you need a more specific text for each instance of your vulnerability,
    override the "title", "description" and/or "solution" values of the
    "DEFAULT" class variable. But don't forget to make a copy of the
    dictionary first! You don't want your changes to affect the parent class.

    The third paragraph is the solution to the vulnerability. Here you want to
    include tips on how to avoid this vulnerability in source code, or maybe
    some general security recommendations. Links to external resources belong
    belong in the "references" section instead, so don't add them here.
    """

    data_type = Data.TYPE_VULNERABILITY
    data_subtype = "vulnerability/abstract"
    max_vulnerabilities = 0

    # Vulnerability levels.
    VULN_LEVELS = ("informational", "low", "middle", "high", "critical")

    # Default vulnerability properties.
    # Note: plugin_id and custom_id must NOT be defined here.
    DEFAULTS = {
        "level":            "low",
        "impact":           0,
        "severity":         0,
        "risk":             0,
        "title":            None,  # special value, do not change!
        "description":      None,  # special value, do not change!
        "solution":         None,  # special value, do not change!
        "references":       (),
        "cvss_base":        "0.0",
        "cvss_score":       None,
        "cvss_vector":      None,
        "tool_id":          None,
    }
    DEFAULTS.update( { x: () for x in TAXONOMY_NAMES } )

    # Class of the target for the vulnerabilities of this type.
    TARGET_CLASS = Resource


    #--------------------------------------------------------------------------
    def __init__(self, target, **kwargs):
        """
        :param target: This is where the vulnerability was found.
            If TARGET_DATA is set for this class, the target object
            must be an instance of that class.
        :type target: Resource

        :keyword title: Title used for vulnerability.
        :type title: str

        :keyword description: Free form text describing the vulnerability.
        :type description: str

        :keyword solution: Free form text describing a possible solution.
        :type solution: str

        :keyword plugin_id: ID of the plugin that found the vulnerability.
            Defaults to the calling plugin ID.
        :type plugin_id: str

        :keyword tool_id: Plugin-defined tool ID. This may be used by plugins
            that run external tools, to track down which tool (or which
            plugin/addon of that tool) has found the vulnerability. Other
            plugins can safely leave this as None (the default).
        :type tool_id: str | None

        :keyword custom_id: Customized vulnerability ID. This advanced argument
            may be used by plugins that know how to uniquely identify their
            own vulnerabilities, in order to detect when the same vulnerability
            was detected multiple times. Most plugins will leave this value as
            None and let GoLismero do the vulnerability duplicates matching.
        :type custom_id: str | None

        :keyword level: User-friendly vulnerability level.
            Must be one of the following values: "critical", "high", "middle",
            "low" or "informational". Ignored if a CVSS vector is given.
        :type level: str

        :keyword impact: Impact rating. A number between 0-4.
        :type impact: int

        :keyword severity: Severity rating. A number between 0-4.
        :type severity: int

        :keyword risk: Risk rating. A number between 0-4.
        :type risk: int

        :keyword references: Reference URLs.
        :type references: tuple(str)

        :keyword cvss_base: CVSS base score. Ignored if a vector is given.
        :type cvss_base: str

        :keyword cvss_score: CVSS score. Ignored if a vector is given.
        :type cvss_score: str

        :keyword cvss_vector: CVSS vector. Overrides cvss_base and cvss_score,
            as well as the user-friendly level.
        :type cvss_vector: str

        :keyword bid: Bugtraq IDs.
        :type bid: tuple( str, ... )

        :keyword ca: CERT Advisory IDs.
        :type ca: tuple( str, ... )

        :keyword capec: CAPEC IDs.
        :type capec: tuple( str, ... )

        :keyword cisco: Cisco Security Advisory IDs.
        :type cisco: tuple( str, ... )

        :keyword cve: CVE IDs.
        :type cve: tuple( str, ... )

        :keyword cwe: CVE IDs.
        :type cwe: tuple( str, ... )

        :keyword dsa: Debian Security Advisory IDs.
        :type dsa: tuple( str, ... )

        :keyword edb: ExploitDB IDs.
        :type edb: tuple( str, ... )

        :keyword glsa: Gentoo Linux Security Advisory IDs.
        :type glsa: tuple( str, ... )

        :keyword mdvsa: Mandriva Security Advisory IDs.
        :type mdvsa: tuple( str, ... )

        :keyword ms: Microsoft Advisory IDs.
        :type ms: tuple( str, ... )

        :keyword mskb: Microsoft Knowledge Base IDs.
        :type mskb: tuple( str, ... )

        :keyword nessus: Nessus Plugin IDs.
        :type nessus: tuple( str, ... )

        :keyword osvdb: OSVDB IDs.
        :type osvdb: tuple( str, ... )

        :keyword rhsa: RedHat Security Advisory IDs.
        :type rhsa: tuple( str, ... )

        :keyword sa: Secunia Advisory IDs.
        :type sa: tuple( str, ... )

        :keyword sectrack: Security Tracker IDs.
        :type sectrack: tuple( str, ... )

        :keyword usn: Ubuntu Security Notice IDs.
        :type usn: tuple( str, ... )

        :keyword vmsa: VMWare Security Advisory IDs.
        :type vmsa: tuple( str, ... )

        :keyword vu: CERT Vulnerability Note IDs.
        :type vu: tuple( str, ... )

        :keyword xf: ISS X-Force IDs.
        :type xf: tuple( str, ... )
        """

        # Do not allow abstract vulnerability types to be instanced.
        if self.data_subtype == "vulnerability/abstract":
            raise TypeError("Class %s is abstract!" % self.__class__.__name__)

        # Check the target parameter.
        if not isinstance(target, self.TARGET_CLASS):
            raise TypeError("Expected %s, got %r instead" %
                            (self.TARGET_CLASS.__name__, type(target)))

        # Store the target ID. Since this is part of our ID, it ensures we'll
        # have different Vulnerability instances for different targets, even
        # if all other parameters are the same.
        self.__target_id = target.identity

        # Store the custom ID, if any.
        custom_id = kwargs.pop("custom_id", None)
        if custom_id is not None and type(custom_id) is not str:
            raise TypeError("Custom ID may only be a string!")
        self.__custom_id = custom_id

        # Newly found vulns are undecided by default.
        # However, if previously marked as false positives and found again,
        # the false positive mark stays put (@keep_true strategy).
        self.__false_positive = None

        # Set the plugin ID. We need to do this before setting the rest of the
        # properties, because some of them use the plugin ID, like for example
        # the vulnerability title.
        self.plugin_id = kwargs.pop("plugin_id", None)

        # Validate the remaining keyword arguments.
        not_found = set(kwargs.iterkeys())
        not_found.difference_update(Vulnerability.DEFAULTS.iterkeys())
        if not_found:
            raise TypeError(
                "Unexpected keyword arguments: %s"
                % ", ".join(sorted(not_found)))
        del not_found

        # If a CVSS vector is given, ignore any other CVSS scores given and
        # recalculate them from the vector instead.
        if "cvss_vector" in kwargs:
            try:
                del kwargs["cvss_base"]
            except KeyError:
                pass
            try:
                del kwargs["cvss_score"]
            except KeyError:
                pass
            try:
                del kwargs["level"]
            except KeyError:
                pass

        # Set the properties, first the ones with a default value defined,
        # then the ones with no default value defined.
        # TODO inherit the dicts instead of forcing subclasses to copy() it
        propnames = [
            k for k,v in Vulnerability.DEFAULTS.iteritems() if v is not None]
        propnames.extend(
            k for k,v in Vulnerability.DEFAULTS.iteritems() if v is None)
        for prop in propnames:
            value = kwargs.get(prop)
            if value is None:
                value = self.DEFAULTS.get(prop)
                if value is None:
                    value = Vulnerability.DEFAULTS.get(prop)
            setattr(self, prop, value)

        # TODO extract URLs from text and add them to the references.

        # Feed back references to vulnerability IDs.
        vuln_ids = convert_references_to_vuln_ids(self.references)
        for key, value in vuln_ids.iteritems():
            value.extend(getattr(self, key, []))
            setattr(self, key, value) # auto-sanitizes

        # Feed back vulnerability IDs to references.
        refs = convert_vuln_ids_to_references(self.taxonomies)
        refs.extend(self.references)
        self.references = refs # auto-sanitizes

        # Call the parent constructor.
        super(Vulnerability, self).__init__()

        # Associate the vulnerability to the target.
        self.add_link(target)


    #--------------------------------------------------------------------------
    @identity
    def target_id(self):
        """
        :returns: Identity hash of the target
            where the vulnerability was found.
        :rtype: str
        """
        return self.__target_id


    #--------------------------------------------------------------------------
    @property
    def target(self):
        """
        :returns: Target where the vulnerability was found.
        :rtype: Data
        """
        return self.resolve(self.target_id)


    #--------------------------------------------------------------------------
    def __repr__(self):
        return "<%s plugin_id=%r level=%r title=%r>" % (
            self.__class__.__name__,
            self.plugin_id,
            self.level,
            self.title,
        )


    #--------------------------------------------------------------------------
    def __set_text(self, name, text):
        if text is None:
            text = self.__get_default_text(name)
        elif isinstance(text, unicode):
            text = text.encode("UTF-8")
        elif type(text) is not str:
            raise TypeError(
                "Expected string, got %r instead" % type(text))
        setattr(self, "_Vulnerability__%s" % name, text)


    #--------------------------------------------------------------------------
    def __get_default_texts_from_docstring(self, section):
        """
        Retrieves the default title, description and solution texts from the
        class docstring.

        :param section: The docstring should be divided into three paragraphs,
            the first for the title, the second for the description, and the
            third for the solution. This parameter specifies which one to get:
            0 for the title, 1 for the description or 2 for the solution.
        :type section: int

        :returns: The retrieved text on success, or an empty string on error.
            Indentation and extra whitespace are removed. Newline characters
            are also removed. Tabs are converted to four spaces.
        :rtype: str
        """
        text = ""
        mro = [
            clazz for clazz in getmro(self.__class__)
            if hasattr(clazz, "vulnerability_type") and
               clazz.vulnerability_type not in ("vulnerability/abstract",
                                                "vulnerability/generic")
        ]
        for clazz in mro:
            text = getattr(clazz, "__doc__", None)
            if text:
                break
        if text:
            try:
                text = dedent(text).strip()
                text = text.split("\n\n", section + 1)[section].strip()
                text = text.replace("\t", "    ")
                text = text.replace("\n", " ")
            except Exception:#, e:
                ##warn(str(e), Warning, stacklevel=2)
                text = ""
        return text


    #--------------------------------------------------------------------------
    @property
    def display_name(self):
        text = self.__get_default_texts_from_docstring(0)
        if text:
            text.strip()
        if text and text.endswith("."):
            text = text[:-1].strip()
        if not text:
            text = super(Vulnerability, self).display_name
        return text


    #--------------------------------------------------------------------------
    def __get_default_text(self, propname):
        text = self.DEFAULTS.get(propname, None)
        if text is None:
            if propname == "title":
                text = self.display_name
                if text == "Uncategorized Vulnerability":
                    if self.level == "informational":
                        text = "User attention required by"
                    else:
                        text = "Vulnerability found by"
                    if not self.plugin_id or \
                                    self.plugin_id.lower() == "golismero":
                        text += " GoLismero"
                    elif self.plugin_id.startswith("ui/"):
                        text += " the user"
                    else:
                        text += ": "
                        try:
                            text += get_plugin_info(
                                self.plugin_id).display_name
                        except Exception:
                            text += self.plugin_id
            elif propname == "description":
                text = self.__get_default_texts_from_docstring(1)
                if not text:
                    if self.references:
                        if len(self.references) > 1:
                            text = ("Please visit the reference website"
                                    " for more information")
                        else:
                            text = ("Please visit the reference websites"
                                    " for more information")
                    else:
                        text = "No additional details are available"
                    if self.level == "informational":
                        text += "."
                    else:
                        text += " for this vulnerability."
            elif propname == "solution":
                text = self.__get_default_texts_from_docstring(2)
                if not text:
                    if self.references:
                        if len(self.references) > 1:
                            text = ("Please visit the reference websites"
                                    " for more information")
                        else:
                            text = ("Please visit the reference website"
                                    " for more information")
                    else:
                        text = "No additional details are available"
                    if self.level == "informational":
                        text += "."
                    else:
                        text += " on how to patch this vulnerability."
            else:
                text = ""
        return text


    #--------------------------------------------------------------------------
    @identity
    def custom_id(self):
        """
        :returns: Customized vulnerability ID. This advanced argument may be
            used by plugins that know how to uniquely identify their own
            vulnerabilities, in order to detect when the same vulnerability was
            found multiple times. Most plugins will leave this value as None
            and let GoLismero do the vulnerability duplicates matching.
        :rtype: str | None
        """
        return self.__custom_id


    #--------------------------------------------------------------------------
    @keep_true
    def false_positive(self):
        """
        :returns: True for false positives, False for real vulnerabilities.
            None means the user hasn't evaluated this vulnerability yet.
        :rtype: bool | None
        """
        return self.__false_positive


    #--------------------------------------------------------------------------
    @false_positive.setter
    def false_positive(self, false_positive):
        """
        :param false_positive:
            True for false positives, False for real vulnerabilities.

            Don't use None here, it will be interpreted as False! Once a
            vulnerability has been marked as false positive or not, you
            can't go back to the undecided state.

        :type false_positive: bool
        """
        self.__false_positive = bool(false_positive)


    #--------------------------------------------------------------------------
    @keep_newer
    def plugin_id(self):
        """
        :returns: ID of the plugin that found the vulnerability.
        :rtype: str
        """
        return self.__plugin_id


    #--------------------------------------------------------------------------
    @plugin_id.setter
    def plugin_id(self, plugin_id):
        """
        :param plugin_id: ID of the plugin that found the vulnerability.
            Defaults to the calling plugin ID.
        :type plugin_id: str
        """
        if not plugin_id:
            try:
                plugin_id = Config.plugin_id
            except Exception:
                plugin_id = "GoLismero"
        elif type(plugin_id) is not str:
            raise TypeError(
                "Expected string, got %r instead" % type(plugin_id))
        self.__plugin_id = plugin_id


    #--------------------------------------------------------------------------
    @keep_newer
    def tool_id(self):
        """
        :returns: Plugin-defined tool ID. This may be used by plugins
            that run external tools, to track down which tool (or which
            plugin/addon of that tool) has found the vulnerability. Other
            plugins can safely leave this as None (the default).
        :rtype: str | None
        """
        return self.__tool_id


    #--------------------------------------------------------------------------
    @tool_id.setter
    def tool_id(self, tool_id):
        """
        :param tool_id: Plugin-defined tool ID. This may be used by plugins
            that run external tools, to track down which tool (or which
            plugin/addon of that tool) has found the vulnerability. Other
            plugins can safely leave this as None (the default).
        :type tool_id: str | None
        """
        tool_id = to_utf8(tool_id)
        if not tool_id:
            tool_id = None
        elif not isinstance(tool_id, str):
            raise TypeError("Expected string, got %r instead" % type(tool_id))
        self.__tool_id = tool_id


    #--------------------------------------------------------------------------
    @keep_newer
    def level(self):
        """
        :return: Vulnerability level.
        :rtype: str
        """
        return self.__level


    #--------------------------------------------------------------------------
    @level.setter
    def level(self, level):
        """
        .. note: Setting this property manually deletes the CVSS vector.

        :param level: User-friendly vulnerability level.
        :type level: str
        """
        level = to_utf8(level)
        if not isinstance(level, str):
            raise TypeError("Expected str, got %r instead" % type(level))
        elif level.lower() not in self.VULN_LEVELS:
            raise ValueError("Unknown level: %r" % level)
        self.__level = level.lower()
        self.__cvss_vector = None


    #--------------------------------------------------------------------------
    @keep_newer
    def impact(self):
        """
        :returns: Impact rating.
        :rtype: int
        """
        return self.__impact


    #--------------------------------------------------------------------------
    @impact.setter
    def impact(self, impact):
        """
        :param impact: Impact rating.
        :type impact: int
        """
        impact = int(impact)
        if impact < 0 or impact > 4:
            raise ValueError("Invalid impact value: %d" % impact)
        self.__impact = impact


    #--------------------------------------------------------------------------
    @keep_newer
    def severity(self):
        """
        :returns: Severity rating.
        :rtype: int
        """
        return self.__severity


    #--------------------------------------------------------------------------
    @severity.setter
    def severity(self, severity):
        """
        :param severity: Severity rating.
        :type severity: int
        """
        severity = int(severity)
        if severity < 0 or severity > 4:
            raise ValueError("Invalid severity value: %d" % severity)
        self.__severity = severity


    #--------------------------------------------------------------------------
    @keep_newer
    def risk(self):
        """
        :returns: Risk rating.
        :rtype: int
        """
        return self.__risk


    #--------------------------------------------------------------------------
    @risk.setter
    def risk(self, risk):
        """
        :param risk: Risk rating.
        :type risk: int
        """
        risk = int(risk)
        if risk < 0 or risk > 4:
            raise ValueError("Invalid risk value: %d" % risk)
        self.__risk = risk


    #--------------------------------------------------------------------------
    @keep_newer
    def title(self):
        """
        :returns: Title of the vulnerability.
        :rtype: str
        """
        return self.__title


    #--------------------------------------------------------------------------
    @title.setter
    def title(self, title):
        """
        :param title: Title of the vulnerability.
            Use None to set the default.
        :type title: str | None
        """
        self.__set_text("title", title)


    #--------------------------------------------------------------------------
    @keep_newer
    def description(self):
        """
        :returns: Free form text describing the vulnerability.
        :rtype: str
        """
        return self.__description


    #--------------------------------------------------------------------------
    @description.setter
    def description(self, description):
        """
        :param description: Free form text describing the vulnerability.
            Use None to set the default.
        :type description: str
        """
        self.__set_text("description", description)


    #--------------------------------------------------------------------------
    @keep_newer
    def solution(self):
        """
        :returns: Free form text describing a possible solution.
            Use None to set the default.
        :rtype: str
        """
        return self.__solution


    #--------------------------------------------------------------------------
    @solution.setter
    def solution(self, solution):
        """
        :param solution: Free form text describing a possible solution.
        :type solution: str
        """
        self.__set_text("solution", solution)


    #--------------------------------------------------------------------------
    @keep_newer
    def references(self):
        """
        :returns: Reference URLs.
        :rtype: tuple(str)
        """
        return self.__references


    #--------------------------------------------------------------------------
    @references.setter
    def references(self, references):
        """
        :param references: Reference URLs.
        :type references: tuple(str)
        """

        # Remove the duplicates and convert to list.
        if not references:
            references = []
        elif isinstance(references, basestring):
            references = [str(to_utf8(references))]
        else:
            references = list(set(str(to_utf8(x)) for x in references))

        # Remove the redundant references and sort the list.
        if references:
            tmp = defaultdict(list)
            for ref in references:
                tmp2 = convert_references_to_vuln_ids([ref])
                for vuln_ids in tmp2.itervalues():
                    for vid in vuln_ids:
                        tmp[vid].append(ref)
            for vid, refs in tmp.iteritems():
                for ref in refs:
                    references.remove(ref)
                tmp3 = convert_vuln_ids_to_references([vid])
                references.append( tmp3[0] )
            references.sort()

        # Save the references as a tuple.
        self.__references = tuple(references)


    #--------------------------------------------------------------------------
    @keep_newer
    def cvss_base(self):
        """
        :returns: CVSS base score.
        :rtype: str | None
        """
        return self.__cvss_base


    #--------------------------------------------------------------------------
    @cvss_base.setter
    def cvss_base(self, cvss_base):
        """
        .. note: Setting this property manually deletes the CVSS vector.

        :param cvss_base: CVSS base score.
        :type cvss_base: str
        """
        if not cvss_base:
            cvss_base = None
        elif isinstance(cvss_base, unicode):
            cvss_base = cvss_base.encode("UTF-8")
            if not cvss_base:
                cvss_base = None
        if cvss_base:
            value = float(cvss_base)
            if value > 10.0 or value < 0.0:
                raise ValueError("Invalid CVSS base score: %s" % cvss_base)
            cvss_base = "%.1f" % value
        self.__cvss_base = cvss_base
        self.__cvss_vector = None


    #--------------------------------------------------------------------------
    @keep_newer
    def cvss_score(self):
        """
        :returns: CVSS score.
        :rtype: str | None
        """
        return self.__cvss_score


    #--------------------------------------------------------------------------
    @cvss_score.setter
    def cvss_score(self, cvss_score):
        """
        .. note: Setting this property manually deletes the CVSS vector.

        :param cvss_score: CVSS score.
        :type cvss_score: str
        """
        if not cvss_score:
            cvss_score = None
        elif isinstance(cvss_score, unicode):
            cvss_score = cvss_score.encode("UTF-8")
        if cvss_score:
            value = float(cvss_score)
            if value > 10.0 or value < 0.0:
                raise ValueError("Invalid CVSS score: %s" % cvss_score)
            cvss_score = "%.1f" % value
        self.__cvss_score = cvss_score
        self.__cvss_vector = None


    #--------------------------------------------------------------------------
    @keep_newer
    def cvss_vector(self):
        """
        :returns: CVSS vector.
        :rtype: str | None
        """
        return self.__cvss_vector


    #--------------------------------------------------------------------------
    @cvss_vector.setter
    def cvss_vector(self, cvss_vector):
        """
        :param cvss_vector: CVSS vector.
        :type cvss_vector: str
        """
        if not cvss_vector:
            cvss_vector = None
        elif isinstance(cvss_vector, unicode):
            cvss_vector = cvss_vector.encode("UTF-8")
        elif type(cvss_vector) is not str:
            raise TypeError(
                "Expected string, got %r instead" % type(cvss_vector))
        if cvss_vector:
            cvss = CVSS(cvss_vector)
            self.__level       = cvss.level.lower()
            self.__cvss_base   = cvss.base_score
            self.__cvss_score  = cvss.score
            self.__cvss_vector = cvss.vector
        else:
            self.__cvss_vector = None


    #--------------------------------------------------------------------------
    @property
    def taxonomies(self):
        """
        This alias concatenates all vulnerability IDs for all supported
        taxonomies into a single list.

        :returns: All vulnerability IDs for all taxonomies.
        :rtype: list(str)
        """
        result = []
        for vuln_type in TAXONOMY_NAMES:
            result.extend( getattr(self, vuln_type) )
        return result


    #--------------------------------------------------------------------------
    @merge_vuln_ids
    def bid(self):
        """
        :returns: Bugtraq IDs.
        :rtype: tuple( str, ... )
        """
        return self.__bid


    #--------------------------------------------------------------------------
    @bid.setter
    def bid(self, bid):
        """
        :param bid: Bugtraq IDs.
        :type bid: tuple( str, ... )
        """
        self.__bid = sanitize_vuln_ids(bid)


    #--------------------------------------------------------------------------
    @merge_vuln_ids
    def ca(self):
        """
        :returns: CERT Advisory IDs.
        :rtype: tuple( str, ... )
        """
        return self.__ca


    #--------------------------------------------------------------------------
    @ca.setter
    def ca(self, ca):
        """
        :param ca: CERT Advisory IDs.
        :type ca: tuple( str, ... )
        """
        self.__ca = sanitize_vuln_ids(ca)


    #--------------------------------------------------------------------------
    @merge_vuln_ids
    def capec(self):
        """
        :returns: CAPEC IDs.
        :rtype: tuple( str, ... )
        """
        return self.__capec


    #--------------------------------------------------------------------------
    @capec.setter
    def capec(self, capec):
        """
        :param capec: CAPEC IDs.
        :type capec: tuple( str, ... )
        """
        self.__capec = sanitize_vuln_ids(capec)


    #--------------------------------------------------------------------------
    @merge_vuln_ids
    def cisco(self):
        """
        :returns: Cisco Security Advisory IDs.
        :rtype: tuple( str, ... )
        """
        return self.__cisco


    #--------------------------------------------------------------------------
    @cisco.setter
    def cisco(self, cisco):
        """
        :param cisco: Cisco Security Advisory IDs.
        :type cisco: tuple( str, ... )
        """
        self.__cisco = sanitize_vuln_ids(cisco)


    #--------------------------------------------------------------------------
    @merge_vuln_ids
    def cve(self):
        """
        :returns: CVE IDs.
        :rtype: tuple( str, ... )
        """
        return self.__cve


    #--------------------------------------------------------------------------
    @cve.setter
    def cve(self, cve):
        """
        :param cve: CVE IDs.
        :type cve: tuple( str, ... )
        """
        self.__cve = sanitize_vuln_ids(cve)


    #--------------------------------------------------------------------------
    @merge_vuln_ids
    def cwe(self):
        """
        :returns: CWE IDs.
        :rtype: tuple( str, ... )
        """
        return self.__cwe


    #--------------------------------------------------------------------------
    @cwe.setter
    def cwe(self, cwe):
        """
        :param cwe: CWE IDs.
        :type cwe: tuple( str, ... )
        """
        self.__cwe = sanitize_vuln_ids(cwe)


    #--------------------------------------------------------------------------
    @merge_vuln_ids
    def dsa(self):
        """
        :returns: Debian Security Advisory IDs.
        :rtype: tuple( str, ... )
        """
        return self.__dsa


    #--------------------------------------------------------------------------
    @dsa.setter
    def dsa(self, dsa):
        """
        :param dsa: Debian Security Advisory IDs.
        :type dsa: tuple( str, ... )
        """
        self.__dsa = sanitize_vuln_ids(dsa)


    #--------------------------------------------------------------------------
    @merge_vuln_ids
    def edb(self):
        """
        :returns: ExploitDB IDs.
        :rtype: tuple( str, ... )
        """
        return self.__edb


    #--------------------------------------------------------------------------
    @edb.setter
    def edb(self, edb):
        """
        :param edb: ExploitDB IDs.
        :type edb: tuple( str, ... )
        """
        self.__edb = sanitize_vuln_ids(edb)


    #--------------------------------------------------------------------------
    @merge_vuln_ids
    def glsa(self):
        """
        :returns: Gentoo Linux Security Advisory IDs.
        :rtype: tuple( str, ... )
        """
        return self.__glsa


    #--------------------------------------------------------------------------
    @glsa.setter
    def glsa(self, glsa):
        """
        :param glsa: Gentoo Linux Security Advisory IDs.
        :type glsa: tuple( str, ... )
        """
        self.__glsa = sanitize_vuln_ids(glsa)


    #--------------------------------------------------------------------------
    @merge_vuln_ids
    def mdvsa(self):
        """
        :returns: Mandriva Security Advisory IDs.
        :rtype: tuple( str, ... )
        """
        return self.__mdvsa


    #--------------------------------------------------------------------------
    @mdvsa.setter
    def mdvsa(self, mdvsa):
        """
        :param mdvsa: Mandriva Security Advisory IDs.
        :type mdvsa: tuple( str, ... )
        """
        self.__mdvsa = sanitize_vuln_ids(mdvsa)


    #--------------------------------------------------------------------------
    @merge_vuln_ids
    def ms(self):
        """
        :returns: Microsoft Advisory IDs.
        :rtype: tuple( str, ... )
        """
        return self.__ms


    #--------------------------------------------------------------------------
    @ms.setter
    def ms(self, ms):
        """
        :param ms: Microsoft Advisory IDs.
        :type ms: tuple( str, ... )
        """
        self.__ms = sanitize_vuln_ids(ms)


    #--------------------------------------------------------------------------
    @merge_vuln_ids
    def mskb(self):
        """
        :returns: Microsoft Knowledge Base IDs.
        :rtype: tuple( str, ... )
        """
        return self.__mskb


    #--------------------------------------------------------------------------
    @mskb.setter
    def mskb(self, mskb):
        """
        :param mskb: Microsoft Knowledge Base IDs.
        :type mskb: tuple( str, ... )
        """
        self.__mskb = sanitize_vuln_ids(mskb)


    #--------------------------------------------------------------------------
    @merge_vuln_ids
    def nessus(self):
        """
        :returns: Nessus Plugin IDs.
        :rtype: tuple( str, ... )
        """
        return self.__nessus


    #--------------------------------------------------------------------------
    @nessus.setter
    def nessus(self, nessus):
        """
        :param nessus: Nessus Plugin IDs.
        :type nessus: tuple( str, ... )
        """
        self.__nessus = sanitize_vuln_ids(nessus)


    #--------------------------------------------------------------------------
    @merge_vuln_ids
    def osvdb(self):
        """
        :returns: OSVDB IDs.
        :rtype: tuple( str, ... )
        """
        return self.__osvdb


    #--------------------------------------------------------------------------
    @osvdb.setter
    def osvdb(self, osvdb):
        """
        :param osvdb: OSVDB IDs.
        :type osvdb: tuple( str, ... )
        """
        self.__osvdb = sanitize_vuln_ids(osvdb)


    #--------------------------------------------------------------------------
    @merge_vuln_ids
    def rhsa(self):
        """
        :returns: RedHat Security Advisory IDs.
        :rtype: tuple( str, ... )
        """
        return self.__rhsa


    #--------------------------------------------------------------------------
    @rhsa.setter
    def rhsa(self, rhsa):
        """
        :param rhsa: RedHat Security Advisory IDs.
        :type rhsa: tuple( str, ... )
        """
        self.__rhsa = sanitize_vuln_ids(rhsa)


    #--------------------------------------------------------------------------
    @merge_vuln_ids
    def sa(self):
        """
        :returns: Secunia Advisory IDs.
        :rtype: tuple( str, ... )
        """
        return self.__sa


    #--------------------------------------------------------------------------
    @sa.setter
    def sa(self, sa):
        """
        :param sa: Secunia Advisory IDs.
        :type sa: tuple( str, ... )
        """
        self.__sa = sanitize_vuln_ids(sa)


    #--------------------------------------------------------------------------
    @merge_vuln_ids
    def sectrack(self):
        """
        :returns: Security Tracker IDs.
        :rtype: tuple( str, ... )
        """
        return self.__sectrack


    #--------------------------------------------------------------------------
    @sectrack.setter
    def sectrack(self, sectrack):
        """
        :param sectrack: Security Tracker IDs.
        :type sectrack: tuple( str, ... )
        """
        self.__sectrack = sanitize_vuln_ids(sectrack)


    #--------------------------------------------------------------------------
    @merge_vuln_ids
    def usn(self):
        """
        :returns: Ubuntu Security Notice IDs.
        :rtype: tuple( str, ... )
        """
        return self.__usn


    #--------------------------------------------------------------------------
    @usn.setter
    def usn(self, usn):
        """
        :param usn: Ubuntu Security Notice IDs.
        :type usn: tuple( str, ... )
        """
        self.__usn = sanitize_vuln_ids(usn)


    #--------------------------------------------------------------------------
    @merge_vuln_ids
    def vmsa(self):
        """
        :returns: VMWare Security Advisory IDs.
        :rtype: tuple( str, ... )
        """
        return self.__vmsa


    #--------------------------------------------------------------------------
    @vmsa.setter
    def vmsa(self, vmsa):
        """
        :param vmsa: VMWare Security Advisory IDs.
        :type vmsa: tuple( str, ... )
        """
        self.__vmsa = sanitize_vuln_ids(vmsa)


    #--------------------------------------------------------------------------
    @merge_vuln_ids
    def vu(self):
        """
        :returns: Vulnerability Note IDs.
        :rtype: tuple( str, ... )
        """
        return self.__vu


    #--------------------------------------------------------------------------
    @vu.setter
    def vu(self, vu):
        """
        :param vu: Vulnerability Note IDs.
        :type vu: tuple( str, ... )
        """
        self.__vu = sanitize_vuln_ids(vu)


    #--------------------------------------------------------------------------
    @merge_vuln_ids
    def xf(self):
        """
        :returns: ISS X-Force IDs.
        :rtype: tuple( str, ... )
        """
        return self.__xf


    #--------------------------------------------------------------------------
    @xf.setter
    def xf(self, xf):
        """
        :param xf: ISS X-Force IDs.
        :type xf: tuple( str, ... )
        """
        self.__xf = sanitize_vuln_ids(xf)


#------------------------------------------------------------------------------
# Simple checks to make sure we're not missing any taxonomy names.

assert set(TAXONOMY_NAMES.iterkeys()) == \
       set(_vuln_ref_regex.iterkeys()),\
    set(TAXONOMY_NAMES.iterkeys()).symmetric_difference(
        set(_vuln_ref_regex.iterkeys()))

assert len(set(TAXONOMY_NAMES.iterkeys()).difference(
    set(Vulnerability.DEFAULTS.iterkeys()))) == 0, \
    set(TAXONOMY_NAMES.iterkeys()).difference(
        set(Vulnerability.DEFAULTS.iterkeys()))

assert len(set(Vulnerability.DEFAULTS.iterkeys()).difference(
    set(dir(Vulnerability)))) == 0, \
    set(Vulnerability.DEFAULTS.iterkeys()).difference(set(dir(Vulnerability)))


#------------------------------------------------------------------------------
class UncategorizedVulnerability(Vulnerability):
    """
    Generic vulnerability.

    This is useful for plugins that for some reason can't categorize the
    vulnerabilities they find. Avoid using it whenever possible! If you must,
    make sure you set the title, description and solution texts accordingly,
    and *ALWAYS* use the custom_id property.
    """

    data_subtype = "vulnerability/generic"


#------------------------------------------------------------------------------
class WebVulnerability(Vulnerability):
    """
    Base class for web vulnerabilities - more specifically, those whose target
    is an URL.

    Do not instance this class! Only use it for subclassing.
    """

    data_subtype = "vulnerability/abstract"

    TARGET_CLASS = URL


    #--------------------------------------------------------------------------
    def __init__(self, target, **kwargs):

        # Sanitize the "target" argument.
        target = self._sanitize_url(self, target)

        # Save the raw URL.
        self.__url = target.url

        # Parent constructor.
        super(WebVulnerability, self).__init__(target, **kwargs)


    #--------------------------------------------------------------------------
    @staticmethod
    def _sanitize_url(self, url, stacklevel = 2):
        if (
            not isinstance(url, URL) and
            not isinstance(url, FolderURL) and
            not isinstance(url, BaseURL)
        ):
            if isinstance(url, basestring):
                msg = "You should pass an URL object" \
                      " to %s instead of a string!"
                msg %= self.__class__.__name__
                url = URL(str(url))
                LocalDataCache.on_autogeneration(url)
            elif hasattr(url, "url"):
                try:
                    t = url.__class__.__name__
                except Exception:
                    t = str(type(url))
                msg = "You should pass an URL object to %s instead of %s!"
                msg %= (self.__class__.__name__, t)
                url = url.url
                if isinstance(url, basestring):
                    url = URL(str(url))
                    LocalDataCache.on_autogeneration(url)
                elif not isinstance(url, URL):
                    raise TypeError("Expected URL, got %r instead" % t)
            warn(msg, RuntimeWarning, stacklevel=stacklevel+1)
        return url


    #--------------------------------------------------------------------------
    def __str__(self):
        return self.url


    #--------------------------------------------------------------------------
    def __repr__(self):
        return "<%s url=%r plugin_id=%r level=%r desc=%r>" % (
            self.__class__.__name__,
            self.url,
            self.plugin_id,
            self.level,
            self.description,
        )


    #--------------------------------------------------------------------------
    @property
    def url(self):
        """
        :return: Raw URL where the vulnerability was found.
        :rtype: str
        """
        return self.__url
