fix(azure): stabilize deployment role assignments

This commit is contained in:
Hugo P.Brito committed 2026-08-21 11:56:04 +01:00
1 parent 0698987007
commit 5f64b34d08
9 files changed
+41 -29

No files matched your search

+3
View File
@@ -62,3 +62,6 @@ jobs:
env:
pnpm_config_ignore_scripts: 'true'
run: git ls-files -z '*.md' | xargs -0 -r pnpm dlx markdownlint-cli@0.45.0 --
- name: Check Azure ARM documentation assets
run: python3 permissions/templates/azure/bicep/sync_docs_template.py --check
@@ -5,7 +5,7 @@
"_generator": {
"name": "bicep",
"version": "0.46.1.21595",
"templateHash": "4092263301802395344"
"templateHash": "16146816613430830317"
}
},
"parameters": {
@@ -19,7 +19,7 @@
"type": "string",
"defaultValue": "Prowler",
"metadata": {
"description": "Cosmetic label used inside the role assignment names so multiple deployments do not collide. Free text; keep the default unless you need to distinguish multiple Prowler deployments."
"description": "Cosmetic label included in the custom role description. Free text; keep the default unless you need to distinguish multiple Prowler deployments."
}
},
"customRoleName": {
@@ -41,7 +41,7 @@
"name": "[variables('customRoleDefinitionName')]",
"properties": {
"roleName": "[parameters('customRoleName')]",
"description": "Role used by Prowler for checks that require Azure actions beyond the built-in Reader role.",
"description": "[format('Role used by {0} for Prowler checks that require Azure actions beyond the built-in Reader role.', parameters('deploymentLabel'))]",
"type": "CustomRole",
"assignableScopes": [
"[subscription().id]"
@@ -62,7 +62,7 @@
{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
"name": "[guid(subscription().id, parameters('servicePrincipalObjectId'), parameters('deploymentLabel'), 'Reader')]",
"name": "[guid(subscription().id, parameters('servicePrincipalObjectId'), variables('readerRoleDefinitionId'))]",
"properties": {
"principalId": "[parameters('servicePrincipalObjectId')]",
"principalType": "ServicePrincipal",
@@ -72,7 +72,7 @@
{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
"name": "[guid(subscription().id, parameters('servicePrincipalObjectId'), parameters('deploymentLabel'), parameters('customRoleName'))]",
"name": "[guid(subscription().id, parameters('servicePrincipalObjectId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('customRoleDefinitionName')))]",
"properties": {
"principalId": "[parameters('servicePrincipalObjectId')]",
"principalType": "ServicePrincipal",
@@ -8,7 +8,7 @@
"_generator": {
"name": "bicep",
"version": "0.46.1.21595",
"templateHash": "4092263301802395344"
"templateHash": "16146816613430830317"
}
},
"parameters": {
@@ -22,7 +22,7 @@
"type": "string",
"defaultValue": "Prowler",
"metadata": {
"description": "Cosmetic label used inside the role assignment names so multiple deployments do not collide. Free text; keep the default unless you need to distinguish multiple Prowler deployments."
"description": "Cosmetic label included in the custom role description. Free text; keep the default unless you need to distinguish multiple Prowler deployments."
}
},
"customRoleName": {
@@ -44,7 +44,7 @@
"name": "[variables('customRoleDefinitionName')]",
"properties": {
"roleName": "[parameters('customRoleName')]",
"description": "Role used by Prowler for checks that require Azure actions beyond the built-in Reader role.",
"description": "[format('Role used by {0} for Prowler checks that require Azure actions beyond the built-in Reader role.', parameters('deploymentLabel'))]",
"type": "CustomRole",
"assignableScopes": [
"[subscription().id]"
@@ -65,7 +65,7 @@
{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
"name": "[guid(subscription().id, parameters('servicePrincipalObjectId'), parameters('deploymentLabel'), 'Reader')]",
"name": "[guid(subscription().id, parameters('servicePrincipalObjectId'), variables('readerRoleDefinitionId'))]",
"properties": {
"principalId": "[parameters('servicePrincipalObjectId')]",
"principalType": "ServicePrincipal",
@@ -75,7 +75,7 @@
{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
"name": "[guid(subscription().id, parameters('servicePrincipalObjectId'), parameters('deploymentLabel'), parameters('customRoleName'))]",
"name": "[guid(subscription().id, parameters('servicePrincipalObjectId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('customRoleDefinitionName')))]",
"properties": {
"principalId": "[parameters('servicePrincipalObjectId')]",
"principalType": "ServicePrincipal",
@@ -267,7 +267,7 @@ $cert = New-SelfSignedCertificate -Subject "CN=Prowler" `
Export-Certificate -Cert $cert -FilePath prowler.cer
# Export a PKCS#12 bundle for Prowler. Keep this value secret.
# Export an unencrypted PKCS#12/PFX bundle for Prowler. Keep this value secret.
$pfxBytes = $cert.Export('Pfx', '')
[Convert]::ToBase64String($pfxBytes)
```
@@ -311,7 +311,7 @@ Paste the following into the Certificate Authentication form of the Azure add-pr
- **Tenant ID** — copy the Directory (tenant) ID from the App Registration overview.
- **Client ID** — copy the Application (client) ID from the App Registration overview.
- **Certificate Content** — provide the base64-encoded **PEM bundle** (certificate + private key) or PKCS#12 export. Prowler needs both parts to sign the token request.
- **Certificate Content** — provide the base64-encoded **PEM bundle** (certificate + private key) or an unencrypted PKCS#12/PFX export. Prowler needs both parts to sign the token request and does not accept password-protected PKCS#12/PFX input.
### Prowler CLI
@@ -325,7 +325,7 @@ export AZURE_CERTIFICATE_CONTENT="<base64-encoded certificate and private key bu
prowler azure --certificate-auth
```
Alternatively, provide a PEM or PKCS#12 file that contains both the certificate and matching private key:
Alternatively, provide a PEM file or an unencrypted PKCS#12/PFX file that contains both the certificate and matching private key. Password-protected PKCS#12/PFX input is not supported:
```console
prowler azure --certificate-auth --certificate-path /path/to/prowler-bundle.pem
+2 -2
View File
@@ -156,8 +156,8 @@ $cert = New-SelfSignedCertificate -Subject "CN=Prowler" `
# Save the .cer to upload in the Portal
Export-Certificate -Cert $cert -FilePath prowler.cer
# Certificate and private key bundle (PKCS#12), base64-encoded — paste into
# Prowler. Keep secret.
# Unencrypted certificate and private key bundle (PKCS#12/PFX), base64-encoded
# for Prowler. Password-protected PKCS#12/PFX input is not supported. Keep secret.
$pfxBytes = $cert.Export('Pfx', '')
$keyBase64 = [Convert]::ToBase64String($pfxBytes)
@@ -38,7 +38,7 @@ targetScope = 'subscription'
@description('Object ID of the Service Principal for the App Registration Prowler will use. Find it in Azure Portal → Microsoft Entra ID → Enterprise applications → your app → Overview → Object ID. This is NOT the same as the App Registration\'s Object ID; the Service Principal has its own separate Object ID.')
param servicePrincipalObjectId string
@description('Cosmetic label used inside the role assignment names so multiple deployments do not collide. Free text; keep the default unless you need to distinguish multiple Prowler deployments.')
@description('Cosmetic label included in the custom role description. Free text; keep the default unless you need to distinguish multiple Prowler deployments.')
param deploymentLabel string = 'Prowler'
@description('Name of the extra role Prowler creates. Keep the default unless your org already uses this name.')
@@ -60,7 +60,7 @@ resource prowlerRole 'Microsoft.Authorization/roleDefinitions@2022-05-01-preview
name: customRoleDefinitionName
properties: {
roleName: customRoleName
description: 'Role used by Prowler for checks that require Azure actions beyond the built-in Reader role.'
description: 'Role used by ${deploymentLabel} for Prowler checks that require Azure actions beyond the built-in Reader role.'
type: 'CustomRole'
assignableScopes: [
subscription().id
@@ -80,7 +80,7 @@ resource prowlerRole 'Microsoft.Authorization/roleDefinitions@2022-05-01-preview
}
resource readerAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(subscription().id, servicePrincipalObjectId, deploymentLabel, 'Reader')
name: guid(subscription().id, servicePrincipalObjectId, readerRoleDefinitionId)
properties: {
principalId: servicePrincipalObjectId
principalType: 'ServicePrincipal'
@@ -89,7 +89,7 @@ resource readerAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' =
}
resource prowlerRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(subscription().id, servicePrincipalObjectId, deploymentLabel, customRoleName)
name: guid(subscription().id, servicePrincipalObjectId, prowlerRole.id)
properties: {
principalId: servicePrincipalObjectId
principalType: 'ServicePrincipal'
@@ -5,7 +5,7 @@
"_generator": {
"name": "bicep",
"version": "0.46.1.21595",
"templateHash": "4092263301802395344"
"templateHash": "16146816613430830317"
}
},
"parameters": {
@@ -19,7 +19,7 @@
"type": "string",
"defaultValue": "Prowler",
"metadata": {
"description": "Cosmetic label used inside the role assignment names so multiple deployments do not collide. Free text; keep the default unless you need to distinguish multiple Prowler deployments."
"description": "Cosmetic label included in the custom role description. Free text; keep the default unless you need to distinguish multiple Prowler deployments."
}
},
"customRoleName": {
@@ -41,7 +41,7 @@
"name": "[variables('customRoleDefinitionName')]",
"properties": {
"roleName": "[parameters('customRoleName')]",
"description": "Role used by Prowler for checks that require Azure actions beyond the built-in Reader role.",
"description": "[format('Role used by {0} for Prowler checks that require Azure actions beyond the built-in Reader role.', parameters('deploymentLabel'))]",
"type": "CustomRole",
"assignableScopes": [
"[subscription().id]"
@@ -62,7 +62,7 @@
{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
"name": "[guid(subscription().id, parameters('servicePrincipalObjectId'), parameters('deploymentLabel'), 'Reader')]",
"name": "[guid(subscription().id, parameters('servicePrincipalObjectId'), variables('readerRoleDefinitionId'))]",
"properties": {
"principalId": "[parameters('servicePrincipalObjectId')]",
"principalType": "ServicePrincipal",
@@ -72,7 +72,7 @@
{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
"name": "[guid(subscription().id, parameters('servicePrincipalObjectId'), parameters('deploymentLabel'), parameters('customRoleName'))]",
"name": "[guid(subscription().id, parameters('servicePrincipalObjectId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('customRoleDefinitionName')))]",
"properties": {
"principalId": "[parameters('servicePrincipalObjectId')]",
"principalType": "ServicePrincipal",
@@ -3,12 +3,9 @@ import json
import sys
from pathlib import Path
REPOSITORY_ROOT = Path(__file__).resolve().parents[4]
DEFAULT_SOURCE = Path(__file__).with_name("prowler-scan.json")
DEFAULT_ASSET = (
REPOSITORY_ROOT / "docs/assets/templates/azure/prowler-scan.json"
)
DEFAULT_ASSET = REPOSITORY_ROOT / "docs/assets/templates/azure/prowler-scan.json"
DEFAULT_SNIPPET = REPOSITORY_ROOT / "docs/snippets/azure-prowler-scan-template.mdx"
SNIPPET_PREFIX = (
b"{/* AUTO-GENERATED from permissions/templates/azure/bicep/"
@@ -1,10 +1,10 @@
import json
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
SCRIPT = Path(__file__).with_name("sync_docs_template.py")
@@ -74,6 +74,18 @@ class SyncDocsTemplateTest(unittest.TestCase):
self.assertEqual(result.returncode, 1)
self.assertIn("Azure documentation template drift detected", result.stderr)
def test_role_assignment_ids_ignore_cosmetic_deployment_label(self):
template = json.loads(Path(__file__).with_name("prowler-scan.json").read_text())
role_assignments = [
resource
for resource in template["resources"]
if resource["type"] == "Microsoft.Authorization/roleAssignments"
]
self.assertEqual(len(role_assignments), 2)
for assignment in role_assignments:
self.assertNotIn("deploymentLabel", assignment["name"])
if __name__ == "__main__":
unittest.main()