From 3be989c5b16d3c8577c2a355b1752d87187eed13 Mon Sep 17 00:00:00 2001 From: Lydia Vilchez Date: Wed, 12 Aug 2026 09:34:38 +0200 Subject: [PATCH] feat(azure): add Deploy-to-Azure Bicep template and certificate auth --- .../providers/azure/authentication.mdx | 4 +-- .../providers/azure/getting-started-azure.mdx | 2 +- prowler/providers/azure/azure_provider.py | 16 +++++---- .../azure-certificate-credentials-form.tsx | 35 ++++++++++--------- ...-group-azure-via-credentials-type-form.tsx | 16 ++++++++- ui/lib/azure-cert-generator.ts | 8 ++--- ui/tests/providers/providers-page.ts | 4 ++- 7 files changed, 53 insertions(+), 32 deletions(-) diff --git a/docs/user-guide/providers/azure/authentication.mdx b/docs/user-guide/providers/azure/authentication.mdx index 47a5b7ecc4..0c3420583f 100644 --- a/docs/user-guide/providers/azure/authentication.mdx +++ b/docs/user-guide/providers/azure/authentication.mdx @@ -294,9 +294,9 @@ Alternatively, point Prowler at a certificate file on disk: prowler azure --certificate-auth --certificate-path /path/to/prowler.key ``` -## Service Principal Application Authentication (Recommended) +## Service Principal Application Authentication -This method is required for Prowler Cloud and recommended for Prowler CLI. +This client-secret flow is the supported fallback when [Certificate Authentication](#certificate-authentication) is not an option — for example on sovereign clouds where the Deploy to Azure quick-start is unavailable, or when an organisation policy forbids certificate-based service principals. New Prowler Cloud onboardings should prefer certificate authentication. ### Creating the Service Principal For more information, see [Creating Prowler Service Principal](/user-guide/providers/azure/create-prowler-service-principal). diff --git a/docs/user-guide/providers/azure/getting-started-azure.mdx b/docs/user-guide/providers/azure/getting-started-azure.mdx index debdf9ee89..77e02ebfeb 100644 --- a/docs/user-guide/providers/azure/getting-started-azure.mdx +++ b/docs/user-guide/providers/azure/getting-started-azure.mdx @@ -57,7 +57,7 @@ Azure supports two authentication methods in the add-provider wizard. Prowler Cl #### Certificate Authentication (Recommended) -The one-click flow: click **Deploy to Azure** in the wizard and let the Bicep template create the App Registration / Service Principal, the `Reader` role assignment, and the custom `ProwlerRole` with a certificate credential in a single deployment. +The quick-start flow: click **Deploy to Azure** in the wizard to start a deployment in the Azure Portal with the Prowler Bicep template pre-loaded. The template creates the App Registration / Service Principal, the `Reader` role assignment, and the custom `ProwlerRole` bound to a certificate credential. You still fill the certificate parameter and confirm the deployment inside the Portal. 1. In the Azure wizard, select **Certificate Authentication (Recommended)**. 2. Click the **Deploy to Azure** button. This opens the Azure Portal deployment blade pre-loaded with the [Prowler Bicep template](https://github.com/prowler-cloud/prowler/blob/master/permissions/templates/azure/bicep/prowler-scan.bicep). diff --git a/prowler/providers/azure/azure_provider.py b/prowler/providers/azure/azure_provider.py index fee7fb16bf..7c7f01b2a1 100644 --- a/prowler/providers/azure/azure_provider.py +++ b/prowler/providers/azure/azure_provider.py @@ -1188,13 +1188,15 @@ class AzureProvider(Provider): ) # since that exception is not considered as critical, we keep filling another identity fields if isinstance(credentials, CertificateCredential): - # `getenv("AZURE_CLIENT_ID", default=client_id)` already - # covers both the env-var cert-auth path (env is set) and - # the static-credentials path (constructor param supplied - # `client_id`). Reaching into `credentials._client_id` - # would be the same class of azure-identity private state - # we deliberately avoided for the thumbprint. - identity.identity_id = getenv("AZURE_CLIENT_ID", default=client_id) + # Prefer the explicit constructor param over the ambient + # env var: with static credentials the caller has + # unambiguously said "this is the client id" and using + # the env var could silently substitute another + # principal's id if the shell happens to have + # `AZURE_CLIENT_ID` set. Fall back to the env var only + # for the env-only cert-auth path where `client_id` is + # `None`. + identity.identity_id = client_id or getenv("AZURE_CLIENT_ID") identity.identity_type = "Service Principal with Certificate" # The SHA-1 thumbprint is computed from the certificate # bytes by `_compute_certificate_thumbprint` at diff --git a/ui/components/providers/workflow/forms/select-credentials-type/azure/credentials-type/azure-certificate-credentials-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/azure/credentials-type/azure-certificate-credentials-form.tsx index d9c539df94..458ae88c4a 100644 --- a/ui/components/providers/workflow/forms/select-credentials-type/azure/credentials-type/azure-certificate-credentials-form.tsx +++ b/ui/components/providers/workflow/forms/select-credentials-type/azure/credentials-type/azure-certificate-credentials-form.tsx @@ -75,9 +75,10 @@ export const AzureCertificateCredentialsForm = ({ Certificate Authentication (Recommended)
- Deploy the Prowler Bicep template to provision the App Registration - / Service Principal and read-only roles in one click, then paste the - resulting credentials below. + The Deploy to Azure quick-start opens the Azure Portal with the + Prowler Bicep template pre-loaded to provision the App Registration / + Service Principal and read-only roles. Paste the resulting credentials + below when the deployment finishes.
@@ -98,12 +99,15 @@ export const AzureCertificateCredentialsForm = ({ Don't have a certificate yet?

- Generate a self-signed X.509 certificate right in your browser. The - private key is generated locally and goes straight - into the field below (it is then submitted with the rest of the - form). The public certificate downloads as a text file for you to - paste into the Bicep Certificate Base64 parameter in - the Portal. + Generate a self-signed X.509 certificate right in your browser. + + {" "} + The certificate and private key are generated locally + + . The base64-encoded certificate/private-key bundle goes into the + field below and is submitted with the rest of the form. The public + certificate downloads as a text file for you to paste into the Bicep{" "} + Certificate Base64 parameter in the Portal.

{generatorError && (

{generatorError}

)} {generatedThumbprint && (

- Done. Certificate SHA-1 thumbprint: {generatedThumbprint} - . Downloaded prowler-cert-base64.txt — paste its - contents into the Bicep Certificate Base64 parameter, - then keep filling the fields below. + Done. Certificate SHA-1 thumbprint:{" "} + {generatedThumbprint}. Downloaded{" "} + prowler-cert-base64.txt — paste its contents into the + Bicep Certificate Base64 parameter, then keep filling + the fields below.

)} diff --git a/ui/components/providers/workflow/forms/select-credentials-type/azure/radio-group-azure-via-credentials-type-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/azure/radio-group-azure-via-credentials-type-form.tsx index fa762a2246..2a8a688cca 100644 --- a/ui/components/providers/workflow/forms/select-credentials-type/azure/radio-group-azure-via-credentials-type-form.tsx +++ b/ui/components/providers/workflow/forms/select-credentials-type/azure/radio-group-azure-via-credentials-type-form.tsx @@ -6,12 +6,26 @@ import { WizardRadioCard } from "@/components/providers/workflow/forms/fields"; import { FormMessage } from "@/components/shadcn/form"; import { RadioGroup } from "@/components/shadcn/radio-group/radio-group"; +// The `via` values shared with the M365 flow (they both authenticate against +// an Entra ID App Registration and reuse the same wizard state machine). +// Exposed as a const object so consumers can reference the values by name +// instead of duplicating string literals — matches the AWS/M365 patterns +// (`AWS_CREDENTIAL_OPTIONS`, `M365_CREDENTIAL_OPTIONS`). +export const AZURE_CREDENTIALS_TYPES = { + CERTIFICATE: "app_certificate", + CLIENT_SECRET: "app_client_secret", + EMPTY: "", +} as const; + +type AzureCredentialsType = + (typeof AZURE_CREDENTIALS_TYPES)[keyof typeof AZURE_CREDENTIALS_TYPES]; + // Local form shape for this selector: only the radio value lives here, the // actual credential fields belong to the downstream credentials form. Kept // exported so `SelectViaAzure` and the tests bind the same type instead of // falling back to `any`. export type AzureCredentialsTypeFormValues = { - azureCredentialsType: "app_certificate" | "app_client_secret" | ""; + azureCredentialsType: AzureCredentialsType; }; type RadioGroupAzureViaCredentialsFormProps = { diff --git a/ui/lib/azure-cert-generator.ts b/ui/lib/azure-cert-generator.ts index f00ed6666f..4ae32370e8 100644 --- a/ui/lib/azure-cert-generator.ts +++ b/ui/lib/azure-cert-generator.ts @@ -148,11 +148,11 @@ export async function generateProwlerCertificate( // private key second. The full bundle is then base64-encoded so it can // live inside a single form field / JSON payload. const bundlePem = `${certPem.trim()}\n${privateKeyPem.trim()}\n`; - const privateKeyBundleBase64Pem = toBase64(new TextEncoder().encode(bundlePem)); - - const thumbprintBytes = new Uint8Array( - await subtle.digest("SHA-1", certDer), + const privateKeyBundleBase64Pem = toBase64( + new TextEncoder().encode(bundlePem), ); + + const thumbprintBytes = new Uint8Array(await subtle.digest("SHA-1", certDer)); const thumbprintHex = bytesToHexUpper(thumbprintBytes); return { diff --git a/ui/tests/providers/providers-page.ts b/ui/tests/providers/providers-page.ts index b1e99e81ec..10806b9b0e 100644 --- a/ui/tests/providers/providers-page.ts +++ b/ui/tests/providers/providers-page.ts @@ -1102,7 +1102,9 @@ export class ProvidersPage extends BasePage { if (type === AZURE_CREDENTIAL_OPTIONS.AZURE_CREDENTIALS) { await this.azureServicePrincipalRadio.click({ force: true }); - } else if (type === AZURE_CREDENTIAL_OPTIONS.AZURE_CERTIFICATE_CREDENTIALS) { + } else if ( + type === AZURE_CREDENTIAL_OPTIONS.AZURE_CERTIFICATE_CREDENTIALS + ) { await this.azureCertificateCredentialsRadio.click({ force: true }); } else { throw new Error(`Invalid Azure credential type: ${type}`);