From fcf06e148c2abe262e7bd7eb70aa1b0d922e09d5 Mon Sep 17 00:00:00 2001 From: Lydia Vilchez Date: Tue, 25 Aug 2026 17:50:10 +0200 Subject: [PATCH] refactor(api): reuse SDK certificate validator and fix schema drift Delete the API copy of `validate_certificate_bundle` and re-export the SDK one. The local copy diverged: it missed `UnsupportedAlgorithm` on PKCS#12 loading, `TypeError` on password-protected PEM keys, `DSA` and `ENCRYPTED`/`OPENSSH` PEM label prefixes, and the leaf-first normalization the SDK now depends on for azure-identity's thumbprint. A single source keeps future SDK fixes from silently skipping the API. Log the caught exception in `AzureProviderSecret.validate_certificate_content` before raising the client-facing ValidationError so root-causing a bundle parse failure doesn't need a rerun with debug on. Drop `additionalProperties: false` from the two new Azure oneOf variants in the ProviderSecretField schema. No other credential variant sets it, so codegen'd clients that enforced the flag would reject payloads the DRF serializer accepts. --- api/src/backend/api/specs/v1.yaml | 9 ---- api/src/backend/api/tests/test_serializers.py | 8 --- .../api/v1/serializer_utils/providers.py | 2 - api/src/backend/api/v1/serializers.py | 3 ++ api/src/backend/api/validators.py | 52 +++---------------- 5 files changed, 11 insertions(+), 63 deletions(-) diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index b44c33f9ec..1677ea3324 100644 --- a/api/src/backend/api/specs/v1.yaml +++ b/api/src/backend/api/specs/v1.yaml @@ -21307,7 +21307,6 @@ components: - client_id - client_secret - tenant_id - additionalProperties: false - type: object title: Azure Certificate Credentials properties: @@ -21327,7 +21326,6 @@ components: - client_id - certificate_content - tenant_id - additionalProperties: false - type: object title: M365 Static Credentials properties: @@ -23458,7 +23456,6 @@ components: - client_id - client_secret - tenant_id - additionalProperties: false - type: object title: Azure Certificate Credentials properties: @@ -23478,7 +23475,6 @@ components: - client_id - certificate_content - tenant_id - additionalProperties: false - type: object title: M365 Static Credentials properties: @@ -23925,7 +23921,6 @@ components: - client_id - client_secret - tenant_id - additionalProperties: false - type: object title: Azure Certificate Credentials properties: @@ -23945,7 +23940,6 @@ components: - client_id - certificate_content - tenant_id - additionalProperties: false - type: object title: M365 Static Credentials properties: @@ -24414,7 +24408,6 @@ components: - client_id - client_secret - tenant_id - additionalProperties: false - type: object title: Azure Certificate Credentials properties: @@ -24434,7 +24427,6 @@ components: - client_id - certificate_content - tenant_id - additionalProperties: false - type: object title: M365 Static Credentials properties: @@ -27073,7 +27065,6 @@ components: required: - type - id - additionalProperties: false properties: type: type: string diff --git a/api/src/backend/api/tests/test_serializers.py b/api/src/backend/api/tests/test_serializers.py index f972e09e52..79a1b48c60 100644 --- a/api/src/backend/api/tests/test_serializers.py +++ b/api/src/backend/api/tests/test_serializers.py @@ -402,10 +402,6 @@ class TestProviderSecretFieldSchema: "client_secret", "tenant_id", } - assert ( - azure_schemas["Azure Client Secret Credentials"]["additionalProperties"] - is False - ) assert azure_schemas["Azure Certificate Credentials"]["required"] == [ "client_id", "certificate_content", @@ -416,10 +412,6 @@ class TestProviderSecretFieldSchema: "certificate_content", "tenant_id", } - assert ( - azure_schemas["Azure Certificate Credentials"]["additionalProperties"] - is False - ) def test_oraclecloud_schema_includes_legacy_region_field(self): schema = ProviderSecretField._spectacular_annotation["field"] diff --git a/api/src/backend/api/v1/serializer_utils/providers.py b/api/src/backend/api/v1/serializer_utils/providers.py index 6741cdd984..8bc8617146 100644 --- a/api/src/backend/api/v1/serializer_utils/providers.py +++ b/api/src/backend/api/v1/serializer_utils/providers.py @@ -96,7 +96,6 @@ from rest_framework_json_api import serializers }, }, "required": ["client_id", "client_secret", "tenant_id"], - "additionalProperties": False, }, { "type": "object", @@ -117,7 +116,6 @@ from rest_framework_json_api import serializers }, }, "required": ["client_id", "certificate_content", "tenant_id"], - "additionalProperties": False, }, { "type": "object", diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index b56aa09b7c..54896f0abb 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -1812,6 +1812,9 @@ class AzureProviderSecret(serializers.Serializer): certificate_data = base64.b64decode(certificate_content, validate=True) validate_certificate_bundle(certificate_data) except Exception as e: + logger.error( + f"{e.__class__.__name__}[{e.__traceback__.tb_lineno}]: {e}" + ) # Field validators are invoked per-field; DRF already knows # this error belongs to `certificate_content` and will nest # the message under that key. Raising a dict here would diff --git a/api/src/backend/api/validators.py b/api/src/backend/api/validators.py index 70bd7083b9..a1bc5617d6 100644 --- a/api/src/backend/api/validators.py +++ b/api/src/backend/api/validators.py @@ -1,17 +1,20 @@ import ipaddress -import re import socket import string from urllib.parse import urlparse -from cryptography import x509 -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.serialization import pkcs12 from django.conf import settings from django.core.exceptions import ValidationError from django.utils.translation import gettext as _ +# Re-exported so the SDK stays the single source of truth for bundle parsing: +# it covers PKCS#12 UnsupportedAlgorithm, encrypted PEM keys (TypeError), the +# full private-key PEM label set, and leaf-first normalization for +# azure-identity's thumbprint. A local copy silently drifted before. +from prowler.providers.azure.lib.certificate import ( # noqa: F401 + validate_certificate_bundle, +) + LIGHTHOUSE_OPENAI_COMPATIBLE_ALLOWED_SCHEMES = frozenset({"https"}) LIGHTHOUSE_NAT64_WELL_KNOWN_PREFIX = ipaddress.IPv6Network("64:ff9b::/96") LIGHTHOUSE_BLOCKED_METADATA_HOSTS = frozenset( @@ -25,45 +28,6 @@ LIGHTHOUSE_BLOCKED_METADATA_HOSTS = frozenset( ) -def validate_certificate_bundle(certificate_data: bytes) -> None: - """Validate that certificate data contains a matching certificate and key.""" - try: - private_key, certificate, _ = pkcs12.load_key_and_certificates( - certificate_data, None, default_backend() - ) - except ValueError: - certificate_match = re.search( - rb"-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----", - certificate_data, - re.DOTALL, - ) - private_key_match = re.search( - rb"-----BEGIN (?:RSA |EC )?PRIVATE KEY-----.*?-----END (?:RSA |EC )?PRIVATE KEY-----", - certificate_data, - re.DOTALL, - ) - if not certificate_match or not private_key_match: - raise ValueError( - "the payload must contain a certificate and its private key" - ) - certificate = x509.load_pem_x509_certificate( - certificate_match.group(), default_backend() - ) - private_key = serialization.load_pem_private_key( - private_key_match.group(), password=None, backend=default_backend() - ) - - if certificate is None or private_key is None: - raise ValueError("the payload must contain a certificate and its private key") - - encoding = serialization.Encoding.DER - public_format = serialization.PublicFormat.SubjectPublicKeyInfo - if certificate.public_key().public_bytes( - encoding, public_format - ) != private_key.public_key().public_bytes(encoding, public_format): - raise ValueError("the certificate does not match the private key") - - def _normalize_hostname(hostname: str) -> str: return hostname.rstrip(".").lower()