mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-11 05:54:17 +00:00
feat(azure): add Deploy-to-Azure Bicep template and certificate auth
This commit is contained in:
1 parent
ab738e8a4e
commit
d2df95546b
9 files changed
+712
-18
No files matched your search
@@ -26,23 +26,23 @@
|
||||
|
||||
targetScope = 'subscription'
|
||||
|
||||
@description('Display name of the Entra ID App Registration that Prowler will authenticate as.')
|
||||
@description('Name for the App Registration Prowler will use. Keep the default unless you need a custom name.')
|
||||
param applicationName string = 'ProwlerApp'
|
||||
|
||||
@description('Base64-encoded DER of the X.509 public certificate that Entra ID will pin to the App Registration as a keyCredential. Generate with `openssl x509 -outform DER | base64` (see README). The matching private key stays with the user — Prowler never receives it during deployment.')
|
||||
@description('Paste the PUBLIC certificate from the Prowler wizard (prowler-cert-base64.txt) or your own openssl output. Prowler never receives the matching private key.')
|
||||
@secure()
|
||||
param certificateBase64 string
|
||||
|
||||
@description('Optional friendly display name for the certificate credential.')
|
||||
@description('Label for the certificate inside the App Registration. Cosmetic — keep the default if unsure.')
|
||||
param certificateDisplayName string = 'Prowler Certificate'
|
||||
|
||||
@description('Start date (ISO 8601) for the certificate credential validity window. Defaults to the deployment time.')
|
||||
@description('When the certificate becomes valid. Defaults to now.')
|
||||
param certificateStartDateTime string = utcNow()
|
||||
|
||||
@description('End date (ISO 8601) for the certificate credential validity window. Defaults to one year after the deployment.')
|
||||
@description('When the certificate expires. Defaults to 1 year from now. Rotate before this date.')
|
||||
param certificateEndDateTime string = dateTimeAdd(utcNow(), 'P1Y')
|
||||
|
||||
@description('Name of the custom role Prowler creates for its extra read actions.')
|
||||
@description('Name of the extra role Prowler creates. Keep the default unless your org already uses this name.')
|
||||
param customRoleName string = 'ProwlerRole'
|
||||
|
||||
// Deterministic GUIDs derived from `subscription().id` and the params so
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"_generator": {
|
||||
"name": "bicep",
|
||||
"version": "0.46.1.21595",
|
||||
"templateHash": "6180354331557312661"
|
||||
"templateHash": "17365420263047938168"
|
||||
}
|
||||
},
|
||||
"parameters": {
|
||||
@@ -14,41 +14,41 @@
|
||||
"type": "string",
|
||||
"defaultValue": "ProwlerApp",
|
||||
"metadata": {
|
||||
"description": "Display name of the Entra ID App Registration that Prowler will authenticate as."
|
||||
"description": "Name for the App Registration Prowler will use. Keep the default unless you need a custom name."
|
||||
}
|
||||
},
|
||||
"certificateBase64": {
|
||||
"type": "securestring",
|
||||
"metadata": {
|
||||
"description": "Base64-encoded DER of the X.509 public certificate that Entra ID will pin to the App Registration as a keyCredential. Generate with `openssl x509 -outform DER | base64` (see README). The matching private key stays with the user — Prowler never receives it during deployment."
|
||||
"description": "Paste the PUBLIC certificate from the Prowler wizard (prowler-cert-base64.txt) or your own openssl output. Prowler never receives the matching private key."
|
||||
}
|
||||
},
|
||||
"certificateDisplayName": {
|
||||
"type": "string",
|
||||
"defaultValue": "Prowler Certificate",
|
||||
"metadata": {
|
||||
"description": "Optional friendly display name for the certificate credential."
|
||||
"description": "Label for the certificate inside the App Registration. Cosmetic — keep the default if unsure."
|
||||
}
|
||||
},
|
||||
"certificateStartDateTime": {
|
||||
"type": "string",
|
||||
"defaultValue": "[utcNow()]",
|
||||
"metadata": {
|
||||
"description": "Start date (ISO 8601) for the certificate credential validity window. Defaults to the deployment time."
|
||||
"description": "When the certificate becomes valid. Defaults to now."
|
||||
}
|
||||
},
|
||||
"certificateEndDateTime": {
|
||||
"type": "string",
|
||||
"defaultValue": "[dateTimeAdd(utcNow(), 'P1Y')]",
|
||||
"metadata": {
|
||||
"description": "End date (ISO 8601) for the certificate credential validity window. Defaults to one year after the deployment."
|
||||
"description": "When the certificate expires. Defaults to 1 year from now. Rotate before this date."
|
||||
}
|
||||
},
|
||||
"customRoleName": {
|
||||
"type": "string",
|
||||
"defaultValue": "ProwlerRole",
|
||||
"metadata": {
|
||||
"description": "Name of the custom role Prowler creates for its extra read actions."
|
||||
"description": "Name of the extra role Prowler creates. Keep the default unless your org already uses this name."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -172,4 +172,4 @@
|
||||
"value": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('customRoleDefinitionName'))]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+83
-3
@@ -1,14 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Control } from "react-hook-form";
|
||||
import { useState } from "react";
|
||||
import { Control, useFormContext } from "react-hook-form";
|
||||
|
||||
import {
|
||||
WizardInputField,
|
||||
WizardTextareaField,
|
||||
} from "@/components/providers/workflow/forms/fields";
|
||||
import { Button } from "@/components/shadcn";
|
||||
import {
|
||||
downloadPublicCertificateFile,
|
||||
generateProwlerCertificate,
|
||||
} from "@/lib/azure-cert-generator";
|
||||
import { getAzureDeploymentQuickLink } from "@/lib/external-urls";
|
||||
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
|
||||
import { AzureCertificateCredentials } from "@/types";
|
||||
|
||||
export const AzureCertificateCredentialsForm = ({
|
||||
@@ -23,6 +29,45 @@ export const AzureCertificateCredentialsForm = ({
|
||||
// or region, thread that value through `getAzureDeploymentQuickLink` here.
|
||||
const deployToAzureUrl = getAzureDeploymentQuickLink();
|
||||
|
||||
// Feedback state for the in-browser certificate generator. Kept local
|
||||
// because the values only matter to this component — nothing else in the
|
||||
// wizard needs to know that the user opted for auto-generation.
|
||||
const [isGeneratingCert, setIsGeneratingCert] = useState(false);
|
||||
const [generatorError, setGeneratorError] = useState<string | null>(null);
|
||||
const [generatedThumbprint, setGeneratedThumbprint] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const { setValue } = useFormContext<AzureCertificateCredentials>();
|
||||
|
||||
const handleGenerateCertificate = async () => {
|
||||
setGeneratorError(null);
|
||||
setGeneratedThumbprint(null);
|
||||
setIsGeneratingCert(true);
|
||||
try {
|
||||
const result = await generateProwlerCertificate();
|
||||
// Auto-fill the private key textarea with the ready-to-paste bundle so
|
||||
// the user does not need to touch the field manually. `shouldValidate`
|
||||
// clears the "Certificate Private Key is required" error immediately.
|
||||
setValue(
|
||||
ProviderCredentialFields.CERTIFICATE_CONTENT,
|
||||
result.privateKeyBundleBase64Pem,
|
||||
{ shouldValidate: true, shouldDirty: true, shouldTouch: true },
|
||||
);
|
||||
// Hand the public certificate to the user as a file: they upload it to
|
||||
// the Bicep `Certificate Base64` parameter in the Azure Portal.
|
||||
downloadPublicCertificateFile(result.publicCertificateBase64Der);
|
||||
setGeneratedThumbprint(result.thumbprintHex);
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to generate the certificate in-browser. Fall back to the openssl or PowerShell instructions in the guide.";
|
||||
setGeneratorError(message);
|
||||
} finally {
|
||||
setIsGeneratingCert(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col">
|
||||
@@ -48,6 +93,40 @@ export const AzureCertificateCredentialsForm = ({
|
||||
“Certificate Private Key” field below.
|
||||
</p>
|
||||
</div>
|
||||
<div className="border-content-neutral-tertiary flex flex-col items-start gap-2 rounded-md border border-dashed p-3">
|
||||
<div className="text-text-neutral-primary text-sm font-semibold">
|
||||
Don'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 never leaves your device</strong> — it goes
|
||||
straight into the field below. 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"
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={handleGenerateCertificate}
|
||||
disabled={isGeneratingCert}
|
||||
>
|
||||
{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.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<WizardInputField
|
||||
control={control}
|
||||
name="tenant_id"
|
||||
@@ -81,8 +160,9 @@ export const AzureCertificateCredentialsForm = ({
|
||||
<p className="text-text-neutral-tertiary text-sm">
|
||||
This is the <strong>base64-encoded private key</strong> that matches the
|
||||
certificate uploaded to Entra ID by the Bicep template (not the public
|
||||
certificate, and not the thumbprint). Generation instructions (openssl /
|
||||
PowerShell) are in the{" "}
|
||||
certificate, and not the thumbprint). Use the{" "}
|
||||
<em>Generate certificate for me</em> button above, or follow the manual
|
||||
openssl / PowerShell instructions in the{" "}
|
||||
<Link
|
||||
href="https://docs.prowler.com/user-guide/providers/azure/authentication#certificate-authentication"
|
||||
target="_blank"
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { webcrypto } from "node:crypto";
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
downloadPublicCertificateFile,
|
||||
generateProwlerCertificate,
|
||||
} from "./azure-cert-generator";
|
||||
|
||||
// jsdom exposes `globalThis.crypto` but historically without a working
|
||||
// `subtle` implementation. Node 20+ ships one on `node:crypto.webcrypto`
|
||||
// that satisfies both @peculiar/x509 and our helper. Bind it once so the
|
||||
// module-level `cryptoProvider.set(...)` inside the SUT resolves it.
|
||||
beforeAll(() => {
|
||||
if (!globalThis.crypto || !globalThis.crypto.subtle) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
Object.defineProperty(globalThis, "crypto", {
|
||||
value: webcrypto,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("generateProwlerCertificate", () => {
|
||||
it("produces a certificate + private-key bundle that round-trips through PEM parsers", async () => {
|
||||
// Given / When
|
||||
const result = await generateProwlerCertificate({
|
||||
// Shrink the modulus so the test finishes in <2s under CI without
|
||||
// giving up any of the code paths the helper touches at 4096 bits.
|
||||
modulusLength: 2048,
|
||||
commonName: "prowler-test",
|
||||
validityDays: 30,
|
||||
});
|
||||
|
||||
// Then — DER base64 of the public certificate decodes to a byte array
|
||||
// that starts with the ASN.1 SEQUENCE tag (0x30). A stray bug that
|
||||
// returned PEM instead of DER, or double-encoded the base64, would trip
|
||||
// this straight away.
|
||||
const publicDer = Uint8Array.from(
|
||||
atob(result.publicCertificateBase64Der),
|
||||
(c) => c.charCodeAt(0),
|
||||
);
|
||||
expect(publicDer[0]).toBe(0x30);
|
||||
expect(publicDer.byteLength).toBeGreaterThan(500);
|
||||
|
||||
// The private-key bundle decodes to a UTF-8 PEM string containing both
|
||||
// markers, in the order azure-identity expects (cert first, key second).
|
||||
const bundlePem = new TextDecoder().decode(
|
||||
Uint8Array.from(atob(result.privateKeyBundleBase64Pem), (c) =>
|
||||
c.charCodeAt(0),
|
||||
),
|
||||
);
|
||||
const certIdx = bundlePem.indexOf("-----BEGIN CERTIFICATE-----");
|
||||
const keyIdx = bundlePem.indexOf("-----BEGIN PRIVATE KEY-----");
|
||||
expect(certIdx).toBeGreaterThanOrEqual(0);
|
||||
expect(keyIdx).toBeGreaterThan(certIdx);
|
||||
expect(bundlePem).toContain("-----END CERTIFICATE-----");
|
||||
expect(bundlePem).toContain("-----END PRIVATE KEY-----");
|
||||
|
||||
// Validity window respects the injected days and is a real ISO 8601
|
||||
// timestamp.
|
||||
const notBefore = new Date(result.notBefore);
|
||||
const notAfter = new Date(result.notAfter);
|
||||
expect(notBefore.getTime()).toBeLessThan(notAfter.getTime());
|
||||
const days = (notAfter.getTime() - notBefore.getTime()) / 86_400_000;
|
||||
expect(days).toBeCloseTo(30, 0);
|
||||
});
|
||||
|
||||
it("returns the correct SHA-1 thumbprint format expected by Entra ID", async () => {
|
||||
// Given / When
|
||||
const result = await generateProwlerCertificate({ modulusLength: 2048 });
|
||||
|
||||
// Then — 40 hex chars, uppercase, no separators.
|
||||
expect(result.thumbprintHex).toMatch(/^[0-9A-F]{40}$/);
|
||||
});
|
||||
|
||||
it("throws a friendly error when SubtleCrypto is unavailable", async () => {
|
||||
// Given the browser doesn't expose subtle (insecure origin, ancient
|
||||
// browser, some sandboxes).
|
||||
const originalCrypto = globalThis.crypto;
|
||||
Object.defineProperty(globalThis, "crypto", {
|
||||
value: {},
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
// When / Then
|
||||
await expect(generateProwlerCertificate()).rejects.toThrow(
|
||||
/Web Crypto API is not available/i,
|
||||
);
|
||||
|
||||
// Cleanup
|
||||
Object.defineProperty(globalThis, "crypto", {
|
||||
value: originalCrypto,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("downloadPublicCertificateFile", () => {
|
||||
const originalCreateElement = document.createElement.bind(document);
|
||||
const originalCreateObjectURL = URL.createObjectURL;
|
||||
const originalRevokeObjectURL = URL.revokeObjectURL;
|
||||
|
||||
afterEach(() => {
|
||||
document.createElement = originalCreateElement;
|
||||
URL.createObjectURL = originalCreateObjectURL;
|
||||
URL.revokeObjectURL = originalRevokeObjectURL;
|
||||
});
|
||||
|
||||
it("triggers an anchor click with the right href and filename, then revokes the blob URL", () => {
|
||||
// Given
|
||||
const clickSpy = vi.fn();
|
||||
const objectUrl = "blob:mock/prowler-cert";
|
||||
URL.createObjectURL = vi.fn(() => objectUrl);
|
||||
const revokeSpy = vi.fn();
|
||||
URL.revokeObjectURL = revokeSpy;
|
||||
|
||||
// The anchor spy is a real HTMLAnchorElement so `document.body.appendChild`
|
||||
// and `removeChild` accept it; we only intercept the `click` method.
|
||||
const realAnchor = originalCreateElement("a");
|
||||
realAnchor.click = clickSpy;
|
||||
document.createElement = vi.fn((tag: string) => {
|
||||
if (tag === "a") return realAnchor;
|
||||
return originalCreateElement(tag);
|
||||
}) as typeof document.createElement;
|
||||
|
||||
// When
|
||||
downloadPublicCertificateFile("MIIBase64Contents", "prowler-cert.txt");
|
||||
|
||||
// Then
|
||||
expect(URL.createObjectURL).toHaveBeenCalledTimes(1);
|
||||
expect(clickSpy).toHaveBeenCalledTimes(1);
|
||||
expect(realAnchor.href).toContain(objectUrl);
|
||||
expect(realAnchor.download).toBe("prowler-cert.txt");
|
||||
// Revoked to avoid leaking the blob URL for the tab's lifetime.
|
||||
expect(revokeSpy).toHaveBeenCalledWith(objectUrl);
|
||||
});
|
||||
|
||||
it("defaults the filename when the caller omits it", () => {
|
||||
// Given
|
||||
URL.createObjectURL = vi.fn(() => "blob:mock");
|
||||
URL.revokeObjectURL = vi.fn();
|
||||
const realAnchor = originalCreateElement("a");
|
||||
realAnchor.click = vi.fn();
|
||||
document.createElement = vi.fn((tag: string) => {
|
||||
if (tag === "a") return realAnchor;
|
||||
return originalCreateElement(tag);
|
||||
}) as typeof document.createElement;
|
||||
|
||||
// When
|
||||
downloadPublicCertificateFile("payload");
|
||||
|
||||
// Then
|
||||
expect(realAnchor.download).toBe("prowler-cert-base64.txt");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,240 @@
|
||||
// In-browser X.509 self-signed certificate generator for the Azure
|
||||
// certificate-authentication onboarding flow.
|
||||
//
|
||||
// The keypair is generated with the browser's native Web Crypto API
|
||||
// (`crypto.subtle.generateKey`) and never leaves the tab. `@peculiar/x509`
|
||||
// wraps the public key in a self-signed X.509 certificate whose SHA-1
|
||||
// thumbprint we can hand back to the user; the private key is exported as
|
||||
// base64-encoded PEM ready to paste into the Prowler wizard's Certificate
|
||||
// Private Key field.
|
||||
//
|
||||
// See the certificate authentication guide in
|
||||
// docs/user-guide/providers/azure/authentication.mdx for the equivalent
|
||||
// openssl/PowerShell recipes, and PROWLER-2378 for the Deploy-to-Azure
|
||||
// quick-start feature this UX affordance belongs to.
|
||||
|
||||
// `@peculiar/x509` transitively depends on `tsyringe`, which pulls in a
|
||||
// decorators/DI runtime that requires the `reflect-metadata` polyfill. The
|
||||
// import has to happen before anything from `@peculiar/x509` is imported so
|
||||
// the metadata store is registered on `Reflect` first.
|
||||
import "reflect-metadata";
|
||||
|
||||
import {
|
||||
cryptoProvider,
|
||||
Extension,
|
||||
X509CertificateGenerator,
|
||||
} from "@peculiar/x509";
|
||||
|
||||
// Bind @peculiar/x509 to the browser's native SubtleCrypto. Without this the
|
||||
// library falls back to a Node-only crypto provider that vitest+jsdom does
|
||||
// not expose, and the helper would throw in tests.
|
||||
if (typeof globalThis !== "undefined" && globalThis.crypto?.subtle) {
|
||||
cryptoProvider.set(globalThis.crypto);
|
||||
}
|
||||
|
||||
export interface GeneratedProwlerCertificate {
|
||||
/**
|
||||
* Base64 of the DER-encoded X.509 public certificate. Feed this into the
|
||||
* `certificateBase64` parameter of the Prowler Bicep quick-start template
|
||||
* (or paste directly into the Azure Portal "Certificate Base64" field).
|
||||
*/
|
||||
publicCertificateBase64Der: string;
|
||||
/**
|
||||
* Base64 of the PEM bundle containing both the X.509 certificate and the
|
||||
* PKCS#8 private key. `azure.identity.CertificateCredential` accepts this
|
||||
* exact shape; the Prowler wizard pastes it into the Certificate Private
|
||||
* Key (Base64) textarea.
|
||||
*/
|
||||
privateKeyBundleBase64Pem: string;
|
||||
/** Human-readable SHA-1 thumbprint, uppercase hex, matching what Entra ID displays. */
|
||||
thumbprintHex: string;
|
||||
/** ISO 8601 not-valid-before timestamp of the generated cert. */
|
||||
notBefore: string;
|
||||
/** ISO 8601 not-valid-after timestamp of the generated cert. */
|
||||
notAfter: string;
|
||||
}
|
||||
|
||||
export interface GenerateProwlerCertificateOptions {
|
||||
/**
|
||||
* Common name to embed in the certificate subject. Defaults to "Prowler"
|
||||
* to match the openssl/PowerShell examples in the docs.
|
||||
*/
|
||||
commonName?: string;
|
||||
/** Certificate lifetime in days. Default 365. */
|
||||
validityDays?: number;
|
||||
/** RSA modulus length. Default 4096 (matches the openssl example). */
|
||||
modulusLength?: 2048 | 3072 | 4096;
|
||||
/**
|
||||
* Injected clock for deterministic tests. Defaults to `Date.now()`.
|
||||
*/
|
||||
now?: () => Date;
|
||||
}
|
||||
|
||||
const DEFAULTS: Required<
|
||||
Pick<
|
||||
GenerateProwlerCertificateOptions,
|
||||
"commonName" | "validityDays" | "modulusLength"
|
||||
>
|
||||
> = {
|
||||
commonName: "Prowler",
|
||||
validityDays: 365,
|
||||
modulusLength: 4096,
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a fresh self-signed X.509 certificate + RSA-4096 keypair in the
|
||||
* browser. Nothing crosses the network — the private key exists only in the
|
||||
* returned object and inside the caller's memory.
|
||||
*
|
||||
* Throws when the browser does not expose SubtleCrypto (e.g. insecure origin
|
||||
* or old browser). Callers should surface a friendly fallback ("use openssl
|
||||
* instead") when that happens.
|
||||
*/
|
||||
export async function generateProwlerCertificate(
|
||||
options: GenerateProwlerCertificateOptions = {},
|
||||
): Promise<GeneratedProwlerCertificate> {
|
||||
const commonName = options.commonName ?? DEFAULTS.commonName;
|
||||
const validityDays = options.validityDays ?? DEFAULTS.validityDays;
|
||||
const modulusLength = options.modulusLength ?? DEFAULTS.modulusLength;
|
||||
const now = options.now ?? (() => new Date());
|
||||
|
||||
const subtle = globalThis.crypto?.subtle;
|
||||
if (!subtle) {
|
||||
throw new Error(
|
||||
"Web Crypto API is not available in this browser. Use the openssl or PowerShell instructions from the certificate generation guide instead.",
|
||||
);
|
||||
}
|
||||
|
||||
const keyPair = (await subtle.generateKey(
|
||||
{
|
||||
name: "RSASSA-PKCS1-v1_5",
|
||||
modulusLength,
|
||||
publicExponent: new Uint8Array([1, 0, 1]),
|
||||
hash: "SHA-256",
|
||||
},
|
||||
true,
|
||||
["sign", "verify"],
|
||||
)) as CryptoKeyPair;
|
||||
|
||||
const notBefore = now();
|
||||
const notAfter = new Date(
|
||||
notBefore.getTime() + validityDays * 24 * 60 * 60 * 1000,
|
||||
);
|
||||
|
||||
const cert = await X509CertificateGenerator.createSelfSigned({
|
||||
// Random serial: 16 hex chars is plenty for identification purposes and
|
||||
// matches how `openssl x509 -req` chooses serials by default.
|
||||
serialNumber: randomHex(16),
|
||||
name: `CN=${commonName}`,
|
||||
notBefore,
|
||||
notAfter,
|
||||
signingAlgorithm: {
|
||||
name: "RSASSA-PKCS1-v1_5",
|
||||
hash: "SHA-256",
|
||||
},
|
||||
keys: keyPair,
|
||||
extensions: [] as Extension[],
|
||||
});
|
||||
|
||||
const certPem = cert.toString("pem");
|
||||
const certDer = new Uint8Array(cert.rawData);
|
||||
const publicCertificateBase64Der = toBase64(certDer);
|
||||
|
||||
const privateKeyPkcs8 = new Uint8Array(
|
||||
await subtle.exportKey("pkcs8", keyPair.privateKey),
|
||||
);
|
||||
const privateKeyPem = pkcs8ToPem(privateKeyPkcs8);
|
||||
|
||||
// Bundle order matches what azure-identity expects: certificate first,
|
||||
// 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 thumbprintHex = bytesToHexUpper(thumbprintBytes);
|
||||
|
||||
return {
|
||||
publicCertificateBase64Der,
|
||||
privateKeyBundleBase64Pem,
|
||||
thumbprintHex,
|
||||
notBefore: notBefore.toISOString(),
|
||||
notAfter: notAfter.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a browser download of the given base64-DER public certificate as a
|
||||
* plain-text file, so the user has a single file to open next to the Portal
|
||||
* deployment blade and copy into the `Certificate Base64` parameter.
|
||||
*
|
||||
* Split from `generateProwlerCertificate` so the pure generator can be unit
|
||||
* tested without stubbing `document.createElement`.
|
||||
*/
|
||||
export function downloadPublicCertificateFile(
|
||||
publicCertificateBase64Der: string,
|
||||
filename = "prowler-cert-base64.txt",
|
||||
): void {
|
||||
const blob = new Blob([publicCertificateBase64Der], {
|
||||
type: "text/plain;charset=utf-8",
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url;
|
||||
anchor.download = filename;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
// Free the blob URL immediately; the browser has already started the
|
||||
// download at this point, so revoking is safe.
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// -- helpers ---------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Uint8Array → base64. Kept private to this module because the codebase does
|
||||
* not yet have a shared helper and this one only needs to handle small
|
||||
* payloads (a cert + key are ~5 KB total). If a shared helper appears later,
|
||||
* swap this out.
|
||||
*/
|
||||
function toBase64(bytes: Uint8Array): string {
|
||||
let binary = "";
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
const byte = bytes[i];
|
||||
binary += String.fromCharCode(byte);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
function randomHex(chars: number): string {
|
||||
const bytes = new Uint8Array(Math.ceil(chars / 2));
|
||||
globalThis.crypto.getRandomValues(bytes);
|
||||
return bytesToHexUpper(bytes).slice(0, chars);
|
||||
}
|
||||
|
||||
function bytesToHexUpper(bytes: Uint8Array): string {
|
||||
let hex = "";
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
const byte = bytes[i];
|
||||
hex += byte.toString(16).padStart(2, "0").toUpperCase();
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
// `@peculiar/x509` exports certs to PEM directly but not PKCS#8 keys — we
|
||||
// build the PEM ourselves so the private key format is deterministic and
|
||||
// matches what `openssl pkey -inform DER` would emit.
|
||||
function pkcs8ToPem(pkcs8: Uint8Array): string {
|
||||
const base64 = toBase64(pkcs8);
|
||||
// 64-char lines is the classic PEM formatting; azure-identity and every
|
||||
// other PEM parser accept both wrapped and unwrapped, but wrapping keeps
|
||||
// the file human-readable.
|
||||
const wrapped = base64.match(/.{1,64}/g)?.join("\n") ?? base64;
|
||||
return `-----BEGIN PRIVATE KEY-----\n${wrapped}\n-----END PRIVATE KEY-----`;
|
||||
}
|
||||
|
||||
// Named export needed by @/lib/shared/base64 fallback below.
|
||||
export const __internal = { pkcs8ToPem, bytesToHexUpper, randomHex };
|
||||
@@ -49,6 +49,7 @@
|
||||
"@langchain/openai": "1.4.5",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@next/third-parties": "16.2.9",
|
||||
"@peculiar/x509": "2.0.0",
|
||||
"@radix-ui/react-alert-dialog": "1.1.14",
|
||||
"@radix-ui/react-avatar": "1.1.11",
|
||||
"@radix-ui/react-checkbox": "1.3.3",
|
||||
@@ -107,6 +108,7 @@
|
||||
"react-hook-form": "7.62.0",
|
||||
"react-markdown": "10.1.0",
|
||||
"recharts": "2.15.4",
|
||||
"reflect-metadata": "0.2.2",
|
||||
"require-in-the-middle": "8.0.1",
|
||||
"server-only": "0.0.1",
|
||||
"sharp": "0.35.3",
|
||||
|
||||
Generated
+177
@@ -85,6 +85,9 @@ importers:
|
||||
'@next/third-parties':
|
||||
specifier: 16.2.9
|
||||
version: 16.2.9(next@16.2.11(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)
|
||||
'@peculiar/x509':
|
||||
specifier: 2.0.0
|
||||
version: 2.0.0
|
||||
'@radix-ui/react-alert-dialog':
|
||||
specifier: 1.1.14
|
||||
version: 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
@@ -259,6 +262,9 @@ importers:
|
||||
recharts:
|
||||
specifier: 2.15.4
|
||||
version: 2.15.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
reflect-metadata:
|
||||
specifier: 0.2.2
|
||||
version: 0.2.2
|
||||
require-in-the-middle:
|
||||
specifier: 8.0.1
|
||||
version: 8.0.1
|
||||
@@ -1787,6 +1793,43 @@ packages:
|
||||
'@panva/hkdf@1.2.1':
|
||||
resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==}
|
||||
|
||||
'@peculiar/asn1-cms@2.8.0':
|
||||
resolution: {integrity: sha512-NgekZOrSJFSBFLFoLfwePguAWAx7z1+f2TEsWFUMyiqqfntZ4+S/S5hzqME3q4pCA0iOsFKdwiQ35dwY24eVqA==}
|
||||
|
||||
'@peculiar/asn1-csr@2.8.0':
|
||||
resolution: {integrity: sha512-akbF8+uvleHs8sejNPQxwmVFuInAg6FMNHOwMILXfP518YfFJwdR3jr6oNUPOaEJfuEhn/vkNOCIT6ASUd4mbg==}
|
||||
|
||||
'@peculiar/asn1-ecc@2.8.0':
|
||||
resolution: {integrity: sha512-ohwlk+u9Rv2NOAY1c6MfHj45ATVF8R1DUN/WCgABiRtLi2ZftlZWZX7KvpAbU8v9xPcmoILfELeEABj/rn18AQ==}
|
||||
|
||||
'@peculiar/asn1-pfx@2.8.0':
|
||||
resolution: {integrity: sha512-5yof1ytoB++RQtaFbqSUJ8pxDJtZT6vbVqZ8XoJ61ph7UjNVvfFwAilnCodqkNsAodpy13gDhoxZXw00pghnyg==}
|
||||
|
||||
'@peculiar/asn1-pkcs8@2.8.0':
|
||||
resolution: {integrity: sha512-qAKXtLpBEw9LqhKpjw3ajZSXlBur+ipW+y2ivVBQAG6F6qRx94yO+1ZR4mvw+YaCfKSaOzLeYEzsPaBp4SJELA==}
|
||||
|
||||
'@peculiar/asn1-pkcs9@2.8.0':
|
||||
resolution: {integrity: sha512-b5nDWCnkV60+cQ141D6sVVwK9nz64R5n3zSVnklGd+ECdkW2Ol3U1a6yYFlalpSOaD557yuJB64A+q42jG7lUQ==}
|
||||
|
||||
'@peculiar/asn1-rsa@2.8.0':
|
||||
resolution: {integrity: sha512-zHEUlCqB2mk7x2lxDwHHJy7hWZOPdGHVlsmITWKB5/PbQo61atbu9PJ/0r9dQNMwFzbKPXZ8uK8/91eUhRznSg==}
|
||||
|
||||
'@peculiar/asn1-schema@2.8.0':
|
||||
resolution: {integrity: sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q==}
|
||||
|
||||
'@peculiar/asn1-x509-attr@2.8.0':
|
||||
resolution: {integrity: sha512-tHjkfS/qhMnmrlB2J9NhflQlQ7In3khO3CfmVrriOlpTeErY9ZIKOso1hQ5JQiyrJ7ShvqVPk7E5fQmbclkSKA==}
|
||||
|
||||
'@peculiar/asn1-x509@2.8.0':
|
||||
resolution: {integrity: sha512-N0CMuhWUzsWEVq6F1q9X6+VKUnWzSW+cSVg+aPaGGwDdbFoFWTYgin5MHwXgpWd6y9COMBxnfy/Qc+Xc7F0Zwg==}
|
||||
|
||||
'@peculiar/utils@2.0.3':
|
||||
resolution: {integrity: sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==}
|
||||
|
||||
'@peculiar/x509@2.0.0':
|
||||
resolution: {integrity: sha512-r10lkuy6BNfRmyYdRAfgu6dq0HOmyIV2OLhXWE3gDEPBdX1b8miztJVyX/UxWhLwemNyDP3CLZHpDxDwSY0xaA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@playwright/test@1.56.1':
|
||||
resolution: {integrity: sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -3835,6 +3878,10 @@ packages:
|
||||
resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
asn1js@3.0.10:
|
||||
resolution: {integrity: sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
assertion-error@2.0.1:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -6157,6 +6204,13 @@ packages:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
pvtsutils@1.3.6:
|
||||
resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==}
|
||||
|
||||
pvutils@1.2.0:
|
||||
resolution: {integrity: sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
|
||||
qs@6.15.2:
|
||||
resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==}
|
||||
engines: {node: '>=0.6'}
|
||||
@@ -6272,6 +6326,9 @@ packages:
|
||||
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
reflect-metadata@0.2.2:
|
||||
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
|
||||
|
||||
reflect.getprototypeof@1.0.10:
|
||||
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -6805,9 +6862,16 @@ packages:
|
||||
resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==}
|
||||
engines: {node: '>=6.10'}
|
||||
|
||||
tslib@1.14.1:
|
||||
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
tsyringe@4.10.0:
|
||||
resolution: {integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==}
|
||||
engines: {node: '>= 6.0.0'}
|
||||
|
||||
type-check@0.4.0:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -8840,6 +8904,99 @@ snapshots:
|
||||
|
||||
'@panva/hkdf@1.2.1': {}
|
||||
|
||||
'@peculiar/asn1-cms@2.8.0':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.8.0
|
||||
'@peculiar/asn1-x509': 2.8.0
|
||||
'@peculiar/asn1-x509-attr': 2.8.0
|
||||
asn1js: 3.0.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/asn1-csr@2.8.0':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.8.0
|
||||
'@peculiar/asn1-x509': 2.8.0
|
||||
asn1js: 3.0.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/asn1-ecc@2.8.0':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.8.0
|
||||
'@peculiar/asn1-x509': 2.8.0
|
||||
asn1js: 3.0.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/asn1-pfx@2.8.0':
|
||||
dependencies:
|
||||
'@peculiar/asn1-cms': 2.8.0
|
||||
'@peculiar/asn1-pkcs8': 2.8.0
|
||||
'@peculiar/asn1-rsa': 2.8.0
|
||||
'@peculiar/asn1-schema': 2.8.0
|
||||
asn1js: 3.0.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/asn1-pkcs8@2.8.0':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.8.0
|
||||
'@peculiar/asn1-x509': 2.8.0
|
||||
asn1js: 3.0.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/asn1-pkcs9@2.8.0':
|
||||
dependencies:
|
||||
'@peculiar/asn1-cms': 2.8.0
|
||||
'@peculiar/asn1-pfx': 2.8.0
|
||||
'@peculiar/asn1-pkcs8': 2.8.0
|
||||
'@peculiar/asn1-schema': 2.8.0
|
||||
'@peculiar/asn1-x509': 2.8.0
|
||||
'@peculiar/asn1-x509-attr': 2.8.0
|
||||
asn1js: 3.0.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/asn1-rsa@2.8.0':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.8.0
|
||||
'@peculiar/asn1-x509': 2.8.0
|
||||
asn1js: 3.0.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/asn1-schema@2.8.0':
|
||||
dependencies:
|
||||
'@peculiar/utils': 2.0.3
|
||||
asn1js: 3.0.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/asn1-x509-attr@2.8.0':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.8.0
|
||||
'@peculiar/asn1-x509': 2.8.0
|
||||
asn1js: 3.0.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/asn1-x509@2.8.0':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.8.0
|
||||
'@peculiar/utils': 2.0.3
|
||||
asn1js: 3.0.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/utils@2.0.3':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/x509@2.0.0':
|
||||
dependencies:
|
||||
'@peculiar/asn1-cms': 2.8.0
|
||||
'@peculiar/asn1-csr': 2.8.0
|
||||
'@peculiar/asn1-ecc': 2.8.0
|
||||
'@peculiar/asn1-pkcs9': 2.8.0
|
||||
'@peculiar/asn1-rsa': 2.8.0
|
||||
'@peculiar/asn1-schema': 2.8.0
|
||||
'@peculiar/asn1-x509': 2.8.0
|
||||
pvtsutils: 1.3.6
|
||||
tslib: 2.8.1
|
||||
tsyringe: 4.10.0
|
||||
|
||||
'@playwright/test@1.56.1':
|
||||
dependencies:
|
||||
playwright: 1.56.1
|
||||
@@ -11035,6 +11192,12 @@ snapshots:
|
||||
get-intrinsic: 1.3.0
|
||||
is-array-buffer: 3.0.5
|
||||
|
||||
asn1js@3.0.10:
|
||||
dependencies:
|
||||
pvtsutils: 1.3.6
|
||||
pvutils: 1.2.0
|
||||
tslib: 2.8.1
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
ast-types-flow@0.0.8: {}
|
||||
@@ -13761,6 +13924,12 @@ snapshots:
|
||||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
pvtsutils@1.3.6:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
pvutils@1.2.0: {}
|
||||
|
||||
qs@6.15.2:
|
||||
dependencies:
|
||||
side-channel: 1.1.0
|
||||
@@ -13888,6 +14057,8 @@ snapshots:
|
||||
indent-string: 4.0.0
|
||||
strip-indent: 3.0.0
|
||||
|
||||
reflect-metadata@0.2.2: {}
|
||||
|
||||
reflect.getprototypeof@1.0.10:
|
||||
dependencies:
|
||||
call-bind: 1.0.8
|
||||
@@ -14545,8 +14716,14 @@ snapshots:
|
||||
|
||||
ts-dedent@2.2.0: {}
|
||||
|
||||
tslib@1.14.1: {}
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
tsyringe@4.10.0:
|
||||
dependencies:
|
||||
tslib: 1.14.1
|
||||
|
||||
type-check@0.4.0:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
|
||||
@@ -123,9 +123,12 @@ export interface AWSProviderCredential {
|
||||
secretAccessKey?: string;
|
||||
}
|
||||
|
||||
// AZURE credential options
|
||||
// AZURE credential options — mirror the M365 selector added for the
|
||||
// Deploy-to-Azure quick-start (PROWLER-2378). "credentials" keeps the
|
||||
// legacy name for the client-secret path so existing specs keep working.
|
||||
export const AZURE_CREDENTIAL_OPTIONS = {
|
||||
AZURE_CREDENTIALS: "credentials",
|
||||
AZURE_CERTIFICATE_CREDENTIALS: "certificate",
|
||||
} as const;
|
||||
|
||||
// AZURE credential type
|
||||
@@ -316,6 +319,10 @@ export class ProvidersPage extends BasePage {
|
||||
readonly roleCredentialsRadio: Locator;
|
||||
readonly staticCredentialsRadio: Locator;
|
||||
|
||||
// Azure credentials type selection
|
||||
readonly azureServicePrincipalRadio: Locator;
|
||||
readonly azureCertificateCredentialsRadio: Locator;
|
||||
|
||||
// M365 credentials type selection
|
||||
readonly m365StaticCredentialsRadio: Locator;
|
||||
readonly m365CertificateCredentialsRadio: Locator;
|
||||
@@ -595,6 +602,16 @@ export class ProvidersPage extends BasePage {
|
||||
name: /Connect via Credentials/i,
|
||||
});
|
||||
|
||||
// Radios for selecting Azure credentials method (PROWLER-2378 added the
|
||||
// certificate flow; the client-secret radio stayed but is now inside a
|
||||
// selector step instead of being the default form).
|
||||
this.azureServicePrincipalRadio = page.getByRole("radio", {
|
||||
name: /Service Principal with Client Secret/i,
|
||||
});
|
||||
this.azureCertificateCredentialsRadio = page.getByRole("radio", {
|
||||
name: /Certificate Authentication/i,
|
||||
});
|
||||
|
||||
// Radios for selecting M365 credentials method
|
||||
this.m365StaticCredentialsRadio = page.getByRole("radio", {
|
||||
name: /App Client Secret Credentials/i,
|
||||
@@ -1076,6 +1093,22 @@ export class ProvidersPage extends BasePage {
|
||||
}
|
||||
}
|
||||
|
||||
async selectAzureCredentialsType(type: AZURECredentialType): Promise<void> {
|
||||
// PROWLER-2378 introduced a credential-type selector for Azure, mirroring
|
||||
// AWS/GCP/M365. The credentials form is now behind a radio choice, so
|
||||
// any spec that reaches Azure credentials must pick the type first.
|
||||
await this.verifyWizardModalOpen();
|
||||
await expect(this.azureServicePrincipalRadio).toBeVisible();
|
||||
|
||||
if (type === AZURE_CREDENTIAL_OPTIONS.AZURE_CREDENTIALS) {
|
||||
await this.azureServicePrincipalRadio.click({ force: true });
|
||||
} else if (type === AZURE_CREDENTIAL_OPTIONS.AZURE_CERTIFICATE_CREDENTIALS) {
|
||||
await this.azureCertificateCredentialsRadio.click({ force: true });
|
||||
} else {
|
||||
throw new Error(`Invalid Azure credential type: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
async selectM365CredentialsType(type: M365CredentialType): Promise<void> {
|
||||
await this.verifyWizardModalOpen();
|
||||
await expect(this.m365StaticCredentialsRadio).toBeVisible();
|
||||
|
||||
@@ -406,6 +406,10 @@ test.describe("Add Provider", () => {
|
||||
await providersPage.fillAZUREProviderDetails(azureProviderData);
|
||||
await providersPage.clickNext();
|
||||
|
||||
// Azure now shows a credential-type selector (PROWLER-2378) — pick
|
||||
// the client-secret path before landing on the credentials form.
|
||||
await providersPage.selectAzureCredentialsType(azureCredentials.type);
|
||||
|
||||
// Fill static credentials details
|
||||
await providersPage.fillAZURECredentials(azureCredentials);
|
||||
await providersPage.clickNext();
|
||||
|
||||
Reference in new issue
Block a user