Skip to content

Commit

Permalink
Handle JSON property casing differences
Browse files Browse the repository at this point in the history
  • Loading branch information
JanneMattila committed Jan 6, 2024
1 parent 08bb53d commit 54bdf16
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 70 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ As this is just an **experiment**, there are many limitations (list is not even
- `field`, `count`, `in`, `notIn`, `allOf`, `anyOf`, `not`, `equals`, `notEquals`, `greater`, `greaterOrEquals`, `less`, `lessOrEquals` are implemented _at least partially_
- [Aliases](https://learn.microsoft.com/en-us/azure/governance/policy/concepts/definition-structure#aliases) are not implemented
- Only `[*]` array alias is implemented
- `"source": "action"` is not implemented ([info](https://github.com/MicrosoftDocs/azure-docs/issues/5899))

## Usage

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"properties": {
"protocol": "*",
"sourcePortRange": "*",
"destinationPortRange": "23",
"destinationPortRange": "22",
"sourceAddressPrefix": "*",
"destinationAddressPrefix": "10.0.0.4",
"access": "Allow",
Expand Down
53 changes: 27 additions & 26 deletions src/AzurePolicyEvaluator/Evaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,42 +64,42 @@ public EvaluationResult Evaluate(string policy, string test)
return result;
}

if (!policyDocument.RootElement.TryGetProperty(PolicyConstants.Properties.Name, out var policyProperties))
if (!policyDocument.RootElement.TryGetPropertyIgnoreCasing(PolicyConstants.Properties.Name, out var policyProperties))
{
result.Result = EvaluationResultTexts.PolicyDoesNotContainProperties;
_logger.LogError(result.Result);
return result;
}

if (!policyProperties.TryGetProperty(PolicyConstants.Properties.PolicyRule, out var policyRule))
if (!policyProperties.TryGetPropertyIgnoreCasing(PolicyConstants.Properties.PolicyRule, out var policyRule))
{
result.Result = EvaluationResultTexts.PolicyDoesNotContainPolicyRule;
_logger.LogError(result.Result);
return result;
}

if (!policyRule.TryGetProperty(PolicyConstants.Properties.If, out var policyRoot))
if (!policyRule.TryGetPropertyIgnoreCasing(PolicyConstants.Properties.If, out var policyRoot))
{
result.Result = EvaluationResultTexts.PolicyRuleDoesNotContainIf;
_logger.LogError(result.Result);
return result;
}

if (!policyRule.TryGetProperty(PolicyConstants.Then, out var thenElement))
if (!policyRule.TryGetPropertyIgnoreCasing(PolicyConstants.Then, out var thenElement))
{
result.Result = EvaluationResultTexts.PolicyRuleDoesNotContainThen;
_logger.LogError(result.Result);
return result;
}

if (!thenElement.TryGetProperty(PolicyConstants.Effect, out var effectElement))
if (!thenElement.TryGetPropertyIgnoreCasing(PolicyConstants.Effect, out var effectElement))
{
result.Result = EvaluationResultTexts.PolicyRuleDoesNotContainEffect;
_logger.LogError(result.Result);
return result;
}

if (policyProperties.TryGetProperty(PolicyConstants.Parameters.Name, out var parameters))
if (policyProperties.TryGetPropertyIgnoreCasing(PolicyConstants.Parameters.Name, out var parameters))
{
_parameters = ParseParameters(parameters);
var effectParameter = _parameters.FirstOrDefault(o => string.Compare(o.Name, PolicyConstants.Effect, true) == 0);
Expand Down Expand Up @@ -148,7 +148,7 @@ internal List<Parameter> ParseParameters(JsonElement parameters)

_logger.LogDebug("Parsing parameter {Name} of type {Type}", parameter.Name, type);

var hasDefaultValue = parameter.Value.TryGetProperty(PolicyConstants.Parameters.DefaultValue, out var defaultValue);
var hasDefaultValue = parameter.Value.TryGetPropertyIgnoreCasing(PolicyConstants.Parameters.DefaultValue, out var defaultValue);

switch (type)
{
Expand Down Expand Up @@ -210,7 +210,8 @@ internal EvaluationResult ExecuteEvaluation(int level, JsonElement policy, JsonE

if (policy.ValueKind == JsonValueKind.Object)
{
if (policy.TryGetProperty(LogicalOperators.Not, out var notObject))
// TODO: Fix case sensitivity
if (policy.TryGetPropertyIgnoreCasing(LogicalOperators.Not, out var notObject))
{
_logger.LogDebug("'not' started");

Expand All @@ -220,7 +221,7 @@ internal EvaluationResult ExecuteEvaluation(int level, JsonElement policy, JsonE
_logger.LogDebug($"'not' return condition {result.Condition}");
return result;
}
else if (policy.TryGetProperty(LogicalOperators.AnyOf, out var anyOfObject))
else if (policy.TryGetPropertyIgnoreCasing(LogicalOperators.AnyOf, out var anyOfObject))
{
_logger.LogDebug("'anyOf' started");

Expand All @@ -246,7 +247,7 @@ internal EvaluationResult ExecuteEvaluation(int level, JsonElement policy, JsonE
_logger.LogDebug("'anyOf' return condition {Condition}", result.Condition);
return result;
}
else if (policy.TryGetProperty(LogicalOperators.AllOf, out var allOfObject))
else if (policy.TryGetPropertyIgnoreCasing(LogicalOperators.AllOf, out var allOfObject))
{
_logger.LogDebug("'allOf' started");

Expand All @@ -271,21 +272,21 @@ internal EvaluationResult ExecuteEvaluation(int level, JsonElement policy, JsonE
_logger.LogDebug("'allOf' return condition {Condition}", result.Condition);
return result;
}
else if (policy.TryGetProperty(PolicyConstants.Count, out var countObject))
else if (policy.TryGetPropertyIgnoreCasing(PolicyConstants.Count, out var countObject))
{
_logger.LogDebug("'count' started");

// More information:
// https://learn.microsoft.com/en-us/azure/governance/policy/concepts/definition-structure#field-count
if (!countObject.TryGetProperty(PolicyConstants.Field, out var fieldObject))
if (!countObject.TryGetPropertyIgnoreCasing(PolicyConstants.Field, out var fieldObject))
{
var error = $"Count expression must have 'field' child element.";
_logger.LogError(error);
result.Details = error;
return result;
}

var hasWhereProperty = countObject.TryGetProperty(PolicyConstants.Where, out var whereObject);
var hasWhereProperty = countObject.TryGetPropertyIgnoreCasing(PolicyConstants.Where, out var whereObject);

result = ExecuteFieldEvaluation(fieldObject, policy, test);
if (hasWhereProperty)
Expand All @@ -300,7 +301,7 @@ internal EvaluationResult ExecuteEvaluation(int level, JsonElement policy, JsonE
_logger.LogDebug("'count' return condition {Condition} with {Count}", result.Condition, result.Count);
return result;
}
else if (policy.TryGetProperty(PolicyConstants.Field, out var fieldObject))
else if (policy.TryGetPropertyIgnoreCasing(PolicyConstants.Field, out var fieldObject))
{
_logger.LogDebug("'field' started");

Expand All @@ -323,7 +324,7 @@ internal EvaluationResult ExecuteEvaluation(int level, JsonElement policy, JsonE
internal EvaluationResult ExecuteCountEvaluation(JsonElement countObject, EvaluationResult childResult)
{
EvaluationResult result = new();
if (countObject.TryGetProperty(Conditions.Greater, out var greaterElement))
if (countObject.TryGetPropertyIgnoreCasing(Conditions.Greater, out var greaterElement))
{
if (greaterElement.ValueKind != JsonValueKind.Number)
{
Expand All @@ -336,7 +337,7 @@ internal EvaluationResult ExecuteCountEvaluation(JsonElement countObject, Evalua
result.Condition = childResult.Count > greaterValue;
_logger.LogDebug("Child count {Count} \"greater\" {Value} is {Condition}", childResult.Count, greaterValue, result.Condition);
}
else if (countObject.TryGetProperty(Conditions.GreaterOrEquals, out var greaterOrEqualsElement))
else if (countObject.TryGetPropertyIgnoreCasing(Conditions.GreaterOrEquals, out var greaterOrEqualsElement))
{
if (greaterOrEqualsElement.ValueKind != JsonValueKind.Number)
{
Expand All @@ -349,7 +350,7 @@ internal EvaluationResult ExecuteCountEvaluation(JsonElement countObject, Evalua
result.Condition = childResult.Count >= greaterOrEqualsValue;
_logger.LogDebug("Child count {Count} \"greaterOrEquals\" {Value} is {Condition}", childResult.Count, greaterOrEqualsValue, result.Condition);
}
else if (countObject.TryGetProperty(Conditions.Less, out var lessElement))
else if (countObject.TryGetPropertyIgnoreCasing(Conditions.Less, out var lessElement))
{
if (lessElement.ValueKind != JsonValueKind.Number)
{
Expand All @@ -362,7 +363,7 @@ internal EvaluationResult ExecuteCountEvaluation(JsonElement countObject, Evalua
result.Condition = childResult.Count < lessValue;
_logger.LogDebug("Child count {Count} \"less\" {Value} is {Condition}", childResult.Count, lessValue, result.Condition);
}
else if (countObject.TryGetProperty(Conditions.LessOrEquals, out var lessOrEqualsElement))
else if (countObject.TryGetPropertyIgnoreCasing(Conditions.LessOrEquals, out var lessOrEqualsElement))
{
if (lessOrEqualsElement.ValueKind != JsonValueKind.Number)
{
Expand Down Expand Up @@ -423,7 +424,7 @@ internal EvaluationResult ExecuteFieldEvaluation(JsonElement fieldObject, JsonEl
}
else
{
if (!properties.TryGetProperty(propertyName, out var propertyElement))
if (!properties.TryGetPropertyIgnoreCasing(propertyName, out var propertyElement))
{
// No property with the given name exists in the test file.
result.Condition = false;
Expand All @@ -435,7 +436,7 @@ internal EvaluationResult ExecuteFieldEvaluation(JsonElement fieldObject, JsonEl
}
else
{
if (!test.TryGetProperty(propertyName, out var propertyElement))
if (!test.TryGetPropertyIgnoreCasing(propertyName, out var propertyElement))
{
// No property with the given name exists in the test file.
result.Condition = false;
Expand All @@ -461,7 +462,7 @@ internal EvaluationResult ExecuteFieldEvaluation(JsonElement fieldObject, JsonEl
internal EvaluationResult FieldComparison(JsonElement policy, string propertyName, string propertyValue)
{
EvaluationResult result = new();
if (policy.TryGetProperty(Conditions.Equals, out var equalsElement))
if (policy.TryGetPropertyIgnoreCasing(Conditions.Equals, out var equalsElement))
{
var equalsValue = equalsElement.GetString();
ArgumentNullException.ThrowIfNull(equalsValue);
Expand All @@ -470,7 +471,7 @@ internal EvaluationResult FieldComparison(JsonElement policy, string propertyNam
result.Condition = propertyValue == value;
_logger.LogDebug("Property {PropertyName} \"equals\" {EqualsValue} is {Condition}", propertyName, equalsValue, result.Condition);
}
else if (policy.TryGetProperty(Conditions.NotEquals, out var notEqualsElement))
else if (policy.TryGetPropertyIgnoreCasing(Conditions.NotEquals, out var notEqualsElement))
{
var notEqualsValue = notEqualsElement.GetString();
ArgumentNullException.ThrowIfNull(notEqualsValue);
Expand All @@ -480,7 +481,7 @@ internal EvaluationResult FieldComparison(JsonElement policy, string propertyNam

_logger.LogDebug("Property {PropertyName} \"notEquals\" {NotEqualsValue} is {Condition}", propertyName, notEqualsValue, result.Condition);
}
else if (policy.TryGetProperty(Conditions.In, out var inElement))
else if (policy.TryGetPropertyIgnoreCasing(Conditions.In, out var inElement))
{
var inValue = inElement.GetString();
ArgumentNullException.ThrowIfNull(inValue);
Expand All @@ -491,7 +492,7 @@ internal EvaluationResult FieldComparison(JsonElement policy, string propertyNam

_logger.LogDebug("Property {PropertyName} \"in\" {InValue} is {Condition}", propertyName, inValue, result.Condition);
}
else if (policy.TryGetProperty(Conditions.NotIn, out var notInElement))
else if (policy.TryGetPropertyIgnoreCasing(Conditions.NotIn, out var notInElement))
{
var notInValue = notInElement.GetString();
ArgumentNullException.ThrowIfNull(notInValue);
Expand Down Expand Up @@ -552,7 +553,7 @@ internal List<EvaluationResult> ExecuteArrayFieldEvaluation(string propertyName,
{
var results = new List<EvaluationResult>();
var arrayName = propertyName.Substring(0, propertyName.IndexOf(PolicyConstants.ArrayMemberReference));
if (!propertiesElement.TryGetProperty(arrayName, out var arrayPropertyElement))
if (!propertiesElement.TryGetPropertyIgnoreCasing(arrayName, out var arrayPropertyElement))
{
// No array property with the given name exists in the test file.
results.Add(new EvaluationResult
Expand Down Expand Up @@ -606,7 +607,7 @@ internal List<EvaluationResult> ExecuteArrayFieldEvaluation(string propertyName,
string? propertyValue = null;
var properties = item.GetProperty(Properties.Name);

if (!properties.TryGetProperty(nextName, out var propertyElement))
if (!properties.TryGetPropertyIgnoreCasing(nextName, out var propertyElement))
{
// No property with the given name exists in the test file.
results.Add(new EvaluationResult
Expand Down
18 changes: 18 additions & 0 deletions src/AzurePolicyEvaluator/JsonElementExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Text.Json;

namespace AzurePolicyEvaluator;

static class JsonElementExtensions
{
public static bool TryGetPropertyIgnoreCasing(this JsonElement element, string propertyName, out JsonElement value)
{
var members = element.EnumerateObject();
var member = members.FirstOrDefault(p => p.Name.Equals(propertyName, StringComparison.InvariantCultureIgnoreCase));
if (member.Value.ValueKind == JsonValueKind.Undefined)
{
value = default;
return false;
}
return element.TryGetProperty(member.Name, out value);
}
}
94 changes: 51 additions & 43 deletions src/AzurePolicyEvaluator/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,62 +143,70 @@ void PolicyFilesChanged(object sender, FileSystemEventArgs e)

logger.LogInformation($"Policy files changed");

var policyFilename = e.FullPath;
List<string> testFiles = [];
if (Path.GetFileNameWithoutExtension(e.Name) == POLICY_FILE_NAME)
try
{
// Azure Policy file has been changed. Look for test files in sub-folders.
var policyFolder = Directory.GetParent(policyFilename);
ArgumentNullException.ThrowIfNull(policyFolder);
var policyFilename = e.FullPath;
List<string> testFiles = [];
if (Path.GetFileNameWithoutExtension(e.Name) == POLICY_FILE_NAME)
{
// Azure Policy file has been changed. Look for test files in sub-folders.
var policyFolder = Directory.GetParent(policyFilename);
ArgumentNullException.ThrowIfNull(policyFolder);

testFiles = Directory.GetFiles(policyFolder.FullName, $"*.{EXTENSION}", SearchOption.AllDirectories)
.Where(f => Path.GetFileNameWithoutExtension(f) != POLICY_FILE_NAME)
.ToList();
}
else
{
// Test file has been changed. Look for policy file in parent folder.
var testFile = new FileInfo(policyFilename);
testFiles.Add(testFile.FullName);
string? policyFile = null;
testFiles = Directory.GetFiles(policyFolder.FullName, $"*.{EXTENSION}", SearchOption.AllDirectories)
.Where(f => Path.GetFileNameWithoutExtension(f) != POLICY_FILE_NAME)
.ToList();
}
else
{
// Test file has been changed. Look for policy file in parent folder.
var testFile = new FileInfo(policyFilename);
testFiles.Add(testFile.FullName);
string? policyFile = null;

var directory = testFile.Directory;
ArgumentNullException.ThrowIfNull(directory);
var directory = testFile.Directory;
ArgumentNullException.ThrowIfNull(directory);

while (directory.FullName.Length >= rootFolder.Length)
{
logger.LogDebug($"Looking for policy file in {directory.FullName}...");
policyFile = Directory.GetFiles(directory.FullName, $"{POLICY_FILE_NAME}.{EXTENSION}").FirstOrDefault();
if (policyFile != null)
while (directory.FullName.Length >= rootFolder.Length)
{
policyFilename = policyFile;
break;
logger.LogDebug($"Looking for policy file in {directory.FullName}...");
policyFile = Directory.GetFiles(directory.FullName, $"{POLICY_FILE_NAME}.{EXTENSION}").FirstOrDefault();
if (policyFile != null)
{
policyFilename = policyFile;
break;
}
directory = directory.Parent;
ArgumentNullException.ThrowIfNull(directory);
}
directory = directory.Parent;
ArgumentNullException.ThrowIfNull(directory);
}

if (policyFile == null)
{
logger.LogWarning($"Could not find policy file. Test file '{Path.GetFileNameWithoutExtension(testFile.Name)}' has been changed.");
return;
if (policyFile == null)
{
logger.LogWarning($"Could not find policy file. Test file '{Path.GetFileNameWithoutExtension(testFile.Name)}' has been changed.");
return;
}
policyFilename = policyFile;
}
policyFilename = policyFile;
}

logger.LogDebug($"Evaluating policy {Path.GetFileNameWithoutExtension(policyFilename)}...");
var policy = SafeFileRead(policyFilename);
logger.LogDebug($"Evaluating policy {Path.GetFileNameWithoutExtension(policyFilename)}...");
var policy = SafeFileRead(policyFilename);

foreach (var testFile in testFiles)
{
var test = SafeFileRead(testFile);
foreach (var testFile in testFiles)
{
var test = SafeFileRead(testFile);

var evaluator = serviceProvider.GetRequiredService<Evaluator>();
var evaluationResult = evaluator.Evaluate(policy, test);
var evaluator = serviceProvider.GetRequiredService<Evaluator>();
var evaluationResult = evaluator.Evaluate(policy, test);

CreateEvaluationReport(policyFilename, testFile, evaluationResult);
CreateEvaluationReport(policyFilename, testFile, evaluationResult);
}
}
};
catch (Exception ex)
{
logger.LogError(ex, "Error while evaluating policy files.");
}
}

string SafeFileRead(string file)
{
for (int i = 0; i < 10; i++)
Expand Down
Loading

0 comments on commit 54bdf16

Please sign in to comment.