<gh_stars>1-10
"""Module contains functionality for parsing HTML page of a particular vulnerability."""

import re
import urllib.request

from cve_connector.vendor_cve.implementation.parsers.general_and_format_parsers\
    .html_parser import HtmlParser
from cve_connector.vendor_cve.implementation.utilities.check_correctness \
    import is_correct_cve_id
from cve_connector.vendor_cve.implementation.utilities.utility_functions \
    import concat_strings, normalize_string, string_to_date, get_number_from_string
from cve_connector.vendor_cve.implementation.vendors_storage_structures.red_hat import RedHat
from cve_connector.vendor_cve.implementation.vulnerability_metrics.cvss_v3_metrics import CvssV3

from cve_connector.vendor_cve.implementation.vulnerability_metrics.cvss_v2_metrics import CvssV2


class RedHatVulnerabilityPageParser(HtmlParser):
    """
    Contains functionality for parsing HTML of specific CVE.
    """
    def __init__(self, url):
        super().__init__(url)
        self.load_content()
        self.date_format = '%Y-%m-%d'  # 2018-04-23

    def get_content_from_ulr(self):
        """
        Gets and returns content from URL.

        :return: content
        """
        req = urllib.request.Request(url=self.url, headers={'User-Agent': 'Chrome/42.0.2311.135'})
        response = urllib.request.urlopen(req)
        # self.url
        if response.getcode() != 200:
            raise ConnectionError('Unable to load ', self.url)
        content = response.read()
        response.close()
        return content

    def parse(self):
        """
        Provides parsing functionality.

        :return: None
        """
        content_list = self.data.xpath('.//div[contains(@class, "product-page-content")]')
        if not content_list:
            return False
        content = content_list[0]

        cve_id = self.get_cve_id(content)
        if cve_id == '' or not is_correct_cve_id(cve_id):
            return False

        description = self.get_description(content)
        statement = self.get_statement(content)
        bugz_description = self.get_bugzilla_description()

        cve_table = content.find('.//dl[@class="cve-details"]')
        if cve_table == []:
            return

        impact = self.get_impact(cve_table)
        public_date = self.get_public_date(cve_table)
        cwes = self.get_cwe(cve_table)

        cvss_v3 = self.get_cvss_v3()
        cvss_v2 = self.get_cvss_v2()

        new_entity = RedHat(cve_id, description, impact, public_date, statement, bugz_description)
        new_entity.cwes.extend(cwes)
        if cvss_v3 is not None and cvss_v3.is_valid():
            new_entity.cvss_v3 = cvss_v3
        if cvss_v2 is not None and cvss_v2.is_valid():
            new_entity.cvss_v2 = cvss_v2

        self.entities.append(new_entity)

    def get_cve_id(self, content):
        """
        Extracts CVE ID from the content.

        :param content: downloaded content
        :return: CVE ID or empty string
        """
        cve_id_xpath = './/h1[@id="page-title"]/text()'
        cve_id_list = content.xpath(cve_id_xpath)
        for cve_id in cve_id_list:
            if is_correct_cve_id(cve_id):
                return cve_id
        return ''

    def get_description(self, content):
        """
        Extracts description.

        :param content: downloaded content
        :return: description
        """
        descr_xpath = './/div[@class="CVE-details"]/div[contains(@class, field-cve-details)]/text()'
        descr_list = content.xpath(descr_xpath)
        return concat_strings(descr_list)

    def get_statement(self, content):
        """
        Extracts statement.

        :param content: downloaded content
        :return: statement
        """
        statement_xpath = './/div[contains(@class, "field-cve-statement")]/p/text()'
        s_list = content.xpath(statement_xpath)
        return concat_strings(s_list, " ")

    def get_impact(self, content):
        """
        Extracts impact.

        :param content: downloaded content
        :return: impact
        """
        impact_xpath = './/a[contains(@href, ' \
                       '"http://www.redhat.com/security/updates/classification/")]' \
                       '/span[contains(@class, "badge")]/text()'
        impact_list = content.xpath(impact_xpath)
        if not impact_list:
            return ''
        return normalize_string(impact_list[0])

    def get_public_date(self, content):
        """
        Extracts date when the vulnerability was made public.

        :param content: downloaded content
        :return: public date
        """
        date_l = content.xpath('.//dd/text()')
        for date in date_l:
            date = normalize_string(date)
            if re.match(r'\d{4}-\d{2}-\d{2}', date):
                return string_to_date(normalize_string(date), self.date_format)

    def get_cwe(self, content):
        """
        Extracts CWE.

        :param content: downloaded content
        :return: CWE
        """
        cwes = []
        dd_list = content.xpath('.//dd/a/text()')
        for item in dd_list:
            cwes.extend(re.findall(r'CWE-\d+', item))
        return cwes

    def get_bugzilla_description(self):
        """
        Extracts bugzilla description.

        :return: description
        """
        b_descr_xpath = './/dl[dt[contains(text(), "Bugzilla")]]/dd/text()'
        b_descr_list = self.data.xpath(b_descr_xpath)
        return concat_strings(b_descr_list)

    def get_metric_element(self, element, xp_element):
        """
        Get element for metric of CVSS.

        :param element: XML element
        :param xp_element: to search for
        :return: value for metric
        """
        if element is None:
            return ''
        res_list = element.xpath(xp_element)
        return normalize_string(concat_strings(res_list))

    def get_cvss_v3(self):
        """
        Extracts data for CVSSv3.

        :return: object of type CVSSv3
        """
        cvss3_table = self.data.xpath(
            './/table[tbody/tr/th[contains(text(), "CVSS3")]][@class="table feature-table"]')
        if len(cvss3_table) == 0:
            return None

        table = cvss3_table[0]

        base_sc_xpath = './/tr[th[contains(text(), "Base Score")]]/td//text()'
        vec_str_xpath = './/tr[th[contains(text(), "Base Metrics")]]/td//text()'
        att_vec_xpath = './/tr[th[contains(text(), "Attack Vector")]]/td//text()'
        att_comp_xpath = './/tr[th[contains(text(), "Attack Complexity")]]/td//text()'
        priv_req_xpath = './/tr[th[contains(text(), "Privileges Required")]]/td//text()'
        user_int_xpath = './/tr[th[contains(text(), "User Interaction")]]/td//text()'
        scope_xpath = './/tr[th[contains(text(), "Scope")]]/td//text()'
        conf_xpath = './/tr[th[contains(text(), "Confidentiality")]]/td//text()'
        int_imp_xpath = './/tr[th[contains(text(), "Integrity Impact")]]/td//text()'
        av_imp_xpath = './/tr[th[contains(text(), "Availability Impact")]]/td//text()'

        base_score_str = self.get_metric_element(table, base_sc_xpath)
        base_score = get_number_from_string(base_score_str)
        string_vector = self.get_metric_element(table, vec_str_xpath)
        attack_vector = self.get_metric_element(table, att_vec_xpath)
        attack_complexity = self.get_metric_element(table, att_comp_xpath)
        privileges_req = self.get_metric_element(table, priv_req_xpath)
        user_interact = self.get_metric_element(table, user_int_xpath)
        scope = self.get_metric_element(table, scope_xpath)
        confidentiality = self.get_metric_element(table, conf_xpath)
        integrity_impact = self.get_metric_element(table, int_imp_xpath)
        availability_impact = self.get_metric_element(table, av_imp_xpath)
        return CvssV3(base_sc=base_score, vec_str=string_vector, att_vec=attack_vector,
                      att_comp=attack_complexity, priv_r=privileges_req, user_in=user_interact,
                      scope=scope, conf=confidentiality, integ=integrity_impact,
                      avail=availability_impact)

    def get_cvss_v2(self):
        """
        Extracts data for CVSSv2.

        :return: object of type CVSSv2
        """
        cvss2_table = self.data.xpath(
            './/table[tbody/tr/th[contains(text(), "Access")]][@class="table feature-table"]')
        if len(cvss2_table) == 0:
            return None
        table = cvss2_table[0]

        base_sc_xpath = './/tr[th[contains(text(), "Base Score")]]/td//text()'
        str_vect_xpath = './/tr[th[contains(text(), "Base Metrics")]]/td//text()'
        acc_vect_xpath = './/tr[th[contains(text(), "Access Vector")]]/td//text()'
        acc_comp_xpath = './/tr[th[contains(text(), "Access Complexity")]]/td//text()'
        auth_xpath = './/tr[th[contains(text(), "Authentication")]]/td//text()'
        conf_imp_xpath = './/tr[th[contains(text(), "Confidentiality")]]/td//text()'
        int_imp_xpath = './/tr[th[contains(text(), "Integrity")]]/td//text()'
        av_imp_xpath = './/tr[th[contains(text(), "Availability")]]/td//text()'
        base_sc2_str = self.get_metric_element(table, base_sc_xpath)
        base_sc2 = get_number_from_string(base_sc2_str)
        string_vector2 = self.get_metric_element(table, str_vect_xpath)
        access_vec2 = self.get_metric_element(table, acc_vect_xpath)
        access_comp2 = self.get_metric_element(table, acc_comp_xpath)
        auth2 = self.get_metric_element(table, auth_xpath)
        conf_imp2 = self.get_metric_element(table, conf_imp_xpath)
        int_imp2 = self.get_metric_element(table, int_imp_xpath)
        avail_imp2 = self.get_metric_element(table, av_imp_xpath)

        return CvssV2(base_sc=base_sc2, vector=string_vector2, acc_vec=access_vec2,
                      acc_comp=access_comp2, auth=auth2, conf=conf_imp2, integ=int_imp2,
                      avail=avail_imp2)
