feat(azure): add Deploy-to-Azure Bicep template and certificate auth

This commit is contained in:
Lydia Vilchez committed 2026-08-12 09:48:34 +02:00
1 parent 2eff97c2cc
commit 3be989c5b1
7 files changed
+53 -32

No files matched your search

@@ -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).
@@ -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).
+9 -7
View File
@@ -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
@@ -75,9 +75,10 @@ export const AzureCertificateCredentialsForm = ({
Certificate Authentication (Recommended)
</div>
<div className="text-text-neutral-tertiary text-sm">
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.
</div>
</div>
<div className="flex flex-col items-start gap-1">
@@ -98,12 +99,15 @@ export const AzureCertificateCredentialsForm = ({
Don&apos;t have a certificate yet?
</div>
<p className="text-text-neutral-tertiary text-xs">
Generate a self-signed X.509 certificate right in your browser. The
<strong> private key is generated locally</strong> 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 <code>Certificate Base64</code> parameter in
the Portal.
Generate a self-signed X.509 certificate right in your browser.
<strong>
{" "}
The certificate and private key are generated locally
</strong>
. 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{" "}
<code>Certificate Base64</code> parameter in the Portal.
</p>
<Button
type="button"
@@ -112,19 +116,18 @@ export const AzureCertificateCredentialsForm = ({
onClick={handleGenerateCertificate}
disabled={isGeneratingCert}
>
{isGeneratingCert
? "Generating…"
: "Generate certificate for me"}
{isGeneratingCert ? "Generating…" : "Generate certificate for me"}
</Button>
{generatorError && (
<p className="text-text-error-primary text-xs">{generatorError}</p>
)}
{generatedThumbprint && (
<p className="text-text-success-primary text-xs">
Done. Certificate SHA-1 thumbprint: <code>{generatedThumbprint}</code>
. Downloaded <code>prowler-cert-base64.txt</code> — paste its
contents into the Bicep <code>Certificate Base64</code> parameter,
then keep filling the fields below.
Done. Certificate SHA-1 thumbprint:{" "}
<code>{generatedThumbprint}</code>. Downloaded{" "}
<code>prowler-cert-base64.txt</code> — paste its contents into the
Bicep <code>Certificate Base64</code> parameter, then keep filling
the fields below.
</p>
)}
</div>
@@ -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 = {
+4 -4
View File
@@ -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 {
+3 -1
View File
@@ -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}`);