From 51f62e7a62255fb95b158e0616ca5a56ed189c94 Mon Sep 17 00:00:00 2001 From: Steve McGrath Date: Tue, 24 Sep 2024 08:41:04 -0500 Subject: [PATCH] Made cryptography an optional dependency for pure-python deployments --- pyproject.toml | 7 ++++++- tenable/sc/__init__.py | 17 +++++++++++++++-- tenable/sc/plugins.py | 13 +++++++++++++ tenable/version.py | 2 +- tests/sc/test___init__.py | 12 ++++++++++-- uv.lock | 25 ++++++++++++++++++++++--- 6 files changed, 67 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a13054282..911ea4017 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,12 +52,16 @@ dependencies = [ "typing-extensions>=4.0.1", "dataclasses>=0.8;python_version=='3.6'", "requests-toolbelt>=1.0.0", - "cryptography>=43.0.1", "gql>=3.5.0", "graphql-core>=3.2.3", ] +[project.optional-dependencies] +pkcs12 = ["cryptography>=43.0.1"] +all = ["pytenable[pkcs12]"] + + [project.urls] Homepage = "https://pytenable.readthedocs.io" Repository = "https://github.com/tenable/pytenable" @@ -113,6 +117,7 @@ docstring-code-line-length = "dynamic" [tool.uv] dev-dependencies = [ "bpython>=0.24", + "mock>=5.1.0", "pytest-cov>=4.1.0", "pytest-datafiles>=3.0.0", "pytest-vcr>=1.0.2", diff --git a/tenable/sc/__init__.py b/tenable/sc/__init__.py index 5d38539d2..d537b67c1 100644 --- a/tenable/sc/__init__.py +++ b/tenable/sc/__init__.py @@ -49,8 +49,6 @@ from typing import Optional from semver import VersionInfo import tempfile -import cryptography.hazmat.primitives.serialization.pkcs12 -from cryptography.hazmat.primitives import serialization from tenable.errors import APIError, ConnectionError from tenable.base.platform import APIPlatform from .accept_risks import AcceptRiskAPI @@ -78,6 +76,12 @@ from .system import SystemAPI from .users import UserAPI +try: + import cryptography.hazmat.primitives.serialization.pkcs12 + from cryptography.hazmat.primitives import serialization +except ImportError: + serialization = None + class TenableSC(APIPlatform): # noqa PLR0904 '''TenableSC API Wrapper @@ -292,6 +296,15 @@ def _p12_auth(self, p12_cert, password): """ PKCS12 Certificate Authentication """ + if not serialization: + raise ModuleNotFoundError( + ( + 'Cryptyography library is required for PKCS12 certificate usage. ' + 'You can either install it with manually or install pytenable with ' + '"pytenable[pkcs12]" to install the optional dependencies.' + ), + name='cryptography', + ) with open(p12_cert, 'rb') as fobj: key, cert, _ = serialization.pkcs12.load_key_and_certificates( fobj.read(), password.encode() diff --git a/tenable/sc/plugins.py b/tenable/sc/plugins.py index 2d7d04137..3fb3a852e 100644 --- a/tenable/sc/plugins.py +++ b/tenable/sc/plugins.py @@ -43,6 +43,19 @@ def _constructor(self, **kwargs): kwargs['value'] = self._check('filter:value', kwargs['filter'][2], str) del kwargs['filter'] + #filters = kwargs.pop('filters', []) + #kwargs['filters'] = [] + #for filter in filters: + # if isinstance(filter, tuple): + # kwargs['filters'].append({ + # 'filterField': filter[0], + # 'filterOperator': filter[1], + # 'filterString': filter[2], + # }) + # elif isinstance(filter, dict): + # kwargs['filters'].append(filter) + + if 'sort_field' in kwargs: # convert the snake_cased variant of the parameter to the camelCased # variant that the API expects to see. diff --git a/tenable/version.py b/tenable/version.py index b147bd6e7..c3d13f933 100644 --- a/tenable/version.py +++ b/tenable/version.py @@ -1,2 +1,2 @@ -version = '1.5.2' +version = '1.5.3' version_info = tuple(int(d) for d in version.split("-")[0].split(".")) diff --git a/tests/sc/test___init__.py b/tests/sc/test___init__.py index 954d76de9..09a76aabf 100644 --- a/tests/sc/test___init__.py +++ b/tests/sc/test___init__.py @@ -2,10 +2,9 @@ test file to test various scenarios in init.py ''' import os - +import sys import pytest from requests.models import Response - from requests.exceptions import ConnectionError as RequestsConnectionError from tenable.errors import ConnectionError from tenable.sc import TenableSC @@ -84,3 +83,12 @@ def test_log_in(vcr): with vcr.use_cassette('sc_login_5_20_0'): tsc.login(access_key='access_key', secret_key='secret_key') assert tsc._auth_mech == 'keys' + + +def test_pkcs12_import_error(): + import tenable.sc + tenable.sc.serialization = None + with pytest.raises(ModuleNotFoundError): + sc = TenableSC( + url='http://something', p12_cert='something', password='something' + ) diff --git a/uv.lock b/uv.lock index a00fd8b37..5c2f45acf 100644 --- a/uv.lock +++ b/uv.lock @@ -594,6 +594,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/53/980a20d789029329fdf1546c315f9c92bf862c7f3e7294e3667afcc464f5/marshmallow-3.19.0-py3-none-any.whl", hash = "sha256:93f0958568da045b0021ec6aeb7ac37c81bfcccbb9a0e7ed8559885070b3a19b", size = 49078 }, ] +[[package]] +name = "mock" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/ab/41d09a46985ead5839d8be987acda54b5bb93f713b3969cc0be4f81c455b/mock-5.1.0.tar.gz", hash = "sha256:5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d", size = 80232 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/20/471f41173930550f279ccb65596a5ac19b9ac974a8d93679bcd3e0c31498/mock-5.1.0-py3-none-any.whl", hash = "sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744", size = 30938 }, +] + [[package]] name = "multidict" version = "6.0.5" @@ -732,10 +741,9 @@ wheels = [ [[package]] name = "pytenable" -version = "1.5.0" +version = "1.5.2" source = { editable = "." } dependencies = [ - { name = "cryptography" }, { name = "defusedxml" }, { name = "gql" }, { name = "graphql-core" }, @@ -750,9 +758,18 @@ dependencies = [ { name = "urllib3" }, ] +[package.optional-dependencies] +all = [ + { name = "cryptography" }, +] +pkcs12 = [ + { name = "cryptography" }, +] + [package.dev-dependencies] dev = [ { name = "bpython" }, + { name = "mock" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-datafiles" }, @@ -764,12 +781,13 @@ dev = [ [package.metadata] requires-dist = [ - { name = "cryptography", specifier = ">=43.0.1" }, + { name = "cryptography", marker = "extra == 'pkcs12'", specifier = ">=43.0.1" }, { name = "dataclasses", marker = "python_full_version == '3.6.*'", specifier = ">=0.8" }, { name = "defusedxml", specifier = ">=0.5.0" }, { name = "gql", specifier = ">=3.5.0" }, { name = "graphql-core", specifier = ">=3.2.3" }, { name = "marshmallow", specifier = ">=3.8" }, + { name = "pytenable", extras = ["pkcs12"], marker = "extra == 'all'" }, { name = "python-box", specifier = ">=4.0" }, { name = "python-dateutil", specifier = ">=2.6" }, { name = "requests", specifier = ">=2.26" }, @@ -783,6 +801,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "bpython", specifier = ">=0.24" }, + { name = "mock", specifier = ">=5.1.0" }, { name = "pytest", specifier = ">=7.4.4" }, { name = "pytest-cov", specifier = ">=4.1.0" }, { name = "pytest-datafiles", specifier = ">=3.0.0" },