fix(ui): wire certificate errors, cap client-side, harden the self-signed cert

This commit is contained in:
Lydia Vilchez
2026-09-01 12:37:26 +02:00
parent 4f0a49eaf1
commit 788458be4e
8 changed files with 44 additions and 12 deletions
@@ -78,13 +78,13 @@ describe("AzureCertificateCredentialsForm browser flow", () => {
view.getByRole("link", { name: "Deploy to Azure" }).element(),
).toHaveAttribute(
"href",
"https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fdocs.prowler.com%2Fassets%2Ftemplates%2Fazure%2Fprowler-scan.json",
"https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fprowler-cloud%2Fprowler%2Fmaster%2Fpermissions%2Ftemplates%2Fazure%2Fbicep%2Fprowler-scan.json",
);
expect(
view.getByRole("link", { name: "Open template" }).element(),
).toHaveAttribute(
"href",
"https://docs.prowler.com/assets/templates/azure/prowler-scan.json",
"https://raw.githubusercontent.com/prowler-cloud/prowler/master/permissions/templates/azure/bicep/prowler-scan.json",
);
await view.getByRole("button", { name: "Generate certificate" }).click();
@@ -36,7 +36,7 @@ describe("AzureCertificateCredentialsForm", () => {
expect(link).toHaveAttribute("rel", "noopener noreferrer");
expect(link).toHaveAttribute(
"href",
"https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fdocs.prowler.com%2Fassets%2Ftemplates%2Fazure%2Fprowler-scan.json",
"https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fprowler-cloud%2Fprowler%2Fmaster%2Fpermissions%2Ftemplates%2Fazure%2Fbicep%2Fprowler-scan.json",
);
});
@@ -48,7 +48,7 @@ describe("AzureCertificateCredentialsForm", () => {
const link = screen.getByRole("link", { name: "Open template" });
expect(link).toHaveAttribute(
"href",
"https://docs.prowler.com/assets/templates/azure/prowler-scan.json",
"https://raw.githubusercontent.com/prowler-cloud/prowler/master/permissions/templates/azure/bicep/prowler-scan.json",
);
expect(link).toHaveAttribute("target", "_blank");
expect(link).toHaveAttribute("rel", "noopener noreferrer");
@@ -83,7 +83,7 @@ describe("AzureCertificateCredentialsForm", () => {
},
{
element: screen.getByRole("link", { name: "Open template" }),
href: "https://docs.prowler.com/assets/templates/azure/prowler-scan.json",
href: "https://raw.githubusercontent.com/prowler-cloud/prowler/master/permissions/templates/azure/bicep/prowler-scan.json",
},
];
+10 -2
View File
@@ -20,8 +20,10 @@
import "reflect-metadata";
import {
BasicConstraintsExtension,
cryptoProvider,
Extension,
KeyUsageFlags,
KeyUsagesExtension,
X509CertificateGenerator,
} from "@peculiar/x509";
@@ -131,7 +133,13 @@ export async function generateProwlerCertificate(
hash: "SHA-256",
},
keys: keyPair,
extensions: [] as Extension[],
// Match `openssl x509 -req` defaults: mark the leaf as end-entity
// (`cA=false`) and declare `digitalSignature` so audit tooling and
// strict CA validators don't flag the certificate as unusual.
extensions: [
new BasicConstraintsExtension(false, undefined, true),
new KeyUsagesExtension(KeyUsageFlags.digitalSignature, true),
],
});
const certPem = cert.toString("pem");
+2
View File
@@ -16,6 +16,8 @@ export const PROVIDER_CREDENTIALS_ERROR_MAPPING: Record<string, string> = {
[ErrorPointers.AWS_SESSION_TOKEN]: ProviderCredentialFields.AWS_SESSION_TOKEN,
[ErrorPointers.CLIENT_ID]: ProviderCredentialFields.CLIENT_ID,
[ErrorPointers.CLIENT_SECRET]: ProviderCredentialFields.CLIENT_SECRET,
[ErrorPointers.CERTIFICATE_CONTENT]:
ProviderCredentialFields.CERTIFICATE_CONTENT,
[ErrorPointers.USER]: ProviderCredentialFields.USER,
[ErrorPointers.PASSWORD]: ProviderCredentialFields.PASSWORD,
[ErrorPointers.TENANT_ID]: ProviderCredentialFields.TENANT_ID,
+2 -2
View File
@@ -294,10 +294,10 @@ describe("getAzureDeploymentQuickLink", () => {
// Then
expect(PROWLER_AZURE_ARM_TEMPLATE_URL).toBe(
"https://docs.prowler.com/assets/templates/azure/prowler-scan.json",
"https://raw.githubusercontent.com/prowler-cloud/prowler/master/permissions/templates/azure/bicep/prowler-scan.json",
);
expect(url).toBe(
"https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fdocs.prowler.com%2Fassets%2Ftemplates%2Fazure%2Fprowler-scan.json",
"https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fprowler-cloud%2Fprowler%2Fmaster%2Fpermissions%2Ftemplates%2Fazure%2Fbicep%2Fprowler-scan.json",
);
expect(url).not.toContain("localhost");
expect(url).not.toContain("prowler-cloud-public.s3");
+5 -1
View File
@@ -40,8 +40,12 @@ export const getAttackPathHubUrl = (queryId: string): string =>
export const PROWLER_CF_TEMPLATE_URL =
"https://prowler-cloud-public.s3.eu-west-1.amazonaws.com/permissions/templates/aws/cloudformation/prowler-scan-role.yml";
// Stopgap: point the Azure Portal at the raw template on GitHub until the
// docs deploy publishes the file under `docs.prowler.com/assets/...`.
// The Portal fetches this URL over HTTPS, so `raw.githubusercontent.com`
// works exactly the same for the Deploy-to-Azure flow.
export const PROWLER_AZURE_ARM_TEMPLATE_URL =
"https://docs.prowler.com/assets/templates/azure/prowler-scan.json";
"https://raw.githubusercontent.com/prowler-cloud/prowler/master/permissions/templates/azure/bicep/prowler-scan.json";
// Prowler Cloud billing/subscription management page.
export const BILLING_URL = "https://cloud.prowler.com/billing";
+2 -2
View File
@@ -1640,7 +1640,7 @@ export class ProvidersPage extends BasePage {
}),
).toHaveAttribute(
"href",
"https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fdocs.prowler.com%2Fassets%2Ftemplates%2Fazure%2Fprowler-scan.json",
"https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fprowler-cloud%2Fprowler%2Fmaster%2Fpermissions%2Ftemplates%2Fazure%2Fbicep%2Fprowler-scan.json",
);
await expect(
this.page.getByRole("link", {
@@ -1649,7 +1649,7 @@ export class ProvidersPage extends BasePage {
}),
).toHaveAttribute(
"href",
"https://docs.prowler.com/assets/templates/azure/prowler-scan.json",
"https://raw.githubusercontent.com/prowler-cloud/prowler/master/permissions/templates/azure/bicep/prowler-scan.json",
);
await expect(
this.page.getByRole("button", { name: "Generate certificate" }),
+18
View File
@@ -7,6 +7,16 @@ import { MAX_SAML_ADDITIONAL_EMAIL_DOMAINS } from "@/types/saml";
import { PROVIDER_TYPES, ProviderType } from "./providers";
// Matches the API's `_MAX_CERTIFICATE_CONTENT_LENGTH` in
// `api/src/backend/api/v1/serializers.py`, i.e. base64 of the SDK's 50 KiB
// `_MAX_CERTIFICATE_BUNDLE_BYTES` cap. Reject oversized certificate
// content client-side so the user sees the error inline before a
// round-trip that the API would 400 with the same message.
export const MAX_CERTIFICATE_CONTENT_LENGTH = 68268;
export const CERTIFICATE_CONTENT_MAX_SIZE_ERROR =
"Certificate content exceeds the maximum size.";
export const KUBECONFIG_UNSUPPORTED_COMMAND_AUTHENTICATION_ERROR =
"Kubernetes kubeconfig command-based authentication is not supported in Prowler Cloud for security reasons.";
@@ -241,6 +251,10 @@ export const addCredentialsFormSchema = (
[ProviderCredentialFields.CLIENT_SECRET]: z.string().optional(),
[ProviderCredentialFields.CERTIFICATE_CONTENT]: z
.string()
.max(
MAX_CERTIFICATE_CONTENT_LENGTH,
CERTIFICATE_CONTENT_MAX_SIZE_ERROR,
)
.optional(),
[ProviderCredentialFields.TENANT_ID]: z.guid({
error: "Tenant ID must be a valid GUID",
@@ -284,6 +298,10 @@ export const addCredentialsFormSchema = (
.optional(),
[ProviderCredentialFields.CERTIFICATE_CONTENT]: z
.string()
.max(
MAX_CERTIFICATE_CONTENT_LENGTH,
CERTIFICATE_CONTENT_MAX_SIZE_ERROR,
)
.optional(),
[ProviderCredentialFields.TENANT_ID]: z
.string()