diff --git a/docs/changelog.md b/docs/changelog.md index 2a2f6deee70..605c6a17b59 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -35,6 +35,8 @@ What's changed since pre-release v1.48.0-B0088: - General improvements: - Updated provider data by @BernieWhite. [#3790](https://github.com/Azure/PSRule.Rules.Azure/issues/3790) + - Added support for Bicep `resourceType` parameter metadata. + [#1474](https://github.com/Azure/PSRule.Rules.Azure/issues/1474) ## v1.48.0-B0088 (pre-release) diff --git a/docs/en/rules/Azure.Template.ParameterStrongType.md b/docs/en/rules/Azure.Template.ParameterStrongType.md index 044114b6be2..b479016d088 100644 --- a/docs/en/rules/Azure.Template.ParameterStrongType.md +++ b/docs/en/rules/Azure.Template.ParameterStrongType.md @@ -15,18 +15,21 @@ Set the parameter value to a value that matches the specified strong type. ## DESCRIPTION Template string parameters can optionally specify a strong type. +Bicep can also emit resource type metadata for string parameters. When parameter files are expanded, if the parameter value does not match the type this rule fails. Support is provided by PSRule for Azure for the following types: - Resource type - Specify a resource type. For example `Microsoft.OperationalInsights/workspaces`. If a resource type is specified the parameter value must be a resource id of that type. + This can be set by using `metadata.strongType` or Bicep `metadata.resourceType`. + For `metadata.resourceType`, any API version after `@` is ignored. - Location - Specify `location` as the strong type. If `location` is specified, the parameter value must be a valid Azure location. ## RECOMMENDATION -Consider updating the parameter value to a value that matches the specifed strong type. +Consider updating the parameter value to a value that matches the specified strong type. ## LINKS diff --git a/src/PSRule.Rules.Azure/Data/Template/TemplateValidator.cs b/src/PSRule.Rules.Azure/Data/Template/TemplateValidator.cs index 55255690989..7a67956ad9e 100644 --- a/src/PSRule.Rules.Azure/Data/Template/TemplateValidator.cs +++ b/src/PSRule.Rules.Azure/Data/Template/TemplateValidator.cs @@ -15,6 +15,7 @@ internal sealed class TemplateValidator { private const string PROPERTY_METADATA = "metadata"; private const string PROPERTY_STRONG_TYPE = "strongType"; + private const string PROPERTY_RESOURCE_TYPE = "resourceType"; private const string STRONG_TYPE_LOCATION = "location"; private const string ISSUE_PARAMETER_STRONG_TYPE = "PSRule.Rules.Azure.Template.ParameterStrongType"; @@ -26,6 +27,7 @@ internal sealed class TemplateValidator private const string RESOURCE_TYPE_RESOURCE_GROUPS = "resourceGroups"; private const string SLASH = "/"; + private const char API_VERSION_SEPARATOR = '@'; private ISet _Locations; @@ -108,14 +110,25 @@ private void ParameterStrongType(IValidationContext context, string parameterNam private static bool TryStrongType(JObject parameter, out string strongType) { strongType = null; - if (parameter.TryGetProperty(PROPERTY_METADATA, out JObject metadata) && - metadata.TryGetProperty(PROPERTY_STRONG_TYPE, out JValue st) && + if (!parameter.TryGetProperty(PROPERTY_METADATA, out JObject metadata)) + return false; + + if (metadata.TryGetProperty(PROPERTY_STRONG_TYPE, out JValue st) && st.Value() is string value) strongType = value; + else if (metadata.TryGetProperty(PROPERTY_RESOURCE_TYPE, out JValue rt) && + rt.Value() is string resourceType) + strongType = ResourceTypeWithoutApiVersion(resourceType); return strongType != null; } + private static string ResourceTypeWithoutApiVersion(string resourceType) + { + var index = resourceType.IndexOf(API_VERSION_SEPARATOR); + return index < 0 ? resourceType : resourceType.Substring(0, index); + } + private void IsValidLocation(IValidationContext context, JObject parameter, string parameterName, object value) { if (!ExpressionHelpers.TryString(value, out var location)) diff --git a/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj b/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj index b0c67b96b31..2c5273a9248 100644 --- a/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj +++ b/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj @@ -173,6 +173,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/tests/PSRule.Rules.Azure.Tests/Template.ResourceType.1.json b/tests/PSRule.Rules.Azure.Tests/Template.ResourceType.1.json new file mode 100644 index 00000000000..79beb7cd398 --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/Template.ResourceType.1.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "storageAccountId": { + "type": "string", + "defaultValue": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.Storage/storageAccounts/store1", + "metadata": { + "resourceType": "Microsoft.Storage/storageAccounts@2021-04-01" + } + }, + "vnetId": { + "type": "string", + "defaultValue": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.Network/virtualNetworks/vnet1", + "metadata": { + "resourceType": "Microsoft.Storage/storageAccounts@2021-04-01" + } + } + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2021-04-01", + "name": "sttest001", + "location": "eastus", + "sku": { + "name": "Standard_LRS" + }, + "kind": "StorageV2" + } + ] +} diff --git a/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs b/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs index fbb19e4f58a..a48a96af6ee 100644 --- a/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs +++ b/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs @@ -368,6 +368,20 @@ public void StrongTypeParameter() Assert.False(actual3["_PSRule"].Value().ContainsKey("issue")); } + [Fact] + public void ResourceTypeParameter() + { + var resources = ProcessTemplate(GetSourcePath("Template.ResourceType.1.json"), null); + Assert.NotNull(resources); + Assert.Equal(2, resources.Length); + + var actual = resources[0]; + Assert.Equal("Microsoft.Resources/deployments", actual["type"].Value()); + Assert.Single(actual["_PSRule"]["issue"].Value()); + Assert.Equal("PSRule.Rules.Azure.Template.ParameterStrongType", actual["_PSRule"]["issue"][0]["type"].Value()); + Assert.Equal("vnetId", actual["_PSRule"]["issue"][0]["name"].Value()); + } + [Fact] public void StrongTypeNestedParameter() {