From 8edbe9861e0b5f6dd59ac14c7d378e89ef316f5a Mon Sep 17 00:00:00 2001 From: Brandon Minnix Date: Fri, 10 May 2024 18:36:57 -0400 Subject: [PATCH] Updates after discussion with Ken today. Removing several functions from public/utils and providing a single function to get urls. Updated docs and tests --- docs/dev/code_reference/platform_mapper.md | 5 - docs/user/include_jinja_list.md | 4 +- docs/user/lib_use_cases_nist.md | 21 +-- docs/user/lib_use_cases_platform_mapper.md | 49 ------- netutils/nist.py | 38 ++++-- netutils/os_version.py | 16 +-- netutils/utils.py | 4 +- tests/unit/test_nist.py | 142 +-------------------- tests/unit/test_os_versions.py | 106 +++++++++++++++ 9 files changed, 150 insertions(+), 235 deletions(-) delete mode 100644 docs/dev/code_reference/platform_mapper.md delete mode 100644 docs/user/lib_use_cases_platform_mapper.md diff --git a/docs/dev/code_reference/platform_mapper.md b/docs/dev/code_reference/platform_mapper.md deleted file mode 100644 index 81cd38df..00000000 --- a/docs/dev/code_reference/platform_mapper.md +++ /dev/null @@ -1,5 +0,0 @@ -# Platform Mapper - -::: netutils.platform_mapper - options: - show_submodules: True \ No newline at end of file diff --git a/docs/user/include_jinja_list.md b/docs/user/include_jinja_list.md index 08c25a07..890cf69c 100644 --- a/docs/user/include_jinja_list.md +++ b/docs/user/include_jinja_list.md @@ -60,12 +60,10 @@ | mac_to_format | netutils.mac.mac_to_format | | mac_to_int | netutils.mac.mac_to_int | | mac_type | netutils.mac.mac_type | -| os_platform_object_builder | netutils.nist.os_platform_object_builder | +| get_nist_urls | netutils.nist.get_nist_urls | | compare_version_loose | netutils.os_version.compare_version_loose | | compare_version_strict | netutils.os_version.compare_version_strict | -| default_metadata | netutils.os_version.default_metadata | | get_upgrade_path | netutils.os_version.get_upgrade_path | -| juniper_junos_metadata | netutils.os_version.juniper_junos_metadata | | version_metadata | netutils.os_version.version_metadata | | compare_cisco_type5 | netutils.password.compare_cisco_type5 | | compare_cisco_type7 | netutils.password.compare_cisco_type7 | diff --git a/docs/user/lib_use_cases_nist.md b/docs/user/lib_use_cases_nist.md index b57441d9..53ba4633 100644 --- a/docs/user/lib_use_cases_nist.md +++ b/docs/user/lib_use_cases_nist.md @@ -22,36 +22,21 @@ For this reason, for certain Vendor/OS combinations, a custom URL needs to be bu ## 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 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") +from netutils.nist import get_nist_urls # Get NIST URL for the Cisco IOS object -cisco_ios.get_nist_urls() +get_nist_urls("Cisco", "IOS", "15.5(2)S1c") # ['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() +get_nist_urls("Juniper", "JunOS", "10.2R2.11") # ['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 - -# 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:*:*:*:*:*:*:*'] -``` - Currently known OS/Other Platform types that require a custom NIST URL: - Juniper JunOS diff --git a/docs/user/lib_use_cases_platform_mapper.md b/docs/user/lib_use_cases_platform_mapper.md deleted file mode 100644 index ef1e61a8..00000000 --- a/docs/user/lib_use_cases_platform_mapper.md +++ /dev/null @@ -1,49 +0,0 @@ -# Platform Mapper - -The platform mapper is used as an abstract class builder to provide unified multi-platform service functionality. - -Methods available to custom and generic classes: - -- get_nist_urls() - -Here are a few examples showing how you would use this in your python code. - -```python - -from netutils.platform_mapper 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:*:*:*:*:*:*'] -``` - -## Custom Parsers - -As stated above, this mapper is meant to provided unified utility usage between different platforms/services. In order to do this, custom parsers are needed so that data can be normalized into values that the services need. There are two parts **potentially** needed for this to work. -- Version Parsing - Document: [netutils.os_version](lib_use_cases_os_version.md) -- Service Ready Output - -### Service Ready Output - -"Service Ready Output" means that the utility is providing a readily usable value for the service it is meant to use. - -Using NIST as the example, the URL building utility responsible for "Service Ready Output" is `netutils.nist`. Some popular vendors do not follow NIST standards in regard to how their CPE are defined using delimiters of `:`. - -- **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. - - Service Ready 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. - - Service Ready 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:*:*:*:*:*:*']` - -## Important Notes -Please see [here](lib_use_cases_nist.md) for documentation on using the NIST utility on its own, as well as information on additional requirements such as obtaining an API Key. diff --git a/netutils/nist.py b/netutils/nist.py index 43f87465..3c8cf341 100644 --- a/netutils/nist.py +++ b/netutils/nist.py @@ -62,7 +62,7 @@ def __getitem__(self, key: str) -> t.Any: return getattr(self, key) -def get_nist_urls_juniper_junos(os_platform_data: t.Dict[str, t.Any]) -> t.List[str]: # pylint: disable=R0911 +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. Returns: @@ -156,7 +156,7 @@ def get_nist_urls_juniper_junos(os_platform_data: t.Dict[str, t.Any]) -> t.List[ raise ValueError("Failure creating Juniper JunOS Version. Format is unknown.") -def get_nist_urls_default(os_platform_data: t.Dict[str, t.Any]) -> t.List[str]: +def _get_nist_urls_default(os_platform_data: t.Dict[str, t.Any]) -> t.List[str]: r"""Create a list of possible NIST Url strings. Child models with NIST URL customizations need their own "get_nist_urls" method. @@ -184,13 +184,7 @@ def get_nist_urls_default(os_platform_data: t.Dict[str, t.Any]) -> t.List[str]: return nist_urls -get_nist_url_funcs: t.Dict[str, t.Any] = { - "default": get_nist_urls_default, - "juniper": {"junos": get_nist_urls_juniper_junos}, -} - - -def os_platform_object_builder(vendor: str, platform: str, version: str) -> object: +def _os_platform_object_builder(vendor: str, platform: str, version: str) -> object: """Creates a platform object relative to its need and definition. Args: @@ -202,7 +196,7 @@ def os_platform_object_builder(vendor: str, platform: str, version: str) -> obje object: Platform object Examples: - >>> jp = os_platform_object_builder("juniper", "junos", "12.1R3-S4.1") + >>> 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:*:*:*:*:*:*:*'] """ @@ -232,3 +226,27 @@ def os_platform_object_builder(vendor: str, platform: str, version: str) -> obje ) return platform_cls(**field_values) + + +get_nist_url_funcs: t.Dict[str, t.Any] = { + "default": _get_nist_urls_default, + "juniper": {"junos": _get_nist_urls_juniper_junos}, +} + + +def get_nist_urls(vendor: str, platform: str, version: str) -> t.List[str]: + """Generate list of possible NIST URLs for the Vendor, OS Platform, and Version. + + Args: + vendor (str): OS Software Platform Vendor/Manufacturer + platform (str): OS Software Platform Name + version (str): OS Software Platform Version + + Returns: + t.List[str]: NIST URLs to search for possible CVE matches + """ + platform_data = _os_platform_object_builder(vendor, platform, version).__dict__ + + if vendor.lower() == "juniper" and platform.lower() == "junos": + return _get_nist_urls_juniper_junos(platform_data) + return _get_nist_urls_default(platform_data) diff --git a/netutils/os_version.py b/netutils/os_version.py index ae7277d5..ae9fc36c 100644 --- a/netutils/os_version.py +++ b/netutils/os_version.py @@ -131,7 +131,7 @@ def compare_version_strict(current_version: str, comparison: str, target_version return _compare_version(current_version, comparison, target_version, "strict") -def juniper_junos_metadata(version: str) -> t.Dict[str, t.Any]: +def _juniper_junos_version_metadata(version: str) -> t.Dict[str, t.Any]: """Parses JunOS Version into usable bits matching JunOS Standards. Args: @@ -141,7 +141,7 @@ def juniper_junos_metadata(version: str) -> t.Dict[str, t.Any]: A dictionary containing parsed version information Examples: - >>> juniper_junos_metadata("12.3R4") + >>> _juniper_junos_version_metadata("12.3R4") {'isservice': False, 'ismaintenance': True, 'isfrs': False, 'isspecial': False, 'service': None, 'service_build': None, 'service_respin': None, 'main': '12', 'minor': '3', 'type': 'R', 'build': '4', 'major': '12', 'patch': '4'} """ # Use regex to group the main, minor, type and build into useable pieces @@ -217,7 +217,7 @@ def juniper_junos_metadata(version: str) -> t.Dict[str, t.Any]: return parsed_version -def default_metadata(version: str) -> t.Dict[str, t.Any]: +def _default_version_metadata(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: @@ -227,13 +227,13 @@ def default_metadata(version: str) -> t.Dict[str, t.Any]: A dictionary containing parsed version information Examples: - >>> default_metadata("10.20.30") + >>> _default_version_metadata("10.20.30") {'major': '10', 'minor': '20', 'patch': '30', 'prerelease': None, 'buildmetadata': None} - >>> default_metadata("1.0.0-alpha.beta.1") + >>> _default_version_metadata("1.0.0-alpha.beta.1") {'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") + >>> _default_version_metadata("1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay") {'major': '1', 'minor': '0', 'patch': '0', 'prerelease': 'alpha-a.b-c-somethinglong', 'buildmetadata': 'build.1-aef.1-its-okay'} """ @@ -280,9 +280,9 @@ def default_metadata(version: str) -> t.Dict[str, t.Any]: version_metadata_parsers = { - "default": default_metadata, + "default": _default_version_metadata, "juniper": { - "junos": juniper_junos_metadata, + "junos": _juniper_junos_version_metadata, }, } diff --git a/netutils/utils.py b/netutils/utils.py index 5fb22150..730c798e 100644 --- a/netutils/utils.py +++ b/netutils/utils.py @@ -93,10 +93,8 @@ "get_upgrade_path": "os_version.get_upgrade_path", "hash_data": "hash.hash_data", "get_ips_sorted": "ip.get_ips_sorted", - "os_platform_object_builder": "nist.os_platform_object_builder", - "juniper_junos_metadata": "os_version.juniper_junos_metadata", "version_metadata": "os_version.version_metadata", - "default_metadata": "os_version.default_metadata", + "get_nist_urls": "nist.get_nist_urls", } diff --git a/tests/unit/test_nist.py b/tests/unit/test_nist.py index f637129a..81d2eb00 100644 --- a/tests/unit/test_nist.py +++ b/tests/unit/test_nist.py @@ -2,136 +2,7 @@ import pytest -from netutils.nist import os_platform_object_builder - -platform_data = [ - # Cisco and Arista use the generic parsing - { - "sent": {"vendor": "cisco", "platform": "ios", "version": "15.7(2.0z)M"}, - "received": { - "vendor": "cisco", - "os_type": "ios", - "version_string": "15.7(2.0z)M", - "major": "15", - "minor": "7", - "patch": None, - "prerelease": None, - "buildmetadata": None, - "vendor_metadata": False, - }, - }, - { - "sent": {"vendor": "arista", "platform": "eos", "version": "4.15.3f"}, - "received": { - "vendor": "arista", - "os_type": "eos", - "version_string": "4.15.3f", - "major": "4", - "minor": "15", - "patch": None, - "prerelease": None, - "buildmetadata": None, - "vendor_metadata": False, - }, - }, - # Juniper Junos uses a custom parser - { - "sent": {"vendor": "juniper", "platform": "junos", "version": "12.4R"}, - "received": { - "vendor": "juniper", - "os_type": "junos", - "version_string": "12.4R", - "isservice": False, - "ismaintenance": False, - "isfrs": True, - "isspecial": False, - "main": "12", - "minor": "4", - "type": "R", - "build": None, - "service": None, - "service_build": None, - "service_respin": None, - "buildmetadata": None, - "vendor_metadata": True, - "prerelease": None, - "major": "12", - "patch": None, - }, - }, - { - "sent": {"vendor": "juniper", "platform": "junos", "version": "12.3x48-d80"}, - "received": { - "vendor": "juniper", - "os_type": "junos", - "version_string": "12.3x48-d80", - "isservice": False, - "ismaintenance": False, - "isfrs": False, - "isspecial": True, - "main": "12", - "minor": "3", - "type": "x", - "build": "48", - "service": "d", - "service_build": "80", - "service_respin": None, - "buildmetadata": None, - "vendor_metadata": True, - "prerelease": None, - "major": "12", - "patch": "48", - }, - }, - { - "sent": {"vendor": "juniper", "platform": "junos", "version": "12.3x48:d80"}, - "received": { - "vendor": "juniper", - "os_type": "junos", - "version_string": "12.3x48:d80", - "isservice": False, - "ismaintenance": False, - "isfrs": False, - "isspecial": True, - "main": "12", - "minor": "3", - "type": "x", - "build": "48", - "service": "d", - "service_build": "80", - "service_respin": None, - "buildmetadata": None, - "vendor_metadata": True, - "prerelease": None, - "major": "12", - "patch": "48", - }, - }, - { - "sent": {"vendor": "juniper", "platform": "junos", "version": "12.3R12-S15"}, - "received": { - "vendor": "juniper", - "os_type": "junos", - "version_string": "12.3R12-S15", - "isservice": True, - "ismaintenance": True, - "isfrs": False, - "isspecial": False, - "main": "12", - "minor": "3", - "type": "R", - "build": "12", - "service": "S", - "service_build": "15", - "service_respin": None, - "buildmetadata": None, - "vendor_metadata": True, - "prerelease": None, - "major": "12", - "patch": "12", - }, - }, -] +from netutils.nist import get_nist_urls platform_nist_urls = [ { @@ -162,15 +33,8 @@ ] -# Testing the parsing of a Vendor, Platform, Version into vendor standardized sections -@pytest.mark.parametrize("data", platform_data) -def test_platform_parsing(data): - platform_obj = os_platform_object_builder(data["sent"]["vendor"], data["sent"]["platform"], data["sent"]["version"]) - assert platform_obj.asdict() == data["received"] - - # Testing the composition of the nist url(s) created for a platform @pytest.mark.parametrize("data", platform_nist_urls) def test_platform_nist(data): - platform_obj = os_platform_object_builder(data["sent"]["vendor"], data["sent"]["platform"], data["sent"]["version"]) - assert platform_obj.get_nist_urls() == data["received"] + platform_obj = get_nist_urls(data["sent"]["vendor"], data["sent"]["platform"], data["sent"]["version"]) + assert platform_obj == data["received"] diff --git a/tests/unit/test_os_versions.py b/tests/unit/test_os_versions.py index 5e1101e0..11d23332 100755 --- a/tests/unit/test_os_versions.py +++ b/tests/unit/test_os_versions.py @@ -32,6 +32,103 @@ {"sent": {"current_version": "1.0.0", "comparison": "!=", "target_version": "2.0.0"}, "received": True}, ] +PLATFORM_VERSION_METADATA = [ + # Cisco and Arista use the generic parsing + { + "sent": {"vendor": "cisco", "platform": "ios", "version": "15.7(2.0z)M"}, + "received": { + "major": "15", + "minor": "7", + "vendor_metadata": False, + }, + }, + { + "sent": {"vendor": "arista", "platform": "eos", "version": "4.15.3f"}, + "received": { + "major": "4", + "minor": "15", + "vendor_metadata": False, + }, + }, + # Juniper Junos uses a custom parser + { + "sent": {"vendor": "juniper", "platform": "junos", "version": "12.4R"}, + "received": { + "isservice": False, + "ismaintenance": False, + "isfrs": True, + "isspecial": False, + "main": "12", + "minor": "4", + "type": "R", + "build": None, + "service": None, + "service_build": None, + "service_respin": None, + "vendor_metadata": True, + "major": "12", + "patch": None, + }, + }, + { + "sent": {"vendor": "juniper", "platform": "junos", "version": "12.3x48-d80"}, + "received": { + "isservice": False, + "ismaintenance": False, + "isfrs": False, + "isspecial": True, + "main": "12", + "minor": "3", + "type": "x", + "build": "48", + "service": "d", + "service_build": "80", + "service_respin": None, + "vendor_metadata": True, + "major": "12", + "patch": "48", + }, + }, + { + "sent": {"vendor": "juniper", "platform": "junos", "version": "12.3x48:d80"}, + "received": { + "isservice": False, + "ismaintenance": False, + "isfrs": False, + "isspecial": True, + "main": "12", + "minor": "3", + "type": "x", + "build": "48", + "service": "d", + "service_build": "80", + "service_respin": None, + "vendor_metadata": True, + "major": "12", + "patch": "48", + }, + }, + { + "sent": {"vendor": "juniper", "platform": "junos", "version": "12.3R12-S15"}, + "received": { + "isservice": True, + "ismaintenance": True, + "isfrs": False, + "isspecial": False, + "main": "12", + "minor": "3", + "type": "R", + "build": "12", + "service": "S", + "service_build": "15", + "service_respin": None, + "vendor_metadata": True, + "major": "12", + "patch": "12", + }, + }, +] + def test_get_upgrade_path(): return_values = ["9.1.15-h1", "10.0.0", "10.0.12", "10.1.0", "10.1.9"] @@ -68,3 +165,12 @@ def test_compare_strict_bad_version(): os_version.compare_version_strict("3.3.3b", "==", "3.3.3b") with pytest.raises(ValueError, match="Invalid comparison operator:"): os_version.compare_version_strict("3.3.3", "not-an-operator", "3.3.3") + + +# Testing the parsing of a Vendor, Platform, Version into vendor standardized sections +@pytest.mark.parametrize("data", PLATFORM_VERSION_METADATA) +def test_platform_parsing(data): + assert ( + os_version.version_metadata(data["sent"]["vendor"], data["sent"]["platform"], data["sent"]["version"]) + == data["received"] + )