From 074d1f6143740564b57f3502220945e5241d9243 Mon Sep 17 00:00:00 2001 From: Brandon Minnix Date: Fri, 26 Apr 2024 15:19:28 -0400 Subject: [PATCH] Making changes suggested during PR and discussion with Ken --- docs/user/lib_use_cases.md | 1 - docs/user/lib_use_cases_nist.md | 50 ++++++++++-- docs/user/lib_use_cases_os_version.md | 18 ++++- netutils/nist.py | 105 +++++++++++++++++++++++++- netutils/os_version.py | 79 +++++++++++++++++-- 5 files changed, 232 insertions(+), 21 deletions(-) diff --git a/docs/user/lib_use_cases.md b/docs/user/lib_use_cases.md index f01b8c79..b853b359 100644 --- a/docs/user/lib_use_cases.md +++ b/docs/user/lib_use_cases.md @@ -26,7 +26,6 @@ Functions are grouped with like functions, such as IP or MAC address based funct - Password - Provides the ability to compare and encrypt common password schemas such as type5 and type7 Cisco passwords. - Ping - Provides the ability to ping, currently only tcp ping. - Platform Mapper - Provides custom parsers for breakdown of OS Software Versions/Revisions. -- Protocol Mapper - Provides a mapping for protocol names to numbers and vice versa. - Regex - Provide convenience methods for regex to be used in Jinja2. - Route - Provides the ability to provide a list of routes and an IP Address and return the longest prefix matched route. - Time - Provides the ability to convert between integer time and string times. diff --git a/docs/user/lib_use_cases_nist.md b/docs/user/lib_use_cases_nist.md index ddce88e3..b57441d9 100644 --- a/docs/user/lib_use_cases_nist.md +++ b/docs/user/lib_use_cases_nist.md @@ -2,22 +2,56 @@ The NIST utility is used for functionality based around NIST DB Queries, and is primarily used to create URLs for the API based queries. +## Requirements + In order to use the URLs generated by `netutils.nist.get_nist_urls*`, you will need an api key provided by NIST [here]('https://nvd.nist.gov/developers/request-an-api-key'). This key will need to be passed in as an additional header in your request in the form of `{"apiKey": ""}` as stated by NIST in their [Getting Started]('https://nvd.nist.gov/developers/start-here') section. -The NIST utility can be used as a standalone module to create generic and defined custom NIST URLs as seen below: + +## Custom URLs + +The largest caveat in this functionality is the consistency of the URL values needed to obtain the CVE information. NIST NVD has specific parameters that can be used for standardization, however this does not mean that entries are standardized. Manually combing through a large amount of CPE Vendor submissions has shown that there are variations in how CPE Vendor data is presented. + +For this reason, for certain Vendor/OS combinations, a custom URL needs to be built. +- **Cisco IOS CPE String** - `cpe:2.3:o:cisco:ios:15.5\\(2\\)s1c:*` + - `15.5\\(2\\)s1c:*` - As seen here, Cisco uses CPE strings that do not include the `:` delimiter, which can be queried using escape characters in the search string. **This is the format of ALL "generic" OS/Other platforms that do not have their own custom NIST URL builder when querying NIST.** + - Default URL Output - `'https://services.nvd.nist.gov/rest/json/cves/2.0?virtualMatchString=cpe:2.3:o:cisco:ios:15.5\\(2\\)s1c:*'` + +- **Juniper JunOS CPE String** - `cpe:2.3:o:juniper:junos:10.2:r2:*:*:*:*:*:*` + - `10.2:r2:*:*:*:*:*:*` - As noted here, one of the provided URLs to query for this Juniper JunOS OS platform includes additional values that follow NIST delimiter structures. In the case where the parser provides multiple URLs, they will both be evaluated and the CVE from both will be added and associated. + - Custom URL Output - `['https://services.nvd.nist.gov/rest/json/cves/2.0?virtualMatchString=cpe:2.3:o:juniper:junos:10.2r2:*:*:*:*:*:*:*', 'https://services.nvd.nist.gov/rest/json/cves/2.0?virtualMatchString=cpe:2.3:o:juniper:junos:10.2:r2:*:*:*:*:*:*']` + + +## Examples + +The easiest way to access this utility is by using the `os_platform_object_builder`, and providing arguments for Vendor, OS/Other Platform, and Version. +Here are a few examples showing how to use this in your python code. + ```python -from nist import get_nist_url_funcs -# Generic URLs only require vendor, os_type, and version_string -generic = get_nist_url_funcs['default']({"vendor": "Cisco", "os_type": "IOS", "version_string": "15.5"}) -# ['https://services.nvd.nist.gov/rest/json/cves/2.0?virtualMatchString=cpe:2.3:o:Cisco:IOS:15.5:*'] +from netutils.nist import os_platform_object_builder + +# Create the platform objects to get NIST query URL(s) for. +cisco_ios = os_platform_object_builder("Cisco", "IOS", "15.5(2)S1c") +juniper_junos = os_platform_object_builder("Juniper", "JunOS", "10.2R2.11") + +# Get NIST URL for the Cisco IOS object +cisco_ios.get_nist_urls() +# ['https://services.nvd.nist.gov/rest/json/cves/2.0?virtualMatchString=cpe:2.3:o:cisco:ios:15.5\\(2\\)s1c:*'] + +# Get NIST URL(s) for the Juniper JunOS object +juniper_junos.get_nist_urls() +# ['https://services.nvd.nist.gov/rest/json/cves/2.0?virtualMatchString=cpe:2.3:o:juniper:junos:10.2r2:*:*:*:*:*:*:*', 'https://services.nvd.nist.gov/rest/json/cves/2.0?virtualMatchString=cpe:2.3:o:juniper:junos:10.2:r2:*:*:*:*:*:*'] +``` + +The NIST URL utility can also be used as a standalone module to create defined custom NIST URLs. This would only be useful if you have defined your own custom URL builders based on a custom input dictionary and defined in `get_nist_url_funcs`. See below: +```python +from netutils.nist import get_nist_url_funcs -# Custom URL builds require a more defined dictionary. -# See the documentation on platform_mapper.os_platform_object_builder, it may be more useful. +# The below example is using the JunOS custom builder. juniper_junos = get_nist_url_funcs['juniper']['junos']({'isservice': False, 'ismaintenance': False, 'isfrs': True, 'isspecial': False, 'service': None, 'service_build': None, 'service_respin': None, 'main': '12', 'minor': '4', 'type': 'R', 'build': None}) # ['https://services.nvd.nist.gov/rest/json/cves/2.0?virtualMatchString=cpe:2.3:o:juniper:junos:12.4r:*:*:*:*:*:*:*'] ``` -Current OS/Other Platform types that require a custom NIST URL: +Currently known OS/Other Platform types that require a custom NIST URL: - Juniper JunOS diff --git a/docs/user/lib_use_cases_os_version.md b/docs/user/lib_use_cases_os_version.md index aab71273..e9fbe2f3 100644 --- a/docs/user/lib_use_cases_os_version.md +++ b/docs/user/lib_use_cases_os_version.md @@ -7,13 +7,21 @@ Version parsing takes the software version given as a string, and deconstructs t Current Version Parsers: +- Default Parser - Juniper JunOS -**See the following Juniper JunOS parsed version:** +**See the following Default and Juniper JunOS parsed versions:** ```python ->>> from os_version import juniper_junos_version_parser ->>> juniper_junos_version_parser("12.2x50:d41.1") +>>> from os_version import default_os_metadata, juniper_junos_metadata + +>>> default_os_metadata("cisco", "ios", "15.5") +{ + "major": "15", + "minor": "5", + "patch": None, +} +>>> juniper_junos_metadata("juniper", "junos", "12.2x50:d41.1") { "isservice": false, "ismaintenance": false, @@ -23,8 +31,10 @@ Current Version Parsers: "service_build": "41", "service_respin": "1", "main": "12", + "major": "12", "minor": "2", "type": "x", - "build": "50" + "build": "50", + "patch": "50" } ``` diff --git a/netutils/nist.py b/netutils/nist.py index cdeea28d..bbf94720 100644 --- a/netutils/nist.py +++ b/netutils/nist.py @@ -1,7 +1,65 @@ -"""Functions building NIST URLs from the os platform values.""" +"""Classes and functions used for building NIST URLs from the os platform values.""" +import abc +import dataclasses import re import typing as t +from netutils.os_version import version_metadata + +# Setting up the dataclass values for specific parsers +PLATFORM_FIELDS: t.Dict[str, t.Any] = { + "default": [ + ("vendor", str), + ("os_type", str), + ("version_string", str), + ("major", str, dataclasses.field(default=None)), # pylint: disable=[E3701] + ("minor", str, dataclasses.field(default=None)), # pylint: disable=[E3701] + ("patch", str, dataclasses.field(default=None)), # pylint: disable=[E3701] + ("prerelease", str, dataclasses.field(default=None)), # pylint: disable=[E3701] + ("buildmetadata", str, dataclasses.field(default=None)), # pylint: disable=[E3701] + ("vendor_metadata", bool, dataclasses.field(default=False)), # pylint: disable=[E3701] + ], + "juniper": { + "junos": [ + ("main", str, dataclasses.field(default=None)), # pylint: disable=[E3701] + ("type", str, dataclasses.field(default=None)), # pylint: disable=[E3701] + ("build", str, dataclasses.field(default=None)), # pylint: disable=[E3701] + ("service", str, dataclasses.field(default=None)), # pylint: disable=[E3701] + ("service_build", int, dataclasses.field(default=None)), # pylint: disable=[E3701] + ("service_respin", str, dataclasses.field(default=None)), # pylint: disable=[E3701] + ("isservice", bool, dataclasses.field(default=False)), # pylint: disable=[E3701] + ("ismaintenance", bool, dataclasses.field(default=False)), # pylint: disable=[E3701] + ("isfrs", bool, dataclasses.field(default=False)), # pylint: disable=[E3701] + ("isspecial", bool, dataclasses.field(default=False)), # pylint: disable=[E3701] + ] + }, +} + + +class OsPlatform(metaclass=abc.ABCMeta): + """Base class for dynamically generated vendor specific platform data classes.""" + + def asdict(self) -> t.Dict[str, t.Any]: + """Returns dictionary representation of the class attributes.""" + return dataclasses.asdict(self) # type: ignore + + @abc.abstractmethod + def get_nist_urls(self) -> t.List[str]: + """Returns list of NIST URLs for the platform.""" + + def get(self, key: str) -> t.Any: + """Return value of the attribute matching provided name or None if no attribute is found.""" + return getattr(self, key, None) + + def keys(self) -> t.KeysView[t.Any]: + """Return attributes and their values as dict keys.""" + # Disabling pylint no-member due to BUG: https://github.com/pylint-dev/pylint/issues/7126 + return self.__annotations__.keys() # pylint: disable=no-member + + def __getitem__(self, key: str) -> t.Any: + """Allow retrieving attributes using subscript notation.""" + return getattr(self, key) + def get_nist_urls_juniper_junos(os_platform_data: t.Dict[str, t.Any]) -> t.List[str]: # pylint: disable=R0911 """Create a list of possible NIST Url strings for JuniperPlatform. @@ -129,3 +187,48 @@ def get_nist_urls_default(os_platform_data: t.Dict[str, t.Any]) -> t.List[str]: "default": get_nist_urls_default, "juniper": {"junos": get_nist_urls_juniper_junos}, } + + +def os_platform_object_builder(vendor: str, platform: str, version: str) -> object: + """Creates a platform object relative to its need and definition. + + Args: + vendor (str): Name of vendor + platform (str): Name of os/other platform + version (str): Version value + + Returns: + object: Platform object + + Examples: + >>> jp = os_platform_object_builder("juniper", "junos", "12.1R3-S4.1") + >>> jp.get_nist_urls() + ['https://services.nvd.nist.gov/rest/json/cves/2.0?virtualMatchString=cpe:2.3:o:juniper:junos:12.1r3:s4.1:*:*:*:*:*:*', 'https://services.nvd.nist.gov/rest/json/cves/2.0?virtualMatchString=cpe:2.3:o:juniper:junos:12.1r3-s4.1:*:*:*:*:*:*:*'] + """ + + platform = platform.lower() + vendor = vendor.lower() + + class_fields = [*PLATFORM_FIELDS["default"]] + vendor_platform_fields = PLATFORM_FIELDS.get(vendor, {}).get(platform, []) + class_fields.extend(vendor_platform_fields) + + version_parser = version_metadata(vendor, platform, version) + + field_values = { + "vendor": vendor, + "os_type": platform, + "version_string": version, + } + + if version_parser: + field_values.update(version_parser) + + class_name = f"{vendor.capitalize()}{platform.capitalize()}" + get_nist_urls_func = get_nist_url_funcs.get(vendor, {}).get(platform) or get_nist_url_funcs["default"] + + platform_cls = dataclasses.make_dataclass( + cls_name=class_name, fields=class_fields, bases=(OsPlatform,), namespace={"get_nist_urls": get_nist_urls_func} + ) + + return platform_cls(**field_values) \ No newline at end of file diff --git a/netutils/os_version.py b/netutils/os_version.py index e181064f..6609f960 100644 --- a/netutils/os_version.py +++ b/netutils/os_version.py @@ -183,6 +183,9 @@ def juniper_junos_metadata(version: str) -> t.Dict[str, t.Any]: # Parse out junos into sections that can be used for logic parsed_version.update(re_main_minor_type_build.search(version_core_part).groupdict()) # type:ignore + + # Adding additional keys for standard major/minor/patch references + parsed_version.update({"major": parsed_version["main"], "patch": parsed_version.get("build")}) if version_service_part: parsed_version.update(re_service_build_respin.search(version_service_part[0]).groupdict()) # type:ignore @@ -214,7 +217,64 @@ def juniper_junos_metadata(version: str) -> t.Dict[str, t.Any]: return parsed_version +def default_metadata(vendor: str, os_platform: str, version: str) -> t.Dict[str, t.Any]: + """Parses version value using SemVer 2.0.0 standards. https://semver.org/spec/v2.0.0.html + + Args: + version (str): String representation of version + + Returns: + A dictionary containing parsed version information + + Examples: + >>> default_metadata("10.20.30").groupdict() + {'major': '10', 'minor': '20', 'patch': '30', 'prerelease': None, 'buildmetadata': None} + + >>> default_metadata("1.0.0-alpha.beta.1").groupdict() + {'major': '1', 'minor': '0', 'patch': '0', 'prerelease': 'alpha.beta.1', 'buildmetadata': None} + + >>> default_metadata("1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay").groupdict() + {'major': '1', 'minor': '0', 'patch': '0', 'prerelease': 'alpha-a.b-c-somethinglong', 'buildmetadata': 'build.1-aef.1-its-okay'} + + """ + # Use regex with named groups. REGEX Pattern Provided by SemVer https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string + semver_regex: re.Pattern[str] = re.compile( + r""" + ^ + (?P0|[1-9]\d*) + \. + (?P0|[1-9]\d*) + \. + (?P0|[1-9]\d*) + (?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))? + (?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$ + """, + re.VERBOSE + ) + + # If version is not SemVer 2.0.0, attempt to find major/minor only. + basic_regex: re.Pattern[str] = re.compile( + r""" + ^ + (?P0|[1-9]\d*) + \. + (?P0|[1-9]\d*)? + .*$ + """, + re.VERBOSE + ) + + # Perform regex match against provided version string + parsed_version = semver_regex.match(version) + + if not parsed_version: + parsed_version = basic_regex.match(version) + + return parsed_version.groupdict() + + version_metadata_parsers = { + "default": default_metadata, "juniper": { "junos": juniper_junos_metadata, } @@ -235,14 +295,19 @@ def version_metadata(vendor: str, os_type: str, version: str) -> t.Dict[str, t.A Examples: >>> from netutils.os_version import version_metadata >>> version_metadata("Cisco", "IOS", "15.5") - {'vendor': 'Cisco', 'os_type': 'IOS', 'version': '15.5', 'metadata': False} + {'vendor': 'Cisco', 'os_type': 'IOS', 'version': '15.5', 'vendor_metadata': False} >>> version_metadata("juniper", "junos", "12.4R") - {'isservice': False, 'ismaintenance': False, 'isfrs': True, 'isspecial': False, 'service': None, 'service_build': None, 'service_respin': None, 'main': '12', 'minor': '4', 'type': 'R', 'build': None, 'metadata': True} + {'isservice': False, 'ismaintenance': False, 'isfrs': True, 'isspecial': False, 'service': None, 'service_build': None, 'service_respin': None, 'main': '12', 'minor': '4', 'type': 'R', 'build': None, 'vendor_metadata': True} """ - try: - parsed_version = version_metadata_parsers[vendor][os_type](version) - parsed_version.update({"metadata": True}) - except KeyError: - parsed_version = {"vendor": vendor, "os_type": os_type, "version": version, "metadata": False} + if vendor in version_metadata_parsers: + try: + parsed_version = version_metadata_parsers[vendor][os_type](version) + parsed_version.update({"vendor_metadata": True}) + except KeyError: + parsed_version = version_metadata_parsers['default'](vendor, os_type, version) + parsed_version.update({"vendor_metadata": False}) + else: + parsed_version = version_metadata_parsers['default'](vendor, os_type, version) + parsed_version.update({"vendor_metadata": False}) return parsed_version