class NotSupportedToolError(PTPError):
    """:mod:`ptp` error when the tool is not supported."""
    pass class NotSupportedVersionError(PTPError):
    """:mod:`ptp` error when the version of the tool is not supported."""
    pass UNKNOWN = 0 RANKING_SCALE = {HIGH: HIGH, MEDIUM: MEDIUM, LOW: LOW, INFO: INFO, UNKNOWN: UNKNOWN} class ArachniXMLParser(XMLParser):
    """Arachni XML specialized parser."""

    __tool__ = 'arachni'
    __format__ = 'xml'
    __version__ = (
        r'(^0\.4\.[0-9]+$)|'
        r'(^1\.[0-9]+(\.[0-9]+)?$)')

    HIGH = 'high'
    MEDIUM = 'medium'
    LOW = 'low'
    INFO = 'informational'

    RANKING_SCALE = {
        HIGH: constants.HIGH,
        MEDIUM: constants.MEDIUM,
        LOW: constants.LOW,
        INFO: constants.INFO}

    @classmethod
    def is_mine(cls, pathname, filename='*.xml', light=False, first=True):
        """Check if it can handle the report file.

        :param str pathname: Path to the report directory.
        :param str filename: Regex matching the report file.
        :param bool light: `True` to only parse the ranking of the findings from the report.
        :param bool first: Only process first file (``True``) or each file that matched (``False``).

        :raises IOError: when the report file cannot be found.
        :raises OSError: when the report file cannot be found.

        :return: `True` if it supports the report, `False` otherwise.
        :rtype: :class:`bool`

        """
        try:
            stream = cls.handle_file(pathname, filename, first=first)
        except (TypeError, XMLSyntaxError):
            return False
        version = stream.find('.//version')
        if version is None:
            return False
        if not re.findall(cls.__version__, version.text, re.IGNORECASE):
            return False
        return True

    def parse_metadata(self):
        """Parse the metadata of the report.

        :raises: :class:`NotSupportedVersionError` -- if it does not support the version of this report.

        :return: The metadata of the report.
        :rtype: dict

        """
        # Find the version of Arachni.
        version = self.stream.find('.//version')
        # Reconstruct the metadata
        # TODO: Retrieve the other metadata likes the date, etc.
        self.metadata = {version.tag: version.text}
        if not self.check_version(self.metadata):
            raise NotSupportedVersionError('PTP does NOT support this version of Arachni.')
        return self.metadata

    def _parse_report_full(self, tree):
        """Parse Arachni XML reports to extract additional information.

        Arachni HTTP traffic is divided into following fields:
            * request
            * response status code
            * response headers
            * response body

        :return: List of dicts where each entry is the HTTP traffic generated for the issue.
        :rtype: :class:`list`

        """
        data = []
        for i in range(len(tree)):
            t_req = tree[i][1]
            t_res = tree[i][2]
            temp = t_req.find('.//body').text
            temp_body = '' if temp is None else temp
            # Somehow follow naming conventions from http://docs.python-requests.org/en/master/
            data.append({
                'request': t_req.find('.//raw').text + temp_body + '\n',
                'status_code': t_res.find('.//code').text,
                'headers': t_res.find('.//raw_headers').text.strip(),
                'body': t_res.find('.//body').text.strip()
            })
        return data

    def parse_report(self):
        """Parse the results of the report.

        :return: List of dicts where each one represents a discovery.
        :rtype: :class:`list`

        """
        self.vulns = [
            {'ranking': self.RANKING_SCALE[vuln.find('.//severity').text.lower()]}
            for vuln in self.stream.find('.//issues')]
        if not self.light:
            temp = []
            for record in self.stream.xpath('//variations//variation//referring_page'):
                temp.append(record.getchildren())
            self.vulns.append({'ranking': constants.UNKNOWN, 'transactions': self._parse_report_full(temp)})
        return self.vulns class ArachniJSONParser(JSONParser):
    """Arachni XML specialized parser."""

    __tool__ = 'arachni'
    __format__ = 'xml'
    __version__ = r'(^1\.[0-9]+(\.[0-9]+)?$)'

    HIGH = 'high'
    MEDIUM = 'medium'
    LOW = 'low'
    INFO = 'informational'

    RANKING_SCALE = {
        HIGH: constants.HIGH,
        MEDIUM: constants.MEDIUM,
        LOW: constants.LOW,
        INFO: constants.INFO}

    @classmethod
    def is_mine(cls, pathname, filename='*.json', light=False, first=True):
        """Check if it can handle the report file.

        :param str pathname: Path to the report directory.
        :param str filename: Regex matching the report file.
        :param bool light: `True` to only parse the ranking of the findings from the report.
        :param bool first: Only process first file (``True``) or each file that matched (``False``).

        :return: `True` if it supports the report, `False` otherwise.
        :rtype: :class:`bool`

        """
        try:
            stream = cls.handle_file(pathname, filename, first=first)
        except (TypeError, ValueError):
            return False
        if 'version' in stream:
            version = stream['version']
        else:
            return False
        if not re.findall(cls.__version__, version, re.IGNORECASE):
            return False
        return True

    def parse_metadata(self):
        """Parse the metadata of the report.

        :raises: :class:`NotSupportedVersionError` -- if it does not support the version of this report.
        :return: The metadata of the report.
        :rtype: dict

        """
        # Find the version of Arachni.
        version = self.stream['version']
        # Reconstruct the metadata
        # TODO: Retrieve the other metadata likes the date, etc.
        self.metadata = {'version': version}
        if not self.check_version(self.metadata):
            raise NotSupportedVersionError('PTP does NOT support this version of Arachni.')
        return self.metadata

    def _parse_report_full(self, issues):
        """Parse Arachni JSON reports to extract additional information.

        Arachni HTTP traffic is divided into following fields:
            * request
            * response status code
            * response headers
            * response body

        :return: List of dicts where each entry is the HTTP traffic generated for the issue.
        :rtype: :class:`list`

        """
        data = []
        for issue in issues:
            for variation in issue['variations']:
                temp_body = '' if variation['request']['body'] is None else variation['request']['body']
                # Somehow follow naming conventions from http://docs.python-requests.org/en/master/
                data.append({
                    'request': variation['request']['headers_string'] + temp_body + '\n',
                    'status_code': variation['response']['code'],
                    'header': variation['response']['headers_string'],
                    'body': variation['response']['body']
                })
        return data

    def parse_report(self):
        """Parse the results of the report.

        :return: List of dicts where each one represents a discovery.
        :rtype: :class:`list`

        """
        self.vulns = [{'ranking': self.RANKING_SCALE[vuln['severity'].lower()]} for vuln in self.stream['issues']]
        if not self.light:
            self.vulns.append({'ranking': constants.UNKNOWN, 'transactions': self._parse_report_full(self.stream['issues'])})
        return self.vulns class SkipfishJSParser(AbstractParser):
    """Skipfish JS specialized parser."""

    __tool__ = 'skipfish'
    __format__ = 'js'
    __version__ = r'2\.10b'

    HIGH = 4
    MEDIUM = 3
    LOW = 2
    WARNINGS = 1
    INFO = 0

    RANKING_SCALE = {
        HIGH: constants.HIGH,
        MEDIUM: constants.MEDIUM,
        LOW: constants.LOW,
        WARNINGS: constants.INFO,
        INFO: constants.INFO}

    _reportfile = 'samples.js'
    _metadatafile = 'summary.js'

    def __init__(self, pathname, light=False):
        """Initialize Skipfish JS parser.

        :param str pathname: Path to the report directory.
        :param bool light: `True` to only parse the ranking of the findings from the report.

        """
        self.search_directory = ''
        self.light = light
        metadatafile = self._recursive_find(pathname, self._metadatafile)
        if metadatafile:
            metadatafile = metadatafile[0]  # Only keep first instance (in case multiple match)
        reportfile = self._recursive_find(pathname, self._reportfile)
        if reportfile:
            self.search_directory = pathname
            reportfile = reportfile[0]  # Only keep first instance (in case multiple match)
        self.metadata_stream, self.report_stream = self.handle_file(metadatafile, reportfile)
        self.re_var_pattern = re.compile(r"var\s+(?P<variables>[a-zA-Z_0-9]+)\s+(?==)")
        self.re_metadata = re.compile(r"var\s+([a-zA-Z_0-9]+)\s+=\s+'{0,1}([^;']*)'{0,1};")
        self._re_reponse_status_code = re.compile(r"^HTTP.*?\/\d\.\d (\d+) .")

    @classmethod
    def handle_file(cls, metadatafile, reportfile):
        """Process the two report files of the Skipfish report.

        :param str metadatafile: Path to the metadata file.
        :param str reportfile: Path to the report file.
        :raises TypeError: if the files have not the right extension.
        :raises OSError: if an error occurs when reading the files.
        :raises IOError: if an error occurs when reading the files.

        :return: Both metadata and report files' contents.
        :rtype: :class:`tuple`

        """
        if not metadatafile.endswith(cls.__format__) or not reportfile.endswith(cls.__format__):
            raise TypeError("This parser only supports '%s' files" % cls.__format__)
        pathname, filename = os.path.split(metadatafile)
        metadata_stream = FileParser.handle_file(pathname=pathname, filename=filename)
        pathname, filename = os.path.split(reportfile)
        report_stream = FileParser.handle_file(pathname=pathname, filename=filename)
        return (metadata_stream, report_stream)

    @classmethod
    def is_mine(cls, pathname, light=False):
        """Check if it can handle the report file.

        :param str pathname: Path to the report directory.
        :param bool light: `True` to only parse the ranking of the findings from the report.

        :raises IOError: when the report file cannot be found.
        :raises OSError: when the report file cannot be found.

        :return: `True` if it supports the report, `False` otherwise.
        :rtype: :class:`bool`

        """
        metadatafile = cls._recursive_find(pathname, cls._metadatafile)
        if not metadatafile:
            return False
        metadatafile = metadatafile[0]
        reportfile = cls._recursive_find(pathname, cls._reportfile)
        if not reportfile:
            return False
        reportfile = reportfile[0]
        try:
            metadata_stream, report_stream = cls.handle_file(metadatafile, reportfile)
        except TypeError:
            return False
        return True

    def parse_metadata(self):
        """Retrieve the metadata of the report.

        :raises: :class:`NotSupportedVersionError` -- if it does not support this version of the report.

        :return: Dictionary containing the metadatas.
        :rtype: :class:`dict`

        .. note::

            In skipfish the metadata are saved into the summary.js file as follow:

            .. code-block:: js

                var sf_version = version<string>;
                var scan_date  = date<'Ddd Mmm d hh:mm:ss yyyy'>;
                var scan_seed  = scan seed<integer>
                var scan_ms    = elapsed time in ms<integer>;

        """
        re_result = self.re_metadata.findall(self.metadata_stream)
        metadata = dict({el[0]: el[1] for el in re_result})
        # Check if the version if the good one.
        if self.check_version(metadata, key='sf_version'):
            return metadata
        raise NotSupportedVersionError('PTP does NOT support this version of Skipfish.')

    def _parse_report_full(self, dir_list):
        """Parse HTTP requests from directories listed in the samples.js file.

        From all the directories, it reads request.dat and response.dat file and return a list of dict resquests and
        responses.

        """
        data = []
        for dirs in dir_list:
            try:
                with open(os.path.join(dirs['dir'], 'request.dat'), 'r') as req_data:
                    request = req_data.read()
            except IOError:
                request = "NOT_FOUND"
            try:
                with open(os.path.join(dirs['dir'], 'response.dat'), 'r') as res_data:
                    response = res_data.read()
                    response_status_code = self._re_reponse_status_code.findall(response)[0]
                    response_header, response_body = response.split('\n\n', 1)
            except IOError:
                response_body = response_header = response_status_code = "NOT_FOUND"
            # Somehow follow naming conventions from http://docs.python-requests.org/en/master/
            data.append({
                'request': request,
                'status_code': response_status_code,
                'headers': response_header,
                'body': response_body
            })
        return data

    def parse_report(self):
        """Retrieve the results from the report.

        :raises: :class:`ReportNotFoundError` -- if the report file was not found.

        :return: List of dicts where each one represents a discovery.
        :rtype: :class:`list`

        .. note::

            Example of retrieved data after conversion (i.e. `raw_report`) using the module :mod:`ast`:

            .. code-block:: js

                [{ 'severity': 3, 'type': 40402, 'samples': [
                    { 'url': 'http://demo.testfire.net/bank/login.aspx', 'extra': 'SQL syntax string', 'sid': '21010', 'dir': '_i2/0' },
                    { 'url': 'http://demo.testfire.net/bank/login.aspx', 'extra': 'SQL syntax string', 'sid': '21010', 'dir': '_i2/1' },
                    { 'url': 'http://demo.testfire.net/subscribe.aspx', 'extra': 'SQL syntax string', 'sid': '21010', 'dir': '_i2/2' } ]
                },]

        """
        REPORT_VAR_NAME = 'issue_samples'
        variables = self.re_var_pattern.findall(self.report_stream)
        split_data = self.report_stream.split(";")
        js_data = [data for data in split_data if data is not None]
        py_data = []
        format_data = {}  # Final python dict after converting js to py
        dirs = []  # List of directories of all urls
        # Converting js to py to make it simple to process
        for data in js_data:
            temp_data = js2py.eval_js(data)
            if temp_data is not None:
                py_data.append(temp_data)

        # Mapping variable to its content
        for i in range(len(py_data)):
            format_data[variables[i]] = py_data[i]

        if REPORT_VAR_NAME not in variables:
            raise ReportNotFoundError('PTP did NOT find issue_samples variable. Is this the correct file?')
        # We now have a raw version of the Skipfish report as a list of dict.
        self.vulns = [
            {'ranking': self.RANKING_SCALE[vuln['severity']]}
            for vuln in format_data[REPORT_VAR_NAME]]
        if not self.light:
            for var in variables:
                for item in format_data[var]:
                    for sample in item['samples']:
                        dirs.append({'url': sample['url'], 'dir': os.path.join(self.search_directory, sample['dir'])})
            self.vulns.append({'ranking': constants.UNKNOWN, 'transactions': self._parse_report_full(dirs)})
        return self.vulns class W3AFXMLParser(XMLParser):
    """W3AF XML specialized parser."""

    __tool__ = 'w3af'
    __format__ = 'xml'
    __version__ = r'1\.[0-9]+(\.[0-9]+)?'
    __httpfile_format__ = "*.http.txt"

    _re_version = re.compile(r'Version: (\S*)\s')
    _re_transaction = re.compile(r"(?<=={30}Request )[0-9]+ .*?={9}\n(.*?)(?=\n={70})", re.S)
    _re_request = re.compile(r"(^.*?)\n==.*?(?=={20}Response )", re.S)
    _re_response = re.compile(r"(?<=={40}Response )[0-9]+ .*?={9}\n(\w.*)", re.S)
    _re_reponse_header = re.compile(r".*?content-type: .*?\n", re.S)
    _re_response_body = re.compile(r"(?<=content-type: )(.*?\n)(.*)", re.S)
    _re_response_status_code = re.compile(r"(?<=HTTP/\w.\w )(.*)")

    HIGH = 'High'
    MEDIUM = 'Medium'
    LOW = 'Low'
    INFO = 'Information'

    RANKING_SCALE = {
        HIGH: constants.HIGH,
        MEDIUM: constants.MEDIUM,
        LOW: constants.LOW,
        INFO: constants.INFO}

    def __init__(self, pathname, filename='*.xml', light=False, first=True):
        self.pathname = pathname  # Keep a copy of the working directory for HTTP requests parsing.
        XMLParser.__init__(self, pathname=pathname, filename=filename, light=light, first=first)

    @classmethod
    def is_mine(cls, pathname, filename='*.xml', light=False, first=True):
        """Check if it can handle the report file.

        :param str pathname: Path to the report directory.
        :param str filename: Regex matching the report file.
        :param bool light: `True` to only parse the ranking of the findings from the report.
        :param bool first: Only process first file (``True``) or each file that matched (``False``).

        :raises IOError: when the report file cannot be found.
        :raises OSError: when the report file cannot be found.

        :return: `True` if it supports the report, `False` otherwise.
        :rtype: :class:`bool`

        """
        try:
            stream = cls.handle_file(pathname, filename, first=first)
        except (TypeError, XMLSyntaxError):
            return False
        version = stream.find('.//w3af-version')
        if version is None:
            return False
        version = cls._re_version.findall(version.text)
        if not version:
            return False
        if len(version) >= 1:  # In case we found several version numbers.
            version = version[0]
        if not re.findall(cls.__version__, version, re.IGNORECASE):
            return False
        return True

    def parse_metadata(self):
        """Parse the metadata of the report.

        :raises: :class:`NotSupportedVersionError` -- if it does not support the version of this report.

        :return: The metadata of the report.
        :rtype: dict

        """
        raw_metadata = self.stream.find('.//w3af-version').text
        # Find the version of w3af.
        version = self._re_version.findall(raw_metadata)
        if len(version) >= 1:  # In case we found several version numbers.
            version = version[0]
        # Reconstruct the metadata
        # TODO: Retrieve the other metadata likes the date, etc.
        metadata = {'version': version}
        if self.check_version(metadata):
            self.metadata = metadata
        else:
            raise NotSupportedVersionError('PTP does NOT support this version of W3AF.')
        return self.metadata

    def _parse_report_full(self, raw_transactions):
        """Parse the captured http transactions of the report.

        :param str raw_transactions: HTTP transactions logged by w3af

        :return: List of dicts where each one has a request and response.
        :rtype: :class:`list`

        """
        data = []
        transactions = self._re_transaction.findall(raw_transactions)
        for count, transaction in enumerate(transactions):
            response = self._re_response.findall(transaction)[0] + '\n'
            response_header = self._re_reponse_header.search(response).group()
            # Somehow follow naming conventions from http://docs.python-requests.org/en/master/
            data.append({
                'request': self._re_request.findall(transaction)[0].strip() + '\n\n',
                'status_code': self._re_response_status_code.search(response_header).group(),
                'headers': response_header,
                'body': self._re_response_body.search(response).group(2)
            })
        return data

    def parse_report(self):
        """Parse the results of the report.

        :return: List of dicts where each one represents a discovery.
        :rtype: :class:`list`

        """
        self.vulns = [
            {'ranking': self.RANKING_SCALE[vuln.get('severity')]}
            for vuln in self.stream.findall('.//vulnerability')]
        if not self.light:
            try:
                self.vulns.append({
                    'ranking': constants.UNKNOWN,
                    'transactions': self._parse_report_full(FileParser.handle_file(self.pathname, self.__httpfile_format__))})
            except (OSError, IOError):
                # There is no additional file referencing the HTTP requests. We silently pass.
                pass
        return self.vulns class WapitiXMLParser(XMLParser):
    """Wapiti XML specialized parser."""

    __tool__ = 'wapiti'
    __format__ = 'xml'
    __version__ = r'^\D*(2\.3(\.[0-9]+)?)$'

    @classmethod
    def is_mine(cls, pathname, filename='*.xml', light=True, first=True):
        """Check if it can handle the report file.

        :param str pathname: Path to the report directory.
        :param str filename: Regex matching the report file.
        :param bool light: `True` to only parse the ranking of the findings from the report.
        :param bool first: Only process first file (``True``) or each file that matched (``False``).

        :raises IOError: when the report file cannot be found.
        :raises OSError: when the report file cannot be found.

        :return: `True` if it supports the report, `False` otherwise.
        :rtype: :class:`bool`

        """
        try:
            stream = cls.handle_file(pathname, filename, first=first)
        except (TypeError, XMLSyntaxError):
            return False
        raw_metadata = stream.find('.//report_infos')
        if raw_metadata is None:
            return False
        metadata = {el.get('name'): el.text for el in raw_metadata}
        if metadata.get('generatorName', '').lower() != cls.__tool__:
            return False
        if not re.match(cls.__version__, metadata.get('generatorVersion', '')):
            return False
        return True

    def parse_metadata(self):
        """Parse the metadata of the report.

        :raises: :class:`NotSupportedVersionError` -- if it does not support the version of this report.

        :return: The metadata of the report.
        :rtype: dict

        """
        # Find the metadata of Wapiti.
        raw_metadata = self.stream.find('.//report_infos')
        # Reconstruct the metadata
        metadata = {el.get('name'): el.text for el in raw_metadata}
        # Only keep the version number
        metadata['generatorVersion'] = metadata['generatorVersion'].lstrip('Wapiti ')
        if not self.check_version(metadata, key='generatorVersion'):
            raise NotSupportedVersionError('PTP does NOT support this version of Wapiti.')
        self.metadata = metadata
        return metadata

    def parse_report(self):
        """Parse the results of the report.

        :return: List of dicts where each one represents a discovery.
        :rtype: :class:`list`

        """
        section_vuln = self.stream.find('.//vulnerabilities')
        if section_vuln is None:
            return []
        vulns = []
        for category in section_vuln.findall('.//vulnerability'):
            entries = category.find('.//entries')
            if not len(entries):
                pass
            # Ensure that there are 'entry' sub-sections that represent the
            # actual discoveries/vulnerabilities.
            if len(entries.findall('.//entry')) and category.get('name') in SIGNATURES:
                vulns.append({
                    'name': category.get('name'),
                    'ranking': SIGNATURES.get(category.get('name'), UNKNOWN),
                    'description': category.find('.//description').text})
        self.vulns = vulns
        return vulns class Wapiti221XMLParser(XMLParser):
    """Wapiti XML specialized parser."""

    #: :class:`str` -- Name of the tool.
    __tool__ = 'wapiti'
    #: :class:`str` -- Format of Wapiti reports it supports.
    __format__ = 'xml'
    #: :class:`list` -- Wapiti versions it supports.
    __version__ = r'2\.2(\.[0-9]+)?'

    @classmethod
    def is_mine(cls, pathname, filename='*.xml', light=True, first=True):
        """Check if it is a supported Wapiti report.

        :param str pathname: Path to the report directory.
        :param str filename: Regex matching the report file.
        :param bool light: `True` to only parse the ranking of the findings from the report.
        :param bool first: Only process first file (``True``) or each file that matched (``False``).

        :return: `True` if it supports the report, `False` otherwise.
        :rtype: :class:`bool`

        """
        try:
            stream = cls.handle_file(pathname, filename, first=first)
        except (IOError, XMLSyntaxError):
            return False
        raw_metadata = stream.find('.//generatedBy')
        if raw_metadata is None:
            return False
        metadata = raw_metadata.get('id')
        if not re.findall(cls.__version__, metadata, re.IGNORECASE):
            return False
        return True

    def parse_metadata(self):
        """Parse the metadata of the report.

        :raises: :class:`NotSupportedVersionError` -- if it does not support the version of this report.

        :return: The metadata of the report.
        :rtype: dict

        """
        # Find the metadata of Wapiti.
        raw_metadata = self.stream.find('.//generatedBy').get('id')
        metadata = {}
        # Only keep the version number
        metadata['version'] = raw_metadata
        if not self.check_version(metadata):
            raise NotSupportedVersionError('PTP does NOT support this version of Wapiti.')
        self.metadata = metadata
        return self.metadata

    def parse_report(self):
        """Parse the results of the report.

        :return: List of dicts where each one represents a discovery.
        :rtype: :class:`list`

        """
        section_vuln = self.stream.find('.//bugTypeList')
        vulns = []
        for category in section_vuln.findall('.//bugType'):
            entries = category.find('.//bugList')
            if not len(entries):
                pass
            # Ensure that there are 'entry' sub-sections that represent the actual discoveries/vulnerabilities.
            if len(entries.findall('.//bug')) and category.get('name') in SIGNATURES:
                vulns.append({
                    'name': category.get('name'),
                    'ranking': SIGNATURES.get(category.get('name'), UNKNOWN),
                    'description': category.find('.//description').text})
        self.vulns = vulns
        return vulns class MetasploitParser(FileParser):
    """Metasploit specialized parser."""

    __tool__ = 'metasploit'
    __plugin__ = ''

    def __init__(self, pathname, filename='*.txt', plugin='', light=True, first=True):
        """Initialize MetasploitParser.

        :param str pathname: Path to the report directory.
        :param str filename: Regex matching the report file.
        :param str plugin: Name of the plugin that generated the report.
        :param bool first: Only process first file (``True``) or each file that matched (``False``).

        """
        self.__plugin__ = plugin
        FileParser.__init__(self, pathname, filename, first=first)

    @classmethod
    def is_mine(cls, pathname, filename='*.txt', plugin='', light=True, first=True):
        """Check if it can handle the report file.

        :param str pathname: Path to the report directory.
        :param str filename: Regex matching the report file.
        :param str plugin: Name of the plugin that generated the report.
        :param bool light: `True` to only parse the ranking of the findings from the report.
        :param bool first: Only process first file (``True``) or each file that matched (``False``).

        :return: `True` if it supports the report, `False` otherwise.
        :rtype: :class:`bool`

        """
        # TODO: Properly check the supported versions.
        if plugin:
            return True
        return False

    def parse_metadata(self):
        """Parse the metadata of the report.

        :return: The metadata of the report.
        :rtype: dict

        """
        # TODO: Properly retrieve the metadata.
        return {}

    def parse_report(self):
        """Parse the results of the report.

        :return: List of dicts where each one represents a discovery.
        :rtype: :class:`list`

        """
        try:
            signatures = SIGNATURES.get(self.__plugin__, {}).iteritems()
        except AttributeError:  # Python3
            signatures = SIGNATURES.get(self.__plugin__, {}).items()
        self.vulns = [{
            'name': self.__plugin__,
            'ranking': ranking}
            for signature, ranking in signatures
            if signature in self.stream]
        return self.vulns class DirbusterParser(LineParser):
    """DirBuster specialized parser."""

    __tool__ = 'dirbuster'
    __format__ = 'dirbuster'
    __version__ = ['1.0-RC1']

    #: :class:`str` -- Regex matching DirBuster section separator.
    _re_sep = r"^-*$"
    #: :class:`str` -- Regex matching DirBuster version.
    _re_version = r"^DirBuster (?P<version>[0-9]+\.[0-9]+(-RC[0-9])?) - Report$"
    #: :class:`str` -- Regex matching DirBuster directories status code.
    _re_dir_status = r"^Dirs found with a (?P<status>[0-9]{3}) response:$"
    #: :class:`str` -- Regex matching DirBuster files status code.
    _re_file_status = r"^Files found with a (?P<status>[0-9]{3}) responce:$"

    def __init__(self, pathname, filename='DirBuster-Report*', light=False, first=True):
        """Initialize DirbusterParser.

        :param str pathname: Path to the report directory.
        :param str filename: Regex matching the report file.
        :param bool first: Only process first file (``True``) or each file that matched (``False``).

        """
        LineParser.__init__(self, pathname, filename, first=first)

    @classmethod
    def is_mine(cls, pathname, filename='DirBuster-Report*', light=False, first=True):
        """Check if it can handle the report file.

        :param str pathname: Path to the report directory.
        :param str filename: Regex matching the report file.
        :param bool light: `True` to only parse the ranking of the findings from the report.
        :param bool first: Only process first file (``True``) or each file that matched (``False``).

        :raises IOError: when the report file cannot be found.
        :raises OSError: when the report file cannot be found.

        :return: `True` if it supports the report, `False` otherwise.
        :rtype: :class:`bool`

        """
        stream = cls.handle_file(pathname, filename, first=first)
        if stream and re.match(cls._re_version, stream[0]):
            return True
        return False

    def parse_metadata(self):
        """Parse the metadata of the report.

        :return: The metadata of the report.
        :rtype: dict

        """
        metadata = {}
        if len(self.stream):
            version = re.match(self._re_version, self.stream[0])
            if version:
                metadata['version'] = version.group('version')
        self.metadata = metadata
        return metadata

    def parse_report(self):
        """Parser the results of a DirBuster report.

        :return: List of dicts where each one represents a vuln.
        :rtype: :class:`list`

        """
        # Retrieve the results from the report.
        discoveries = {'directories': [], 'files': [], 'errors': []}
        type_disco = 'errors'
        status_code = -1
        for line in self.stream:
            match_dir_status = re.match(self._re_dir_status, line)
            match_file_status = re.match(self._re_file_status, line)
            if match_dir_status:
                type_disco = 'directories'
                status_code = match_dir_status.group('status')
            elif match_file_status:
                type_disco = 'files'
                status_code = match_file_status.group('status')
            elif line.startswith('/'):  # This line contains a discovery
                status = int(status_code)
                # If this is the section of the successful ones (2xx).
                if status >= 200 and status < 300:
                    discoveries[type_disco].append(line)
        # Match found discoveries against signatures database.
        vulns = []
        matching = ((DIRECTORIES, 'directories'), (FILES, 'files'))
        for signatures, type_disco in matching:
            try:
                signatures = signatures.iteritems()
            except AttributeError:  # Python3
                signatures = signatures.items()
            for signature, ranking in signatures:
                matched = [True for disco in discoveries[type_disco] if re.match(signature, disco)]
                if True in matched:
                    vulns.extend([{'ranking': ranking}])
        self.vulns = vulns
        return vulns class NmapXMLParser(XMLParser):

    __tool__ = 'nmap'
    __version__ = r'6\.46'

    @classmethod
    def is_mine(cls, pathname, filename='*.xml', light=False, first=True):
        """Check if it can handle the report file.

        :param str pathname: Path to the report directory.
        :param str filename: Regex matching the report file.
        :param bool light: `True` to only parse the ranking of the findings from the report.
        :param bool first: Only process first file (``True``) or each file that matched (``False``).

        :raises IOError: when the report file cannot be found.
        :raises OSError: when the report file cannot be found.

        :return: `True` if it supports the report, `False` otherwise.
        :rtype: :class:`bool`

        """
        try:
            stream = cls.handle_file(pathname, filename, first=first)
        except (TypeError, XMLSyntaxError):
            return False
        if stream.get('scanner') != cls.__tool__:
            return False
        if not re.findall(cls.__version__, stream.get('version'), re.IGNORECASE):
            return False
        return True

    def parse_metadata(self):
        """Parse the metadatas of the report.

        :return: The metadatas of the report.
        :rtype: dict

        :raises: :class:`NotSupportedVersionError` -- if it does not support the version of this report.

        """
        # Find the metadata of Nmap.
        metadata = {key: value for key, value in self.stream.items()}
        if self.check_version(metadata):
            self.metadata = metadata
        else:
            raise NotSupportedVersionError('PTP does NOT support this version of Nmap.')

    def parse_report(self):
        """Parse the results of the report.

        :return: List of dicts where each one represents a vuln.
        :rtype: :class:`list`

        .. warning::

            Not implemented yet.

        """
        # TODO: Parse Nmap result
        ports = self.stream.findall('.//port')
        return ports class OWASPCM008Parser(LineParser):
    """OWASPCM008 specialized parser."""

    __tool__ = 'owasp-cm-008'

    @classmethod
    def is_mine(cls, pathname, filename='*', light=True, first=False):
        """Check if it can handle the report file.

        :param str pathname: Path to the report directory.
        :param str filename: Regex matching the report file.
        :param bool light: `True` to only parse the ranking of the findings from the report.
        :param bool first: Only process first file (``True``) or each file that matched (``False``).

        :raises IOError: when the report file cannot be found.
        :raises OSError: when the report file cannot be found.

        :return: `True` if it supports the report, `False` otherwise.
        :rtype: :class:`bool`

        """
        stream = cls.handle_file(pathname, filename, first=first)
        if stream and stream[0].startswith('HTTP'):  # FIXME: Weak check here...
            return True
        return False

    def parse_metadata(self):
        """Parse the metadata of the report.

        :return: The metadata of the report.
        :rtype: dict

        """
        self.metadata = {'date': line.lstrip('Date: ') for line in self.stream if line.startswith('Date')}
        return self.metadata

    def parse_report(self):
        """Parser the results of OWASP-CM-008 results.

        :return: List of dicts where each one represents a discovery.
        :rtype: :class:`list`

        """
        allowed_methods = [
            line.lstrip('Allow: ').split(', ')
            for line in self.stream
            if line.startswith('Allow')]
        if not allowed_methods:
            return []
        self.vulns = [
            {'ranking': SIGNATURES.get(method, UNKNOWN)}
            for methods in allowed_methods
            for method in methods]
        return self.vulns class RobotsParser(LineParser):
    """Robots specialized parser."""

    __tool__ = 'robots'
    __format__ = 'txt'

    def __init__(self, pathname, filename='*robots.txt', light=False, first=True):
        LineParser.__init__(self, pathname, filename, light=light, first=first)

    @classmethod
    def is_mine(cls, pathname, filename='*robots.txt', light=False, first=True):
        """Check if it can handle the report file.

        :param str pathname: Path to the report directory.
        :param str filename: Regex matching the report file.
        :param bool light: `True` to only parse the ranking of the findings from the report.
        :param bool first: Only process first file (``True``) or each file that matched (``False``).

        :raises IOError: when the report file cannot be found.
        :raises OSError: when the report file cannot be found.

        :return: `True` if it supports the report, `False` otherwise.
        :rtype: :class:`bool`

        """
        stream = cls.handle_file(pathname, filename, first=first)
        if stream and stream[0].startswith(('User-agent:', 'Disallow:', 'Allow:')):  # FIXME: Weak check here...
            return True
        return False

    def parse_metadata(self):
        """Parse the metadata of the report.

        :return: The metadata of the report.
        :rtype: dict

        """
        return {}  # No metadata to retrieve in robots.txt

    def parse_report(self):
        """Parser the results of a Robots.txt report.

        :return: List of dicts where each one represents a vuln.
        :rtype: :class:`list`

        """
        disallowed_entries = [line.lstrip('Disallow: ') for line in self.stream if line.startswith('Disallow')]
        if not disallowed_entries:
            return []
        self.vulns = [
            {'ranking': SIGNATURES.get(entry, UNKNOWN)}
            for entry in disallowed_entries]
        return self.vulns class BurpXMLParser(XMLParser):
    """BurpSuite XML specialized parser."""

    __tool__ = 'burpsuite'
    __version__ = r'1\.[0-9]+[\.[0-9]+]?'

    @classmethod
    def is_mine(cls, pathname, filename='*.xml', light=False, first=True):
        """Check if it can handle the report file.

        :param str pathname: Path to the report directory.
        :param str filename: Regex matching the report file.
        :param bool light: `True` to only parse the ranking of the findings from the report.
        :param bool first: Only process first file (``True``) or each file that matched (``False``).

        :return: `True` if it supports the report, `False` otherwise.
        :rtype: :class:`bool`

        """
        try:
            stream = cls.handle_file(pathname, filename, first=first)
        except (TypeError, XMLSyntaxError):
            return False
        version = stream.attrib.get('burpVersion', '')
        if not version:
            return False
        if not re.findall(cls.__version__, version, re.IGNORECASE):
            return False
        return True

    def parse_metadata(self):
        """Parse the metadata of the report.

        :raises: :class:`NotSupportedVersionError` -- if it does not support the version of this report.

        :return: The metadata of the report.
        :rtype: dict

        """
        raw_metadata = self.stream.attrib.get('burpVersion', '')
        # Reconstruct the metadata
        # TODO: Retrieve the other metadata likes the date, etc.
        metadata = {'version': raw_metadata}
        if self.check_version(metadata):
            self.metadata = metadata
        else:
            raise NotSupportedVersionError('PTP does NOT support this version of BurpSuite.')
        return self.metadata

    def parse_report(self):
        """Parse the results of the report.

        :return: List of dicts where each one has a request and response.
        :rtype: :class:`list`

        """
        self.data = []
        data = []
        # TODO: maybe a better way to get base64 value
        if self.stream.find('.//request').attrib['base64'] == 'false':
            is_base64 = False
        else:
            is_base64 = True
        for item in self.stream.findall('.//item'):
            response_status_code = item.find('status').text
            if is_base64:
                request = base64.b64decode(item.find('request').text).decode('utf-8')
                response = base64.b64decode(item.find('response').text).decode('utf-8')
            else:
                request = item.find('request').text
                response = item.find('response').text
            response_headers, response_body = response.split('\r\n\r\n', 1)
            # Somehow follow naming conventions from http://docs.python-requests.org/en/master/
            data.append({
                'request': request,
                'status_code': response_status_code,
                'headers': response_headers,
                'body': response_body
            })
        self.data.append({'ranking': constants.UNKNOWN, 'transactions': data})
        return self.data class HoppyParser(FileParser):
    """Hoppy specialized parser."""

    __tool__ = 'hoppy'
    __version__ = r'1\.[0-9]+(\.[0-9]+)?'

    _re_version = re.compile(r"\D{3} \S+ (\d\.\d+\.\d+) \S")
    _re_transaction = re.compile(r"(?<=We Sent:)\n.*?(\S+ /.*?)\n(?=\n\t\D{3} Parsed Response:)", re.S)
    _re_request = re.compile(r"(\S+ /.*?)\n(?=Server)", re.S)
    _re_response = re.compile(r"(?<=Responded:\n\n)(.*)", re.S)
    _re_response_status_code = re.compile(r"(?<=HTTP/\w.\w )(.*)")
    _re_response_parse = re.compile(r"(?P<headers>HTTP.*?)\n(?=\r\n)(?P<body>.*)", re.S)

    def __init__(self, pathname, filename='*.spider', **kwargs):
        """Initialize HoppyParser.

        :param str pathname: Path to the report directory.
        :param str filename: Regex matching the report file.

        """
        FileParser.__init__(self, pathname, filename, **kwargs)

    @classmethod
    def is_mine(cls, pathname, filename='*.summary', light=False, first=True):
        """Check if it can handle the report file.

        :param str pathname: Path to the report directory.
        :param str filename: Regex matching the report file.
        :param bool light: `True` to only parse the ranking of the findings from the report.
        :param bool first: Only process first file (``True``) or each file that matched (``False``).

        :return: `True` if it supports the report, `False` otherwise.
        :rtype: :class:`bool`

        """
        cls.pathname = pathname
        try:
            stream = cls.handle_file(pathname, filename, first=first)
        except (TypeError):
            return False
        version = cls._re_version.findall(stream)
        if not version:
            return False
        if len(version) >= 1:  # In case we found several version numbers.
            version = version[0]
        if not re.findall(cls.__version__, version, re.IGNORECASE):
            return False
        cls.version = version
        return True

    def parse_metadata(self):
        """Parse the metadata of the report.

        :raises: :class:`NotSupportedVersionError` -- if it does not support the version of this report.

        :return: The metadata of the report.
        :rtype: dict

        """
        # TODO: Retrieve the other metadata likes the date, etc.
        metadata = {'version': self.version}
        if self.check_version(metadata):
            self.metadata = metadata
        else:
            raise NotSupportedVersionError('PTP does NOT support this version of Hoppy.')
        return self.metadata

    def parse_report(self):
        """Parse the results of the report.

        :return: List of dicts where each one has a request and response.
        :rtype: :class:`list`

        """
        self.data = []
        data = []
        transactions = self._re_transaction.findall(self.stream)
        if not transactions:
            return None
        for count, transaction in enumerate(transactions):
            response = self._re_response.search(transaction).group().strip() + '\n\n'
            status_code = self._re_response_status_code.findall(response)
            parsed_response = self._re_response_parse.findall(response)
            # Somehow follow naming conventions from http://docs.python-requests.org/en/master/
            data.append({
                'request': self._re_request.findall(transaction)[0].strip() + '\n\n',
                'status_code': status_code[0].strip() + '\n',
                'headers': parsed_response[0][0].strip() + '\n\n',
                'body': parsed_response[0][1].strip() + '\n\n'
            })
        self.data.append({'ranking': constants.UNKNOWN, 'transactions': data})
        return self.data
from .libptp.exceptions import NotSupportedToolError, NotSupportedVersionError
from .libptp.constants import UNKNOWN, RANKING_SCALE
from .tools.arachni.parser import ArachniXMLParser, ArachniJSONParser
from .tools.skipfish.parser import SkipfishJSParser
from .tools.w3af.parser import W3AFXMLParser
from .tools.wapiti.parser import WapitiXMLParser, Wapiti221XMLParser
from .tools.metasploit.parser import MetasploitParser
from .tools.dirbuster.parser import DirbusterParser
from .tools.nmap.parser import NmapXMLParser
from .tools.owasp.cm008.parser import OWASPCM008Parser
from .tools.robots.parser import RobotsParser
from .tools.burpsuite.parser import BurpXMLParser
from .tools.hoppy.parser import HoppyParser
"""

:synopsis: :mod:`ptp` library entry point that exposes the public API.

.. moduleauthor:: Tao Sauvage

"""



class PTP(object):

    """:class:`PTP` class exposing :mod:`ptp`'s public API.

    Example::

        ptp = PTP(pathname='my/path')
        ptp.parse()

    """

    #: :class:`dict` -- Supported tools and their parser(s).
    supported = {
        'arachni': [ArachniXMLParser, ArachniJSONParser],
        'skipfish': [SkipfishJSParser],
        'w3af': [W3AFXMLParser],

        'wapiti': [WapitiXMLParser, Wapiti221XMLParser],