mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-21 19:04:19 +00:00
feat(azure): add Deploy-to-Azure Bicep template and certificate auth
This commit is contained in:
1 parent
d2df95546b
commit
a47d94954a
16 files changed
+206
-92
No files matched your search
@@ -1808,12 +1808,13 @@ class AzureProviderSecret(serializers.Serializer):
|
||||
try:
|
||||
base64.b64decode(certificate_content, validate=True)
|
||||
except Exception as e:
|
||||
raise ValidationError(
|
||||
{
|
||||
"certificate_content": [
|
||||
f"The provided certificate content is not valid base64 encoded data: {str(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
|
||||
# double-nest the JSON:API pointer as
|
||||
# `/certificate_content/certificate_content`.
|
||||
raise serializers.ValidationError(
|
||||
f"The provided certificate content is not valid base64 encoded data: {str(e)}",
|
||||
code="azure-certificate-content",
|
||||
)
|
||||
return certificate_content
|
||||
@@ -1847,12 +1848,10 @@ class M365ProviderSecret(serializers.Serializer):
|
||||
try:
|
||||
base64.b64decode(certificate_content, validate=True)
|
||||
except Exception as e:
|
||||
raise ValidationError(
|
||||
{
|
||||
"certificate_content": [
|
||||
f"The provided certificate content is not valid base64 encoded data: {str(e)}"
|
||||
]
|
||||
},
|
||||
# DRF field validators are already keyed to `certificate_content`,
|
||||
# so raising a dict here would double-nest the JSON:API pointer.
|
||||
raise serializers.ValidationError(
|
||||
f"The provided certificate content is not valid base64 encoded data: {str(e)}",
|
||||
code="m365-certificate-content",
|
||||
)
|
||||
return certificate_content
|
||||
|
||||
@@ -226,8 +226,8 @@ Use it from the Prowler Cloud **add-provider wizard**:
|
||||
|
||||
1. Pick **Azure**, then choose **Certificate Authentication (Recommended)** in the credential-type selector.
|
||||
2. Click **Deploy to Azure**. The button opens the Azure Portal deployment blade pre-loaded with the template.
|
||||
3. Generate a certificate (see [Certificate Authentication](#certificate-authentication) below) and paste the base64-encoded public certificate and its SHA-1 thumbprint into the Bicep parameters.
|
||||
4. Deploy and, when the stack finishes, copy the `tenantId`, `applicationId`, and `certificateThumbprint` outputs plus the private key back into Prowler.
|
||||
3. Generate a certificate (see [Certificate Authentication](#certificate-authentication) below) and paste the base64-encoded public certificate into the Bicep `certificateBase64` parameter.
|
||||
4. Deploy and, when the stack finishes, copy the `tenantId` and `applicationId` outputs into Prowler along with the base64-encoded PEM bundle (certificate + private key) you generated locally. The `certificateThumbprint` output is informational — use it to reconcile that the deployed keyCredential matches your local certificate.
|
||||
|
||||
<Note>
|
||||
The Deploy to Azure button is not available for sovereign clouds (Azure Government, Azure China, Azure Germany) or personal Microsoft accounts. Follow the manual [Certificate Authentication](#certificate-authentication) or [Service Principal](#service-principal-application-authentication-recommended) instructions instead.
|
||||
@@ -250,12 +250,10 @@ openssl req -x509 -newkey rsa:4096 -keyout prowler.key -out prowler.crt \
|
||||
# Base64 of the public certificate — feeds `certificateBase64` in the Bicep template
|
||||
openssl x509 -in prowler.crt -outform DER | base64 | tr -d '\n'
|
||||
|
||||
# SHA-1 thumbprint — feeds `certificateThumbprint` in the Bicep template
|
||||
openssl x509 -in prowler.crt -noout -fingerprint -sha1 \
|
||||
| cut -d= -f2 | tr -d ':' | tr '[:lower:]' '[:upper:]'
|
||||
|
||||
# Base64 of the private key — paste into Prowler as "Certificate Content"
|
||||
base64 -i prowler.key | tr -d '\n'
|
||||
# Base64 of a PEM bundle containing the certificate AND the private key —
|
||||
# paste into Prowler as "Certificate Content". A key-only file fails.
|
||||
cat prowler.crt prowler.key > prowler-bundle.pem
|
||||
base64 < prowler-bundle.pem | tr -d '\n'
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
@@ -276,7 +274,7 @@ Paste the following into the Certificate Authentication form of the Azure add-pr
|
||||
|
||||
- **Tenant ID** — copy from the deployment outputs.
|
||||
- **Client ID** — copy from the deployment outputs (`applicationId`).
|
||||
- **Certificate Content** — the base64-encoded **private key** (never the public certificate).
|
||||
- **Certificate Content** — the base64-encoded **PEM bundle** (certificate + private key) or PKCS#12 export. The Bicep template only received the public certificate; Prowler needs both parts here to sign the token request.
|
||||
|
||||
### Prowler CLI
|
||||
|
||||
|
||||
@@ -61,8 +61,8 @@ The one-click flow: click **Deploy to Azure** in the wizard and let the Bicep te
|
||||
|
||||
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).
|
||||
3. Generate a certificate locally and fill the Bicep parameters (`certificateBase64` and `certificateThumbprint`). See [Certificate Authentication → Generating the certificate](/user-guide/providers/azure/authentication#generating-the-certificate) for the exact commands.
|
||||
4. When the deployment finishes, copy the `tenantId` and `applicationId` outputs back to Prowler along with the base64-encoded **private key** you generated earlier, and pastes them into the Certificate Content field.
|
||||
3. Generate a certificate locally and paste the base64-encoded public certificate into the Bicep `certificateBase64` parameter. See [Certificate Authentication → Generating the certificate](/user-guide/providers/azure/authentication#generating-the-certificate) for the exact commands.
|
||||
4. When the deployment finishes, copy the `tenantId` and `applicationId` outputs into Prowler along with the base64-encoded **PEM bundle** (certificate + private key) you generated earlier, and paste it into the Certificate Content field.
|
||||
5. Click **Next**, then **Launch Scan**.
|
||||
|
||||
#### Service Principal with Client Secret
|
||||
|
||||
@@ -51,9 +51,13 @@ After editing `prowler-scan.bicep`, run:
|
||||
make build
|
||||
```
|
||||
|
||||
Requires the Bicep CLI (`>= 0.30`, for the Microsoft.Graph extension).
|
||||
Install with `az bicep install` or grab the standalone binary from
|
||||
<https://github.com/Azure/bicep/releases>.
|
||||
Requires the Bicep CLI **>= 0.36.1** (needed by the GA release of the
|
||||
Microsoft Graph Bicep extension, `v1.0.0`). Install with `az bicep install`
|
||||
or grab the standalone binary from
|
||||
<https://github.com/Azure/bicep/releases>. Note: the `apiVersion: v1.0` on
|
||||
`Microsoft.Graph/applications` in the .bicep source refers to the Graph API
|
||||
version, not the extension release channel — the extension version is pinned
|
||||
in `bicepconfig.json`.
|
||||
|
||||
## Generating the certificate
|
||||
|
||||
@@ -74,13 +78,15 @@ openssl req -x509 -newkey rsa:4096 -keyout prowler.key -out prowler.crt \
|
||||
# file through base64 double-encodes it and the deployment will fail.
|
||||
CERT_BASE64=$(openssl x509 -in prowler.crt -outform DER | base64 | tr -d '\n')
|
||||
|
||||
# 3. Base64-encode the private key so you can paste it into Prowler as the
|
||||
# Certificate Private Key field. Keep this value secret. Using `< file`
|
||||
# (not `-i`) works identically on macOS (BSD) and Linux (GNU).
|
||||
CERT_KEY_BASE64=$(base64 < prowler.key | tr -d '\n')
|
||||
# 3. Concatenate the certificate AND the private key into a single PEM
|
||||
# bundle, then base64-encode the whole thing. `azure.identity.CertificateCredential`
|
||||
# needs BOTH parts — a key-only file fails with "No certificate found".
|
||||
# Keep this value secret. Using `< file` (not `-i`) works on macOS and Linux.
|
||||
cat prowler.crt prowler.key > prowler-bundle.pem
|
||||
CERT_BUNDLE_BASE64=$(base64 < prowler-bundle.pem | tr -d '\n')
|
||||
|
||||
echo "certificateBase64 (paste into the Bicep parameter): $CERT_BASE64"
|
||||
echo "private key (paste into Prowler UI, keep secret): $CERT_KEY_BASE64"
|
||||
echo "certificateBase64 (paste into the Bicep parameter): $CERT_BASE64"
|
||||
echo "certificate bundle (paste into Prowler UI, keep secret): $CERT_BUNDLE_BASE64"
|
||||
```
|
||||
|
||||
### Windows (PowerShell)
|
||||
@@ -129,9 +135,10 @@ Paste the following into the Prowler wizard's Certificate Authentication form:
|
||||
|
||||
- **Tenant ID** — `tenantId` output.
|
||||
- **Client ID** — `applicationId` output.
|
||||
- **Certificate Content** — the base64-encoded **private key** you generated
|
||||
above. This is *not* the certificate you fed into the Bicep template — the
|
||||
public cert is already stored inside Entra ID.
|
||||
- **Certificate Content** — the base64-encoded **PEM bundle** (certificate +
|
||||
private key) or PKCS#12 export you generated above. The Bicep template
|
||||
received only the public certificate; Prowler needs the certificate paired
|
||||
with the private key to sign token requests.
|
||||
|
||||
The `certificateThumbprint` output is informational; use it to verify the
|
||||
deployed keyCredential matches the certificate you generated locally.
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
"extensibility": true
|
||||
},
|
||||
"extensions": {
|
||||
"microsoftGraphV1": "br:mcr.microsoft.com/bicep/extensions/microsoftgraph/v1.0:0.1.8-preview"
|
||||
"microsoftGraphV1": "br:mcr.microsoft.com/bicep/extensions/microsoftgraph/v1.0:1.0.0"
|
||||
}
|
||||
}
|
||||
@@ -39,8 +39,11 @@ param certificateDisplayName string = 'Prowler Certificate'
|
||||
@description('When the certificate becomes valid. Defaults to now.')
|
||||
param certificateStartDateTime string = utcNow()
|
||||
|
||||
@description('When the certificate expires. Defaults to 1 year from now. Rotate before this date.')
|
||||
param certificateEndDateTime string = dateTimeAdd(utcNow(), 'P1Y')
|
||||
// Derived from `certificateStartDateTime` (not a fresh `utcNow()`) so the
|
||||
// pair is always exactly 1 year apart — Microsoft Graph rejects certificates
|
||||
// whose `endDateTime - startDateTime` exceeds the 1-year cap.
|
||||
@description('When the certificate expires. Defaults to 1 year after certificateStartDateTime. Rotate before this date.')
|
||||
param certificateEndDateTime string = dateTimeAdd(certificateStartDateTime, 'P1Y')
|
||||
|
||||
@description('Name of the extra role Prowler creates. Keep the default unless your org already uses this name.')
|
||||
param customRoleName string = 'ProwlerRole'
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"_generator": {
|
||||
"name": "bicep",
|
||||
"version": "0.46.1.21595",
|
||||
"templateHash": "17365420263047938168"
|
||||
"templateHash": "10591927438861815055"
|
||||
}
|
||||
},
|
||||
"parameters": {
|
||||
@@ -39,9 +39,9 @@
|
||||
},
|
||||
"certificateEndDateTime": {
|
||||
"type": "string",
|
||||
"defaultValue": "[dateTimeAdd(utcNow(), 'P1Y')]",
|
||||
"defaultValue": "[dateTimeAdd(parameters('certificateStartDateTime'), 'P1Y')]",
|
||||
"metadata": {
|
||||
"description": "When the certificate expires. Defaults to 1 year from now. Rotate before this date."
|
||||
"description": "When the certificate expires. Defaults to 1 year after certificateStartDateTime. Rotate before this date."
|
||||
}
|
||||
},
|
||||
"customRoleName": {
|
||||
@@ -59,7 +59,7 @@
|
||||
"imports": {
|
||||
"microsoftGraphV1": {
|
||||
"provider": "MicrosoftGraph",
|
||||
"version": "0.1.8-preview"
|
||||
"version": "1.0.0"
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
|
||||
@@ -76,6 +76,36 @@ from prowler.providers.common.provider import Provider
|
||||
_PROWLER_CERT_THUMBPRINT_ATTR = "_prowler_certificate_thumbprint"
|
||||
|
||||
|
||||
def _build_certificate_credential(
|
||||
tenant_id: str,
|
||||
client_id: str,
|
||||
certificate_data: bytes,
|
||||
authority: Optional[str],
|
||||
) -> "CertificateCredential":
|
||||
"""Build a `CertificateCredential` and stash the SHA-1 thumbprint on it.
|
||||
|
||||
Centralises the three-step ritual (construct credential, compute
|
||||
thumbprint, `setattr` it under `_PROWLER_CERT_THUMBPRINT_ATTR`) that
|
||||
`setup_session` and friends would otherwise repeat identically for every
|
||||
cert entry point (static content, static path, env-var cert-auth). Missing
|
||||
the `setattr` in any one call site silently degrades the identity report
|
||||
to "Unknown certificate thumbprint", so keeping it in one place is a real
|
||||
correctness win.
|
||||
"""
|
||||
credentials = CertificateCredential(
|
||||
tenant_id=tenant_id,
|
||||
client_id=client_id,
|
||||
certificate_data=certificate_data,
|
||||
authority=authority,
|
||||
)
|
||||
setattr(
|
||||
credentials,
|
||||
_PROWLER_CERT_THUMBPRINT_ATTR,
|
||||
_compute_certificate_thumbprint(certificate_data),
|
||||
)
|
||||
return credentials
|
||||
|
||||
|
||||
def _compute_certificate_thumbprint(cert_data: bytes) -> Optional[str]:
|
||||
"""Compute the SHA-1 thumbprint of an X.509 certificate.
|
||||
|
||||
@@ -635,36 +665,25 @@ class AzureProvider(Provider):
|
||||
if azure_credentials:
|
||||
try:
|
||||
if azure_credentials.get("certificate_content"):
|
||||
certificate_data = base64.b64decode(
|
||||
azure_credentials["certificate_content"]
|
||||
)
|
||||
credentials = CertificateCredential(
|
||||
credentials = _build_certificate_credential(
|
||||
tenant_id=azure_credentials["tenant_id"],
|
||||
client_id=azure_credentials["client_id"],
|
||||
certificate_data=certificate_data,
|
||||
certificate_data=base64.b64decode(
|
||||
azure_credentials["certificate_content"]
|
||||
),
|
||||
authority=region_config.authority,
|
||||
)
|
||||
setattr(
|
||||
credentials,
|
||||
_PROWLER_CERT_THUMBPRINT_ATTR,
|
||||
_compute_certificate_thumbprint(certificate_data),
|
||||
)
|
||||
elif azure_credentials.get("certificate_path"):
|
||||
with open(
|
||||
azure_credentials["certificate_path"], "rb"
|
||||
) as cert_file:
|
||||
certificate_data = cert_file.read()
|
||||
credentials = CertificateCredential(
|
||||
credentials = _build_certificate_credential(
|
||||
tenant_id=azure_credentials["tenant_id"],
|
||||
client_id=azure_credentials["client_id"],
|
||||
certificate_data=certificate_data,
|
||||
authority=region_config.authority,
|
||||
)
|
||||
setattr(
|
||||
credentials,
|
||||
_PROWLER_CERT_THUMBPRINT_ATTR,
|
||||
_compute_certificate_thumbprint(certificate_data),
|
||||
)
|
||||
else:
|
||||
credentials = ClientSecretCredential(
|
||||
tenant_id=azure_credentials["tenant_id"],
|
||||
@@ -703,17 +722,12 @@ class AzureProvider(Provider):
|
||||
certificate_data = base64.b64decode(
|
||||
getenv("AZURE_CERTIFICATE_CONTENT")
|
||||
)
|
||||
credentials = CertificateCredential(
|
||||
credentials = _build_certificate_credential(
|
||||
tenant_id=getenv("AZURE_TENANT_ID"),
|
||||
client_id=getenv("AZURE_CLIENT_ID"),
|
||||
certificate_data=certificate_data,
|
||||
authority=region_config.authority,
|
||||
)
|
||||
setattr(
|
||||
credentials,
|
||||
_PROWLER_CERT_THUMBPRINT_ATTR,
|
||||
_compute_certificate_thumbprint(certificate_data),
|
||||
)
|
||||
return credentials
|
||||
except ClientAuthenticationError as error:
|
||||
logger.error(
|
||||
@@ -1174,9 +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
|
||||
) or getattr(credentials, "_client_id", "")
|
||||
)
|
||||
identity.identity_type = "Service Principal with Certificate"
|
||||
# The SHA-1 thumbprint is computed from the certificate
|
||||
# bytes by `_compute_certificate_thumbprint` at
|
||||
@@ -1600,7 +1620,13 @@ class AzureProvider(Provider):
|
||||
"client_secret": client_secret,
|
||||
"scope": region_config.graph_scope,
|
||||
}
|
||||
response = requests.post(url, headers=headers, data=data).json()
|
||||
# Hard timeout so a hung Entra ID endpoint cannot stall the
|
||||
# calling worker (this runs on request threads and Celery tasks
|
||||
# in the API path). 30s covers the p99 of the token endpoint
|
||||
# comfortably.
|
||||
response = requests.post(
|
||||
url, headers=headers, data=data, timeout=30
|
||||
).json()
|
||||
if (
|
||||
"access_token" not in response.keys()
|
||||
and "error_codes" in response.keys()
|
||||
|
||||
@@ -16,13 +16,6 @@ class AzureIdentityInfo(BaseModel):
|
||||
locations: dict = {}
|
||||
|
||||
|
||||
class AzureCredentials(BaseModel):
|
||||
client_id: str = ""
|
||||
client_secret: Optional[str] = None
|
||||
tenant_id: str = ""
|
||||
certificate_content: Optional[str] = None
|
||||
|
||||
|
||||
class AzureRegionConfig(BaseModel):
|
||||
name: str = ""
|
||||
authority: Optional[str] = None
|
||||
|
||||
@@ -1401,6 +1401,80 @@ class TestAzureProviderCertificateAuth:
|
||||
assert kwargs["tenant_id"] == self._TENANT_ID
|
||||
assert kwargs["client_id"] == self._CLIENT_ID
|
||||
|
||||
def test_setup_session_attaches_computed_thumbprint(self):
|
||||
# Regression guard for the thumbprint handoff added in PROWLER-2378.
|
||||
# The earlier `test_setup_session_static_credentials_cert_content_...`
|
||||
# test patches `CertificateCredential` with a `MagicMock` which
|
||||
# silently accepts any `setattr`, so even a missing hand-off would
|
||||
# look green. This test uses a REAL certificate so
|
||||
# `_compute_certificate_thumbprint` returns a real value and asserts
|
||||
# the credential object actually carries it under
|
||||
# `_PROWLER_CERT_THUMBPRINT_ATTR` — the sole reason that attribute
|
||||
# exists is `setup_identity`'s cert-branch reading it.
|
||||
import base64
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.hazmat.primitives.serialization import pkcs12
|
||||
from cryptography.x509.oid import NameOID
|
||||
|
||||
from prowler.providers.azure.azure_provider import (
|
||||
_PROWLER_CERT_THUMBPRINT_ATTR,
|
||||
)
|
||||
|
||||
key = rsa.generate_private_key(
|
||||
public_exponent=65537, key_size=2048, backend=default_backend()
|
||||
)
|
||||
subject = issuer = x509.Name(
|
||||
[x509.NameAttribute(NameOID.COMMON_NAME, "prowler-test")]
|
||||
)
|
||||
cert = (
|
||||
x509.CertificateBuilder()
|
||||
.subject_name(subject)
|
||||
.issuer_name(issuer)
|
||||
.public_key(key.public_key())
|
||||
.serial_number(x509.random_serial_number())
|
||||
.not_valid_before(datetime.now(timezone.utc))
|
||||
.not_valid_after(datetime.now(timezone.utc) + timedelta(days=1))
|
||||
.sign(key, hashes.SHA256(), default_backend())
|
||||
)
|
||||
expected_thumbprint = cert.fingerprint(hashes.SHA1()).hex().upper()
|
||||
# azure-identity accepts PKCS#12; encode it so the dict looks like
|
||||
# what the API layer stores after the serializer runs.
|
||||
pfx = pkcs12.serialize_key_and_certificates(
|
||||
name=b"prowler",
|
||||
key=key,
|
||||
cert=cert,
|
||||
cas=None,
|
||||
encryption_algorithm=serialization.NoEncryption(),
|
||||
)
|
||||
cert_content_b64 = base64.b64encode(pfx).decode("ascii")
|
||||
|
||||
credentials = AzureProvider.setup_session(
|
||||
az_cli_auth=False,
|
||||
sp_env_auth=False,
|
||||
browser_auth=False,
|
||||
managed_identity_auth=False,
|
||||
certificate_auth=False,
|
||||
certificate_path=None,
|
||||
tenant_id=self._TENANT_ID,
|
||||
azure_credentials={
|
||||
"tenant_id": self._TENANT_ID,
|
||||
"client_id": self._CLIENT_ID,
|
||||
"client_secret": None,
|
||||
"certificate_content": cert_content_b64,
|
||||
"certificate_path": None,
|
||||
},
|
||||
region_config=self._region_config(),
|
||||
)
|
||||
assert (
|
||||
getattr(credentials, _PROWLER_CERT_THUMBPRINT_ATTR, None)
|
||||
== expected_thumbprint
|
||||
)
|
||||
|
||||
def test_setup_session_static_credentials_client_secret_still_works(self):
|
||||
# Regression guard: adding cert branches to setup_session must not
|
||||
# break the pre-existing client-secret static credentials flow.
|
||||
|
||||
+5
-4
@@ -99,10 +99,11 @@ export const AzureCertificateCredentialsForm = ({
|
||||
</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.
|
||||
<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.
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
+9
-1
@@ -6,8 +6,16 @@ import { WizardRadioCard } from "@/components/providers/workflow/forms/fields";
|
||||
import { FormMessage } from "@/components/shadcn/form";
|
||||
import { RadioGroup } from "@/components/shadcn/radio-group/radio-group";
|
||||
|
||||
// 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" | "";
|
||||
};
|
||||
|
||||
type RadioGroupAzureViaCredentialsFormProps = {
|
||||
control: Control<any>;
|
||||
control: Control<AzureCredentialsTypeFormValues>;
|
||||
isInvalid: boolean;
|
||||
errorMessage?: string;
|
||||
onChange?: (value: string) => void;
|
||||
|
||||
+9
-3
@@ -5,7 +5,10 @@ import { useForm } from "react-hook-form";
|
||||
|
||||
import { Form } from "@/components/shadcn/form";
|
||||
|
||||
import { RadioGroupAzureViaCredentialsTypeForm } from "./radio-group-azure-via-credentials-type-form";
|
||||
import {
|
||||
AzureCredentialsTypeFormValues,
|
||||
RadioGroupAzureViaCredentialsTypeForm,
|
||||
} from "./radio-group-azure-via-credentials-type-form";
|
||||
|
||||
interface SelectViaAzureProps {
|
||||
initialVia?: string;
|
||||
@@ -17,9 +20,12 @@ export const SelectViaAzure = ({
|
||||
onViaChange,
|
||||
}: SelectViaAzureProps) => {
|
||||
const router = useRouter();
|
||||
const form = useForm({
|
||||
const form = useForm<AzureCredentialsTypeFormValues>({
|
||||
defaultValues: {
|
||||
azureCredentialsType: initialVia || "",
|
||||
azureCredentialsType:
|
||||
initialVia === "app_certificate" || initialVia === "app_client_secret"
|
||||
? initialVia
|
||||
: "",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { webcrypto } from "node:crypto";
|
||||
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
@@ -12,7 +13,6 @@ import {
|
||||
// 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,
|
||||
|
||||
@@ -25,13 +25,6 @@ import {
|
||||
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
|
||||
@@ -104,6 +97,12 @@ export async function generateProwlerCertificate(
|
||||
"Web Crypto API is not available in this browser. Use the openssl or PowerShell instructions from the certificate generation guide instead.",
|
||||
);
|
||||
}
|
||||
// Bind @peculiar/x509 to the browser's native SubtleCrypto here rather
|
||||
// than at module load time: doing it inside the function keeps the check
|
||||
// above authoritative (the crypto object could have been swapped by a
|
||||
// test harness between import and call) and avoids side-effects when
|
||||
// consumers only import the types.
|
||||
cryptoProvider.set(globalThis.crypto);
|
||||
|
||||
const keyPair = (await subtle.generateKey(
|
||||
{
|
||||
|
||||
@@ -417,9 +417,9 @@ export const getAWSOrgDeploymentQuickLink = ({
|
||||
|
||||
// Builds the Azure Portal deep link that opens the "Custom deployment" wizard
|
||||
// pre-loaded with the Prowler Bicep template. The Portal expects
|
||||
// `#create/Microsoft.Template/uri/<double-encoded-template-url>` — the URL
|
||||
// fragment is re-parsed by the Portal SPA which URL-decodes the segment once
|
||||
// more, so the template URL must be percent-encoded exactly once via
|
||||
// `#create/Microsoft.Template/uri/<url-encoded-template-url>` — the URL
|
||||
// fragment is re-parsed by the Portal SPA which URL-decodes the segment
|
||||
// once, so the template URL must be percent-encoded exactly once via
|
||||
// `encodeURIComponent`. This is the Azure analogue of the AWS CloudFormation
|
||||
// quick-create link built by `getAWSOrgDeploymentQuickLink` above.
|
||||
//
|
||||
|
||||
Reference in new issue
Block a user