Skip to content

Commit

Permalink
Merge patch v1.41.2 (#3256)
Browse files Browse the repository at this point in the history
  • Loading branch information
BernieWhite authored Feb 13, 2025
1 parent 145e6c5 commit f147ef1
Show file tree
Hide file tree
Showing 26 changed files with 755 additions and 238 deletions.
26 changes: 12 additions & 14 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,16 +137,6 @@
"webhooks",
"xunit"
],
"cSpell.enabledLanguageIds": [
"csharp",
"git-commit",
"markdown",
"plaintext",
"powershell",
"text",
"yaml",
"yml"
],
"json.schemas": [
{
"fileMatch": [
Expand All @@ -165,9 +155,17 @@
"release/*"
],
"dotnet.completion.showCompletionItemsFromUnimportedNamespaces": true,
"cSpell.enableFiletypes": [
"bicep",
"python"
],
"cSpell.enabledFileTypes": {
"csharp": true,
"git-commit": true,
"markdown": true,
"plaintext": true,
"powershell": true,
"text": true,
"yaml": true,
"yml": true,
"python": true,
"bicep": true,
},
"dotnet.formatting.organizeImportsOnFormat": true
}
14 changes: 13 additions & 1 deletion docs/CHANGELOG-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,24 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers

## Unreleased

What's changed since v1.41.1:
What's changed since v1.41.2:

- General improvements:
- Added a new quickstart guide for using Azure Pipelines with PSRule by @that-ar-guy.
[#3220](https://github.com/Azure/PSRule.Rules.Azure/pull/3220)

## v1.41.2

What's changed since v1.41.1:

- Bug fixes:
- Fixed recursive lookup of cross module resources in the deployment by @BernieWhite.
[#3251](https://github.com/Azure/PSRule.Rules.Azure/issues/3251)
- This improves the ability to reference resource properties in the same parent deployment.
- Additionally, projection of runtime properties has been improved.
- Fixed literal strings may be incorrectly interpreted as expressions by @BernieWhite.
[#3252](https://github.com/Azure/PSRule.Rules.Azure/issues/3252)

## v1.41.1

What's changed since v1.41.0:
Expand Down
36 changes: 34 additions & 2 deletions src/PSRule.Rules.Azure/Common/ResourceHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ internal static bool IsResourceType(string resourceId, string resourceType)
return i == idParts.Length;
}

/// <summary>
/// Determines if the string is a valid resource Id.
/// </summary>
internal static bool IsResourceId(string? s)
{
if (s == null || string.IsNullOrWhiteSpace(s))
return false;

return s == SLASH || TrySubscriptionId(s, out _) || TryManagementGroup(s, out _) || TryTenantResourceProvider(s, out _, out _, out _);
}

/// <summary>
/// Determine if the resource type is a sub-resource of the parent resource Id.
/// </summary>
Expand All @@ -53,7 +64,7 @@ internal static bool IsResourceType(string resourceId, string resourceType)
/// <returns>Returns <c>true</c> if the resource type is a sub-resource. Otherwise <c>false</c> is returned.</returns>
internal static bool IsSubResourceType(string parentId, string resourceType)
{
if (string.IsNullOrEmpty(parentId) || string.IsNullOrEmpty(parentId))
if (string.IsNullOrWhiteSpace(parentId) || string.IsNullOrWhiteSpace(resourceType))
return false;

// Is the resource type has no provider namespace dot it is a sub type.
Expand Down Expand Up @@ -134,6 +145,27 @@ internal static bool TryManagementGroup(string resourceId, out string? managemen
return TryConsumeManagementGroupPart(idParts, ref i, out managementGroup);
}

/// <summary>
/// Get the resource type and name from the specified resource Id from a tenant scope.
/// </summary>
internal static bool TryTenantResourceProvider(string resourceId, out string? provider, out string[]? type, out string[]? name)
{
provider = null;
type = null;
name = null;
if (string.IsNullOrWhiteSpace(resourceId) || resourceId == SLASH)
return false;

var idParts = resourceId.Split(SLASH_C);
var i = 0;
if (TryConsumeTenantPart(idParts, ref i, out _))
{
_ = TryConsumeProviderPart(idParts, ref i, out provider, out type, out name);
return true;
}
return false;
}

/// <summary>
/// Combines Id fragments to form a resource Id.
/// </summary>
Expand Down Expand Up @@ -322,7 +354,7 @@ internal static string ResourceId(string? scopeTenant, string? scopeManagementGr
parts += scopeResourceGroup != null ? 4 : 0;
parts += depth >= resourceTypeLength ? resourceTypeLength * 4 : depth * 4;

// Add additional provider segements.
// Add additional provider segments.
for (var p = 0; resourceType != null && p < resourceType.Length; p++)
{
if (resourceType[p].Contains(DOT))
Expand Down
4 changes: 2 additions & 2 deletions src/PSRule.Rules.Azure/Data/APIM/APIMPolicyReader.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Xml;
using System.Text.RegularExpressions;
using System.IO;
using System.Text.RegularExpressions;
using System.Xml;

namespace PSRule.Rules.Azure.Data.APIM;

Expand Down
40 changes: 31 additions & 9 deletions src/PSRule.Rules.Azure/Data/Template/ExpressionHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ internal static bool IsAnyString(object[] o)
return false;
}

internal static bool TryString(object o, out string value)
internal static bool TryString(object o, out string value, bool allowMocks = true)
{
if (o is string s)
{
Expand All @@ -49,7 +49,7 @@ internal static bool TryString(object o, out string value)
value = token.Value<string>();
return true;
}
else if (o is IMock mock && (mock.BaseType == TypePrimitive.String || mock is IUnknownMock))
else if (allowMocks && o is IMock mock && (mock.BaseType == TypePrimitive.String || mock is IUnknownMock))
{
value = mock.GetValue<string>();
return true;
Expand All @@ -73,7 +73,7 @@ internal static bool TryConvertString(object o, out string value)

internal static bool TryConvertStringArray(object[] o, out string[] value)
{
value = Array.Empty<string>();
value = [];
if (o == null || o.Length == 0 || !TryConvertString(o[0], out var s))
return false;

Expand Down Expand Up @@ -278,6 +278,28 @@ internal static bool Equal(object o1, object o2)
return ObjectEquals(o1, o2);
}

/// <summary>
/// Wrap a literal string that could be interpreted as an expression in a later evaluation.
/// If the input string starts with '[' and ends with ']' it should be escaped.
/// </summary>
internal static object WrapLiteralString(object o)
{
if (!TryString(o, out var s, allowMocks: false)) return o;

return !s.IsExpressionString() ? s : string.Concat('[', s);
}

/// <summary>
/// Remove expression escaping for a literal string if it exists.
/// If the input string starts with '[[' and ends with ']' it should be unescaped by removing the first character.
/// </summary>
internal static object UnwrapLiteralString(object o)
{
if (!TryString(o, out var s, allowMocks: false)) return o;

return s == null || s.Length <= 3 || s[0] != '[' || s[1] != '[' || s[s.Length - 1] != ']' ? s : s.Substring(1);
}

private static bool IsNull(object o)
{
return o == null || (o is JToken token && token.Type == JTokenType.Null);
Expand Down Expand Up @@ -802,9 +824,9 @@ internal static bool TryConvertDateTime(object o, out DateTime value, DateTimeSt
if (TryDateTime(o, out value))
return true;

return TryString(o, out var svalue) &&
(DateTime.TryParseExact(svalue, "yyyyMMddTHHmmssZ", AzureCulture, style, out value) ||
DateTime.TryParse(svalue, AzureCulture, style, out value));
return TryString(o, out var s) &&
(DateTime.TryParseExact(s, "yyyyMMddTHHmmssZ", AzureCulture, style, out value) ||
DateTime.TryParse(s, AzureCulture, style, out value));
}

internal static bool TryJToken(object o, out JToken value)
Expand Down Expand Up @@ -877,11 +899,11 @@ internal static byte[] GetUnique(object[] args)
{
if (GetStringForMock(args[i], out var s) || TryString(args[i], out s))
{
var bvalue = Encoding.UTF8.GetBytes(s);
var b = Encoding.UTF8.GetBytes(s);
if (i == args.Length - 1)
algorithm.TransformFinalBlock(bvalue, 0, bvalue.Length);
algorithm.TransformFinalBlock(b, 0, b.Length);
else
algorithm.TransformBlock(bvalue, 0, bvalue.Length, null, 0);
algorithm.TransformBlock(b, 0, b.Length, null, 0);
}
}
return algorithm.Hash;
Expand Down
54 changes: 30 additions & 24 deletions src/PSRule.Rules.Azure/Data/Template/FunctionDescriptor.cs
Original file line number Diff line number Diff line change
@@ -1,38 +1,44 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Diagnostics;
using System.Threading;
using PSRule.Rules.Azure.Resources;

namespace PSRule.Rules.Azure.Data.Template
namespace PSRule.Rules.Azure.Data.Template;

/// <summary>
/// A wrapper for the function definition that can be invoked.
/// </summary>
[DebuggerDisplay("Function: {Name}")]
internal sealed class FunctionDescriptor(string name, ExpressionFn fn, bool delayBinding = false) : IFunctionDescriptor
{
/// <summary>
/// A wrapper for the function definition that can be invoked.
/// </summary>
[DebuggerDisplay("Function: {Name}")]
internal sealed class FunctionDescriptor : IFunctionDescriptor
private readonly ExpressionFn _Fn = fn;
private readonly bool _DelayBinding = delayBinding;

/// <inheritdoc/>
public string Name { get; } = name;

/// <inheritdoc/>
public object Invoke(ITemplateContext context, DebugSymbol debugSymbol, ExpressionFnOuter[] args)
{
private readonly ExpressionFn _Fn;
private readonly bool _DelayBinding;
var parameters = new object[args.Length];
for (var i = 0; i < args.Length; i++)
parameters[i] = _DelayBinding ? args[i] : ExpressionHelpers.UnwrapLiteralString(args[i](context));

public FunctionDescriptor(string name, ExpressionFn fn, bool delayBinding = false)
context.DebugSymbol = debugSymbol;
try
{
Name = name;
_Fn = fn;
_DelayBinding = delayBinding;
return ExpressionHelpers.WrapLiteralString(_Fn(context, parameters));
}

/// <inheritdoc/>
public string Name { get; }

/// <inheritdoc/>
public object Invoke(ITemplateContext context, DebugSymbol debugSymbol, ExpressionFnOuter[] args)
catch (TemplateFunctionException ex)
{
var parameters = new object[args.Length];
for (var i = 0; i < args.Length; i++)
parameters[i] = _DelayBinding ? args[i] : args[i](context);

context.DebugSymbol = debugSymbol;
return _Fn(context, parameters);
throw new TemplateFunctionException(Name, ex.ErrorType, string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.FunctionGenericError, Name, ex.Message), ex);
}
catch (Exception ex)
{
throw new TemplateFunctionException(Name, FunctionErrorType.Unknown, string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.FunctionGenericError, Name, ex.Message), ex);
}
}
}
12 changes: 11 additions & 1 deletion src/PSRule.Rules.Azure/Data/Template/FunctionErrorType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,18 @@ namespace PSRule.Rules.Azure.Data.Template;
/// </summary>
public enum FunctionErrorType
{
/// <summary>
/// A generic error.
/// </summary>
Unknown,

/// <summary>
/// An error cause by mismatching resource segments.
/// </summary>
MismatchingResourceSegments
MismatchingResourceSegments,

/// <summary>
/// An error caused by failed deserialization.
/// </summary>
DeserializationFailure,
}
32 changes: 26 additions & 6 deletions src/PSRule.Rules.Azure/Data/Template/Functions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ internal static class Functions
private const char SINGLE_QUOTE = '\'';
private const char DOUBLE_QUOTE = '"';

internal static readonly IFunctionDescriptor[] Common = new IFunctionDescriptor[]
{
internal static readonly IFunctionDescriptor[] Common =
[
// Array and object
new FunctionDescriptor("array", Array),
new FunctionDescriptor("concat", Concat),
Expand Down Expand Up @@ -175,7 +175,7 @@ internal static class Functions
new FunctionDescriptor("parseCidr", ParseCidr),
new FunctionDescriptor("cidrSubnet", CidrSubnet),
new FunctionDescriptor("cidrHost", CidrHost),
};
];

/// <summary>
/// Functions specific to Azure Policy.
Expand Down Expand Up @@ -480,7 +480,14 @@ internal static object Json(ITemplateContext context, object[] args)
if (args == null || args.Length != 1 || !ExpressionHelpers.TryString(args[0], out var json))
throw ArgumentsOutOfRange(nameof(Json), args);

return JsonConvert.DeserializeObject(DecodeJsonString(json));
try
{
return JsonConvert.DeserializeObject(DecodeJsonString(json));
}
catch (Exception ex)
{
throw DeserializationFailure(nameof(Json), json, ex);
}
}

private static string DecodeJsonString(string s)
Expand Down Expand Up @@ -1996,7 +2003,7 @@ internal static object Split(ITemplateContext context, object[] args)
string[] delimiter = null;
if (ExpressionHelpers.TryString(args[1], out var single))
{
delimiter = new string[] { single };
delimiter = [single];
}
else if (ExpressionHelpers.TryStringArray(args[1], out var delimiterArray))
{
Expand Down Expand Up @@ -2406,7 +2413,7 @@ private static void GetResourceIdParts(string[] segments, int start, out string[

private static object GetExpression(ITemplateContext context, object o)
{
return o is ExpressionFnOuter fn ? fn(context) : o;
return o is ExpressionFnOuter fn ? ExpressionHelpers.UnwrapLiteralString(fn(context)) : o;
}

#endregion Helper functions
Expand Down Expand Up @@ -2514,6 +2521,19 @@ private static TemplateFunctionException MismatchingResourceSegments(string expr
);
}

/// <summary>
/// Failed to deserialize '{0}'. {1}
/// </summary>
private static TemplateFunctionException DeserializationFailure(string expression, string s, Exception inner)
{
return new TemplateFunctionException(
expression,
FunctionErrorType.DeserializationFailure,
string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.DeserializationFailure, s, inner.Message),
inner
);
}

/// <summary>
/// One or more arguments for '{0}' are null when null was not expected.
/// </summary>
Expand Down
Loading

0 comments on commit f147ef1

Please sign in to comment.