Skip to content

Commit

Permalink
Making changes suggested during PR and discussion with Ken
Browse files Browse the repository at this point in the history
  • Loading branch information
Brandon Minnix authored and Brandon Minnix committed Apr 26, 2024
1 parent 8d9b534 commit 074d1f6
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 21 deletions.
1 change: 0 additions & 1 deletion docs/user/lib_use_cases.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
50 changes: 42 additions & 8 deletions docs/user/lib_use_cases_nist.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<key_value>"}` 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
18 changes: 14 additions & 4 deletions docs/user/lib_use_cases_os_version.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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"
}
```
105 changes: 104 additions & 1 deletion netutils/nist.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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)
79 changes: 72 additions & 7 deletions netutils/os_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"""
^
(?P<major>0|[1-9]\d*)
\.
(?P<minor>0|[1-9]\d*)
\.
(?P<patch>0|[1-9]\d*)
(?:-(?P<prerelease>(?: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<buildmetadata>[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"""
^
(?P<major>0|[1-9]\d*)
\.
(?P<minor>0|[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,
}
Expand All @@ -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

0 comments on commit 074d1f6

Please sign in to comment.