Describe the bug
When building the roles plan for a policy set (initiative) assignment, Build-DeploymentPlans computes the assignment identity's required roles as the union of roleDefinitionIds across every member policy, without considering each member's effective effect. Members whose effect is hardcoded in the initiative to AuditIfNotExists (or Disabled/Audit/Deny — effects that never deploy anything) still contribute their roleDefinitionIds to the assignment's required role assignments.
The result is standing role grants — including data-plane roles — at the assignment scope (often an intermediate-root management group) for capabilities the assignment can structurally never exercise.
Steps to reproduce
- Assign the built-in Microsoft Cloud Security Benchmark v2 initiative (
/providers/Microsoft.Authorization/policySetDefinitions/e3ec7e09-768c-4b64-882c-fcada3772047, definitionVersion: 1.*.*-preview) at a management group via an EPAC assignment file.
- Run
Build-DeploymentPlans.
- Inspect
roles-plan.json.
Actual: the plan adds Azure Event Hubs Data Owner (f526a384-b230-433a-b45c-95f59c4a2dec) for the assignment MI at the MG scope:
{
"scope": "/providers/Microsoft.Management/managementGroups/<root>",
"roleDisplayName": "Azure Event Hubs Data Owner",
"properties": {
"roleDefinitionId": "/providers/Microsoft.Authorization/roleDefinitions/f526a384-b230-433a-b45c-95f59c4a2dec",
"description": "Policy Assignment '.../Deploy-MCSB2-Monitoring': Role Assignment required by Policy, ..."
}
}
Expected: no Event Hubs Data Owner grant. The only members of MCSB v2 that list that role are the 22 "Enable logging by category group for … to Event Hub" policies (Azure Firewalls, App Gateways, VNets, NSGs, Bastions, Load Balancers, Public IPs, ExpressRoute, Front Door/CDN, VPN/P2S gateways, Traffic Manager, Network Managers, …), and the initiative hardcodes every one of them to AuditIfNotExists (literal value in the member's parameters.effect.value, not a parameter reference — verifiable via az policy set-definition show --name e3ec7e09-768c-4b64-882c-fcada3772047). None of these members can ever run a deployment, so the identity never needs the role.
Root cause
In Build-DeploymentPlans.ps1 (module v11.5.5, same on main), the policy-set role map is built by unioning member roles with no effect check:
# Calculate roleDefinitionIds for built-in and inherited PolicySets
$readOnlyPolicySetDefinitions = $deployedPolicyResources.policysetdefinitions.readOnly
foreach ($id in $readOnlyPolicySetDefinitions.Keys) {
$policySetProperties = Get-PolicyResourceProperties -PolicyResource $readOnlyPolicySetDefinitions.$id
$roleIds = @{}
foreach ($policyDefinition in $policySetProperties.policyDefinitions) {
$policyId = $policyDefinition.policyDefinitionId
if ($policyRoleIds.ContainsKey($policyId)) {
$addRoleDefinitionIds = $PolicyRoleIds.$policyId
foreach ($roleDefinitionId in $addRoleDefinitionIds) {
$roleIds[$roleDefinitionId] = "added"
}
}
}
...
}
The same union pattern exists in Build-PolicySetPolicyDefinitionIds.ps1 (repo-declared policy sets) and the Build-Hydration* mirrors. Build-AssignmentDefinitionAtLeaf.ps1 then consumes the pre-computed union verbatim for requiredRoleAssignments.
Note that $policyDefinition in the loop above already carries the member's parameters — including the hardcoded effect literal — so the information needed to filter is in scope at the defect site.
Proposed fix
Skip a member's roleDefinitionIds when its effect can never resolve to DeployIfNotExists, Modify, or Manual:
- the member's
parameters.effect.value is a literal Audit / AuditIfNotExists / Disabled / Deny (the MCSB v2 case), or
- the effect is a parameter reference whose initiative-level parameter has
allowedValues that exclude every deploying effect.
Where the effect is parameterizable and could flip to a deploying effect at assignment time, keep today's conservative union — the over-grant is only provable in the statically-resolvable case, and this keeps the change small and safe. (A fuller fix could resolve effective effects against assignment parameters at leaf-build time, but the static filter removes the concrete harm without a refactor.)
Happy to submit a PR along these lines if the approach is acceptable.
Why this matters
- The grant is a data-plane owner role at MG scope (every Event Hub namespace under the root), held by an identity that can never use it. This is exactly what customer least-privilege / security reviews flag, and it must be explained away on every review.
- Deployment SPs constrained by ABAC conditions on
roleDefinitionIds (the delegation-constraint pattern) must allow-list roles nothing uses, widening what the SP itself may grant.
- MCSB v2 makes this newly visible (large initiative, many audit-pinned members with data-plane roles), and its adoption is spreading through ALZ.
Ecosystem corroboration
Environment
- EnterprisePolicyAsCode 11.5.5 (behavior verified in module source; union code unchanged on
main)
- PowerShell 7.4 / Azure DevOps hosted agents and local
- Repro initiative: built-in MCSB v2
e3ec7e09-768c-4b64-882c-fcada3772047 @ 1.*.*-preview
Describe the bug
When building the roles plan for a policy set (initiative) assignment,
Build-DeploymentPlanscomputes the assignment identity's required roles as the union ofroleDefinitionIdsacross every member policy, without considering each member's effective effect. Members whose effect is hardcoded in the initiative toAuditIfNotExists(orDisabled/Audit/Deny— effects that never deploy anything) still contribute theirroleDefinitionIdsto the assignment's required role assignments.The result is standing role grants — including data-plane roles — at the assignment scope (often an intermediate-root management group) for capabilities the assignment can structurally never exercise.
Steps to reproduce
/providers/Microsoft.Authorization/policySetDefinitions/e3ec7e09-768c-4b64-882c-fcada3772047,definitionVersion: 1.*.*-preview) at a management group via an EPAC assignment file.Build-DeploymentPlans.roles-plan.json.Actual: the plan adds Azure Event Hubs Data Owner (
f526a384-b230-433a-b45c-95f59c4a2dec) for the assignment MI at the MG scope:{ "scope": "/providers/Microsoft.Management/managementGroups/<root>", "roleDisplayName": "Azure Event Hubs Data Owner", "properties": { "roleDefinitionId": "/providers/Microsoft.Authorization/roleDefinitions/f526a384-b230-433a-b45c-95f59c4a2dec", "description": "Policy Assignment '.../Deploy-MCSB2-Monitoring': Role Assignment required by Policy, ..." } }Expected: no Event Hubs Data Owner grant. The only members of MCSB v2 that list that role are the 22 "Enable logging by category group for … to Event Hub" policies (Azure Firewalls, App Gateways, VNets, NSGs, Bastions, Load Balancers, Public IPs, ExpressRoute, Front Door/CDN, VPN/P2S gateways, Traffic Manager, Network Managers, …), and the initiative hardcodes every one of them to
AuditIfNotExists(literal value in the member'sparameters.effect.value, not a parameter reference — verifiable viaaz policy set-definition show --name e3ec7e09-768c-4b64-882c-fcada3772047). None of these members can ever run a deployment, so the identity never needs the role.Root cause
In
Build-DeploymentPlans.ps1(module v11.5.5, same onmain), the policy-set role map is built by unioning member roles with no effect check:The same union pattern exists in
Build-PolicySetPolicyDefinitionIds.ps1(repo-declared policy sets) and theBuild-Hydration*mirrors.Build-AssignmentDefinitionAtLeaf.ps1then consumes the pre-computed union verbatim forrequiredRoleAssignments.Note that
$policyDefinitionin the loop above already carries the member'sparameters— including the hardcoded effect literal — so the information needed to filter is in scope at the defect site.Proposed fix
Skip a member's
roleDefinitionIdswhen its effect can never resolve toDeployIfNotExists,Modify, orManual:parameters.effect.valueis a literalAudit/AuditIfNotExists/Disabled/Deny(the MCSB v2 case), orallowedValuesthat exclude every deploying effect.Where the effect is parameterizable and could flip to a deploying effect at assignment time, keep today's conservative union — the over-grant is only provable in the statically-resolvable case, and this keeps the change small and safe. (A fuller fix could resolve effective effects against assignment parameters at leaf-build time, but the static filter removes the concrete harm without a refactor.)
Happy to submit a PR along these lines if the approach is acceptable.
Why this matters
roleDefinitionIds(the delegation-constraint pattern) must allow-list roles nothing uses, widening what the SP itself may grant.Ecosystem corroboration
Deploy-MCSB2-Monitoringmodule in feat: Add MCSB policy assignment and notScopes support ALZ-Bicep#1139 granting only Cognitive Services OpenAI Contributor, Cognitive Services Contributor, and Azure Connected Machine Resource Administrator — no Event Hubs Data Owner. The curated reference behavior for the same initiative omits the role this union demands.policy_role_assignmentsignoresdefinitionVersionwhen resolving policy set definitions, causing spurious role assignment errors on upstream version bumps Azure-Landing-Zones#4190 — version-resolution rather than effect-filtering, but the same "computed MI role grants for initiative assignments" surface).Environment
main)e3ec7e09-768c-4b64-882c-fcada3772047@1.*.*-preview