diff --git a/CveXplore/VERSION b/CveXplore/VERSION index 72f9fa82..28af839c 100644 --- a/CveXplore/VERSION +++ b/CveXplore/VERSION @@ -1 +1 @@ -0.2.4 \ No newline at end of file +0.2.5 \ No newline at end of file diff --git a/CveXplore/common/cpe_converters.py b/CveXplore/common/cpe_converters.py new file mode 100644 index 00000000..6ed695b3 --- /dev/null +++ b/CveXplore/common/cpe_converters.py @@ -0,0 +1,73 @@ +""" +cpe_converters.py +================= +""" +from urllib.parse import unquote + + +def from2to3CPE(cpe, autofill=False): + """ + Method to transform cpe2.2 to cpe2.3 format + + :param cpe: cpe2.2 string + :type cpe: str + :param autofill: Whether to cpe string should be autofilled with double quotes and hyphens + :type autofill: bool + :return: cpe2.3 formatted string + :rtype: str + """ + cpe = cpe.strip() + if not cpe.startswith("cpe:2.3:"): + if not cpe.startswith("cpe:/"): + # can not do anything with this; returning original string + return cpe + cpe = cpe.replace("cpe:/", "cpe:2.3:") + cpe = cpe.replace("::", ":-:") + cpe = cpe.replace("~-", "~") + cpe = cpe.replace("~", ":-:") + cpe = cpe.replace("::", ":") + cpe = cpe.strip(":-") + cpe = unquote(cpe) + if autofill: + e = cpe.split(":") + for x in range(0, 13 - len(e)): + cpe += ":*" + return cpe + + +def from3to2CPE(cpe): + """ + Method to transform cpe2.3 to cpe2.2 format + + :param cpe: cpe2.3 string + :type cpe: str + :return: cpe2.2 string + :rtype: str + """ + cpe = cpe.strip() + if not cpe.startswith("cpe:/"): + if not cpe.startswith("cpe:2.3:"): + # can not do anything with this; returning original string + return cpe + cpe = cpe.replace("cpe:2.3:", "") + parts = cpe.split(":") + next = [] + first = "cpe:/" + ":".join(parts[:5]) + last = parts[5:] + if last: + for x in last: + next.append("~") if x == "-" else next.append(x) + if "~" in next: + pad(next, 6, "~") + cpe = "%s:%s" % (first, "".join(next)) + cpe = cpe.replace(":-:", "::") + cpe = cpe.strip(":") + return cpe + + +def pad(seq, target_length, padding=None): + length = len(seq) + if length > target_length: + return seq + seq.extend([padding] * (target_length - length)) + return seq diff --git a/CveXplore/main.py b/CveXplore/main.py index f965a7a7..42a57842 100644 --- a/CveXplore/main.py +++ b/CveXplore/main.py @@ -5,11 +5,13 @@ import functools import json import os +import re from collections import defaultdict from pymongo import DESCENDING from CveXplore.api.connection.api_db import ApiDatabaseSource +from CveXplore.common.cpe_converters import from2to3CPE from CveXplore.common.db_mapping import database_mapping from CveXplore.database.connection.mongo_db import MongoDBConnection from CveXplore.errors import DatabaseIllegalCollection @@ -194,29 +196,38 @@ def get_multi_store_entries(self, *queries, limit=10): return list(joined_list) - def cves_for_cpe(self, cpe_string, vuln_prod_search=False): + def cves_for_cpe(self, cpe_string): """ Method for retrieving Cves that match a single CPE string. By default the search will be made matching the configuration fields of the cves documents. :param cpe_string: CPE string: e.g. ``cpe:2.3:o:microsoft:windows_7:*:sp1:*:*:*:*:*:*`` :type cpe_string: str - :param vuln_prod_search: Search for matching products instead of configurations - :type vuln_prod_search: bool :return: List with Cves :rtype: list """ - e = cpe_string.split(":") - for x in range(0, 13 - len(e)): - cpe_string += ":*" + # format to cpe2.3 + cpe_string = from2to3CPE(cpe_string) - cpe = self.get_single_store_entry("cpe", {"cpe_2_2": cpe_string}) + if cpe_string.startswith("cpe"): + # strict search with term starting with cpe; e.g: cpe:2.3:o:microsoft:windows_7:*:sp1:*:*:*:*:*:* - if cpe is not None: - return list(cpe.iter_cves_matching_cpe(vuln_prod_search)) + remove_trailing_regex_stars = r"(?:\:|\:\:|\:\*)+$" + + cpe_regex = re.escape(re.sub(remove_trailing_regex_stars, "", cpe_string)) + + cpe_regex_string = r"^{}:".format(cpe_regex) else: - return cpe + # more general search on same field; e.g. microsoft:windows_7 + cpe_regex_string = "{}".format(re.escape(cpe_string)) + + cves = self.get_single_store_entries( + ("cves", {"vulnerable_configuration": {"$regex": cpe_regex_string}}), + limit=0, + ) + + return cves def cve_by_id(self, cve_id): """ diff --git a/CveXplore/objects/cpe.py b/CveXplore/objects/cpe.py index c8ea2f4b..73f2bbfd 100644 --- a/CveXplore/objects/cpe.py +++ b/CveXplore/objects/cpe.py @@ -2,8 +2,11 @@ cpe === """ +import re + from pymongo import DESCENDING +from CveXplore.common.cpe_converters import from2to3CPE from CveXplore.common.data_source_connection import DatasourceConnection @@ -34,8 +37,23 @@ def iter_cves_matching_cpe(self, vuln_prod_search=False): "vulnerable_product" if vuln_prod_search else "vulnerable_configuration" ) + # format to cpe2.3 + cpe_string = from2to3CPE(self.cpe_2_2) + + if cpe_string.startswith("cpe"): + # strict search with term starting with cpe; e.g: cpe:2.3:o:microsoft:windows_7:*:sp1:*:*:*:*:*:* + + remove_trailing_regex_stars = r"(?:\:|\:\:|\:\*)+$" + + cpe_regex = re.escape(re.sub(remove_trailing_regex_stars, "", cpe_string)) + + cpe_regex_string = r"^{}:".format(cpe_regex) + else: + # more general search on same field; e.g. microsoft:windows_7 + cpe_regex_string = "{}".format(re.escape(cpe_string)) + results = self._datasource_connection.store_cves.find( - {cpe_searchField: self.cpe_2_2} + {cpe_searchField: {"$regex": cpe_regex_string}} ).sort("cvss", DESCENDING) for each in results: