diff --git a/docs/CHANGELOG-v1.md b/docs/CHANGELOG-v1.md
index 73d69c1b5c..8b75d2883a 100644
--- a/docs/CHANGELOG-v1.md
+++ b/docs/CHANGELOG-v1.md
@@ -46,6 +46,9 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers
- Removed the `If` Premium SKU.
- Added check for Premium SKU.
- Bumped rule set to `2024_06`
+- General improvements:
+ - Added support for `split` and `concat` functions during policy export by @BernieWhite.
+ [#2851](https://github.com/Azure/PSRule.Rules.Azure/issues/2851)
- Engineering:
- Bump xunit to v2.8.1.
[#2892](https://github.com/Azure/PSRule.Rules.Azure/pull/2892)
diff --git a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs
index 5f25de117c..fb369e4e76 100644
--- a/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs
+++ b/src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs
@@ -76,6 +76,11 @@ internal abstract class PolicyAssignmentVisitor : ResourceManagerVisitor
private const string PROPERTY_HASVALUE = "hasValue";
private const string PROPERTY_EMPTY = "empty";
private const string PROPERTY_LENGTH = "length";
+ private const string PROPERTY_CONCAT = "concat";
+ private const string PROPERTY_SPLIT = "split";
+ private const string PROPERTY_STRING = "string";
+ private const string PROPERTY_INTEGER = "integer";
+ private const string PROPERTY_DELIMITER = "delimiter";
private const string EFFECT_DISABLED = "Disabled";
private const string EFFECT_AUDITIFNOTEXISTS = "AuditIfNotExists";
@@ -1291,7 +1296,7 @@ private static JObject VisitValueExpression(PolicyAssignmentContext context, JOb
}
// Handle [field('string')]
- else if (tokens.HasFieldTokens())
+ else if (tokens.HasFieldTokens() && !tokens.HasPolicyRuntimeTokens())
{
condition = VisitFieldTokens(context, condition, tokens);
}
@@ -1331,14 +1336,14 @@ private static JObject VisitFieldTokens(PolicyAssignmentContext context, JObject
else if (tokens.ConsumeFunction(PROPERTY_IF) &&
tokens.TryTokenType(ExpressionTokenType.GroupStart, out _))
{
- var orginal = condition;
+ var original = condition;
// Condition
var leftCondition = VisitFieldTokens(context, new JObject(), tokens);
var rightCondition = ReverseCondition(Clone(leftCondition));
- var leftEvaluation = VisitFieldTokens(context, Clone(orginal), tokens);
- var rightEvaluation = VisitFieldTokens(context, Clone(orginal), tokens);
+ var leftEvaluation = VisitFieldTokens(context, Clone(original), tokens);
+ var rightEvaluation = VisitFieldTokens(context, Clone(original), tokens);
var left = new JObject
{
@@ -1498,6 +1503,8 @@ private static JObject VisitFieldTokens(PolicyAssignmentContext context, JObject
private static JObject VisitRuntimeTokens(PolicyAssignmentContext context, TokenStream tokens)
{
var o = VisitRuntimeToken(context, tokens);
+ if (tokens.Count > 0) throw new NotImplementedException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.PolicyRuntimeTokenNotProcessed, tokens.AsString()));
+
return o == null ? null : new JObject
{
{ DOLLAR, o }
@@ -1524,7 +1531,7 @@ private static JObject VisitRuntimeToken(PolicyAssignmentContext context, TokenS
}
else if (tokens.ConsumeFunction(FUNCTION_CURRENT) && tokens.Skip(ExpressionTokenType.GroupStart))
{
- var fieldTarget = "";
+ var fieldTarget = string.Empty;
if (tokens.TryTokenType(ExpressionTokenType.String, out var current))
{
fieldTarget = current.Content;
@@ -1538,6 +1545,72 @@ private static JObject VisitRuntimeToken(PolicyAssignmentContext context, TokenS
tokens.Skip(ExpressionTokenType.GroupEnd);
return o;
}
+ else if (tokens.ConsumeFunction(PROPERTY_CONCAT) &&
+ tokens.TryTokenType(ExpressionTokenType.GroupStart, out _))
+ {
+ var items = new JArray();
+
+ while (tokens.Current.Type == ExpressionTokenType.Element ||
+ tokens.Current.Type == ExpressionTokenType.String)
+ {
+ var child = VisitRuntimeToken(context, tokens);
+ items.Add(child);
+ }
+ var o = new JObject
+ {
+ [PROPERTY_CONCAT] = items
+ };
+ tokens.TryTokenType(ExpressionTokenType.GroupEnd, out _);
+ return o;
+ }
+ else if (tokens.ConsumeFunction(PROPERTY_SPLIT) &&
+ tokens.TryTokenType(ExpressionTokenType.GroupStart, out _))
+ {
+ var child = VisitRuntimeToken(context, tokens);
+
+ var delimiter = new JArray();
+ if (tokens.ConsumeString(out var d))
+ {
+ delimiter.Add(d);
+ }
+
+ var o = new JObject
+ {
+ { PROPERTY_SPLIT, child },
+ { PROPERTY_DELIMITER, delimiter }
+ };
+ tokens.TryTokenType(ExpressionTokenType.GroupEnd, out _);
+ return o;
+ }
+ else if (tokens.ConsumeFunction(PROPERTY_FIELD) &&
+ tokens.TryTokenType(ExpressionTokenType.GroupStart, out _) &&
+ tokens.ConsumeString(out var field))
+ {
+ field = context.TryPolicyAliasPath(field, out var aliasPath) ? TrimFieldName(context, aliasPath) : field;
+
+ var o = new JObject
+ {
+ { PROPERTY_PATH, field }
+ };
+ tokens.TryTokenType(ExpressionTokenType.GroupEnd, out _);
+ return o;
+ }
+ else if (tokens.ConsumeString(out var s))
+ {
+ var o = new JObject
+ {
+ { PROPERTY_STRING, s }
+ };
+ return o;
+ }
+ else if (tokens.ConsumeInteger(out var i))
+ {
+ var o = new JObject
+ {
+ { PROPERTY_INTEGER, i }
+ };
+ return o;
+ }
return null;
}
diff --git a/src/PSRule.Rules.Azure/Data/Template/ExpressionToken.cs b/src/PSRule.Rules.Azure/Data/Template/ExpressionToken.cs
new file mode 100644
index 0000000000..82db9709a8
--- /dev/null
+++ b/src/PSRule.Rules.Azure/Data/Template/ExpressionToken.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Diagnostics;
+
+namespace PSRule.Rules.Azure.Data.Template
+{
+ ///
+ /// An individual expression token.
+ ///
+ [DebuggerDisplay("Type = {Type}, Content = {Content}")]
+ internal sealed class ExpressionToken
+ {
+ internal readonly ExpressionTokenType Type;
+ internal readonly long Value;
+ internal readonly string Content;
+
+ internal ExpressionToken(ExpressionTokenType type, string content)
+ {
+ Type = type;
+ Content = content;
+ }
+
+ internal ExpressionToken(long value)
+ {
+ Type = ExpressionTokenType.Numeric;
+ Value = value;
+ }
+ }
+}
diff --git a/src/PSRule.Rules.Azure/Data/Template/ExpressionTokenType.cs b/src/PSRule.Rules.Azure/Data/Template/ExpressionTokenType.cs
new file mode 100644
index 0000000000..7afe503854
--- /dev/null
+++ b/src/PSRule.Rules.Azure/Data/Template/ExpressionTokenType.cs
@@ -0,0 +1,57 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace PSRule.Rules.Azure.Data.Template
+{
+ ///
+ /// The available token types used for Azure Resource Manager expressions.
+ ///
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1720:Identifier contains type name", Justification = "Represents standard type.")]
+ public enum ExpressionTokenType : byte
+ {
+ ///
+ /// Null token.
+ ///
+ None,
+
+ ///
+ /// A function name.
+ ///
+ Element,
+
+ ///
+ /// A property .property_name.
+ ///
+ Property,
+
+ ///
+ /// A string literal.
+ ///
+ String,
+
+ ///
+ /// A numeric literal.
+ ///
+ Numeric,
+
+ ///
+ /// Start a grouping '('.
+ ///
+ GroupStart,
+
+ ///
+ /// End a grouping ')'.
+ ///
+ GroupEnd,
+
+ ///
+ /// Start an index '['.
+ ///
+ IndexStart,
+
+ ///
+ /// End an index ']'.
+ ///
+ IndexEnd
+ }
+}
diff --git a/src/PSRule.Rules.Azure/Data/Template/TokenStream.cs b/src/PSRule.Rules.Azure/Data/Template/TokenStream.cs
index 8534ee0a02..ade9910686 100644
--- a/src/PSRule.Rules.Azure/Data/Template/TokenStream.cs
+++ b/src/PSRule.Rules.Azure/Data/Template/TokenStream.cs
@@ -1,224 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-using System;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Linq;
using System.Text;
namespace PSRule.Rules.Azure.Data.Template
{
- ///
- /// The available token types used for Azure Resource Manager expressions.
- ///
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1720:Identifier contains type name", Justification = "Represents standard type.")]
- public enum ExpressionTokenType : byte
- {
- ///
- /// Null token.
- ///
- None,
-
- ///
- /// A function name.
- ///
- Element,
-
- ///
- /// A property .property_name.
- ///
- Property,
-
- ///
- /// A string literal.
- ///
- String,
-
- ///
- /// A numeric literal.
- ///
- Numeric,
-
- ///
- /// Start a grouping '('.
- ///
- GroupStart,
-
- ///
- /// End a grouping ')'.
- ///
- GroupEnd,
-
- ///
- /// Start an index '['.
- ///
- IndexStart,
-
- ///
- /// End an index ']'.
- ///
- IndexEnd
- }
-
- ///
- /// An individual expression token.
- ///
- [DebuggerDisplay("Type = {Type}, Content = {Content}")]
- internal sealed class ExpressionToken
- {
- internal readonly ExpressionTokenType Type;
- internal readonly long Value;
- internal readonly string Content;
-
- internal ExpressionToken(ExpressionTokenType type, string content)
- {
- Type = type;
- Content = content;
- }
-
- internal ExpressionToken(long value)
- {
- Type = ExpressionTokenType.Numeric;
- Value = value;
- }
- }
-
- ///
- /// Add an expression token to a token stream.
- ///
- internal static class TokenStreamExtensions
- {
- internal static void Function(this TokenStream stream, string functionName)
- {
- stream.Add(new ExpressionToken(ExpressionTokenType.Element, functionName));
- }
-
- internal static void Numeric(this TokenStream stream, long value)
- {
- stream.Add(new ExpressionToken(value));
- }
-
- internal static void String(this TokenStream stream, string content)
- {
- stream.Add(new ExpressionToken(ExpressionTokenType.String, content));
- }
-
- internal static void Property(this TokenStream stream, string propertyName)
- {
- stream.Add(new ExpressionToken(ExpressionTokenType.Property, propertyName));
- }
-
- internal static void GroupStart(this TokenStream stream)
- {
- stream.Add(new ExpressionToken(ExpressionTokenType.GroupStart, null));
- }
-
- internal static void GroupEnd(this TokenStream stream)
- {
- stream.Add(new ExpressionToken(ExpressionTokenType.GroupEnd, null));
- }
-
- internal static void IndexStart(this TokenStream stream)
- {
- stream.Add(new ExpressionToken(ExpressionTokenType.IndexStart, null));
- }
-
- internal static void IndexEnd(this TokenStream stream)
- {
- stream.Add(new ExpressionToken(ExpressionTokenType.IndexEnd, null));
- }
-
- internal static bool ConsumeFunction(this TokenStream stream, string name)
- {
- if (stream == null ||
- stream.Count == 0 ||
- stream.Current.Type != ExpressionTokenType.Element ||
- !string.Equals(stream.Current.Content, name, StringComparison.OrdinalIgnoreCase))
- return false;
-
- stream.Pop();
- return true;
- }
-
- internal static bool ConsumePropertyName(this TokenStream stream, string propertyName)
- {
- if (stream == null ||
- stream.Count == 0 ||
- stream.Current.Type != ExpressionTokenType.Property ||
- !string.Equals(stream.Current.Content, propertyName, StringComparison.OrdinalIgnoreCase))
- return false;
-
- stream.Pop();
- return true;
- }
-
- internal static bool ConsumeString(this TokenStream stream, string s)
- {
- if (stream == null ||
- stream.Count == 0 ||
- stream.Current.Type != ExpressionTokenType.String ||
- !string.Equals(stream.Current.Content, s, StringComparison.OrdinalIgnoreCase))
- return false;
-
- stream.Pop();
- return true;
- }
-
- internal static bool ConsumeString(this TokenStream stream, out string s)
- {
- s = null;
- if (stream == null ||
- stream.Count == 0 ||
- stream.Current.Type != ExpressionTokenType.String)
- return false;
-
- s = stream.Current.Content;
- stream.Pop();
- return true;
- }
-
- internal static bool ConsumeInteger(this TokenStream stream, out int? i)
- {
- i = null;
- if (stream == null ||
- stream.Count == 0 ||
- stream.Current.Type != ExpressionTokenType.Numeric)
- return false;
-
- i = (int)stream.Current.Value;
- stream.Pop();
- return true;
- }
-
- internal static bool ConsumeGroup(this TokenStream stream)
- {
- if (stream == null ||
- stream.Count == 0 ||
- stream.Current.Type != ExpressionTokenType.GroupStart)
- return false;
-
- stream.SkipGroup();
- return true;
- }
-
- internal static bool HasFieldTokens(this TokenStream stream)
- {
- return stream.ToArray().Any(t =>
- t.Type == ExpressionTokenType.Element &&
- string.Equals("field", t.Content, StringComparison.OrdinalIgnoreCase)
- );
- }
-
- internal static bool HasPolicyRuntimeTokens(this TokenStream stream)
- {
- return stream.ToArray().Any(t =>
- t.Type == ExpressionTokenType.Element &&
- string.Equals("current", t.Content, StringComparison.OrdinalIgnoreCase)
- );
- }
- }
-
///
/// A stream of template expression tokens.
///
diff --git a/src/PSRule.Rules.Azure/Data/Template/TokenStreamExtensions.cs b/src/PSRule.Rules.Azure/Data/Template/TokenStreamExtensions.cs
new file mode 100644
index 0000000000..4c6e8a2286
--- /dev/null
+++ b/src/PSRule.Rules.Azure/Data/Template/TokenStreamExtensions.cs
@@ -0,0 +1,152 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Linq;
+
+namespace PSRule.Rules.Azure.Data.Template
+{
+ ///
+ /// Add an expression token to a token stream.
+ ///
+ internal static class TokenStreamExtensions
+ {
+ private const string TOKEN_CURRENT = "current";
+ private const string TOKEN_FIELD = "field";
+ private const string TOKEN_CONCAT = "concat";
+ private const string TOKEN_SPLIT = "split";
+
+ internal static void Function(this TokenStream stream, string functionName)
+ {
+ stream.Add(new ExpressionToken(ExpressionTokenType.Element, functionName));
+ }
+
+ internal static void Numeric(this TokenStream stream, long value)
+ {
+ stream.Add(new ExpressionToken(value));
+ }
+
+ internal static void String(this TokenStream stream, string content)
+ {
+ stream.Add(new ExpressionToken(ExpressionTokenType.String, content));
+ }
+
+ internal static void Property(this TokenStream stream, string propertyName)
+ {
+ stream.Add(new ExpressionToken(ExpressionTokenType.Property, propertyName));
+ }
+
+ internal static void GroupStart(this TokenStream stream)
+ {
+ stream.Add(new ExpressionToken(ExpressionTokenType.GroupStart, null));
+ }
+
+ internal static void GroupEnd(this TokenStream stream)
+ {
+ stream.Add(new ExpressionToken(ExpressionTokenType.GroupEnd, null));
+ }
+
+ internal static void IndexStart(this TokenStream stream)
+ {
+ stream.Add(new ExpressionToken(ExpressionTokenType.IndexStart, null));
+ }
+
+ internal static void IndexEnd(this TokenStream stream)
+ {
+ stream.Add(new ExpressionToken(ExpressionTokenType.IndexEnd, null));
+ }
+
+ internal static bool ConsumeFunction(this TokenStream stream, string name)
+ {
+ if (stream == null ||
+ stream.Count == 0 ||
+ stream.Current.Type != ExpressionTokenType.Element ||
+ !string.Equals(stream.Current.Content, name, StringComparison.OrdinalIgnoreCase))
+ return false;
+
+ stream.Pop();
+ return true;
+ }
+
+ internal static bool ConsumePropertyName(this TokenStream stream, string propertyName)
+ {
+ if (stream == null ||
+ stream.Count == 0 ||
+ stream.Current.Type != ExpressionTokenType.Property ||
+ !string.Equals(stream.Current.Content, propertyName, StringComparison.OrdinalIgnoreCase))
+ return false;
+
+ stream.Pop();
+ return true;
+ }
+
+ internal static bool ConsumeString(this TokenStream stream, string s)
+ {
+ if (stream == null ||
+ stream.Count == 0 ||
+ stream.Current.Type != ExpressionTokenType.String ||
+ !string.Equals(stream.Current.Content, s, StringComparison.OrdinalIgnoreCase))
+ return false;
+
+ stream.Pop();
+ return true;
+ }
+
+ internal static bool ConsumeString(this TokenStream stream, out string s)
+ {
+ s = null;
+ if (stream == null ||
+ stream.Count == 0 ||
+ stream.Current.Type != ExpressionTokenType.String)
+ return false;
+
+ s = stream.Current.Content;
+ stream.Pop();
+ return true;
+ }
+
+ internal static bool ConsumeInteger(this TokenStream stream, out int? i)
+ {
+ i = null;
+ if (stream == null ||
+ stream.Count == 0 ||
+ stream.Current.Type != ExpressionTokenType.Numeric)
+ return false;
+
+ i = (int)stream.Current.Value;
+ stream.Pop();
+ return true;
+ }
+
+ internal static bool ConsumeGroup(this TokenStream stream)
+ {
+ if (stream == null ||
+ stream.Count == 0 ||
+ stream.Current.Type != ExpressionTokenType.GroupStart)
+ return false;
+
+ stream.SkipGroup();
+ return true;
+ }
+
+ internal static bool HasFieldTokens(this TokenStream stream)
+ {
+ return stream.ToArray().Any(t =>
+ t.Type == ExpressionTokenType.Element &&
+ string.Equals(TOKEN_FIELD, t.Content, StringComparison.OrdinalIgnoreCase)
+ );
+ }
+
+ internal static bool HasPolicyRuntimeTokens(this TokenStream stream)
+ {
+ return stream.ToArray().Any(t =>
+ t.Type == ExpressionTokenType.Element &&
+ (
+ string.Equals(TOKEN_CURRENT, t.Content, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(TOKEN_CONCAT, t.Content, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(TOKEN_SPLIT, t.Content, StringComparison.OrdinalIgnoreCase)
+ )
+ );
+ }
+ }
+}
diff --git a/src/PSRule.Rules.Azure/Resources/PSRuleResources.Designer.cs b/src/PSRule.Rules.Azure/Resources/PSRuleResources.Designer.cs
index d3080776a0..f6e217c20a 100644
--- a/src/PSRule.Rules.Azure/Resources/PSRuleResources.Designer.cs
+++ b/src/PSRule.Rules.Azure/Resources/PSRuleResources.Designer.cs
@@ -411,6 +411,15 @@ internal static string PolicyIgnoreNotApplicable {
}
}
+ ///
+ /// Looks up a localized string similar to Failed to process all runtime tokens: {0}.
+ ///
+ internal static string PolicyRuntimeTokenNotProcessed {
+ get {
+ return ResourceManager.GetString("PolicyRuntimeTokenNotProcessed", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to The language expression property '{0}' doesn't exist..
///
diff --git a/src/PSRule.Rules.Azure/Resources/PSRuleResources.resx b/src/PSRule.Rules.Azure/Resources/PSRuleResources.resx
index 28b16ed3a8..89dea1fef7 100644
--- a/src/PSRule.Rules.Azure/Resources/PSRuleResources.resx
+++ b/src/PSRule.Rules.Azure/Resources/PSRuleResources.resx
@@ -241,6 +241,9 @@
Policy definition has been ignored because it is not applicable to Infrastructure as Code: {0}
+
+ Failed to process all runtime tokens: {0}
+
The language expression property '{0}' doesn't exist.
Occurs when a property that don't exist is referenced.
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 500ea555dc..721e187474 100644
--- a/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj
+++ b/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj
@@ -50,6 +50,9 @@
PreserveNewest
+
+ PreserveNewest
+
PreserveNewest
diff --git a/tests/PSRule.Rules.Azure.Tests/Policy.assignment.8.json b/tests/PSRule.Rules.Azure.Tests/Policy.assignment.8.json
new file mode 100644
index 0000000000..29a28d65e1
--- /dev/null
+++ b/tests/PSRule.Rules.Azure.Tests/Policy.assignment.8.json
@@ -0,0 +1,93 @@
+[
+ {
+ "Identity": null,
+ "Location": null,
+ "Name": "assignment.8",
+ "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyAssignments/assignment.8",
+ "ResourceName": "assignment.8",
+ "ResourceGroupName": null,
+ "ResourceType": "Microsoft.Authorization/policyAssignments",
+ "SubscriptionId": "00000000-0000-0000-0000-000000000000",
+ "Sku": null,
+ "PolicyAssignmentId": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyAssignments/assignment.8",
+ "Properties": {
+ "Scope": "/subscriptions/00000000-0000-0000-0000-000000000000",
+ "NotScopes": [],
+ "DisplayName": "Prevent cross-subscription private endpoints.",
+ "Description": null,
+ "Metadata": {
+ "assignedBy": "",
+ "parameterScopes": {},
+ "createdBy": "",
+ "createdOn": "",
+ "updatedBy": null,
+ "updatedOn": null
+ },
+ "EnforcementMode": 1,
+ "PolicyDefinitionId": "/providers/Microsoft.Management/managementGroups/mg-01/providers/Microsoft.Authorization/policyDefinitions/00000000-0000-0000-0000-000000000000",
+ "Parameters": {},
+ "NonComplianceMessages": []
+ },
+ "PolicyDefinitions": [
+ {
+ "Name": "00000000-0000-0000-0000-000000000001",
+ "ResourceId": "/providers/Microsoft.Management/managementGroups/mg-01/providers/Microsoft.Authorization/policyDefinitions/00000000-0000-0000-0000-000000000001",
+ "ResourceName": "00000000-0000-0000-0000-000000000001",
+ "ResourceType": "Microsoft.Authorization/policyDefinitions",
+ "SubscriptionId": null,
+ "Properties": {
+ "Description": "Example",
+ "DisplayName": "Prevent cross-subscription private endpoints.",
+ "Metadata": {
+ "version": "0.0.1",
+ "category": "Storage"
+ },
+ "Mode": "Indexed",
+ "Parameters": {
+ "effect": {
+ "type": "String",
+ "metadata": {
+ "description": "Enable or disable the execution of the policy",
+ "displayName": "Effect"
+ },
+ "allowedValues": [
+ "Audit",
+ "Deny",
+ "Disabled"
+ ],
+ "defaultValue": "Deny"
+ }
+ },
+ "PolicyRule": {
+ "if": {
+ "allOf": [
+ {
+ "equals": "Microsoft.Storage/storageAccounts/privateEndpointConnections",
+ "field": "type"
+ },
+ {
+ "anyOf": [
+ {
+ "exists": false,
+ "field": "Microsoft.Storage/storageAccounts/privateEndpointConnections/privateEndpoint.id"
+ },
+ {
+ "notEquals": "[subscription().subscriptionId]",
+ "value": "[split(concat(field('Microsoft.Storage/storageAccounts/privateEndpointConnections/privateEndpoint.id'), '//'), '/')]"
+ }
+ ]
+ }
+ ]
+ },
+ "then": {
+ "effect": "[parameters('effect')]"
+ }
+ },
+ "PolicyType": 1
+ },
+ "PolicyDefinitionId": "/providers/Microsoft.Management/managementGroups/mg-01/providers/Microsoft.Authorization/policyDefinitions/00000000-0000-0000-0000-000000000001"
+ }
+ ],
+ "exemptions": []
+ }
+]
diff --git a/tests/PSRule.Rules.Azure.Tests/PolicyAssignmentVisitorTests.cs b/tests/PSRule.Rules.Azure.Tests/PolicyAssignmentVisitorTests.cs
index 1dfe50f6df..8a2da7a088 100644
--- a/tests/PSRule.Rules.Azure.Tests/PolicyAssignmentVisitorTests.cs
+++ b/tests/PSRule.Rules.Azure.Tests/PolicyAssignmentVisitorTests.cs
@@ -269,7 +269,7 @@ public void Expand_field_with_less()
}
[Fact]
- public void Expand_field_with_length()
+ public void Visit_ShouldComplete_WhenLengthField()
{
var context = new PolicyAssignmentContext(GetContext());
var visitor = new PolicyAssignmentDataExportVisitor();
@@ -293,6 +293,25 @@ public void Expand_field_with_length()
Assert.Equal("{\"anyOf\":[{\"exists\":false,\"field\":\"properties.networkAcls.defaultAction\"},{\"equals\":\"Allow\",\"field\":\"properties.networkAcls.defaultAction\"},{\"exists\":false,\"field\":\"properties.networkAcls.virtualNetworkRules\"},{\"field\":\"properties.networkAcls.virtualNetworkRules\",\"notCount\":0},{\"exists\":false,\"field\":\"properties.networkAcls.ipRules\"},{\"field\":\"properties.networkAcls.ipRules\",\"notCount\":0}]}", actual.Where.ToString(Formatting.None));
}
+ [Fact]
+ public void Visit_ShouldComplete_WhenSplitConcatField()
+ {
+ var context = new PolicyAssignmentContext(GetContext());
+ var visitor = new PolicyAssignmentDataExportVisitor();
+ foreach (var assignment in GetAssignmentData("Policy.assignment.8.json").Where(a => a["Name"].Value() == "assignment.8"))
+ visitor.Visit(context, assignment);
+
+ var definitions = context.GetDefinitions();
+ Assert.NotNull(definitions);
+ Assert.Single(definitions);
+
+ var actual = definitions.FirstOrDefault();
+ Assert.Single(actual.Types);
+ Assert.Equal("Microsoft.Storage/storageAccounts/privateEndpointConnections", actual.Types[0]);
+ var temp = actual.Where.ToString(Formatting.None);
+ Assert.Equal("{\"anyOf\":[{\"exists\":false,\"field\":\"properties.privateEndpoint.id\"},{\"notEquals\":\"ffffffff-ffff-ffff-ffff-ffffffffffff\",\"value\":{\"$\":{\"split\":{\"concat\":[{\"path\":\"properties.privateEndpoint.id\"},{\"string\":\"//\"}]},\"delimiter\":[\"/\"]}}}]}", temp);
+ }
+
#region Helper methods
private static PipelineContext GetContext(PSRuleOption option = null)