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()