diff --git a/docs/CHANGELOG-v1.md b/docs/CHANGELOG-v1.md
index 8e887e6f297..7ccfd53fd5b 100644
--- a/docs/CHANGELOG-v1.md
+++ b/docs/CHANGELOG-v1.md
@@ -34,6 +34,8 @@ What's changed since v1.31.0:
- Fixed additional non-sensitive parameter name patterns by `Azure.Deployment.SecureParameter` by @BernieWhite.
[#2528](https://github.com/Azure/PSRule.Rules.Azure/issues/2528)
- Added support for configuration of the rule by setting `AZURE_DEPLOYMENT_NONSENSITIVE_PARAMETER_NAMES`.
+ - Fixed incorrect handling of expressions with contains with JValue string by @BernieWhite.
+ [#2531](https://github.com/Azure/PSRule.Rules.Azure/issues/2531)
## v1.31.0
diff --git a/src/PSRule.Rules.Azure/Data/Template/Functions.cs b/src/PSRule.Rules.Azure/Data/Template/Functions.cs
index 2a3b8761007..17b1e7d405d 100644
--- a/src/PSRule.Rules.Azure/Data/Template/Functions.cs
+++ b/src/PSRule.Rules.Azure/Data/Template/Functions.cs
@@ -2177,8 +2177,8 @@ private static bool HasChild(object value, object child)
{
if (ExpressionHelpers.TryArray(value, out var array))
return Contains(array, child);
- else if (value is string svalue)
- return svalue.Contains(child.ToString());
+ else if (ExpressionHelpers.TryString(value, out var s))
+ return s.Contains(child.ToString());
else if (value is JObject jObject)
return jObject.ContainsKeyInsensitive(child.ToString());
diff --git a/tests/PSRule.Rules.Azure.Tests/FunctionTests.cs b/tests/PSRule.Rules.Azure.Tests/FunctionTests.cs
index 1b72c205a8c..a1a2ac54529 100644
--- a/tests/PSRule.Rules.Azure.Tests/FunctionTests.cs
+++ b/tests/PSRule.Rules.Azure.Tests/FunctionTests.cs
@@ -97,6 +97,10 @@ public void Contains()
// String
Assert.True((bool)Functions.Contains(context, new object[] { "OneTwoThree", "e" }));
Assert.False((bool)Functions.Contains(context, new object[] { "OneTwoThree", "z" }));
+ Assert.True((bool)Functions.Contains(context, new object[] { "abcd", new JValue("bc") }));
+ Assert.False((bool)Functions.Not(context, new object[] { Functions.Contains(context, new object[] { "abcd", new JValue("bc") }) }));
+ Assert.True((bool)Functions.Contains(context, new object[] { new JValue("abcd"), new JValue("bc") }));
+ Assert.False((bool)Functions.Not(context, new object[] { Functions.Contains(context, new object[] { new JValue("abcd"), new JValue("bc") }) }));
// Object
Assert.True((bool)Functions.Contains(context, new object[] { JObject.Parse("{ \"one\": \"a\", \"two\": \"b\", \"three\": \"c\" }"), "two" }));
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 d9adfaca758..3b402468941 100644
--- a/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj
+++ b/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj
@@ -206,6 +206,9 @@
PreserveNewest
+
+ PreserveNewest
+
PreserveNewest
diff --git a/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs b/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs
index 0d8a38c0417..66daffa889f 100644
--- a/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs
+++ b/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs
@@ -932,6 +932,21 @@ public void MockWellKnownProperties()
Assert.Equal("/subscriptions/ffffffff-ffff-ffff-ffff-ffffffffffff/providers/Microsoft.Web/locations/eastus/managedApis/servicebus", actual["properties"]["api"]["id"].Value());
}
+ [Fact]
+ public void ContainsWithJValue()
+ {
+ var resources = ProcessTemplate(GetSourcePath("Tests.Bicep.30.json"), null, out _);
+ Assert.Equal(3, resources.Length);
+
+ var actual = resources[2];
+ Assert.Equal("Microsoft.ManagedIdentity/userAssignedIdentities", actual["type"].Value());
+ Assert.False(actual["properties"]["doesNotContain"].Value());
+ Assert.True(actual["properties"]["doesContain"].Value());
+ Assert.Equal(1, actual["properties"]["indexOfSubstring"].Value());
+ Assert.Equal("abcd", actual["properties"]["stringToCheck"].Value());
+ Assert.Equal("bc", actual["properties"]["stringToFind"].Value());
+ }
+
#region Helper methods
private static string GetSourcePath(string fileName)
diff --git a/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.30.bicep b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.30.bicep
new file mode 100644
index 00000000000..db32c6acd71
--- /dev/null
+++ b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.30.bicep
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+// Community provided sample from: https://github.com/Azure/PSRule.Rules.Azure/issues/2531
+
+var stringToCheck = 'abcd'
+var stringToFind = 'bc'
+var doesNotContain = !(contains(stringToCheck, stringToFind))
+var doesContain = contains(stringToCheck, stringToFind)
+var indexOfSubstring = indexOf(stringToCheck, stringToFind)
+
+#disable-next-line BCP081 no-deployments-resources
+resource taskDeployment 'Microsoft.Resources/deployments@2020-10-01' = {
+ name: 'name'
+ properties: {
+ mode: 'Incremental'
+ template: {
+ '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#'
+ contentVersion: '1.0.0.0'
+ resources: [
+ {
+ apiVersion: '2019-12-01'
+ type: 'Microsoft.ManagedIdentity/userAssignedIdentities'
+ name: 'test'
+ properties: {
+ doesNotContain: doesNotContain
+ doesContain: doesContain
+ indexOfSubstring: indexOfSubstring
+ stringToCheck: stringToCheck
+ stringToFind: stringToFind
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.30.json b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.30.json
new file mode 100644
index 00000000000..9d470deae38
--- /dev/null
+++ b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.30.json
@@ -0,0 +1,46 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.23.1.45101",
+ "templateHash": "15051303941734638046"
+ }
+ },
+ "variables": {
+ "stringToCheck": "abcd",
+ "stringToFind": "bc",
+ "doesNotContain": "[not(contains(variables('stringToCheck'), variables('stringToFind')))]",
+ "doesContain": "[contains(variables('stringToCheck'), variables('stringToFind'))]",
+ "indexOfSubstring": "[indexOf(variables('stringToCheck'), variables('stringToFind'))]"
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2020-10-01",
+ "name": "name",
+ "properties": {
+ "mode": "Incremental",
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "resources": [
+ {
+ "apiVersion": "2019-12-01",
+ "type": "Microsoft.ManagedIdentity/userAssignedIdentities",
+ "name": "test",
+ "properties": {
+ "doesNotContain": "[variables('doesNotContain')]",
+ "doesContain": "[variables('doesContain')]",
+ "indexOfSubstring": "[variables('indexOfSubstring')]",
+ "stringToCheck": "[variables('stringToCheck')]",
+ "stringToFind": "[variables('stringToFind')]"
+ }
+ }
+ ]
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file