Skip to content

Commit

Permalink
Add support for pydantic 2.x
Browse files Browse the repository at this point in the history
  • Loading branch information
nabla-c0d3 committed Aug 28, 2023
1 parent ed68b41 commit 0850dd1
Show file tree
Hide file tree
Showing 13 changed files with 90 additions and 29 deletions.
30 changes: 30 additions & 0 deletions .github/workflows/run_tests_with_pydantic_1_10.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# TODO(#617): Delete this file
name: Run tests with pydantic 1.10

on:
push:
branches: [release]
pull_request:
branches: [ release, dev ]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.7

- name: Install sslyze dependencies
run: |
python -m pip install --upgrade pip setuptools
python -m pip install -e .
- name: Install pydantic 1.x
run: python -m pip install "pydantic<2"

- name: Run tests
run: python -m invoke test
2 changes: 1 addition & 1 deletion api_sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def example_json_result_output(
date_scans_started=date_scans_started,
date_scans_completed=date_scans_completed,
)
json_output_as_str = json_output.json(sort_keys=True, indent=4, ensure_ascii=True)
json_output_as_str = json_output.json() # TODO(#617): Switch to model_dump_json()
json_file_out.write_text(json_output_as_str)


Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def get_include_files() -> List[Tuple[str, str]]:
"nassl>=5,<6",
"cryptography>=2.6,<42",
"tls-parser>=2,<3",
"pydantic>=1.7,<1.11",
"pydantic>=1.10,<2.4",
"pyOpenSSL>=23,<24",
],
# cx_freeze info for Windows builds with Python embedded
Expand Down
2 changes: 1 addition & 1 deletion sslyze/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def main() -> None:
date_scans_started=date_scans_started,
date_scans_completed=datetime.utcnow(),
)
json_output_as_str = json_output.json(sort_keys=True, indent=4, ensure_ascii=True)
json_output_as_str = json_output.json() # TODO(#617): Switch to model_dump_json()
json_file_out.write(json_output_as_str)

# If we printed the JSON results to the console, don't run the Mozilla compliance check so we return valid JSON
Expand Down
25 changes: 19 additions & 6 deletions sslyze/json/json_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
from typing import List, Optional, TYPE_CHECKING
from uuid import UUID

import pydantic
try:
# pydantic 2.x
from pydantic.v1 import BaseModel # TODO(#617): Remove v1
except ImportError:
# pydantic 1.x
from pydantic import BaseModel # type: ignore

