Azure / Azure/PSRule.Rules.Azure

[BUG] Expansion fails with "Hash must be finalized before the hash value is retrieved" when using an object output from a cross-scope module

Open
#3,909 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
PowerShell
Stars
447
Forks
109
Avg merge
2d 19h
Merged PRs (30d)
23

Description

### Existing rule

N/A

### Description of the issue

When a Bicep module is deployed to a different scope using the `scope` property (for example a nested management group), the resource ID recorded for the nested deployment does not account for the explicit scope.

`DeploymentVisitor.GetDeploymentScope` resolves the `subscriptionId`, `resourceGroup` and `managementGroup` properties of a deployment resource, but ignores the `scope` property. As a result the nested deployment is registered under its real scope, while the symbol resolves to the parent context scope. These do not match, so `TemplateContext.TryGetResource` fails to find the deployment.

`reference()` then falls back to a synthetic mock, and a chain such as `reference('customRoles').outputs.policyReader.value.id` resolves to an empty value instead of the expected string. When that empty value is passed to `guid()` as the last argument, expansion fails with:

```
Hash must be finalized before the hash value is retrieved
```

There are two defects here:

1. The explicit `scope` property of a deployment resource is not used when calculating the resource ID, so cross-scope module outputs do not resolve.
2. `ExpressionHelpers.GetUnique` only calls `TransformFinalBlock` when the final argument can be converted to a string. If it cannot, the hash is never finalized and reading `HashAlgorithm.Hash` throws. An argument that cannot be converted in any other position is silently skipped instead, which would produce a duplicate name rather than an error.

### Error messages

```
Hash must be finalized before the hash value is retrieved
```

Inner stack trace:

```
at System.Security.Cryptography.HashAlgorithm.get_Hash()
at PSRule.Rules.Azure.Arm.Expressions.ExpressionHelpers.GetUnique(Object[] args)
at PSRule.Rules.Azure.Arm.Expressions.Functions.Guid(ITemplateContext context, Object[] args)
```

### Reproduction

The module deployed with `scope` set to a child management group is required to reproduce the issue. Without `scope` the same template expands correctly.

`main.bicep`:

```bicep
targetScope = 'managementGroup'

resource intermediateRoot 'Microsoft.Management/managementGroups@2023-04-01' = {
scope: tenant()
name: 'mg-intermediate-root'
properties: {
displayName: 'Intermediate Root'
}
}

module customRoles './child.bicep' = {
name: 'customRoles'
scope: intermediateRoot
}

module roleAssignments './assignments.bicep' = {
name: 'roleAssignments'
scope: intermediateRoot
params: {
roleAssignments: [
{
principalId: '00000000-0000-0000-0000-000000000001'
roleDefinitionId: customRoles.outputs.policyReader.id
}
{
principalId: '00000000-0000-0000-0000-000000000002'
roleDefinitionId: customRoles.outputs.policyReader.id
}
]
}
}
```

`child.bicep`:

```bicep
targetScope = 'managementGroup'

resource roleDefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' = {
name: guid('policyReader', managementGroup().id)
properties: {
roleName: 'Policy Reader'
description: 'Read only access to policy.'
type: 'CustomRole'
permissions: [
{
actions: [
'Microsoft.Authorization/policyAssignments/read'
]
}
]
assignableScopes: [
managementGroup().id
]
}
}

output policyReader object = {
id: roleDefinition.id
name: roleDefinition.name
}
```

`assignments.bicep`:

```bicep
targetScope = 'managementGroup'

type roleAssignmentInput = {
principalId: string
roleDefinitionId: string
}

param roleAssignments roleAssignmentInput[]

resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [
for assignment in roleAssignments: {
name: guid(managementGroup().id, assignment.principalId, assignment.roleDefinitionId)
properties: {
principalId: assignment.principalId
principalType: 'ServicePrincipal'
roleDefinitionId: assignment.roleDefinitionId
}
}
]
```

Expansion of `main.bicep` fails. Reducing the name to `guid(assignment.roleDefinitionId)` also fails, which isolates the failure to that argument. Removing `scope: intermediateRoot` from the modules allows expansion to succeed.

The template requires `symbolicNameCodegen` to be enabled in `bicepconfig.json` to produce the `reference('customRoles')` form.

### Version of PSRule

2.9.0

### Version of PSRule for Azure

1.48.0

### Additional context

Note that fixing only the hash finalization is not sufficient, and would be worse than the current behaviour. The unresolved argument would be silently skipped, so every role assignment sharing a principal would collapse to the same `guid()` value and expansion would emit duplicate resource names instead of an error. The scope resolution needs to be fixed so the value resolves correctly.

Contributor guide

Open the contributing guide

Research direction

Start by locating DeploymentVisitor.GetDeploymentScope and ExpressionHelpers.GetUnique, then reproduce the failure with the provided main.bicep, child.bicep, and assignments.bicep files with symbolicNameCodegen enabled. Done means the explicit cross-scope deployment resolves its object output correctly and guid() expands without the hash-finalization error or duplicate resource names.

Written by the indexing model from the issue text.

Assessment

Tech stack
azure, powershell
Domain
cloud, devops, infrastructure
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
62/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.