from sslyze import (
ServerNetworkConfiguration,
Expand Down Expand Up @@ -73,7 +78,7 @@ class AllScanCommandsAttemptsAsJson(BaseModelWithOrmModeAndForbid):
@classmethod
def from_orm(cls, all_scan_commands_attempts: AllScanCommandsAttempts) -> "AllScanCommandsAttemptsAsJson":
all_scan_commands_attempts_json = {}
for field_name, field in cls.__fields__.items():
for field_name, field in cls.__fields__.items(): # type: ignore
scan_command_attempt = getattr(all_scan_commands_attempts, field_name)

# Convert the error trace to a string; this is why we have to override from_orm()
Expand All @@ -84,7 +89,15 @@ def from_orm(cls, all_scan_commands_attempts: AllScanCommandsAttempts) -> "AllSc
error_trace_as_str += line

# Create the JSON version of the scan command attempt
scan_command_attempt_json_cls = field.type_
if hasattr(field, "type_"):
# pydantic 1.x; TODO(#617): Remove
scan_command_attempt_json_cls = field.type_
elif hasattr(field, "annotation"):
# pydantic 2.x
scan_command_attempt_json_cls = field.annotation
else:
raise TypeError("Unexpected version of pydantic?")

all_scan_commands_attempts_json[field_name] = scan_command_attempt_json_cls(
status=scan_command_attempt.status,
error_reason=scan_command_attempt.error_reason,
Expand All @@ -104,7 +117,7 @@ class _HttpProxySettingsAsJson(BaseModelWithOrmModeAndForbid):
basic_auth_password: Optional[str] = None


class _ClientAuthenticationCredentialsAsJson(pydantic.BaseModel):
class _ClientAuthenticationCredentialsAsJson(BaseModel):
# Compared to the ClientAuthenticationCredentials class, this model does not have the key_password field
certificate_chain_path: Path
key_path: Path
Expand Down Expand Up @@ -231,10 +244,10 @@ def from_orm(cls, invalid_server_string_error: "InvalidServerStringError") -> "I
)


class SslyzeOutputAsJson(pydantic.BaseModel):
class SslyzeOutputAsJson(BaseModel):
"""The "root" dictionary of the JSON output when using the --json command line option."""

invalid_server_strings: List[InvalidServerStringAsJson] = [] # TODO(AD): Remove default value starting with v6.x.x
invalid_server_strings: List[InvalidServerStringAsJson] = [] # TODO(6.0.0): Remove default value
server_scan_results: List[ServerScanResultAsJson]

date_scans_started: datetime
Expand Down
11 changes: 8 additions & 3 deletions sslyze/json/pydantic_utils.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import pydantic
try:
# pydantic 2.x
from pydantic.v1 import BaseModel # TODO(#617): Remove v1
except ImportError:
# pydantic 1.x
from pydantic import BaseModel # type: ignore


class BaseModelWithOrmMode(pydantic.BaseModel):
class BaseModelWithOrmMode(BaseModel):
class Config:
orm_mode = True


class BaseModelWithOrmModeAndForbid(pydantic.BaseModel):
class BaseModelWithOrmModeAndForbid(BaseModel):
class Config:
orm_mode = True
extra = "forbid" # Fields must match between the JSON representation and the result objects
4 changes: 2 additions & 2 deletions sslyze/json/scan_attempt_json.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from abc import ABC
from typing import Optional

import pydantic
from pydantic.v1 import BaseModel # TODO(#617): Remove pydantic.v1

from sslyze.scanner.scan_command_attempt import ScanCommandAttemptStatusEnum, ScanCommandErrorReasonEnum


# Must be subclassed in order to add the result field
class ScanCommandAttemptAsJson(pydantic.BaseModel, ABC):
class ScanCommandAttemptAsJson(BaseModel, ABC):
class Config:
orm_mode = True
extra = "forbid" # Fields must match between the JSON representation and the actual objects
Expand Down
10 changes: 6 additions & 4 deletions sslyze/plugins/certificate_info/json_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,15 @@ def from_orm(cls, name: Name) -> "_X509NameAsJson":
)


class _SubjAltNameAsJson(pydantic.BaseModel):
# TODO(#617): Remove pydantic.v1
class _SubjAltNameAsJson(pydantic.v1.BaseModel):

# TODO(6.0.0): Remove the Config, alias and default value as the name "dns" is deprecated
class Config:
allow_population_by_field_name = True

dns_names: List[str] = pydantic.Field(alias="dns")
ip_addresses: List[pydantic.IPvAnyAddress] = []
ip_addresses: List[pydantic.v1.IPvAnyAddress] = []


class _HashAlgorithmAsJson(BaseModelWithOrmMode):
Expand Down Expand Up @@ -179,10 +180,11 @@ def from_orm(cls, certificate: Certificate) -> "_CertificateAsJson":
not_valid_after=certificate.not_valid_after,
subject_alternative_name=_SubjAltNameAsJson(
dns_names=subj_alt_name_ext.dns_names,
ip_addresses=subj_alt_name_ext.ip_addresses,
# TODO(#617): Remove pydantic.v1
ip_addresses=[pydantic.v1.IPvAnyAddress(ip) for ip in subj_alt_name_ext.ip_addresses],
),
signature_hash_algorithm=signature_hash_algorithm,
signature_algorithm_oid=certificate.signature_algorithm_oid,
signature_algorithm_oid=_ObjectIdentifierAsJson.from_orm(certificate.signature_algorithm_oid),
subject=subject_field,
issuer=issuer_field,
public_key=_PublicKeyAsJson.from_orm(certificate.public_key()),
Expand Down
12 changes: 9 additions & 3 deletions sslyze/plugins/elliptic_curves_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@
from operator import attrgetter
from typing import List, Optional

import pydantic
try:
# pydantic 2.x
from pydantic.v1 import BaseModel # TODO(#617): Remove v1
except ImportError:
# pydantic 1.x
from pydantic import BaseModel # type: ignore

from nassl._nassl import OpenSSLError
from nassl.ephemeral_key_info import OpenSslEcNidEnum, EcDhEphemeralKeyInfo, _OPENSSL_NID_TO_SECG_ANSI_X9_62
from nassl.ssl_client import ClientCertificateRequested, SslClient
Expand Down Expand Up @@ -59,7 +65,7 @@ def __post_init__(self) -> None:
self.rejected_curves.sort(key=attrgetter("name"))


class _EllipticCurveAsJson(pydantic.BaseModel):
class _EllipticCurveAsJson(BaseModel):
name: str
openssl_nid: int

Expand All @@ -68,7 +74,7 @@ class _EllipticCurveAsJson(pydantic.BaseModel):
_EllipticCurveAsJson.__doc__ = EllipticCurve.__doc__


class SupportedEllipticCurvesScanResultAsJson(pydantic.BaseModel):
class SupportedEllipticCurvesScanResultAsJson(BaseModel):
supports_ecdh_key_exchange: bool
supported_curves: Optional[List[_EllipticCurveAsJson]]
rejected_curves: Optional[List[_EllipticCurveAsJson]]
Expand Down
3 changes: 2 additions & 1 deletion sslyze/plugins/http_headers_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ class HttpHeadersScanResult(ScanCommandResult):
expect_ct_header: None = None # TODO(6.0.0): Remove as this is a deprecated field


class _StrictTransportSecurityHeaderAsJson(pydantic.BaseModel):
# TODO(#617): Remove pydantic.v1
class _StrictTransportSecurityHeaderAsJson(pydantic.v1.BaseModel):
max_age: Optional[int]
preload: bool
include_subdomains: bool
Expand Down
8 changes: 6 additions & 2 deletions sslyze/plugins/openssl_cipher_suites/json_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,12 @@ def from_orm(cls, scan_result: CipherSuitesScanResult) -> "CipherSuitesScanResul
return cls(
tls_version_used=scan_result.tls_version_used.name,
is_tls_version_supported=scan_result.is_tls_version_supported,
accepted_cipher_suites=scan_result.accepted_cipher_suites,
rejected_cipher_suites=scan_result.rejected_cipher_suites,
accepted_cipher_suites=[
_CipherSuiteAcceptedByServerAsJson.from_orm(ciph) for ciph in scan_result.accepted_cipher_suites
],
rejected_cipher_suites=[
_CipherSuiteRejectedByServerAsJson.from_orm(ciph) for ciph in scan_result.rejected_cipher_suites
],
)


Expand Down
8 changes: 4 additions & 4 deletions tests/json_tests/test_json_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def test(self):
date_scans_started=datetime.utcnow(),
date_scans_completed=datetime.utcnow(),
)
json_output_as_str = json_output.json(sort_keys=True, indent=4, ensure_ascii=True)
json_output_as_str = json_output.json() # TODO(#617): Switch to model_dump_json()
assert json_output_as_str

# And it can be parsed again
Expand All @@ -39,7 +39,7 @@ def test_connectivity_test_failed(self):
date_scans_started=datetime.utcnow(),
date_scans_completed=datetime.utcnow(),
)
json_output_as_str = json_output.json(sort_keys=True, indent=4, ensure_ascii=True)
json_output_as_str = json_output.json() # TODO(#617): Switch to model_dump_json()
assert json_output_as_str

# And it can be parsed again
Expand All @@ -63,7 +63,7 @@ def test_server_scan_completed_scan_command(self):
date_scans_started=datetime.utcnow(),
date_scans_completed=datetime.utcnow(),
)
json_output_as_str = json_output.json(sort_keys=True, indent=4, ensure_ascii=True)
json_output_as_str = json_output.json() # TODO(#617): Switch to model_dump_json()
assert json_output_as_str
assert "supports_compression" in json_output_as_str

Expand All @@ -90,7 +90,7 @@ def test_server_scan_completed_but_scan_command_returned_error(self):
date_scans_started=datetime.utcnow(),
date_scans_completed=datetime.utcnow(),
)
json_output_as_str = json_output.json(sort_keys=True, indent=4, ensure_ascii=True)
json_output_as_str = json_output.json() # TODO(#617): Switch to model_dump_json()
assert json_output_as_str
assert error_trace.exc_type.__name__ in json_output_as_str

Expand Down
2 changes: 1 addition & 1 deletion tests/web_servers/scan_localhost.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def main(server_software_running_on_localhost: WebServerSoftwareEnum) -> None:
date_scans_started=date_scans_started,
date_scans_completed=datetime.utcnow(),
)
final_json_output.json(sort_keys=True, indent=4, ensure_ascii=True)
final_json_output.json() # TODO(#617): Switch to model_dump_json()
print("OK: Was able to generate JSON output.")


Expand Down

0 comments on commit 0850dd1

Please sign in to comment.