diff --git a/src/PSRule.Rules.Azure/Common/DictionaryExtensions.cs b/src/PSRule.Rules.Azure/Common/DictionaryExtensions.cs deleted file mode 100644 index 19526b129f8..00000000000 --- a/src/PSRule.Rules.Azure/Common/DictionaryExtensions.cs +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.Management.Automation; - -namespace PSRule.Rules.Azure -{ - /// - /// Extension methods for dictionary instances. - /// - internal static class DictionaryExtensions - { - [DebuggerStepThrough] - public static bool TryPopValue(this IDictionary dictionary, string key, out object value) - { - return dictionary.TryGetValue(key, out value) && dictionary.Remove(key); - } - - [DebuggerStepThrough] - public static bool TryPopValue(this IDictionary dictionary, string key, out T value) - { - value = default; - if (dictionary.TryGetValue(key, out var v) && dictionary.Remove(key) && v is T result) - { - value = result; - return true; - } - return false; - } - - public static bool TryPopHashtable(this IDictionary dictionary, string key, out Hashtable value) - { - value = null; - if (dictionary.TryPopValue(key, out var o) && o is Hashtable result) - { - value = result; - return true; - } - if (dictionary.TryPopValue(key, out PSObject pso)) - { - value = pso.ToHashtable(); - return true; - } - - return false; - } - - [DebuggerStepThrough] - public static bool TryGetValue(this IDictionary dictionary, string key, out T value) - { - value = default; - if (dictionary.TryGetValue(key, out var v) && v is T result) - { - value = result; - return true; - } - return false; - } - - [DebuggerStepThrough] - public static bool TryGetValue(this IDictionary dictionary, string key, out object value) - { - value = default; - if (dictionary.Contains(key)) - { - value = dictionary[key]; - return true; - } - return false; - } - - [DebuggerStepThrough] - public static bool TryPopBool(this IDictionary dictionary, string key, out bool value) - { - value = default; - return dictionary.TryGetValue(key, out var v) && dictionary.Remove(key) && bool.TryParse(v.ToString(), out value); - } - - public static bool TryGetString(this IDictionary dictionary, string key, out string value) - { - value = null; - if (!dictionary.TryGetValue(key, out var o)) - return false; - - if (o is string result) - { - value = result; - return true; - } - return false; - } - - public static bool TryGetBool(this IDictionary dictionary, string key, out bool? value) - { - value = null; - if (!dictionary.TryGetValue(key, out var o)) - return false; - - if (o is bool result || (o is string svalue && bool.TryParse(svalue, out result))) - { - value = result; - return true; - } - return false; - } - - public static bool TryGetLong(this IDictionary dictionary, string key, out long? value) - { - value = null; - if (!dictionary.TryGetValue(key, out var o)) - return false; - - if (o is long result || (o is string svalue && long.TryParse(svalue, out result))) - { - value = result; - return true; - } - return false; - } - - /// - /// Add an item to the dictionary if it doesn't already exist in the dictionary. - /// - [DebuggerStepThrough] - public static void AddUnique(this IDictionary dictionary, IEnumerable> values) - { - if (values == null || dictionary == null) - return; - - foreach (var kv in values) - { - if (!dictionary.ContainsKey(kv.Key)) - dictionary.Add(kv.Key, kv.Value); - } - } - } -} diff --git a/src/PSRule.Rules.Azure/Common/EnvironmentHelper.cs b/src/PSRule.Rules.Azure/Common/EnvironmentHelper.cs deleted file mode 100644 index 30a8e308237..00000000000 --- a/src/PSRule.Rules.Azure/Common/EnvironmentHelper.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; - -namespace PSRule.Rules.Azure -{ - internal sealed class EnvironmentHelper - { - public static readonly EnvironmentHelper Default = new(); - - internal bool TryBool(string key, out bool value) - { - value = default; - return TryVariable(key, out var variable) && TryParseBool(variable, out value); - } - - private static bool TryVariable(string key, out string variable) - { - variable = Environment.GetEnvironmentVariable(key); - return variable != null; - } - - private static bool TryParseBool(string variable, out bool value) - { - if (bool.TryParse(variable, out value)) - return true; - - if (int.TryParse(variable, out var ivalue)) - { - value = ivalue > 0; - return true; - } - return false; - } - } -} diff --git a/src/PSRule.Rules.Azure/Common/JsonConverters.cs b/src/PSRule.Rules.Azure/Common/JsonConverters.cs deleted file mode 100644 index 9a56e65fbd7..00000000000 --- a/src/PSRule.Rules.Azure/Common/JsonConverters.cs +++ /dev/null @@ -1,326 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Management.Automation; -using Newtonsoft.Json; -using PSRule.Rules.Azure.Data.Policy; -using PSRule.Rules.Azure.Pipeline; -using PSRule.Rules.Azure.Resources; - -namespace PSRule.Rules.Azure -{ - /// - /// A custom serializer to correctly convert PSObject properties to JSON instead of CLIXML. - /// - internal sealed class PSObjectJsonConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - { - return objectType == typeof(PSObject); - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - if (value is not PSObject obj) - throw new ArgumentException(message: PSRuleResources.SerializeNullPSObject, paramName: nameof(value)); - - if (value is FileSystemInfo fileSystemInfo) - { - WriteFileSystemInfo(writer, fileSystemInfo, serializer); - return; - } - writer.WriteStartObject(); - foreach (var property in obj.Properties) - { - // Ignore properties that are not readable or can cause race condition - if (!property.IsGettable || property.Value is PSDriveInfo || property.Value is ProviderInfo || property.Value is DirectoryInfo) - continue; - - writer.WritePropertyName(property.Name); - serializer.Serialize(writer, property.Value); - } - writer.WriteEndObject(); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - // Create target object based on JObject - var result = existingValue as PSObject ?? new PSObject(); - - // Read tokens - ReadObject(value: result, reader: reader); - return result; - } - - private static void ReadObject(PSObject value, JsonReader reader) - { - if (reader.TokenType != JsonToken.StartObject) - throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed); - - reader.Read(); - string name = null; - - // Read each token - while (reader.TokenType != JsonToken.EndObject) - { - switch (reader.TokenType) - { - case JsonToken.PropertyName: - name = reader.Value.ToString(); - break; - - case JsonToken.StartObject: - var child = new PSObject(); - ReadObject(value: child, reader: reader); - value.Properties.Add(new PSNoteProperty(name: name, value: child)); - break; - - case JsonToken.StartArray: - var items = new List(); - reader.Read(); - - while (reader.TokenType != JsonToken.EndArray) - { - items.Add(ReadValue(reader)); - reader.Read(); - } - - value.Properties.Add(new PSNoteProperty(name: name, value: items.ToArray())); - break; - - default: - value.Properties.Add(new PSNoteProperty(name: name, value: reader.Value)); - break; - } - reader.Read(); - } - } - - private static object ReadValue(JsonReader reader) - { - if (reader.TokenType != JsonToken.StartObject) - return reader.Value; - - var value = new PSObject(); - ReadObject(value, reader); - return value; - } - - private static void WriteFileSystemInfo(JsonWriter writer, FileSystemInfo value, JsonSerializer serializer) - { - serializer.Serialize(writer, value.FullName); - } - } - - /// - /// A custom serializer to convert PSObjects that may or maynot be in a JSON array to an a PSObject array. - /// - internal sealed class PSObjectArrayJsonConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - { - return objectType == typeof(PSObject[]); - } - - public override bool CanWrite => false; - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new NotImplementedException(); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType is not JsonToken.StartObject and not JsonToken.StartArray) - throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed); - - var result = new List(); - var isArray = reader.TokenType == JsonToken.StartArray; - - if (isArray) - reader.Read(); - - while (!isArray || (isArray && reader.TokenType != JsonToken.EndArray)) - { - var value = ReadObject(reader: reader); - result.Add(value); - - // Consume the EndObject token - if (isArray) - { - reader.Read(); - } - } - return result.ToArray(); - } - - private static PSObject ReadObject(JsonReader reader) - { - if (reader.TokenType != JsonToken.StartObject) - throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed); - - reader.Read(); - var result = new PSObject(); - string name = null; - - // Read each token - while (reader.TokenType != JsonToken.EndObject) - { - switch (reader.TokenType) - { - case JsonToken.PropertyName: - name = reader.Value.ToString(); - break; - - case JsonToken.StartObject: - var value = ReadObject(reader: reader); - result.Properties.Add(new PSNoteProperty(name: name, value: value)); - break; - - case JsonToken.StartArray: - var items = ReadArray(reader: reader); - result.Properties.Add(new PSNoteProperty(name: name, value: items)); - - break; - - default: - result.Properties.Add(new PSNoteProperty(name: name, value: reader.Value)); - break; - } - reader.Read(); - } - return result; - } - - private static PSObject[] ReadArray(JsonReader reader) - { - if (reader.TokenType != JsonToken.StartArray) - throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed); - - reader.Read(); - var result = new List(); - - while (reader.TokenType != JsonToken.EndArray) - { - if (reader.TokenType == JsonToken.StartObject) - { - result.Add(ReadObject(reader: reader)); - } - else if (reader.TokenType == JsonToken.StartArray) - { - result.Add(PSObject.AsPSObject(ReadArray(reader))); - } - else - { - result.Add(PSObject.AsPSObject(reader.Value)); - } - reader.Read(); - } - return result.ToArray(); - } - } - - internal sealed class PolicyDefinitionConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - { - return objectType == typeof(PolicyDefinition); - } - - public override bool CanWrite => true; - - public override bool CanRead => false; - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - PolicyJsonRuleMapper.MapRule(writer, serializer, value as PolicyDefinition); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - throw new NotImplementedException(); - } - } - - internal sealed class PolicyBaselineConverter : JsonConverter - { - private const string SYNOPSIS_COMMENT = "Synopsis: "; - private const string PROPERTY_APIVERSION = "apiVersion"; - private const string APIVERSION_VALUE = "github.com/microsoft/PSRule/v1"; - private const string PROPERTY_KIND = "kind"; - private const string KIND_VALUE = "Baseline"; - private const string PROPERTY_METADATA = "metadata"; - private const string PROPERTY_NAME = "name"; - private const string PROPERTY_SPEC = "spec"; - private const string PROPERTY_RULE = "rule"; - private const string PROPERTY_INCLUDE = "include"; - - public override bool CanConvert(Type objectType) - { - throw new NotImplementedException(); - } - - public override bool CanWrite => true; - - public override bool CanRead => false; - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - Map(writer, serializer, value as PolicyBaseline); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - throw new NotImplementedException(); - } - - private static void Map(JsonWriter writer, JsonSerializer serializer, PolicyBaseline baseline) - { - writer.WriteStartObject(); - - // Synopsis - writer.WriteComment(string.Concat(SYNOPSIS_COMMENT, baseline.Description)); - - // Api Version - writer.WritePropertyName(PROPERTY_APIVERSION); - writer.WriteValue(APIVERSION_VALUE); - - // Kind - writer.WritePropertyName(PROPERTY_KIND); - writer.WriteValue(KIND_VALUE); - - // Metadata - writer.WritePropertyName(PROPERTY_METADATA); - writer.WriteStartObject(); - writer.WritePropertyName(PROPERTY_NAME); - writer.WriteValue(baseline.Name); - writer.WriteEndObject(); - - // Spec - writer.WritePropertyName(PROPERTY_SPEC); - writer.WriteStartObject(); - WriteRule(writer, serializer, baseline); - - writer.WriteEndObject(); - } - - private static void WriteRule(JsonWriter writer, JsonSerializer serializer, PolicyBaseline baseline) - { - if (baseline.Include == null || baseline.Include.Length == 0) - return; - - var types = new HashSet(baseline.Include, StringComparer.OrdinalIgnoreCase); - writer.WritePropertyName(PROPERTY_RULE); - writer.WriteStartObject(); - - writer.WritePropertyName(PROPERTY_INCLUDE); - serializer.Serialize(writer, types); - writer.WriteEndObject(); - - writer.WriteEndObject(); - } - } -} diff --git a/src/PSRule.Rules.Azure/Common/JsonExtensions.cs b/src/PSRule.Rules.Azure/Common/JsonExtensions.cs deleted file mode 100644 index 8082c38ba98..00000000000 --- a/src/PSRule.Rules.Azure/Common/JsonExtensions.cs +++ /dev/null @@ -1,638 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using PSRule.Rules.Azure.Data.Template; - -namespace PSRule.Rules.Azure -{ - internal static class JsonExtensions - { - private const string PROPERTY_DEPENDS_ON = "dependsOn"; - private const string PROPERTY_RESOURCES = "resources"; - private const string PROPERTY_NAME = "name"; - private const string PROPERTY_TYPE = "type"; - private const string PROPERTY_FIELD = "field"; - private const string PROPERTY_EXISTING = "existing"; - private const string PROPERTY_IMPORT = "import"; - private const string PROPERTY_SCOPE = "scope"; - private const string PROPERTY_SUBSCRIPTION_ID = "subscriptionId"; - private const string PROPERTY_RESOURCE_GROUP = "resourceGroup"; - - private const string TARGETINFO_KEY = "_PSRule"; - private const string TARGETINFO_SOURCE = "source"; - private const string TARGETINFO_FILE = "file"; - private const string TARGETINFO_LINE = "line"; - private const string TARGETINFO_POSITION = "position"; - private const string TARGETINFO_TYPE = "type"; - private const string TARGETINFO_TYPE_TEMPLATE = "Template"; - private const string TARGETINFO_TYPE_PARAMETER = "Parameter"; - private const string TARGETINFO_ISSUE = "issue"; - private const string TARGETINFO_NAME = "name"; - private const string TARGETINFO_PATH = "path"; - private const string TARGETINFO_MESSAGE = "message"; - - private const string TENANT_SCOPE = "/"; - - private static readonly string[] JSON_PATH_SEPARATOR = ["."]; - - internal static IJsonLineInfo TryLineInfo(this JToken token) - { - if (token == null) - return null; - - var annotation = token.Annotation(); - return annotation ?? (IJsonLineInfo)token; - } - - internal static JToken CloneTemplateJToken(this JToken token) - { - if (token == null) - return null; - - var annotation = token.UseTokenAnnotation(); - var clone = token.DeepClone(); - if (annotation != null) - clone.AddAnnotation(annotation); - - return clone; - } - - internal static void CopyTemplateAnnotationFrom(this JToken token, JToken source) - { - if (token == null || source == null) - return; - - var annotation = source.Annotation(); - if (annotation != null) - token.AddAnnotation(annotation); - } - - internal static bool TryGetResources(this JObject resource, out JObject[] resources) - { - if (!resource.TryGetProperty(PROPERTY_RESOURCES, out var jArray) || jArray.Count == 0) - { - resources = null; - return false; - } - resources = jArray.Values().ToArray(); - return true; - } - - internal static bool TryGetResources(this JObject resource, string type, out JObject[] resources) - { - if (!resource.TryGetProperty(PROPERTY_RESOURCES, out var jArray) || jArray.Count == 0) - { - resources = null; - return false; - } - var results = new List(); - foreach (var item in jArray.Values()) - if (item.PropertyEquals(PROPERTY_TYPE, type)) - results.Add(item); - - resources = results.Count > 0 ? results.ToArray() : null; - return results.Count > 0; - } - - internal static bool TryGetResourcesArray(this JObject resource, out JArray resources) - { - if (resource.TryGetProperty(PROPERTY_RESOURCES, out resources)) - return true; - - return false; - } - - internal static bool PropertyEquals(this JObject o, string propertyName, string value) - { - return o.TryGetProperty(propertyName, out var s) && string.Equals(s, value, StringComparison.OrdinalIgnoreCase); - } - - internal static bool ResourceNameEquals(this JObject o, string name) - { - if (!o.TryGetProperty(PROPERTY_NAME, out var n)) - return false; - - n = n.SplitLastSegment('/'); - return string.Equals(n, name, StringComparison.OrdinalIgnoreCase); - } - - internal static bool ContainsKeyInsensitive(this JObject o, string propertyName) - { - return o.TryGetValue(propertyName, StringComparison.OrdinalIgnoreCase, out _); - } - - /// - /// Determine if the token is a value. - /// - internal static bool HasValue(this JToken o) - { - return o.Type is JTokenType.String or - JTokenType.Integer or - JTokenType.Object or - JTokenType.Array or - JTokenType.Boolean or - JTokenType.Bytes or - JTokenType.Date or - JTokenType.Float or - JTokenType.Guid or - JTokenType.TimeSpan or - JTokenType.Uri; - } - - /// - /// Add items to the array. - /// - /// The to add items to. - /// A set of items to add. - internal static void AddRange(this JArray o, IEnumerable items) - { - foreach (var item in items) - o.Add(item); - } - - /// - /// Add items to the start of the array instead of the end. - /// - /// The to add items to. - /// A set of items to add. - internal static void AddRangeFromStart(this JArray o, IEnumerable items) - { - var counter = 0; - foreach (var item in items) - o.Insert(counter++, item); - } - - internal static IEnumerable GetPeerConditionByField(this JObject o, string field) - { - return o.BeforeSelf().OfType().Where(peer => peer.TryGetProperty(PROPERTY_FIELD, out var peerField) && - string.Equals(field, peerField, StringComparison.OrdinalIgnoreCase)); - } - - internal static bool TryGetProperty(this JObject o, string propertyName, out TValue value) where TValue : JToken - { - value = null; - if (o.TryGetValue(propertyName, StringComparison.OrdinalIgnoreCase, out var v) && v.Type != JTokenType.Null) - { - value = v.Value(); - return value != null; - } - return false; - } - - internal static bool TryGetProperty(this JObject o, string propertyName, out string value) - { - value = null; - if (!TryGetProperty(o, propertyName, out var v)) - return false; - - value = v.Value(); - return true; - } - - internal static bool TryGetProperty(this JProperty property, string propertyName, out string value) - { - value = null; - if (property == null || property.Value.Type != JTokenType.String || !property.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)) - return false; - - value = property.Value.Value(); - return true; - } - - internal static bool TryRenameProperty(this JObject o, string oldName, string newName) - { - var p = o.Property(oldName, StringComparison.OrdinalIgnoreCase); - if (p != null) - { - p.Replace(new JProperty(newName, p.Value)); - return true; - } - return false; - } - - internal static void ReplaceProperty(this JObject o, string propertyName, TValue value) where TValue : JToken - { - var p = o.Property(propertyName, StringComparison.OrdinalIgnoreCase); - if (p != null) - p.Value = value; - } - - internal static void ReplaceProperty(this JObject o, string propertyName, string value) - { - var p = o.Property(propertyName, StringComparison.OrdinalIgnoreCase); - if (p != null) - p.Value = JValue.CreateString(value); - } - - internal static void ReplaceProperty(this JObject o, string propertyName, bool value) - { - var p = o.Property(propertyName, StringComparison.OrdinalIgnoreCase); - if (p != null) - p.Value = new JValue(value); - } - - internal static void ReplaceProperty(this JObject o, string propertyName, int value) - { - var p = o.Property(propertyName, StringComparison.OrdinalIgnoreCase); - if (p != null) - p.Value = new JValue(value); - } - - internal static void RemoveProperty(this JObject o, string propertyName) - { - var p = o.Property(propertyName, StringComparison.OrdinalIgnoreCase); - p?.Remove(); - } - - /// - /// Convert a string property to an integer. - /// - /// The target object with properties. - /// The name of the property to convert. - internal static void ConvertPropertyToInt(this JObject o, string propertyName) - { - if (o.TryStringProperty(propertyName, out var s) && int.TryParse(s, out var value)) - o.ReplaceProperty(propertyName, value); - } - - /// - /// Convert a string property to a boolean. - /// - /// The target object with properties. - /// The name of the property to convert. - internal static void ConvertPropertyToBool(this JObject o, string propertyName) - { - if (o.TryStringProperty(propertyName, out var s) && bool.TryParse(s, out var value)) - o.ReplaceProperty(propertyName, value); - } - - internal static bool TryRenameProperty(this JProperty property, string oldName, string newName) - { - if (property == null || !property.Name.Equals(oldName, StringComparison.OrdinalIgnoreCase)) - return false; - - property.Parent[newName] = property.Value; - property.Remove(); - return true; - } - - internal static bool TryRenameProperty(this JProperty property, string newName) - { - if (property == null || property.Name == newName) - return false; - - property.Parent[newName] = property.Value; - property.Remove(); - return true; - } - - internal static void UseProperty(this JObject o, string propertyName, out TValue value) where TValue : JToken, new() - { - if (!o.TryGetValue(propertyName, StringComparison.OrdinalIgnoreCase, out var v)) - { - value = new TValue(); - o.Add(propertyName, value); - return; - } - value = (TValue)v; - } - - internal static void UseTokenAnnotation(this JToken[] token, string parentPath = null, string propertyPath = null) - { - for (var i = 0; token != null && i < token.Length; i++) - token[i].UseTokenAnnotation(parentPath, string.Concat(propertyPath, '[', i, ']')); - } - - internal static TemplateTokenAnnotation UseTokenAnnotation(this JToken token, string parentPath = null, string propertyPath = null) - { - var annotation = token.Annotation(); - if (annotation != null) - return annotation; - - if (token is IJsonLineInfo lineInfo && lineInfo.HasLineInfo()) - annotation = new TemplateTokenAnnotation(lineInfo.LineNumber, lineInfo.LinePosition, JsonPathJoin(parentPath, propertyPath ?? token.Path)); - else - annotation = new TemplateTokenAnnotation(0, 0, JsonPathJoin(parentPath, propertyPath ?? token.Path)); - - token.AddAnnotation(annotation); - return annotation; - } - - private static string JsonPathJoin(string parent, string child) - { - if (string.IsNullOrEmpty(parent)) - return child; - - return string.IsNullOrEmpty(child) ? parent : string.Concat(parent, ".", child); - } - - /// - /// Get the dependencies for a resource by extracting any identifiers configured on the dependsOn property. - /// - /// The resource object to check. - /// An array of dependencies if set. - /// - internal static bool TryGetDependencies(this JObject resource, out string[] dependencies) - { - dependencies = null; - if (!(resource.ContainsKey(PROPERTY_DEPENDS_ON) && resource[PROPERTY_DEPENDS_ON] is JArray d && d.Count > 0)) - return false; - - dependencies = d.Values().ToArray(); - return true; - } - - internal static string GetResourcePath(this JObject resource, int parentLevel = 0) - { - var annotation = resource.Annotation(); - var path = annotation?.Path ?? resource.Path; - if (parentLevel == 0 || string.IsNullOrEmpty(path)) - return path; - - var parts = path.Split(JSON_PATH_SEPARATOR, StringSplitOptions.None); - return parts.Length > parentLevel ? string.Join(JSON_PATH_SEPARATOR[0], parts, 0, parts.Length - parentLevel) : string.Empty; - } - - internal static void SetTargetInfo(this JObject resource, string templateFile, string parameterFile, string path = null) - { - // Get line information. - var lineInfo = resource.TryLineInfo(); - - // Populate target info. - resource.UseProperty(TARGETINFO_KEY, out JObject targetInfo); - - // Path. - path ??= resource.GetResourcePath(); - targetInfo.Add(TARGETINFO_PATH, path); - - var sources = new JArray(); - - // Template file. - if (!string.IsNullOrEmpty(templateFile)) - { - var source = new JObject - { - [TARGETINFO_FILE] = templateFile, - [TARGETINFO_TYPE] = TARGETINFO_TYPE_TEMPLATE - }; - if (lineInfo.HasLineInfo()) - { - source[TARGETINFO_LINE] = lineInfo.LineNumber; - source[TARGETINFO_POSITION] = lineInfo.LinePosition; - } - sources.Add(source); - } - - // Parameter file. - if (!string.IsNullOrEmpty(parameterFile)) - { - var source = new JObject - { - [TARGETINFO_FILE] = parameterFile, - [TARGETINFO_TYPE] = TARGETINFO_TYPE_PARAMETER - }; - if (lineInfo.HasLineInfo()) - { - source[TARGETINFO_LINE] = 1; - } - sources.Add(source); - } - targetInfo.Add(TARGETINFO_SOURCE, sources); - } - - internal static void SetValidationIssue(this JObject resource, string issueId, string name, string path, string message, params object[] args) - { - // Populate target info - resource.UseProperty(TARGETINFO_KEY, out JObject targetInfo); - - var issues = targetInfo.ContainsKey(TARGETINFO_ISSUE) ? targetInfo.Value(TARGETINFO_ISSUE) : new JArray(); - - // Format message - message = args.Length > 0 ? string.Format(Thread.CurrentThread.CurrentCulture, message, args) : message; - - // Build issue - var issue = new JObject - { - [TARGETINFO_TYPE] = issueId, - [TARGETINFO_NAME] = name, - [TARGETINFO_PATH] = path, - [TARGETINFO_MESSAGE] = message, - }; - issues.Add(issue); - targetInfo[TARGETINFO_ISSUE] = issues; - } - - internal static bool TryArrayProperty(this JObject obj, string propertyName, out JArray propertyValue) - { - propertyValue = null; - if (!obj.TryGetValue(propertyName, StringComparison.OrdinalIgnoreCase, out var value) || value.Type != JTokenType.Array) - return false; - - propertyValue = value.Value(); - return true; - } - - internal static bool TryObjectProperty(this JObject obj, string propertyName, out JObject propertyValue) - { - propertyValue = null; - if (!obj.TryGetValue(propertyName, StringComparison.OrdinalIgnoreCase, out var value) || value.Type != JTokenType.Object) - return false; - - propertyValue = value as JObject; - return true; - } - - internal static bool TryStringProperty(this JObject obj, string propertyName, out string propertyValue) - { - propertyValue = null; - if (!obj.TryGetValue(propertyName, StringComparison.OrdinalIgnoreCase, out var value) || value.Type != JTokenType.String) - return false; - - propertyValue = value.Value(); - return true; - } - - internal static bool TryBoolProperty(this JObject o, string propertyName, out bool? value) - { - value = null; - if (o.TryGetValue(propertyName, StringComparison.OrdinalIgnoreCase, out var token) && token.Type == JTokenType.Boolean) - { - value = token.Value(); - return value != null; - } - else if (token != null && token.Type == JTokenType.String && bool.TryParse(token.Value(), out var v)) - { - value = v; - return true; - } - return false; - } - - internal static bool TryIntegerProperty(this JObject o, string propertyName, out long? value) - { - value = null; - if (o.TryGetValue(propertyName, StringComparison.OrdinalIgnoreCase, out var token) && token.Type == JTokenType.Integer) - { - value = token.Value(); - return value != null; - } - else if (token != null && token.Type == JTokenType.String && long.TryParse(token.Value(), out var v)) - { - value = v; - return true; - } - return false; - } - - /// - /// Check if the script uses an expression. - /// - internal static bool IsExpressionString(this JToken token) - { - if (token == null || token.Type != JTokenType.String) - return false; - - var value = token.Value(); - return value != null && value.IsExpressionString(); - } - - internal static bool IsEmpty(this JToken value) - { - return value.Type == JTokenType.None || - value.Type == JTokenType.Null || - (value.Type == JTokenType.String && string.IsNullOrEmpty(value.Value())); - } - - /// - /// Resources with the existing property set to true are references to existing resources. - /// - /// The resource . - /// Returns true if the resource is a reference to an existing resource. - internal static bool IsExisting(this JObject o) - { - return o != null && o.TryBoolProperty(PROPERTY_EXISTING, out var existing) && existing != null && existing.Value; - } - - /// - /// Resources with an import property set to a non-empty string refer to extensibility provided resources. - /// For example, Microsoft Graph. - /// - /// The resource . - /// Returns true if the resource is imported from an extensibility provider. - internal static bool IsImport(this JObject o) - { - return o != null && o.TryStringProperty(PROPERTY_IMPORT, out var s) && !string.IsNullOrEmpty(s); - } - - internal static bool TryResourceType(this JObject o, out string type) - { - type = default; - return o != null && o.TryGetProperty(PROPERTY_TYPE, out type); - } - - internal static bool TryResourceName(this JObject o, out string name) - { - name = default; - return o != null && o.TryGetProperty(PROPERTY_NAME, out name); - } - - internal static bool TryResourceNameAndType(this JObject o, out string name, out string type) - { - name = default; - type = default; - return o != null && o.TryResourceName(out name) && o.TryResourceType(out type); - } - - /// - /// Read the scope from a specified scope property. - /// - /// The resource object. - /// A valid context to resolve properties. - /// The scope if set. - /// Returns true if the scope property was set on the resource. - internal static bool TryResourceScope(this JObject resource, ITemplateContext context, out string scopeId) - { - scopeId = default; - if (resource == null) - return false; - - return TryExplicitScope(resource, context, out scopeId) - || TryExplicitSubscriptionResourceGroupScope(resource, context, out scopeId) - || TryParentScope(resource, context, out scopeId) - || TryDeploymentScope(context, out scopeId); - } - - private static bool TryExplicitScope(JObject resource, ITemplateContext context, out string scopeId) - { - scopeId = context.ExpandProperty(resource, PROPERTY_SCOPE); - if (string.IsNullOrEmpty(scopeId)) - return false; - - // Check for full scope. - ResourceHelper.ResourceIdComponents(scopeId, out var tenant, out var managementGroup, out var subscriptionId, out var resourceGroup, out var resourceType, out var resourceName); - if (tenant != null || managementGroup != null || subscriptionId != null) - return true; - - scopeId = ResourceHelper.ResourceId(resourceType, resourceName, scopeId: context.ScopeId); - return true; - } - - /// - /// Get the scope from subscriptionId and resourceGroup properties set on the resource. - /// - private static bool TryExplicitSubscriptionResourceGroupScope(JObject resource, ITemplateContext context, out string scopeId) - { - var subscriptionId = context.ExpandProperty(resource, PROPERTY_SUBSCRIPTION_ID); - var resourceGroup = context.ExpandProperty(resource, PROPERTY_RESOURCE_GROUP); - - // Fill subscriptionId if resourceGroup is specified. - if (!string.IsNullOrEmpty(resourceGroup) && string.IsNullOrEmpty(subscriptionId)) - subscriptionId = context.Subscription.SubscriptionId; - - scopeId = !string.IsNullOrEmpty(subscriptionId) ? ResourceHelper.ResourceId(scopeTenant: null, scopeManagementGroup: null, scopeSubscriptionId: subscriptionId, scopeResourceGroup: resourceGroup, resourceType: null, resourceName: null) : null; - return scopeId != null; - } - - /// - /// Read the scope from the name and type properties if this is a sub-resource. - /// For example: A sub-resource may use name segments such as vnet-1/subnet-1. - /// - /// The resource object. - /// A valid context to resolve properties. - /// The calculated scope. - /// Returns true if the scope could be calculated from name segments. - private static bool TryParentScope(JObject resource, ITemplateContext context, out string scopeId) - { - scopeId = null; - var name = context.ExpandProperty(resource, PROPERTY_NAME); - var type = context.ExpandProperty(resource, PROPERTY_TYPE); - - if (string.IsNullOrEmpty(name) || - string.IsNullOrEmpty(type) || - !ResourceHelper.TryResourceIdComponents(type, name, out var resourceTypeComponents, out var nameComponents) || - resourceTypeComponents.Length == 1) - return false; - - scopeId = ResourceHelper.GetParentResourceId(context.Subscription.SubscriptionId, context.ResourceGroup.Name, resourceTypeComponents, nameComponents); - return true; - } - - /// - /// Get the scope of the resource based on the scope of the deployment. - /// - /// A valid context to resolve the deployment scope. - /// The scope of the deployment. - /// Returns true if a deployment scope was found. - private static bool TryDeploymentScope(ITemplateContext context, out string scopeId) - { - scopeId = context.Deployment?.Scope; - return scopeId != null; - } - } -} diff --git a/src/PSRule.Rules.Azure/Common/LazyValue.cs b/src/PSRule.Rules.Azure/Common/LazyValue.cs deleted file mode 100644 index ea955f886cc..00000000000 --- a/src/PSRule.Rules.Azure/Common/LazyValue.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; - -namespace PSRule.Rules.Azure -{ - internal interface ILazyValue - { - object GetValue(); - } - - internal interface ILazyObject - { - bool TryProperty(string propertyName, out object value); - } - - internal sealed class LazyValue : ILazyValue - { - private readonly Func _Value; - - public LazyValue(Func value) - { - _Value = value; - } - - public object GetValue() - { - return _Value(); - } - } -} diff --git a/src/PSRule.Rules.Azure/Common/TaskExtensions.cs b/src/PSRule.Rules.Azure/Common/TaskExtensions.cs deleted file mode 100644 index b18b2327d56..00000000000 --- a/src/PSRule.Rules.Azure/Common/TaskExtensions.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Threading.Tasks; - -namespace PSRule.Rules.Azure -{ - /// - /// Helpers for instances. - /// - internal static class TaskExtensions - { - /// - /// Call the Dispose method all the specified tasks. - /// - public static void DisposeAll(this Task[] tasks) - { - for (var i = 0; tasks != null && i < tasks.Length; i++) - tasks[i].Dispose(); - } - } -} diff --git a/src/PSRule.Rules.Azure/Common/TypeExtensions.cs b/src/PSRule.Rules.Azure/Common/TypeExtensions.cs deleted file mode 100644 index 08587bab201..00000000000 --- a/src/PSRule.Rules.Azure/Common/TypeExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Reflection; - -namespace PSRule.Rules.Azure -{ - /// - /// Extensions for types. - /// - internal static class TypeExtensions - { - public static bool TryGetValue(this Type type, object instance, string propertyOrField, out object value) - { - value = null; - var property = type.GetProperty(propertyOrField, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.Public); - if (property != null) - { - value = property.GetValue(instance); - return true; - } - - // Try field - var field = type.GetField(propertyOrField, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.GetField | BindingFlags.Public); - if (field != null) - { - value = field.GetValue(instance); - return true; - } - return false; - } - } -} diff --git a/src/PSRule.Rules.Azure/Data/Bicep/BicepHelper.cs b/src/PSRule.Rules.Azure/Data/Bicep/BicepHelper.cs index 49717e790fb..fd65e69af6b 100644 --- a/src/PSRule.Rules.Azure/Data/Bicep/BicepHelper.cs +++ b/src/PSRule.Rules.Azure/Data/Bicep/BicepHelper.cs @@ -18,499 +18,498 @@ using PSRule.Rules.Azure.Resources; using PSRule.Rules.Azure.Runtime; -namespace PSRule.Rules.Azure.Data.Bicep +namespace PSRule.Rules.Azure.Data.Bicep; + +/// +/// A helper for calling the Bicep CLI. +/// +internal sealed class BicepHelper { - /// - /// A helper for calling the Bicep CLI. - /// - internal sealed class BicepHelper - { - private const int ERROR_FILE_NOT_FOUND = 2; - private const string ENV_AZURE_BICEP_ARGS = "PSRULE_AZURE_BICEP_ARGS"; - private const string ENV_AZURE_BICEP_USE_AZURE_CLI = "PSRULE_AZURE_BICEP_USE_AZURE_CLI"; + private const int ERROR_FILE_NOT_FOUND = 2; + private const string ENV_AZURE_BICEP_ARGS = "PSRULE_AZURE_BICEP_ARGS"; + private const string ENV_AZURE_BICEP_USE_AZURE_CLI = "PSRULE_AZURE_BICEP_USE_AZURE_CLI"; - private readonly PipelineContext _Context; - private readonly RuntimeService _Service; - private readonly int _Timeout; + private readonly PipelineContext _Context; + private readonly RuntimeService _Service; + private readonly int _Timeout; + + private static BicepInfo _Bicep; + + public BicepHelper(PipelineContext context, RuntimeService service) + { + _Context = context; + _Service = service; + _Timeout = _Service.Timeout; + } - private static BicepInfo _Bicep; + internal sealed class BicepInfo + { + private readonly string _BinPath; + private readonly bool _UseAzCLI; - public BicepHelper(PipelineContext context, RuntimeService service) + private BicepInfo(string version, string binPath, bool useAzCLI) { - _Context = context; - _Service = service; - _Timeout = _Service.Timeout; + Version = version; + _BinPath = binPath; + _UseAzCLI = useAzCLI; } - internal sealed class BicepInfo + public string Version { get; } + + public static BicepInfo Create(string binPath, bool useAzCLI) { - private readonly string _BinPath; - private readonly bool _UseAzCLI; + var version = GetVersionInfo(binPath, useAzCLI); + return string.IsNullOrEmpty(version) ? null : new BicepInfo(version, binPath, useAzCLI); + } - private BicepInfo(string version, string binPath, bool useAzCLI) + public BicepProcess Spawn(string sourcePath, int timeout) + { + try { - Version = version; - _BinPath = binPath; - _UseAzCLI = useAzCLI; + var args = GetBicepBuildArgs(sourcePath, _UseAzCLI); + var startInfo = new ProcessStartInfo(_BinPath, args) + { + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + WorkingDirectory = PSRuleOption.GetWorkingPath(), + }; + return new BicepProcess(Process.Start(startInfo), timeout, Version); } - - public string Version { get; } - - public static BicepInfo Create(string binPath, bool useAzCLI) + catch (Exception ex) { - var version = GetVersionInfo(binPath, useAzCLI); - return string.IsNullOrEmpty(version) ? null : new BicepInfo(version, binPath, useAzCLI); + throw new BicepCompileException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.BicepCommandError, ex.Message), ex); } + } - public BicepProcess Spawn(string sourcePath, int timeout) + private static string GetVersionInfo(string binPath, bool useAzCLI) + { + try { + var args = GetBicepVersionArgs(useAzCLI); + var versionStartInfo = new ProcessStartInfo(binPath, args) + { + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + WorkingDirectory = PSRuleOption.GetWorkingPath(), + }; + var bicep = new BicepProcess(Process.Start(versionStartInfo), 5); try { - var args = GetBicepBuildArgs(sourcePath, _UseAzCLI); - var startInfo = new ProcessStartInfo(_BinPath, args) - { - CreateNoWindow = true, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - WorkingDirectory = PSRuleOption.GetWorkingPath(), - }; - return new BicepProcess(Process.Start(startInfo), timeout, Version); + if (bicep.WaitForExit(out _)) + return TrimVersion(bicep.GetOutput()); } - catch (Exception ex) + finally { - throw new BicepCompileException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.BicepCommandError, ex.Message), ex); + bicep.Dispose(); } + return null; } - - private static string GetVersionInfo(string binPath, bool useAzCLI) + catch (Exception ex) { - try - { - var args = GetBicepVersionArgs(useAzCLI); - var versionStartInfo = new ProcessStartInfo(binPath, args) - { - CreateNoWindow = true, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - WorkingDirectory = PSRuleOption.GetWorkingPath(), - }; - var bicep = new BicepProcess(Process.Start(versionStartInfo), 5); - try - { - if (bicep.WaitForExit(out _)) - return TrimVersion(bicep.GetOutput()); - } - finally - { - bicep.Dispose(); - } - return null; - } - catch (Exception ex) + if (ex is Win32Exception win32 && win32.NativeErrorCode == ERROR_FILE_NOT_FOUND) { - if (ex is Win32Exception win32 && win32.NativeErrorCode == ERROR_FILE_NOT_FOUND) - { - throw new BicepCompileException(PSRuleResources.BicepNotFound, ex); - } - throw new BicepCompileException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.BicepCommandError, ex.Message), ex); + throw new BicepCompileException(PSRuleResources.BicepNotFound, ex); } + throw new BicepCompileException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.BicepCommandError, ex.Message), ex); } + } - private static string TrimVersion(string s) - { - if (string.IsNullOrEmpty(s)) - return string.Empty; + private static string TrimVersion(string s) + { + if (string.IsNullOrEmpty(s)) + return string.Empty; - s = s.Trim(' ', '\r', '\n'); - var versionParts = s.Split(' '); - return versionParts.Length < 3 ? string.Empty : versionParts[versionParts.Length - 2]; - } + s = s.Trim(' ', '\r', '\n'); + var versionParts = s.Split(' '); + return versionParts.Length < 3 ? string.Empty : versionParts[versionParts.Length - 2]; } + } - internal sealed class BicepProcess : IDisposable - { - private readonly Process _Process; - private readonly StringBuilder _Output; - private readonly StringBuilder _Error; - private readonly AutoResetEvent _ErrorWait; - private readonly AutoResetEvent _OutputWait; - private readonly int _Interval; - private readonly int _Timeout; + internal sealed class BicepProcess : IDisposable + { + private readonly Process _Process; + private readonly StringBuilder _Output; + private readonly StringBuilder _Error; + private readonly AutoResetEvent _ErrorWait; + private readonly AutoResetEvent _OutputWait; + private readonly int _Interval; + private readonly int _Timeout; - private bool _Disposed; + private bool _Disposed; - public BicepProcess(Process process, int timeout, string version = null) - { - _Output = new StringBuilder(); - _Error = new StringBuilder(); - _Interval = 1000; - _Timeout = timeout; + public BicepProcess(Process process, int timeout, string version = null) + { + _Output = new StringBuilder(); + _Error = new StringBuilder(); + _Interval = 1000; + _Timeout = timeout; - Version = version; - _Process = process; - _Process.ErrorDataReceived += Bicep_ErrorDataReceived; - _Process.OutputDataReceived += Bicep_OutputDataReceived; + Version = version; + _Process = process; + _Process.ErrorDataReceived += Bicep_ErrorDataReceived; + _Process.OutputDataReceived += Bicep_OutputDataReceived; - _Process.BeginErrorReadLine(); - _Process.BeginOutputReadLine(); + _Process.BeginErrorReadLine(); + _Process.BeginOutputReadLine(); - _ErrorWait = new AutoResetEvent(false); - _OutputWait = new AutoResetEvent(false); - } + _ErrorWait = new AutoResetEvent(false); + _OutputWait = new AutoResetEvent(false); + } - public string Version { get; } + public string Version { get; } - public bool HasExited => _Process.HasExited; + public bool HasExited => _Process.HasExited; - public bool WaitForExit(out int exitCode) + public bool WaitForExit(out int exitCode) + { + try { - try + if (!_Process.HasExited) { - if (!_Process.HasExited) - { - var timeoutCount = 0; - while (!_Process.WaitForExit(_Interval) && !_Process.HasExited && timeoutCount < _Timeout) - timeoutCount++; - } - - exitCode = _Process.HasExited ? _Process.ExitCode : -1; - return _Process.HasExited && _ErrorWait.WaitOne(_Interval) && _OutputWait.WaitOne(); + var timeoutCount = 0; + while (!_Process.WaitForExit(_Interval) && !_Process.HasExited && timeoutCount < _Timeout) + timeoutCount++; } - catch (Exception ex) - { - throw new BicepCompileException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.BicepCommandError, ex.Message), ex); - } - } - public string GetOutput() - { - lock (_Output) - { - return _Output.ToString(); - } + exitCode = _Process.HasExited ? _Process.ExitCode : -1; + return _Process.HasExited && _ErrorWait.WaitOne(_Interval) && _OutputWait.WaitOne(); } - - public string GetError() + catch (Exception ex) { - lock (_Error) - { - return _Error.ToString(); - } + throw new BicepCompileException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.BicepCommandError, ex.Message), ex); } + } - private void Bicep_OutputDataReceived(object sender, DataReceivedEventArgs e) + public string GetOutput() + { + lock (_Output) { - if (e.Data == null) - { - _OutputWait.Set(); - } - else - { - lock (_Output) - { - _Output.AppendLine(e.Data); - } - } + return _Output.ToString(); } + } - private void Bicep_ErrorDataReceived(object sender, DataReceivedEventArgs e) + public string GetError() + { + lock (_Error) { - if (e.Data == null) - { - _ErrorWait.Set(); - } - else - { - var errors = GetErrorLine(e.Data); - if (errors.Length == 0) - return; - - lock (_Error) - { - for (var i = 0; i < errors.Length; i++) - _Error.AppendLine(errors[i]); - } - } + return _Error.ToString(); } + } - private static string[] GetErrorLine(string input) + private void Bicep_OutputDataReceived(object sender, DataReceivedEventArgs e) + { + if (e.Data == null) { - var lines = input.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); - var result = new List(); - for (var i = 0; i < lines.Length; i++) - if (!lines[i].Contains(": Warning ") && !lines[i].Contains(": Info ")) - result.Add(lines[i]); - - return result.ToArray(); + _OutputWait.Set(); } - - private void Dispose(bool disposing) + else { - if (!_Disposed) + lock (_Output) { - if (disposing) - { - _ErrorWait.Dispose(); - _OutputWait.Dispose(); - _Process.Dispose(); - } - lock (_Error) - { - _Error.Clear(); - } - lock (_Output) - { - _Output.Clear(); - } - _Disposed = true; + _Output.AppendLine(e.Data); } } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } } - /// - /// The version of Bicep. - /// - internal string Version + private void Bicep_ErrorDataReceived(object sender, DataReceivedEventArgs e) { - get + if (e.Data == null) { - _Bicep ??= GetBicepInfo(); - return _Bicep?.Version; + _ErrorWait.Set(); } - } - - internal PSObject[] ProcessFile(string templateFile, string parameterFile) - { - if (!File.Exists(templateFile)) - throw new FileNotFoundException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.TemplateFileNotFound, templateFile), templateFile); + else + { + var errors = GetErrorLine(e.Data); + if (errors.Length == 0) + return; - var json = ReadBicepFile(templateFile); - return json == null ? Array.Empty() : ProcessJson(json, templateFile, parameterFile); + lock (_Error) + { + for (var i = 0; i < errors.Length; i++) + _Error.AppendLine(errors[i]); + } + } } - internal PSObject[] ProcessParamFile(string parameterFile) + private static string[] GetErrorLine(string input) { - if (!File.Exists(parameterFile)) - throw new FileNotFoundException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.TemplateFileNotFound, parameterFile), parameterFile); - - var json = ReadBicepFile(parameterFile); - if (json == null || !json.TryGetProperty("templateJson", out var templateJson) || !json.TryGetProperty("parametersJson", out var parametersJson)) - return Array.Empty(); + var lines = input.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + var result = new List(); + for (var i = 0; i < lines.Length; i++) + if (!lines[i].Contains(": Warning ") && !lines[i].Contains(": Info ")) + result.Add(lines[i]); - return ProcessJson(JObject.Parse(templateJson), JObject.Parse(parametersJson), parameterFile); + return result.ToArray(); } - private PSObject[] ProcessJson(JObject templateObject, string templateFile, string parameterFile) + private void Dispose(bool disposing) { - var visitor = new RuleDataExportVisitor(); - - // Load context - var templateContext = new TemplateVisitor.TemplateContext(_Context); - if (!string.IsNullOrEmpty(parameterFile)) + if (!_Disposed) { - var rootedParameterFile = PSRuleOption.GetRootedPath(parameterFile); - if (!File.Exists(rootedParameterFile)) - throw new FileNotFoundException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.ParameterFileNotFound, rootedParameterFile), rootedParameterFile); - - try + if (disposing) { - var parametersObject = ReadJsonFile(rootedParameterFile); - templateContext.Load(parametersObject); + _ErrorWait.Dispose(); + _OutputWait.Dispose(); + _Process.Dispose(); } - catch (Exception inner) + lock (_Error) { - throw new TemplateReadException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.TemplateExpandInvalid, templateFile, parameterFile, inner.Message), inner, templateFile, parameterFile); + _Error.Clear(); } + lock (_Output) + { + _Output.Clear(); + } + _Disposed = true; } + } - // Process - try - { - templateContext.SetSource(templateFile, parameterFile); - visitor.Visit(templateContext, "helper", templateObject); - } - catch (Exception inner) - { - throw new TemplateReadException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.BicepExpandInvalid, templateFile, inner.Message), inner, templateFile, null); - } - - // Return results - return GetResources(templateContext); + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); } + } - private PSObject[] ProcessJson(JObject templateObject, JObject parametersObject, string parameterFile) + /// + /// The version of Bicep. + /// + internal string Version + { + get { - var visitor = new RuleDataExportVisitor(); + _Bicep ??= GetBicepInfo(); + return _Bicep?.Version; + } + } - // Load context - var templateContext = new TemplateVisitor.TemplateContext(_Context); - try - { - templateContext.Load(parametersObject); - } - catch (Exception inner) - { - throw new TemplateReadException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.TemplateExpandInvalid, null, parameterFile, inner.Message), inner, null, parameterFile); - } + internal PSObject[] ProcessFile(string templateFile, string parameterFile) + { + if (!File.Exists(templateFile)) + throw new FileNotFoundException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.TemplateFileNotFound, templateFile), templateFile); - // Process - try - { - templateContext.SetSource(null, parameterFile); - visitor.Visit(templateContext, "helper", templateObject); - } - catch (Exception inner) - { - throw new TemplateReadException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.BicepExpandInvalid, parameterFile, inner.Message), inner, null, parameterFile); - } + var json = ReadBicepFile(templateFile); + return json == null ? Array.Empty() : ProcessJson(json, templateFile, parameterFile); + } - // Return results - return GetResources(templateContext); - } + internal PSObject[] ProcessParamFile(string parameterFile) + { + if (!File.Exists(parameterFile)) + throw new FileNotFoundException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.TemplateFileNotFound, parameterFile), parameterFile); - /// - /// Get resulting resources from expansion. - /// - private static PSObject[] GetResources(TemplateVisitor.TemplateContext templateContext) - { - var results = new List(); - var serializer = new JsonSerializer(); - serializer.Converters.Add(new PSObjectJsonConverter()); - foreach (var resource in templateContext.GetResources()) - results.Add(resource.Value.ToObject(serializer)); + var json = ReadBicepFile(parameterFile); + if (json == null || !json.TryGetProperty("templateJson", out var templateJson) || !json.TryGetProperty("parametersJson", out var parametersJson)) + return Array.Empty(); - return results.ToArray(); - } + return ProcessJson(JObject.Parse(templateJson), JObject.Parse(parametersJson), parameterFile); + } + + private PSObject[] ProcessJson(JObject templateObject, string templateFile, string parameterFile) + { + var visitor = new RuleDataExportVisitor(); - private JObject ReadBicepFile(string path) + // Load context + var templateContext = new TemplateVisitor.TemplateContext(_Context); + if (!string.IsNullOrEmpty(parameterFile)) { - var bicep = GetBicep(path, _Timeout); - if (bicep == null) - throw new BicepCompileException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.BicepNotFound), null, path, null); + var rootedParameterFile = PSRuleOption.GetRootedPath(parameterFile); + if (!File.Exists(rootedParameterFile)) + throw new FileNotFoundException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.ParameterFileNotFound, rootedParameterFile), rootedParameterFile); try { - if (!bicep.WaitForExit(out var exitCode) || exitCode != 0) - { - var error = bicep.HasExited ? bicep.GetError() : PSRuleResources.BicepCompileTimeout; - throw new BicepCompileException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.BicepCompileError, bicep.Version, path, error), null, path, bicep.Version); - } - - try - { - using var reader = new JsonTextReader(new StringReader(bicep.GetOutput())); - return JObject.Load(reader); - } - catch (Exception e) - { - throw new BicepCompileException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.BicepCompileError, bicep.Version, path, e.Message), e, path, bicep.Version); - } + var parametersObject = ReadJsonFile(rootedParameterFile); + templateContext.Load(parametersObject); } - finally + catch (Exception inner) { - bicep.Dispose(); + throw new TemplateReadException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.TemplateExpandInvalid, templateFile, parameterFile, inner.Message), inner, templateFile, parameterFile); } } - private static JObject ReadJsonFile(string path) + // Process + try { - using var stream = new StreamReader(path); - using var reader = new JsonTextReader(stream); - return JObject.Load(reader); + templateContext.SetSource(templateFile, parameterFile); + visitor.Visit(templateContext, "helper", templateObject); } - - private BicepProcess GetBicep(string sourcePath, int timeout) + catch (Exception inner) { - _Bicep ??= GetBicepInfo(); - return _Bicep?.Spawn(sourcePath, timeout); + throw new TemplateReadException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.BicepExpandInvalid, templateFile, inner.Message), inner, templateFile, null); } - private BicepInfo GetBicepInfo() - { - return _Service.Bicep ??= BicepInfo.Create(GetBinaryPath(out var useAzCLI), useAzCLI); - } + // Return results + return GetResources(templateContext); + } - private string GetBinaryPath(out bool useAzCLI) - { - useAzCLI = false; - return GetBicepEnvironmentVariable() ?? - GetAzCLIPath(out useAzCLI) ?? - GetBicepBinary(); - } + private PSObject[] ProcessJson(JObject templateObject, JObject parametersObject, string parameterFile) + { + var visitor = new RuleDataExportVisitor(); - private static string GetAzCLIPath(out bool useAzCLI) + // Load context + var templateContext = new TemplateVisitor.TemplateContext(_Context); + try { - useAzCLI = UseAzCLI(); - return useAzCLI ? GetAzBinaryName() : null; + templateContext.Load(parametersObject); } - - /// - /// Check if the PSRULE_AZURE_BICEP_PATH environment variable is set and use this path if set. - /// - private static string GetBicepEnvironmentVariable() + catch (Exception inner) { - var binaryPath = Environment.GetEnvironmentVariable("PSRULE_AZURE_BICEP_PATH"); - return string.IsNullOrEmpty(binaryPath) ? null : binaryPath; + throw new TemplateReadException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.TemplateExpandInvalid, null, parameterFile, inner.Message), inner, null, parameterFile); } - /// - /// Get the file name of the Bicep CLI binary. - /// - private static string GetBicepBinary() + // Process + try { - return RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || - RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "bicep" : "bicep.exe"; + templateContext.SetSource(null, parameterFile); + visitor.Visit(templateContext, "helper", templateObject); } - - /// - /// Get the file name of the Azure CLI binary. - /// - private static string GetAzBinaryName() + catch (Exception inner) { - return RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || - RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "az" : "az.exe"; + throw new TemplateReadException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.BicepExpandInvalid, parameterFile, inner.Message), inner, null, parameterFile); } - private static string GetBicepBuildArgs(string sourcePath, bool useAzCLI) - { - var command = GetBicepBuildCommand(sourcePath); - var args = GetBicepBuildAdditionalArgs(); - return string.Concat(command, args, useAzCLI ? " --file" : string.Empty, " \"", sourcePath, "\""); - } + // Return results + return GetResources(templateContext); + } - private static string GetBicepBuildCommand(string sourcePath) - { - return sourcePath.EndsWith(".bicepparam") ? "build-params --stdout " : "build --stdout "; - } + /// + /// Get resulting resources from expansion. + /// + private static PSObject[] GetResources(TemplateVisitor.TemplateContext templateContext) + { + var results = new List(); + var serializer = new JsonSerializer(); + serializer.Converters.Add(new PSObjectJsonConverter()); + foreach (var resource in templateContext.GetResources()) + results.Add(resource.Value.ToObject(serializer)); - private static string GetBicepVersionArgs(bool useAzCLI) - { - return useAzCLI ? "version" : "--version"; - } + return results.ToArray(); + } - /// - /// Check if the PSRULE_AZURE_BICEP_ARGS environment variable is set. - /// - private static string GetBicepBuildAdditionalArgs() + private JObject ReadBicepFile(string path) + { + var bicep = GetBicep(path, _Timeout); + if (bicep == null) + throw new BicepCompileException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.BicepNotFound), null, path, null); + + try { - return Environment.GetEnvironmentVariable(ENV_AZURE_BICEP_ARGS) ?? string.Empty; - } + if (!bicep.WaitForExit(out var exitCode) || exitCode != 0) + { + var error = bicep.HasExited ? bicep.GetError() : PSRuleResources.BicepCompileTimeout; + throw new BicepCompileException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.BicepCompileError, bicep.Version, path, error), null, path, bicep.Version); + } - /// - /// Check if the PSRULE_AZURE_BICEP_USE_AZURE_CLI environment variable is set. - /// - private static bool UseAzCLI() + try + { + using var reader = new JsonTextReader(new StringReader(bicep.GetOutput())); + return JObject.Load(reader); + } + catch (Exception e) + { + throw new BicepCompileException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.BicepCompileError, bicep.Version, path, e.Message), e, path, bicep.Version); + } + } + finally { - return EnvironmentHelper.Default.TryBool(ENV_AZURE_BICEP_USE_AZURE_CLI, out var value) && value; + bicep.Dispose(); } } + + private static JObject ReadJsonFile(string path) + { + using var stream = new StreamReader(path); + using var reader = new JsonTextReader(stream); + return JObject.Load(reader); + } + + private BicepProcess GetBicep(string sourcePath, int timeout) + { + _Bicep ??= GetBicepInfo(); + return _Bicep?.Spawn(sourcePath, timeout); + } + + private BicepInfo GetBicepInfo() + { + return _Service.Bicep ??= BicepInfo.Create(GetBinaryPath(out var useAzCLI), useAzCLI); + } + + private string GetBinaryPath(out bool useAzCLI) + { + useAzCLI = false; + return GetBicepEnvironmentVariable() ?? + GetAzCLIPath(out useAzCLI) ?? + GetBicepBinary(); + } + + private static string GetAzCLIPath(out bool useAzCLI) + { + useAzCLI = UseAzCLI(); + return useAzCLI ? GetAzBinaryName() : null; + } + + /// + /// Check if the PSRULE_AZURE_BICEP_PATH environment variable is set and use this path if set. + /// + private static string GetBicepEnvironmentVariable() + { + var binaryPath = Environment.GetEnvironmentVariable("PSRULE_AZURE_BICEP_PATH"); + return string.IsNullOrEmpty(binaryPath) ? null : binaryPath; + } + + /// + /// Get the file name of the Bicep CLI binary. + /// + private static string GetBicepBinary() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || + RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "bicep" : "bicep.exe"; + } + + /// + /// Get the file name of the Azure CLI binary. + /// + private static string GetAzBinaryName() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || + RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "az" : "az.exe"; + } + + private static string GetBicepBuildArgs(string sourcePath, bool useAzCLI) + { + var command = GetBicepBuildCommand(sourcePath); + var args = GetBicepBuildAdditionalArgs(); + return string.Concat(command, args, useAzCLI ? " --file" : string.Empty, " \"", sourcePath, "\""); + } + + private static string GetBicepBuildCommand(string sourcePath) + { + return sourcePath.EndsWith(".bicepparam") ? "build-params --stdout " : "build --stdout "; + } + + private static string GetBicepVersionArgs(bool useAzCLI) + { + return useAzCLI ? "version" : "--version"; + } + + /// + /// Check if the PSRULE_AZURE_BICEP_ARGS environment variable is set. + /// + private static string GetBicepBuildAdditionalArgs() + { + return Environment.GetEnvironmentVariable(ENV_AZURE_BICEP_ARGS) ?? string.Empty; + } + + /// + /// Check if the PSRULE_AZURE_BICEP_USE_AZURE_CLI environment variable is set. + /// + private static bool UseAzCLI() + { + return EnvironmentHelper.Default.TryBool(ENV_AZURE_BICEP_USE_AZURE_CLI, out var value) && value; + } } diff --git a/src/PSRule.Rules.Azure/Data/CloudEnvironment.cs b/src/PSRule.Rules.Azure/Data/CloudEnvironment.cs index 80a67e8f9e9..ef69da2edd8 100644 --- a/src/PSRule.Rules.Azure/Data/CloudEnvironment.cs +++ b/src/PSRule.Rules.Azure/Data/CloudEnvironment.cs @@ -3,130 +3,96 @@ using Newtonsoft.Json; -namespace PSRule.Rules.Azure.Data +namespace PSRule.Rules.Azure.Data; + +/// +/// Properties of an Azure Cloud Environment. +/// This object is exposed by the ARM environment() function. +/// See docs. +/// +public sealed class CloudEnvironment { /// - /// Properties of an Azure Cloud Environment. - /// This object is exposed by the ARM environment() function. - /// See docs. + /// The name of the cloud environment. /// - public sealed class CloudEnvironment - { - /// - /// The name of the cloud environment. - /// - [JsonProperty(PropertyName = "name")] - public string Name { get; set; } + [JsonProperty(PropertyName = "name")] + public string Name { get; set; } - /// - /// The gallery endpoint. - /// - [JsonProperty(PropertyName = "gallery")] - public string Gallery { get; set; } - - /// - /// The Microsoft Graph endpoint. - /// - [JsonProperty(PropertyName = "graph")] - public string Graph { get; set; } - - /// - /// The Azure Portal endpoint. - /// - [JsonProperty(PropertyName = "portal")] - public string Portal { get; set; } + /// + /// The gallery endpoint. + /// + [JsonProperty(PropertyName = "gallery")] + public string Gallery { get; set; } - /// - /// The audience for Microsoft Graph. - /// - [JsonProperty(PropertyName = "graphAudience")] - public string GraphAudience { get; set; } + /// + /// The Microsoft Graph endpoint. + /// + [JsonProperty(PropertyName = "graph")] + public string Graph { get; set; } - /// - /// The audience for Azure Data Lake. - /// Defaults to https://datalake.azure.net/. - /// - [JsonProperty(PropertyName = "activeDirectoryDataLake")] - public string ActiveDirectoryDataLake { get; set; } + /// + /// The Azure Portal endpoint. + /// + [JsonProperty(PropertyName = "portal")] + public string Portal { get; set; } - /// - /// The audience for Azure Branch. - /// Defaults to https://batch.core.windows.net/. - /// - [JsonProperty(PropertyName = "batch")] - public string Batch { get; set; } + /// + /// The audience for Microsoft Graph. + /// + [JsonProperty(PropertyName = "graphAudience")] + public string GraphAudience { get; set; } - /// - /// The audience for Azure Media Services. - /// Defaults to https://rest.media.azure.net. - /// - [JsonProperty(PropertyName = "media")] - public string Media { get; set; } + /// + /// The audience for Azure Data Lake. + /// Defaults to https://datalake.azure.net/. + /// + [JsonProperty(PropertyName = "activeDirectoryDataLake")] + public string ActiveDirectoryDataLake { get; set; } - /// - /// The audience for Sql Management. - /// Defaults to https://management.core.windows.net:8443/. - /// - [JsonProperty(PropertyName = "sqlManagement")] - public string SqlManagement { get; set; } + /// + /// The audience for Azure Branch. + /// Defaults to https://batch.core.windows.net/. + /// + [JsonProperty(PropertyName = "batch")] + public string Batch { get; set; } - /// - /// Documentation reference for VM image aliases. - /// - [JsonProperty(PropertyName = "vmImageAliasDoc")] - public string VmImageAliasDoc { get; set; } + /// + /// The audience for Azure Media Services. + /// Defaults to https://rest.media.azure.net. + /// + [JsonProperty(PropertyName = "media")] + public string Media { get; set; } - /// - /// The URL for Azure Resource Manager. - /// Defaults to https://management.azure.com/. - /// - [JsonProperty(PropertyName = "resourceManager")] - public string ResourceManager { get; set; } + /// + /// The audience for Sql Management. + /// Defaults to https://management.core.windows.net:8443/. + /// + [JsonProperty(PropertyName = "sqlManagement")] + public string SqlManagement { get; set; } - /// - /// Authentication properties for the cloud environment. - /// - [JsonProperty(PropertyName = "authentication")] - public CloudEnvironmentAuthentication Authentication { get; set; } + /// + /// Documentation reference for VM image aliases. + /// + [JsonProperty(PropertyName = "vmImageAliasDoc")] + public string VmImageAliasDoc { get; set; } - /// - /// A list of common suffixes that may be cloud environment specific. - /// Use these values instead of hard coding in IaC. - /// - [JsonProperty(PropertyName = "suffixes")] - public CloudEnvironmentSuffixes Suffixes { get; set; } - } + /// + /// The URL for Azure Resource Manager. + /// Defaults to https://management.azure.com/. + /// + [JsonProperty(PropertyName = "resourceManager")] + public string ResourceManager { get; set; } /// /// Authentication properties for the cloud environment. /// - public sealed class CloudEnvironmentAuthentication - { - /// - /// Azure AD login endpoint. - /// Defaults to https://login.microsoftonline.com/. - /// - [JsonProperty(PropertyName = "loginEndpoint")] - public string loginEndpoint { get; set; } - - /// - /// Azure Resource Manager audiences. - /// - [JsonProperty(PropertyName = "audiences")] - public string[] audiences { get; set; } + [JsonProperty(PropertyName = "authentication")] + public CloudEnvironmentAuthentication Authentication { get; set; } - /// - /// Azure AD tenant to use. - /// Default to common. - /// - [JsonProperty(PropertyName = "tenant")] - public string tenant { get; set; } - - /// - /// The identity provider to use. - /// Defaults to AAD. - /// - [JsonProperty(PropertyName = "identityProvider")] - public string identityProvider { get; set; } - } + /// + /// A list of common suffixes that may be cloud environment specific. + /// Use these values instead of hard coding in IaC. + /// + [JsonProperty(PropertyName = "suffixes")] + public CloudEnvironmentSuffixes Suffixes { get; set; } } diff --git a/src/PSRule.Rules.Azure/Data/CloudEnvironmentAuthentication.cs b/src/PSRule.Rules.Azure/Data/CloudEnvironmentAuthentication.cs new file mode 100644 index 00000000000..9ab0e45b0d5 --- /dev/null +++ b/src/PSRule.Rules.Azure/Data/CloudEnvironmentAuthentication.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Newtonsoft.Json; + +namespace PSRule.Rules.Azure.Data; + +/// +/// Authentication properties for the cloud environment. +/// +public sealed class CloudEnvironmentAuthentication +{ + /// + /// Azure AD login endpoint. + /// Defaults to https://login.microsoftonline.com/. + /// + [JsonProperty(PropertyName = "loginEndpoint")] + public string loginEndpoint { get; set; } + + /// + /// Azure Resource Manager audiences. + /// + [JsonProperty(PropertyName = "audiences")] + public string[] audiences { get; set; } + + /// + /// Azure AD tenant to use. + /// Default to common. + /// + [JsonProperty(PropertyName = "tenant")] + public string tenant { get; set; } + + /// + /// The identity provider to use. + /// Defaults to AAD. + /// + [JsonProperty(PropertyName = "identityProvider")] + public string identityProvider { get; set; } +} diff --git a/src/PSRule.Rules.Azure/Data/EnvironmentData.cs b/src/PSRule.Rules.Azure/Data/EnvironmentData.cs index f28c2ba4758..6c4e48e73ee 100644 --- a/src/PSRule.Rules.Azure/Data/EnvironmentData.cs +++ b/src/PSRule.Rules.Azure/Data/EnvironmentData.cs @@ -6,40 +6,39 @@ using Newtonsoft.Json; using PSRule.Rules.Azure.Resources; -namespace PSRule.Rules.Azure.Data +namespace PSRule.Rules.Azure.Data; + +/// +/// Defines a datastore of Azure environment data. +/// +internal sealed class EnvironmentData : ResourceLoader { + private const string RESOURCE_PATH = "environments.min.json.deflated"; + /// - /// Defines a datastore of Azure environment data. + /// Settings for JSON deserialization. /// - internal sealed class EnvironmentData : ResourceLoader + private static readonly JsonSerializerSettings _Settings = new() { - private const string RESOURCE_PATH = "environments.min.json.deflated"; + Converters = + [ + new ReadOnlyDictionaryConverter(StringComparer.OrdinalIgnoreCase), + ] + }; - /// - /// Settings for JSON deserialization. - /// - private static readonly JsonSerializerSettings _Settings = new() - { - Converters = new List - { - new ReadOnlyDictionaryConverter(StringComparer.OrdinalIgnoreCase), - } - }; + private IReadOnlyDictionary _Environments; - private IReadOnlyDictionary _Environments; - - public CloudEnvironment Get(string name) - { - _Environments ??= ReadEnvironments(GetContent(RESOURCE_PATH)); - return _Environments[name]; - } + public CloudEnvironment Get(string name) + { + _Environments ??= ReadEnvironments(GetContent(RESOURCE_PATH)); + return _Environments[name]; + } - /// - /// Deserialize an environments from JSON. - /// - private static IReadOnlyDictionary ReadEnvironments(string content) - { - return JsonConvert.DeserializeObject>(content, _Settings) ?? throw new JsonException(PSRuleResources.CloudEnvironmentInvalid); - } + /// + /// Deserialize an environments from JSON. + /// + private static IReadOnlyDictionary ReadEnvironments(string content) + { + return JsonConvert.DeserializeObject>(content, _Settings) ?? throw new JsonException(PSRuleResources.CloudEnvironmentInvalid); } } diff --git a/src/PSRule.Rules.Azure/Data/Metadata/ITemplateLink.cs b/src/PSRule.Rules.Azure/Data/Metadata/ITemplateLink.cs index bbc87a11838..d3ebf9b4225 100644 --- a/src/PSRule.Rules.Azure/Data/Metadata/ITemplateLink.cs +++ b/src/PSRule.Rules.Azure/Data/Metadata/ITemplateLink.cs @@ -1,21 +1,20 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace PSRule.Rules.Azure.Data.Metadata +namespace PSRule.Rules.Azure.Data.Metadata; + +/// +/// A naming or metadata link between a parameter file to a template or Bicep code. +/// +public interface ITemplateLink { /// - /// A naming or metadata link between a parameter file to a template or Bicep code. + /// The linked template or Bicep code. /// - public interface ITemplateLink - { - /// - /// The linked template or Bicep code. - /// - string TemplateFile { get; } + string TemplateFile { get; } - /// - /// The linked parameter file. - /// - string ParameterFile { get; } - } + /// + /// The linked parameter file. + /// + string ParameterFile { get; } } diff --git a/src/PSRule.Rules.Azure/Data/Network/NetworkSecurityGroupEvaluator.cs b/src/PSRule.Rules.Azure/Data/Network/NetworkSecurityGroupEvaluator.cs index 2c56986e530..881be244a91 100644 --- a/src/PSRule.Rules.Azure/Data/Network/NetworkSecurityGroupEvaluator.cs +++ b/src/PSRule.Rules.Azure/Data/Network/NetworkSecurityGroupEvaluator.cs @@ -5,111 +5,110 @@ using System.Collections.Generic; using System.Management.Automation; -namespace PSRule.Rules.Azure.Data.Network +namespace PSRule.Rules.Azure.Data.Network; + +/// +/// A basic implementation of an evaluator for checking NSG rules. +/// +internal sealed class NetworkSecurityGroupEvaluator : INetworkSecurityGroupEvaluator { - /// - /// A basic implementation of an evaluator for checking NSG rules. - /// - internal sealed class NetworkSecurityGroupEvaluator : INetworkSecurityGroupEvaluator + private const string PROPERTIES = "properties"; + private const string DIRECTION = "direction"; + private const string ACCESS = "access"; + private const string DESTINATION_ADDRESS_PREFIX = "destinationAddressPrefix"; + private const string DESTINATION_ADDRESS_PREFIXES = "destinationAddressPrefixes"; + private const string DESTINATION_PORT_RANGE = "destinationPortRange"; + private const string DESTINATION_PORT_RANGES = "destinationPortRanges"; + private const string ANY = "*"; + + private readonly List _Outbound; + + internal NetworkSecurityGroupEvaluator() { - private const string PROPERTIES = "properties"; - private const string DIRECTION = "direction"; - private const string ACCESS = "access"; - private const string DESTINATION_ADDRESS_PREFIX = "destinationAddressPrefix"; - private const string DESTINATION_ADDRESS_PREFIXES = "destinationAddressPrefixes"; - private const string DESTINATION_PORT_RANGE = "destinationPortRange"; - private const string DESTINATION_PORT_RANGES = "destinationPortRanges"; - private const string ANY = "*"; - - private readonly List _Outbound; - - internal NetworkSecurityGroupEvaluator() - { - _Outbound = new List(); - } + _Outbound = []; + } - internal enum Direction - { - Inbound = 1, + internal enum Direction + { + Inbound = 1, - Outbound = 2 - } + Outbound = 2 + } - private sealed class SecurityRule + private sealed class SecurityRule + { + public SecurityRule(Direction direction, Access access, string[] destinationAddressPrefixes, string[] destinationPortRanges) { - public SecurityRule(Direction direction, Access access, string[] destinationAddressPrefixes, string[] destinationPortRanges) - { - Direction = direction; - Access = access; - DestinationAddressPrefixes = destinationAddressPrefixes == null ? null : new HashSet(destinationAddressPrefixes, StringComparer.OrdinalIgnoreCase); - DestinationPortRanges = destinationPortRanges == null ? null : new HashSet(destinationPortRanges, StringComparer.OrdinalIgnoreCase); - } - - public Access Access { get; } + Direction = direction; + Access = access; + DestinationAddressPrefixes = destinationAddressPrefixes == null ? null : new HashSet(destinationAddressPrefixes, StringComparer.OrdinalIgnoreCase); + DestinationPortRanges = destinationPortRanges == null ? null : new HashSet(destinationPortRanges, StringComparer.OrdinalIgnoreCase); + } - public Direction Direction { get; } + public Access Access { get; } - public HashSet DestinationAddressPrefixes { get; } + public Direction Direction { get; } - public HashSet DestinationPortRanges { get; } + public HashSet DestinationAddressPrefixes { get; } - internal bool TryDestinationPrefix(string prefix) - { - return DestinationAddressPrefixes == null || DestinationAddressPrefixes.Contains(prefix); - } + public HashSet DestinationPortRanges { get; } - internal bool TryDestinationPort(int port) - { - return DestinationPortRanges == null || DestinationPortRanges.Contains(port.ToString()); - } + internal bool TryDestinationPrefix(string prefix) + { + return DestinationAddressPrefixes == null || DestinationAddressPrefixes.Contains(prefix); } - public void With(PSObject[] items) + internal bool TryDestinationPort(int port) { - if (items == null || items.Length == 0) - return; - - for (var i = 0; i < items.Length; i++) - { - var r = GetRule(items[i]); - if (r.Direction == Direction.Outbound) - _Outbound.Add(r); - } + return DestinationPortRanges == null || DestinationPortRanges.Contains(port.ToString()); } + } + + public void With(PSObject[] items) + { + if (items == null || items.Length == 0) + return; - /// - public Access Outbound(string prefix, int port) + for (var i = 0; i < items.Length; i++) { - for (var i = 0; i < _Outbound.Count; i++) - { - if (_Outbound[i].TryDestinationPrefix(prefix) && _Outbound[i].TryDestinationPort(port)) - return _Outbound[i].Access; - } - return Access.Default; + var r = GetRule(items[i]); + if (r.Direction == Direction.Outbound) + _Outbound.Add(r); } + } - private static SecurityRule GetRule(PSObject o) + /// + public Access Outbound(string prefix, int port) + { + for (var i = 0; i < _Outbound.Count; i++) { - var properties = o.GetPropertyValue(PROPERTIES); - var direction = (Direction)Enum.Parse(typeof(Direction), properties.GetPropertyValue(DIRECTION), ignoreCase: true); - var access = (Access)Enum.Parse(typeof(Access), properties.GetPropertyValue(ACCESS), ignoreCase: true); - var destinationAddressPrefixes = GetFilter(properties, DESTINATION_ADDRESS_PREFIX) ?? GetFilter(properties, DESTINATION_ADDRESS_PREFIXES); - var destinationPortRanges = GetFilter(properties, DESTINATION_PORT_RANGE) ?? GetFilter(properties, DESTINATION_PORT_RANGES); - var result = new SecurityRule( - direction, - access, - destinationAddressPrefixes, - destinationPortRanges - ); - return result; + if (_Outbound[i].TryDestinationPrefix(prefix) && _Outbound[i].TryDestinationPort(port)) + return _Outbound[i].Access; } + return Access.Default; + } - private static string[] GetFilter(PSObject o, string propertyName) - { - if (o.TryProperty(propertyName, out string[] value) && value.Length > 0) - return value; + private static SecurityRule GetRule(PSObject o) + { + var properties = o.GetPropertyValue(PROPERTIES); + var direction = (Direction)Enum.Parse(typeof(Direction), properties.GetPropertyValue(DIRECTION), ignoreCase: true); + var access = (Access)Enum.Parse(typeof(Access), properties.GetPropertyValue(ACCESS), ignoreCase: true); + var destinationAddressPrefixes = GetFilter(properties, DESTINATION_ADDRESS_PREFIX) ?? GetFilter(properties, DESTINATION_ADDRESS_PREFIXES); + var destinationPortRanges = GetFilter(properties, DESTINATION_PORT_RANGE) ?? GetFilter(properties, DESTINATION_PORT_RANGES); + var result = new SecurityRule( + direction, + access, + destinationAddressPrefixes, + destinationPortRanges + ); + return result; + } - return o.TryProperty(propertyName, out string s) && s != ANY ? (new string[] { s }) : null; - } + private static string[] GetFilter(PSObject o, string propertyName) + { + if (o.TryProperty(propertyName, out string[] value) && value.Length > 0) + return value; + + return o.TryProperty(propertyName, out string s) && s != ANY ? (new string[] { s }) : null; } } diff --git a/src/PSRule.Rules.Azure/Data/PolicyIgnoreData.cs b/src/PSRule.Rules.Azure/Data/PolicyIgnoreData.cs index b787c1e57ef..cff534b72af 100644 --- a/src/PSRule.Rules.Azure/Data/PolicyIgnoreData.cs +++ b/src/PSRule.Rules.Azure/Data/PolicyIgnoreData.cs @@ -5,33 +5,32 @@ using Newtonsoft.Json; using PSRule.Rules.Azure.Resources; -namespace PSRule.Rules.Azure.Data +namespace PSRule.Rules.Azure.Data; + +internal sealed class PolicyIgnoreData : ResourceLoader { - internal sealed class PolicyIgnoreData : ResourceLoader - { - private const string RESOURCE_PATH = "policy-ignore.min.json.deflated"; + private const string RESOURCE_PATH = "policy-ignore.min.json.deflated"; - /// - /// Settings for JSON deserialization. - /// - private static readonly JsonSerializerSettings _Settings = new() - { - Converters = new List - { - new PolicyIgnoreResultConverter(), - } - }; - private Dictionary _Index; + /// + /// Settings for JSON deserialization. + /// + private static readonly JsonSerializerSettings _Settings = new() + { + Converters = + [ + new PolicyIgnoreResultConverter(), + ] + }; + private Dictionary _Index; - internal Dictionary GetIndex() - { - _Index ??= ReadIndex(GetContent(RESOURCE_PATH)); - return _Index; - } + internal Dictionary GetIndex() + { + _Index ??= ReadIndex(GetContent(RESOURCE_PATH)); + return _Index; + } - private static Dictionary ReadIndex(string content) - { - return JsonConvert.DeserializeObject>(content, _Settings) ?? throw new JsonException(PSRuleResources.PolicyIgnoreInvalid); - } + private static Dictionary ReadIndex(string content) + { + return JsonConvert.DeserializeObject>(content, _Settings) ?? throw new JsonException(PSRuleResources.PolicyIgnoreInvalid); } } diff --git a/src/PSRule.Rules.Azure/Data/PolicyIgnoreResultConverter.cs b/src/PSRule.Rules.Azure/Data/PolicyIgnoreResultConverter.cs index ec011684feb..e01cb88cffd 100644 --- a/src/PSRule.Rules.Azure/Data/PolicyIgnoreResultConverter.cs +++ b/src/PSRule.Rules.Azure/Data/PolicyIgnoreResultConverter.cs @@ -5,47 +5,46 @@ using System.Collections.Generic; using Newtonsoft.Json; -namespace PSRule.Rules.Azure.Data +namespace PSRule.Rules.Azure.Data; + +internal sealed class PolicyIgnoreResultConverter : JsonConverter { - internal sealed class PolicyIgnoreResultConverter : JsonConverter + public override bool CanConvert(Type objectType) { - public override bool CanConvert(Type objectType) - { - return objectType == typeof(Dictionary); - } + return objectType == typeof(Dictionary); + } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType != JsonToken.StartArray) - return null; + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType != JsonToken.StartArray) + return null; - reader.Read(); - var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + reader.Read(); + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); - while (reader.TokenType != JsonToken.EndArray) + while (reader.TokenType != JsonToken.EndArray) + { + var entry = serializer.Deserialize(reader); + for (var i = 0; i < entry.PolicyDefinitionIds.Length; i++) { - var entry = serializer.Deserialize(reader); - for (var i = 0; i < entry.PolicyDefinitionIds.Length; i++) + if (!result.TryGetValue(entry.PolicyDefinitionIds[i], out var ignoreResult)) { - if (!result.TryGetValue(entry.PolicyDefinitionIds[i], out var ignoreResult)) + ignoreResult = new PolicyIgnoreResult { - ignoreResult = new PolicyIgnoreResult - { - Reason = entry.Reason, - Value = new List() - }; - result.Add(entry.PolicyDefinitionIds[i], ignoreResult); - } - ignoreResult.Value.Add(entry.Value); + Reason = entry.Reason, + Value = [] + }; + result.Add(entry.PolicyDefinitionIds[i], ignoreResult); } - reader.Read(); + ignoreResult.Value.Add(entry.Value); } - return result; + reader.Read(); } + return result; + } - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new NotImplementedException(); - } + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); } } diff --git a/src/PSRule.Rules.Azure/Data/ProviderData.cs b/src/PSRule.Rules.Azure/Data/ProviderData.cs index eab7639bfa4..4e64ac5775e 100644 --- a/src/PSRule.Rules.Azure/Data/ProviderData.cs +++ b/src/PSRule.Rules.Azure/Data/ProviderData.cs @@ -7,92 +7,85 @@ using Newtonsoft.Json; using PSRule.Rules.Azure.Resources; -namespace PSRule.Rules.Azure.Data +namespace PSRule.Rules.Azure.Data; + +/// +/// Defines a datastore of Azure resource provider data. +/// +internal sealed class ProviderData(bool useCache = true) : ResourceLoader() { + private const char SLASH = '/'; + private const string INDEX_PATH = "types_index.min.json.deflated"; + /// - /// Defines a datastore of Azure resource provider data. + /// Settings for JSON deserialization. /// - internal sealed class ProviderData : ResourceLoader + private static readonly JsonSerializerSettings _Settings = new() { - private const char SLASH = '/'; - private const string INDEX_PATH = "types_index.min.json.deflated"; - - /// - /// Settings for JSON deserialization. - /// - private static readonly JsonSerializerSettings _Settings = new() - { - Converters = new List - { - new TypeIndexConverter(), - new ReadOnlyDictionaryConverter(StringComparer.OrdinalIgnoreCase), - } - }; - private readonly Dictionary _ProviderCache; - private TypeIndex _Index; - - public ProviderData(bool useCache = true) - : base() - { - _ProviderCache = useCache ? new Dictionary() : null; - } + Converters = + [ + new TypeIndexConverter(), + new ReadOnlyDictionaryConverter(StringComparer.OrdinalIgnoreCase), + ] + }; + private readonly Dictionary _ProviderCache = useCache ? new Dictionary() : null; + private TypeIndex _Index; - public TypeIndexEntry FindResourceType(string provider, string resourceType) - { - return GetIndex().Resources.TryGetValue(string.Concat(provider, SLASH, resourceType), out var entry) ? entry : null; - } + public TypeIndexEntry FindResourceType(string provider, string resourceType) + { + return GetIndex().Resources.TryGetValue(string.Concat(provider, SLASH, resourceType), out var entry) ? entry : null; + } - public bool TryResourceType(TypeIndexEntry entry, out ResourceProviderType type) - { - return TryLoadType(entry, out type); - } + public bool TryResourceType(TypeIndexEntry entry, out ResourceProviderType type) + { + return TryLoadType(entry, out type); + } - public bool TryResourceType(string provider, string resourceType, out ResourceProviderType type) - { - type = null; - return GetIndex().Resources.TryGetValue(string.Concat(provider, SLASH, resourceType), out var entry) && TryLoadType(entry, out type); - } + public bool TryResourceType(string provider, string resourceType, out ResourceProviderType type) + { + type = null; + return GetIndex().Resources.TryGetValue(string.Concat(provider, SLASH, resourceType), out var entry) && TryLoadType(entry, out type); + } - public ResourceProviderType[] GetProviderTypes(string provider) - { - return ReadProviderTypes(TypeIndex.GetRelativePath(provider.ToLower(Thread.CurrentThread.CurrentCulture))); - } + public ResourceProviderType[] GetProviderTypes(string provider) + { + return ReadProviderTypes(TypeIndex.GetRelativePath(provider.ToLower(Thread.CurrentThread.CurrentCulture))); + } - internal TypeIndex GetIndex() - { - _Index ??= ReadIndex(GetContent(INDEX_PATH)); - return _Index; - } + internal TypeIndex GetIndex() + { + _Index ??= ReadIndex(GetContent(INDEX_PATH)); + return _Index; + } - private bool TryLoadType(TypeIndexEntry entry, out ResourceProviderType type) - { - var types = ReadProviderTypes(entry.RelativePath); - type = types[entry.Index]; - return type != null; - } + private bool TryLoadType(TypeIndexEntry entry, out ResourceProviderType type) + { + var types = ReadProviderTypes(entry.RelativePath); + type = types[entry.Index]; + return type != null; + } - /// - /// Deserialize an index from JSON. - /// - private static TypeIndex ReadIndex(string content) - { - return JsonConvert.DeserializeObject(content, _Settings) ?? throw new JsonException(PSRuleResources.ProviderIndexInvalid); - } + /// + /// Deserialize an index from JSON. + /// + private static TypeIndex ReadIndex(string content) + { + return JsonConvert.DeserializeObject(content, _Settings) ?? throw new JsonException(PSRuleResources.ProviderIndexInvalid); + } - /// - /// Deserialize resource provider types from JSON. - /// - private ResourceProviderType[] ReadProviderTypes(string name) - { - if (_ProviderCache != null && _ProviderCache.TryGetValue(name, out var types)) - return types; + /// + /// Deserialize resource provider types from JSON. + /// + private ResourceProviderType[] ReadProviderTypes(string name) + { + if (_ProviderCache != null && _ProviderCache.TryGetValue(name, out var types)) + return types; - var content = GetContent(name); - types = JsonConvert.DeserializeObject(content, _Settings) ?? throw new JsonException(PSRuleResources.ProviderContentInvalid); - if (_ProviderCache != null) - _ProviderCache[name] = types; + var content = GetContent(name); + types = JsonConvert.DeserializeObject(content, _Settings) ?? throw new JsonException(PSRuleResources.ProviderContentInvalid); + if (_ProviderCache != null) + _ProviderCache[name] = types; - return types; - } + return types; } } diff --git a/src/PSRule.Rules.Azure/Data/ResourceLoader.cs b/src/PSRule.Rules.Azure/Data/ResourceLoader.cs index da609fcd12d..4ef18548035 100644 --- a/src/PSRule.Rules.Azure/Data/ResourceLoader.cs +++ b/src/PSRule.Rules.Azure/Data/ResourceLoader.cs @@ -7,38 +7,34 @@ using System.Threading; using PSRule.Rules.Azure.Resources; -namespace PSRule.Rules.Azure.Data +namespace PSRule.Rules.Azure.Data; + +/// +/// A base class for a helper that loads resources from the assembly. +/// +public abstract class ResourceLoader { /// - /// A base class for a helper that loads resources from the assembly. + /// Create an instance of the resource loader. /// - public abstract class ResourceLoader - { - /// - /// Create an instance of the resource loader. - /// - protected internal ResourceLoader() { } - - /// - /// Get the content of a resource stream by name. - /// - /// The name of the resource stream. - /// The string content of the resource stream. - /// Returns if the name of the resource stream is null or empty. - /// Returned when the specified name does not exist as a resource stream. - protected static string GetContent(string name) - { - if (string.IsNullOrEmpty(name)) - throw new ArgumentNullException(nameof(name)); + protected internal ResourceLoader() { } - var fileStream = typeof(ResourceLoader).Assembly.GetManifestResourceStream(name); - if (fileStream is null) - throw new ArgumentException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.ManifestResourceNotFound, name), nameof(name)); + /// + /// Get the content of a resource stream by name. + /// + /// The name of the resource stream. + /// The string content of the resource stream. + /// Returns if the name of the resource stream is null or empty. + /// Returned when the specified name does not exist as a resource stream. + protected static string GetContent(string name) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentNullException(nameof(name)); - using (fileStream) - using (var decompressStream = new DeflateStream(fileStream, CompressionMode.Decompress)) - using (var streamReader = new StreamReader(decompressStream)) - return streamReader.ReadToEnd(); - } + var fileStream = typeof(ResourceLoader).Assembly.GetManifestResourceStream(name) ?? throw new ArgumentException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.ManifestResourceNotFound, name), nameof(name)); + using (fileStream) + using (var decompressStream = new DeflateStream(fileStream, CompressionMode.Decompress)) + using (var streamReader = new StreamReader(decompressStream)) + return streamReader.ReadToEnd(); } } diff --git a/src/PSRule.Rules.Azure/Data/ResourceManagerVisitor.cs b/src/PSRule.Rules.Azure/Data/ResourceManagerVisitor.cs index aa45df28242..c2e2078458e 100644 --- a/src/PSRule.Rules.Azure/Data/ResourceManagerVisitor.cs +++ b/src/PSRule.Rules.Azure/Data/ResourceManagerVisitor.cs @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace PSRule.Rules.Azure.Data +namespace PSRule.Rules.Azure.Data; + +/// +/// A base class for all Azure Resource Manager visitors that implement common methods. +/// +internal abstract class ResourceManagerVisitor { - /// - /// A base class for all Azure Resource Manager visitors that implement common methods. - /// - internal abstract class ResourceManagerVisitor - { - } } diff --git a/src/PSRule.Rules.Azure/Data/SecretTemplateData.cs b/src/PSRule.Rules.Azure/Data/SecretTemplateData.cs index 0cc25359521..0b64713381c 100644 --- a/src/PSRule.Rules.Azure/Data/SecretTemplateData.cs +++ b/src/PSRule.Rules.Azure/Data/SecretTemplateData.cs @@ -7,53 +7,52 @@ using Newtonsoft.Json.Linq; using PSRule.Rules.Azure.Resources; -namespace PSRule.Rules.Azure.Data +namespace PSRule.Rules.Azure.Data; + +/// +/// Defines a datastore of possible secret templates that can be used with the list*() functions. +/// +internal sealed class SecretTemplateData : ResourceLoader { + private const string RESOURCE_PATH = "secret-template.min.json.deflated"; + /// - /// Defines a datastore of possible secret templates that can be used with the list*() functions. + /// Settings for JSON deserialization. /// - internal sealed class SecretTemplateData : ResourceLoader + private static readonly JsonSerializerSettings _Settings = new() { - private const string RESOURCE_PATH = "secret-template.min.json.deflated"; - - /// - /// Settings for JSON deserialization. - /// - private static readonly JsonSerializerSettings _Settings = new() - { - Converters = new List - { - new ReadOnlyDictionaryConverter(StringComparer.OrdinalIgnoreCase), - } - }; + Converters = + [ + new ReadOnlyDictionaryConverter(StringComparer.OrdinalIgnoreCase), + ] + }; - private IReadOnlyDictionary _SecretTemplate; + private IReadOnlyDictionary _SecretTemplate; #nullable enable - /// - /// Get a template for a secret. - /// - /// The resource type. - /// The secret method to call. e.g. listKeys. - /// The for the template. - /// Return true when the resource type and method were found. Otherwise, false is returned. - public bool TryGetValue(string resourceType, string method, out JObject? value) - { - value = null; - _SecretTemplate ??= ReadSecretTemplate(GetContent(RESOURCE_PATH)); - return _SecretTemplate.TryGetValue(resourceType, out var type) && - type.TryObjectProperty(method, out value); - } + /// + /// Get a template for a secret. + /// + /// The resource type. + /// The secret method to call. e.g. listKeys. + /// The for the template. + /// Return true when the resource type and method were found. Otherwise, false is returned. + public bool TryGetValue(string resourceType, string method, out JObject? value) + { + value = null; + _SecretTemplate ??= ReadSecretTemplate(GetContent(RESOURCE_PATH)); + return _SecretTemplate.TryGetValue(resourceType, out var type) && + type.TryObjectProperty(method, out value); + } #nullable restore - /// - /// Deserialize a secret template from JSON. - /// - private static IReadOnlyDictionary ReadSecretTemplate(string content) - { - return JsonConvert.DeserializeObject>(content, _Settings) ?? throw new JsonException(PSRuleResources.CloudEnvironmentInvalid); - } + /// + /// Deserialize a secret template from JSON. + /// + private static IReadOnlyDictionary ReadSecretTemplate(string content) + { + return JsonConvert.DeserializeObject>(content, _Settings) ?? throw new JsonException(PSRuleResources.CloudEnvironmentInvalid); } } diff --git a/src/PSRule.Rules.Azure/Data/Template/ILazyObject.cs b/src/PSRule.Rules.Azure/Data/Template/ILazyObject.cs new file mode 100644 index 00000000000..aa4b5e23d6a --- /dev/null +++ b/src/PSRule.Rules.Azure/Data/Template/ILazyObject.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Rules.Azure.Data.Template; + +internal interface ILazyObject +{ + bool TryProperty(string propertyName, out object value); +} diff --git a/src/PSRule.Rules.Azure/Data/Template/ILazyValue.cs b/src/PSRule.Rules.Azure/Data/Template/ILazyValue.cs new file mode 100644 index 00000000000..450c1e60025 --- /dev/null +++ b/src/PSRule.Rules.Azure/Data/Template/ILazyValue.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Rules.Azure.Data.Template; + +internal interface ILazyValue +{ + object GetValue(); +} diff --git a/src/PSRule.Rules.Azure/Data/Template/LazyValue.cs b/src/PSRule.Rules.Azure/Data/Template/LazyValue.cs new file mode 100644 index 00000000000..dc722f71193 --- /dev/null +++ b/src/PSRule.Rules.Azure/Data/Template/LazyValue.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace PSRule.Rules.Azure.Data.Template; + +internal sealed class LazyValue(Func value) : ILazyValue +{ + private readonly Func _Value = value; + + public object GetValue() + { + return _Value(); + } +} diff --git a/src/PSRule.Rules.Azure/Data/TypeIndex.cs b/src/PSRule.Rules.Azure/Data/TypeIndex.cs index 5119c7ce5e8..e9e09cc0426 100644 --- a/src/PSRule.Rules.Azure/Data/TypeIndex.cs +++ b/src/PSRule.Rules.Azure/Data/TypeIndex.cs @@ -3,26 +3,21 @@ using System.Collections.Generic; -namespace PSRule.Rules.Azure.Data +namespace PSRule.Rules.Azure.Data; + +/// +/// Defines an index of Azure resource provider types. +/// +internal sealed class TypeIndex(IReadOnlyDictionary resources) { + /// - /// Defines an index of Azure resource provider types. + /// Available resource types, indexed by resource type name. /// - internal sealed class TypeIndex - { - public TypeIndex(IReadOnlyDictionary resources) - { - Resources = resources; - } + public IReadOnlyDictionary Resources { get; } = resources; - /// - /// Available resource types, indexed by resource type name. - /// - public IReadOnlyDictionary Resources { get; } - - public static string GetRelativePath(string provider) - { - return string.Concat("providers/", provider, "/types.min.json.deflated"); - } + public static string GetRelativePath(string provider) + { + return string.Concat("providers/", provider, "/types.min.json.deflated"); } } diff --git a/src/PSRule.Rules.Azure/DictionaryExtensions.cs b/src/PSRule.Rules.Azure/DictionaryExtensions.cs new file mode 100644 index 00000000000..bb8644ccbad --- /dev/null +++ b/src/PSRule.Rules.Azure/DictionaryExtensions.cs @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Management.Automation; + +namespace PSRule.Rules.Azure; + +/// +/// Extension methods for dictionary instances. +/// +internal static class DictionaryExtensions +{ + [DebuggerStepThrough] + public static bool TryPopValue(this IDictionary dictionary, string key, out object value) + { + return dictionary.TryGetValue(key, out value) && dictionary.Remove(key); + } + + [DebuggerStepThrough] + public static bool TryPopValue(this IDictionary dictionary, string key, out T value) + { + value = default; + if (dictionary.TryGetValue(key, out var v) && dictionary.Remove(key) && v is T result) + { + value = result; + return true; + } + return false; + } + + public static bool TryPopHashtable(this IDictionary dictionary, string key, out Hashtable value) + { + value = null; + if (dictionary.TryPopValue(key, out var o) && o is Hashtable result) + { + value = result; + return true; + } + if (dictionary.TryPopValue(key, out PSObject pso)) + { + value = pso.ToHashtable(); + return true; + } + + return false; + } + + [DebuggerStepThrough] + public static bool TryGetValue(this IDictionary dictionary, string key, out T value) + { + value = default; + if (dictionary.TryGetValue(key, out var v) && v is T result) + { + value = result; + return true; + } + return false; + } + + [DebuggerStepThrough] + public static bool TryGetValue(this IDictionary dictionary, string key, out object value) + { + value = default; + if (dictionary.Contains(key)) + { + value = dictionary[key]; + return true; + } + return false; + } + + [DebuggerStepThrough] + public static bool TryPopBool(this IDictionary dictionary, string key, out bool value) + { + value = default; + return dictionary.TryGetValue(key, out var v) && dictionary.Remove(key) && bool.TryParse(v.ToString(), out value); + } + + public static bool TryGetString(this IDictionary dictionary, string key, out string value) + { + value = null; + if (!dictionary.TryGetValue(key, out var o)) + return false; + + if (o is string result) + { + value = result; + return true; + } + return false; + } + + public static bool TryGetBool(this IDictionary dictionary, string key, out bool? value) + { + value = null; + if (!dictionary.TryGetValue(key, out var o)) + return false; + + if (o is bool result || (o is string svalue && bool.TryParse(svalue, out result))) + { + value = result; + return true; + } + return false; + } + + public static bool TryGetLong(this IDictionary dictionary, string key, out long? value) + { + value = null; + if (!dictionary.TryGetValue(key, out var o)) + return false; + + if (o is long result || (o is string svalue && long.TryParse(svalue, out result))) + { + value = result; + return true; + } + return false; + } + + /// + /// Add an item to the dictionary if it doesn't already exist in the dictionary. + /// + [DebuggerStepThrough] + public static void AddUnique(this IDictionary dictionary, IEnumerable> values) + { + if (values == null || dictionary == null) + return; + + foreach (var kv in values) + { + if (!dictionary.ContainsKey(kv.Key)) + dictionary.Add(kv.Key, kv.Value); + } + } +} diff --git a/src/PSRule.Rules.Azure/EnvironmentHelper.cs b/src/PSRule.Rules.Azure/EnvironmentHelper.cs new file mode 100644 index 00000000000..aece5622257 --- /dev/null +++ b/src/PSRule.Rules.Azure/EnvironmentHelper.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace PSRule.Rules.Azure; + +/// +/// Helper to read configuration from environment variables. +/// +/// +/// This can be replaced with helpers from PSRule v3 in the future. +/// +internal sealed class EnvironmentHelper +{ + public static readonly EnvironmentHelper Default = new(); + + internal bool TryBool(string key, out bool value) + { + value = default; + return TryVariable(key, out var variable) && TryParseBool(variable, out value); + } + + private static bool TryVariable(string key, out string variable) + { + variable = Environment.GetEnvironmentVariable(key); + return variable != null; + } + + private static bool TryParseBool(string variable, out bool value) + { + if (bool.TryParse(variable, out value)) + return true; + + if (int.TryParse(variable, out var ivalue)) + { + value = ivalue > 0; + return true; + } + return false; + } +} diff --git a/src/PSRule.Rules.Azure/JsonConverters.cs b/src/PSRule.Rules.Azure/JsonConverters.cs new file mode 100644 index 00000000000..9cd4fcb50f1 --- /dev/null +++ b/src/PSRule.Rules.Azure/JsonConverters.cs @@ -0,0 +1,325 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Management.Automation; +using Newtonsoft.Json; +using PSRule.Rules.Azure.Data.Policy; +using PSRule.Rules.Azure.Pipeline; +using PSRule.Rules.Azure.Resources; + +namespace PSRule.Rules.Azure; + +/// +/// A custom serializer to correctly convert PSObject properties to JSON instead of CLIXML. +/// +internal sealed class PSObjectJsonConverter : JsonConverter +{ + public override bool CanConvert(Type objectType) + { + return objectType == typeof(PSObject); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value is not PSObject obj) + throw new ArgumentException(message: PSRuleResources.SerializeNullPSObject, paramName: nameof(value)); + + if (value is FileSystemInfo fileSystemInfo) + { + WriteFileSystemInfo(writer, fileSystemInfo, serializer); + return; + } + writer.WriteStartObject(); + foreach (var property in obj.Properties) + { + // Ignore properties that are not readable or can cause race condition + if (!property.IsGettable || property.Value is PSDriveInfo || property.Value is ProviderInfo || property.Value is DirectoryInfo) + continue; + + writer.WritePropertyName(property.Name); + serializer.Serialize(writer, property.Value); + } + writer.WriteEndObject(); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + // Create target object based on JObject + var result = existingValue as PSObject ?? new PSObject(); + + // Read tokens + ReadObject(value: result, reader: reader); + return result; + } + + private static void ReadObject(PSObject value, JsonReader reader) + { + if (reader.TokenType != JsonToken.StartObject) + throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed); + + reader.Read(); + string name = null; + + // Read each token + while (reader.TokenType != JsonToken.EndObject) + { + switch (reader.TokenType) + { + case JsonToken.PropertyName: + name = reader.Value.ToString(); + break; + + case JsonToken.StartObject: + var child = new PSObject(); + ReadObject(value: child, reader: reader); + value.Properties.Add(new PSNoteProperty(name: name, value: child)); + break; + + case JsonToken.StartArray: + var items = new List(); + reader.Read(); + + while (reader.TokenType != JsonToken.EndArray) + { + items.Add(ReadValue(reader)); + reader.Read(); + } + + value.Properties.Add(new PSNoteProperty(name: name, value: items.ToArray())); + break; + + default: + value.Properties.Add(new PSNoteProperty(name: name, value: reader.Value)); + break; + } + reader.Read(); + } + } + + private static object ReadValue(JsonReader reader) + { + if (reader.TokenType != JsonToken.StartObject) + return reader.Value; + + var value = new PSObject(); + ReadObject(value, reader); + return value; + } + + private static void WriteFileSystemInfo(JsonWriter writer, FileSystemInfo value, JsonSerializer serializer) + { + serializer.Serialize(writer, value.FullName); + } +} + +/// +/// A custom serializer to convert PSObjects that may or maynot be in a JSON array to an a PSObject array. +/// +internal sealed class PSObjectArrayJsonConverter : JsonConverter +{ + public override bool CanConvert(Type objectType) + { + return objectType == typeof(PSObject[]); + } + + public override bool CanWrite => false; + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType is not JsonToken.StartObject and not JsonToken.StartArray) + throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed); + + var result = new List(); + var isArray = reader.TokenType == JsonToken.StartArray; + + if (isArray) + reader.Read(); + + while (!isArray || (isArray && reader.TokenType != JsonToken.EndArray)) + { + var value = ReadObject(reader: reader); + result.Add(value); + + // Consume the EndObject token + if (isArray) + { + reader.Read(); + } + } + return result.ToArray(); + } + + private static PSObject ReadObject(JsonReader reader) + { + if (reader.TokenType != JsonToken.StartObject) + throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed); + + reader.Read(); + var result = new PSObject(); + string name = null; + + // Read each token + while (reader.TokenType != JsonToken.EndObject) + { + switch (reader.TokenType) + { + case JsonToken.PropertyName: + name = reader.Value.ToString(); + break; + + case JsonToken.StartObject: + var value = ReadObject(reader: reader); + result.Properties.Add(new PSNoteProperty(name: name, value: value)); + break; + + case JsonToken.StartArray: + var items = ReadArray(reader: reader); + result.Properties.Add(new PSNoteProperty(name: name, value: items)); + + break; + + default: + result.Properties.Add(new PSNoteProperty(name: name, value: reader.Value)); + break; + } + reader.Read(); + } + return result; + } + + private static PSObject[] ReadArray(JsonReader reader) + { + if (reader.TokenType != JsonToken.StartArray) + throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed); + + reader.Read(); + var result = new List(); + + while (reader.TokenType != JsonToken.EndArray) + { + if (reader.TokenType == JsonToken.StartObject) + { + result.Add(ReadObject(reader: reader)); + } + else if (reader.TokenType == JsonToken.StartArray) + { + result.Add(PSObject.AsPSObject(ReadArray(reader))); + } + else + { + result.Add(PSObject.AsPSObject(reader.Value)); + } + reader.Read(); + } + return [.. result]; + } +} + +internal sealed class PolicyDefinitionConverter : JsonConverter +{ + public override bool CanConvert(Type objectType) + { + return objectType == typeof(PolicyDefinition); + } + + public override bool CanWrite => true; + + public override bool CanRead => false; + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + PolicyJsonRuleMapper.MapRule(writer, serializer, value as PolicyDefinition); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new NotImplementedException(); + } +} + +internal sealed class PolicyBaselineConverter : JsonConverter +{ + private const string SYNOPSIS_COMMENT = "Synopsis: "; + private const string PROPERTY_APIVERSION = "apiVersion"; + private const string APIVERSION_VALUE = "github.com/microsoft/PSRule/v1"; + private const string PROPERTY_KIND = "kind"; + private const string KIND_VALUE = "Baseline"; + private const string PROPERTY_METADATA = "metadata"; + private const string PROPERTY_NAME = "name"; + private const string PROPERTY_SPEC = "spec"; + private const string PROPERTY_RULE = "rule"; + private const string PROPERTY_INCLUDE = "include"; + + public override bool CanConvert(Type objectType) + { + throw new NotImplementedException(); + } + + public override bool CanWrite => true; + + public override bool CanRead => false; + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + Map(writer, serializer, value as PolicyBaseline); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + private static void Map(JsonWriter writer, JsonSerializer serializer, PolicyBaseline baseline) + { + writer.WriteStartObject(); + + // Synopsis + writer.WriteComment(string.Concat(SYNOPSIS_COMMENT, baseline.Description)); + + // Api Version + writer.WritePropertyName(PROPERTY_APIVERSION); + writer.WriteValue(APIVERSION_VALUE); + + // Kind + writer.WritePropertyName(PROPERTY_KIND); + writer.WriteValue(KIND_VALUE); + + // Metadata + writer.WritePropertyName(PROPERTY_METADATA); + writer.WriteStartObject(); + writer.WritePropertyName(PROPERTY_NAME); + writer.WriteValue(baseline.Name); + writer.WriteEndObject(); + + // Spec + writer.WritePropertyName(PROPERTY_SPEC); + writer.WriteStartObject(); + WriteRule(writer, serializer, baseline); + + writer.WriteEndObject(); + } + + private static void WriteRule(JsonWriter writer, JsonSerializer serializer, PolicyBaseline baseline) + { + if (baseline.Include == null || baseline.Include.Length == 0) + return; + + var types = new HashSet(baseline.Include, StringComparer.OrdinalIgnoreCase); + writer.WritePropertyName(PROPERTY_RULE); + writer.WriteStartObject(); + + writer.WritePropertyName(PROPERTY_INCLUDE); + serializer.Serialize(writer, types); + writer.WriteEndObject(); + + writer.WriteEndObject(); + } +} diff --git a/src/PSRule.Rules.Azure/JsonExtensions.cs b/src/PSRule.Rules.Azure/JsonExtensions.cs new file mode 100644 index 00000000000..f6d37a09e97 --- /dev/null +++ b/src/PSRule.Rules.Azure/JsonExtensions.cs @@ -0,0 +1,637 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PSRule.Rules.Azure.Data.Template; + +namespace PSRule.Rules.Azure; + +internal static class JsonExtensions +{ + private const string PROPERTY_DEPENDS_ON = "dependsOn"; + private const string PROPERTY_RESOURCES = "resources"; + private const string PROPERTY_NAME = "name"; + private const string PROPERTY_TYPE = "type"; + private const string PROPERTY_FIELD = "field"; + private const string PROPERTY_EXISTING = "existing"; + private const string PROPERTY_IMPORT = "import"; + private const string PROPERTY_SCOPE = "scope"; + private const string PROPERTY_SUBSCRIPTION_ID = "subscriptionId"; + private const string PROPERTY_RESOURCE_GROUP = "resourceGroup"; + + private const string TARGETINFO_KEY = "_PSRule"; + private const string TARGETINFO_SOURCE = "source"; + private const string TARGETINFO_FILE = "file"; + private const string TARGETINFO_LINE = "line"; + private const string TARGETINFO_POSITION = "position"; + private const string TARGETINFO_TYPE = "type"; + private const string TARGETINFO_TYPE_TEMPLATE = "Template"; + private const string TARGETINFO_TYPE_PARAMETER = "Parameter"; + private const string TARGETINFO_ISSUE = "issue"; + private const string TARGETINFO_NAME = "name"; + private const string TARGETINFO_PATH = "path"; + private const string TARGETINFO_MESSAGE = "message"; + + private const string TENANT_SCOPE = "/"; + + private static readonly string[] JSON_PATH_SEPARATOR = ["."]; + + internal static IJsonLineInfo TryLineInfo(this JToken token) + { + if (token == null) + return null; + + var annotation = token.Annotation(); + return annotation ?? (IJsonLineInfo)token; + } + + internal static JToken CloneTemplateJToken(this JToken token) + { + if (token == null) + return null; + + var annotation = token.UseTokenAnnotation(); + var clone = token.DeepClone(); + if (annotation != null) + clone.AddAnnotation(annotation); + + return clone; + } + + internal static void CopyTemplateAnnotationFrom(this JToken token, JToken source) + { + if (token == null || source == null) + return; + + var annotation = source.Annotation(); + if (annotation != null) + token.AddAnnotation(annotation); + } + + internal static bool TryGetResources(this JObject resource, out JObject[] resources) + { + if (!resource.TryGetProperty(PROPERTY_RESOURCES, out var jArray) || jArray.Count == 0) + { + resources = null; + return false; + } + resources = jArray.Values().ToArray(); + return true; + } + + internal static bool TryGetResources(this JObject resource, string type, out JObject[] resources) + { + if (!resource.TryGetProperty(PROPERTY_RESOURCES, out var jArray) || jArray.Count == 0) + { + resources = null; + return false; + } + var results = new List(); + foreach (var item in jArray.Values()) + if (item.PropertyEquals(PROPERTY_TYPE, type)) + results.Add(item); + + resources = results.Count > 0 ? results.ToArray() : null; + return results.Count > 0; + } + + internal static bool TryGetResourcesArray(this JObject resource, out JArray resources) + { + if (resource.TryGetProperty(PROPERTY_RESOURCES, out resources)) + return true; + + return false; + } + + internal static bool PropertyEquals(this JObject o, string propertyName, string value) + { + return o.TryGetProperty(propertyName, out var s) && string.Equals(s, value, StringComparison.OrdinalIgnoreCase); + } + + internal static bool ResourceNameEquals(this JObject o, string name) + { + if (!o.TryGetProperty(PROPERTY_NAME, out var n)) + return false; + + n = n.SplitLastSegment('/'); + return string.Equals(n, name, StringComparison.OrdinalIgnoreCase); + } + + internal static bool ContainsKeyInsensitive(this JObject o, string propertyName) + { + return o.TryGetValue(propertyName, StringComparison.OrdinalIgnoreCase, out _); + } + + /// + /// Determine if the token is a value. + /// + internal static bool HasValue(this JToken o) + { + return o.Type is JTokenType.String or + JTokenType.Integer or + JTokenType.Object or + JTokenType.Array or + JTokenType.Boolean or + JTokenType.Bytes or + JTokenType.Date or + JTokenType.Float or + JTokenType.Guid or + JTokenType.TimeSpan or + JTokenType.Uri; + } + + /// + /// Add items to the array. + /// + /// The to add items to. + /// A set of items to add. + internal static void AddRange(this JArray o, IEnumerable items) + { + foreach (var item in items) + o.Add(item); + } + + /// + /// Add items to the start of the array instead of the end. + /// + /// The to add items to. + /// A set of items to add. + internal static void AddRangeFromStart(this JArray o, IEnumerable items) + { + var counter = 0; + foreach (var item in items) + o.Insert(counter++, item); + } + + internal static IEnumerable GetPeerConditionByField(this JObject o, string field) + { + return o.BeforeSelf().OfType().Where(peer => peer.TryGetProperty(PROPERTY_FIELD, out var peerField) && + string.Equals(field, peerField, StringComparison.OrdinalIgnoreCase)); + } + + internal static bool TryGetProperty(this JObject o, string propertyName, out TValue value) where TValue : JToken + { + value = null; + if (o.TryGetValue(propertyName, StringComparison.OrdinalIgnoreCase, out var v) && v.Type != JTokenType.Null) + { + value = v.Value(); + return value != null; + } + return false; + } + + internal static bool TryGetProperty(this JObject o, string propertyName, out string value) + { + value = null; + if (!TryGetProperty(o, propertyName, out var v)) + return false; + + value = v.Value(); + return true; + } + + internal static bool TryGetProperty(this JProperty property, string propertyName, out string value) + { + value = null; + if (property == null || property.Value.Type != JTokenType.String || !property.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)) + return false; + + value = property.Value.Value(); + return true; + } + + internal static bool TryRenameProperty(this JObject o, string oldName, string newName) + { + var p = o.Property(oldName, StringComparison.OrdinalIgnoreCase); + if (p != null) + { + p.Replace(new JProperty(newName, p.Value)); + return true; + } + return false; + } + + internal static void ReplaceProperty(this JObject o, string propertyName, TValue value) where TValue : JToken + { + var p = o.Property(propertyName, StringComparison.OrdinalIgnoreCase); + if (p != null) + p.Value = value; + } + + internal static void ReplaceProperty(this JObject o, string propertyName, string value) + { + var p = o.Property(propertyName, StringComparison.OrdinalIgnoreCase); + if (p != null) + p.Value = JValue.CreateString(value); + } + + internal static void ReplaceProperty(this JObject o, string propertyName, bool value) + { + var p = o.Property(propertyName, StringComparison.OrdinalIgnoreCase); + if (p != null) + p.Value = new JValue(value); + } + + internal static void ReplaceProperty(this JObject o, string propertyName, int value) + { + var p = o.Property(propertyName, StringComparison.OrdinalIgnoreCase); + if (p != null) + p.Value = new JValue(value); + } + + internal static void RemoveProperty(this JObject o, string propertyName) + { + var p = o.Property(propertyName, StringComparison.OrdinalIgnoreCase); + p?.Remove(); + } + + /// + /// Convert a string property to an integer. + /// + /// The target object with properties. + /// The name of the property to convert. + internal static void ConvertPropertyToInt(this JObject o, string propertyName) + { + if (o.TryStringProperty(propertyName, out var s) && int.TryParse(s, out var value)) + o.ReplaceProperty(propertyName, value); + } + + /// + /// Convert a string property to a boolean. + /// + /// The target object with properties. + /// The name of the property to convert. + internal static void ConvertPropertyToBool(this JObject o, string propertyName) + { + if (o.TryStringProperty(propertyName, out var s) && bool.TryParse(s, out var value)) + o.ReplaceProperty(propertyName, value); + } + + internal static bool TryRenameProperty(this JProperty property, string oldName, string newName) + { + if (property == null || !property.Name.Equals(oldName, StringComparison.OrdinalIgnoreCase)) + return false; + + property.Parent[newName] = property.Value; + property.Remove(); + return true; + } + + internal static bool TryRenameProperty(this JProperty property, string newName) + { + if (property == null || property.Name == newName) + return false; + + property.Parent[newName] = property.Value; + property.Remove(); + return true; + } + + internal static void UseProperty(this JObject o, string propertyName, out TValue value) where TValue : JToken, new() + { + if (!o.TryGetValue(propertyName, StringComparison.OrdinalIgnoreCase, out var v)) + { + value = new TValue(); + o.Add(propertyName, value); + return; + } + value = (TValue)v; + } + + internal static void UseTokenAnnotation(this JToken[] token, string parentPath = null, string propertyPath = null) + { + for (var i = 0; token != null && i < token.Length; i++) + token[i].UseTokenAnnotation(parentPath, string.Concat(propertyPath, '[', i, ']')); + } + + internal static TemplateTokenAnnotation UseTokenAnnotation(this JToken token, string parentPath = null, string propertyPath = null) + { + var annotation = token.Annotation(); + if (annotation != null) + return annotation; + + if (token is IJsonLineInfo lineInfo && lineInfo.HasLineInfo()) + annotation = new TemplateTokenAnnotation(lineInfo.LineNumber, lineInfo.LinePosition, JsonPathJoin(parentPath, propertyPath ?? token.Path)); + else + annotation = new TemplateTokenAnnotation(0, 0, JsonPathJoin(parentPath, propertyPath ?? token.Path)); + + token.AddAnnotation(annotation); + return annotation; + } + + private static string JsonPathJoin(string parent, string child) + { + if (string.IsNullOrEmpty(parent)) + return child; + + return string.IsNullOrEmpty(child) ? parent : string.Concat(parent, ".", child); + } + + /// + /// Get the dependencies for a resource by extracting any identifiers configured on the dependsOn property. + /// + /// The resource object to check. + /// An array of dependencies if set. + /// + internal static bool TryGetDependencies(this JObject resource, out string[] dependencies) + { + dependencies = null; + if (!(resource.ContainsKey(PROPERTY_DEPENDS_ON) && resource[PROPERTY_DEPENDS_ON] is JArray d && d.Count > 0)) + return false; + + dependencies = d.Values().ToArray(); + return true; + } + + internal static string GetResourcePath(this JObject resource, int parentLevel = 0) + { + var annotation = resource.Annotation(); + var path = annotation?.Path ?? resource.Path; + if (parentLevel == 0 || string.IsNullOrEmpty(path)) + return path; + + var parts = path.Split(JSON_PATH_SEPARATOR, StringSplitOptions.None); + return parts.Length > parentLevel ? string.Join(JSON_PATH_SEPARATOR[0], parts, 0, parts.Length - parentLevel) : string.Empty; + } + + internal static void SetTargetInfo(this JObject resource, string templateFile, string parameterFile, string path = null) + { + // Get line information. + var lineInfo = resource.TryLineInfo(); + + // Populate target info. + resource.UseProperty(TARGETINFO_KEY, out JObject targetInfo); + + // Path. + path ??= resource.GetResourcePath(); + targetInfo.Add(TARGETINFO_PATH, path); + + var sources = new JArray(); + + // Template file. + if (!string.IsNullOrEmpty(templateFile)) + { + var source = new JObject + { + [TARGETINFO_FILE] = templateFile, + [TARGETINFO_TYPE] = TARGETINFO_TYPE_TEMPLATE + }; + if (lineInfo.HasLineInfo()) + { + source[TARGETINFO_LINE] = lineInfo.LineNumber; + source[TARGETINFO_POSITION] = lineInfo.LinePosition; + } + sources.Add(source); + } + + // Parameter file. + if (!string.IsNullOrEmpty(parameterFile)) + { + var source = new JObject + { + [TARGETINFO_FILE] = parameterFile, + [TARGETINFO_TYPE] = TARGETINFO_TYPE_PARAMETER + }; + if (lineInfo.HasLineInfo()) + { + source[TARGETINFO_LINE] = 1; + } + sources.Add(source); + } + targetInfo.Add(TARGETINFO_SOURCE, sources); + } + + internal static void SetValidationIssue(this JObject resource, string issueId, string name, string path, string message, params object[] args) + { + // Populate target info + resource.UseProperty(TARGETINFO_KEY, out JObject targetInfo); + + var issues = targetInfo.ContainsKey(TARGETINFO_ISSUE) ? targetInfo.Value(TARGETINFO_ISSUE) : new JArray(); + + // Format message + message = args.Length > 0 ? string.Format(Thread.CurrentThread.CurrentCulture, message, args) : message; + + // Build issue + var issue = new JObject + { + [TARGETINFO_TYPE] = issueId, + [TARGETINFO_NAME] = name, + [TARGETINFO_PATH] = path, + [TARGETINFO_MESSAGE] = message, + }; + issues.Add(issue); + targetInfo[TARGETINFO_ISSUE] = issues; + } + + internal static bool TryArrayProperty(this JObject obj, string propertyName, out JArray propertyValue) + { + propertyValue = null; + if (!obj.TryGetValue(propertyName, StringComparison.OrdinalIgnoreCase, out var value) || value.Type != JTokenType.Array) + return false; + + propertyValue = value.Value(); + return true; + } + + internal static bool TryObjectProperty(this JObject obj, string propertyName, out JObject propertyValue) + { + propertyValue = null; + if (!obj.TryGetValue(propertyName, StringComparison.OrdinalIgnoreCase, out var value) || value.Type != JTokenType.Object) + return false; + + propertyValue = value as JObject; + return true; + } + + internal static bool TryStringProperty(this JObject obj, string propertyName, out string propertyValue) + { + propertyValue = null; + if (!obj.TryGetValue(propertyName, StringComparison.OrdinalIgnoreCase, out var value) || value.Type != JTokenType.String) + return false; + + propertyValue = value.Value(); + return true; + } + + internal static bool TryBoolProperty(this JObject o, string propertyName, out bool? value) + { + value = null; + if (o.TryGetValue(propertyName, StringComparison.OrdinalIgnoreCase, out var token) && token.Type == JTokenType.Boolean) + { + value = token.Value(); + return value != null; + } + else if (token != null && token.Type == JTokenType.String && bool.TryParse(token.Value(), out var v)) + { + value = v; + return true; + } + return false; + } + + internal static bool TryIntegerProperty(this JObject o, string propertyName, out long? value) + { + value = null; + if (o.TryGetValue(propertyName, StringComparison.OrdinalIgnoreCase, out var token) && token.Type == JTokenType.Integer) + { + value = token.Value(); + return value != null; + } + else if (token != null && token.Type == JTokenType.String && long.TryParse(token.Value(), out var v)) + { + value = v; + return true; + } + return false; + } + + /// + /// Check if the script uses an expression. + /// + internal static bool IsExpressionString(this JToken token) + { + if (token == null || token.Type != JTokenType.String) + return false; + + var value = token.Value(); + return value != null && value.IsExpressionString(); + } + + internal static bool IsEmpty(this JToken value) + { + return value.Type == JTokenType.None || + value.Type == JTokenType.Null || + (value.Type == JTokenType.String && string.IsNullOrEmpty(value.Value())); + } + + /// + /// Resources with the existing property set to true are references to existing resources. + /// + /// The resource . + /// Returns true if the resource is a reference to an existing resource. + internal static bool IsExisting(this JObject o) + { + return o != null && o.TryBoolProperty(PROPERTY_EXISTING, out var existing) && existing != null && existing.Value; + } + + /// + /// Resources with an import property set to a non-empty string refer to extensibility provided resources. + /// For example, Microsoft Graph. + /// + /// The resource . + /// Returns true if the resource is imported from an extensibility provider. + internal static bool IsImport(this JObject o) + { + return o != null && o.TryStringProperty(PROPERTY_IMPORT, out var s) && !string.IsNullOrEmpty(s); + } + + internal static bool TryResourceType(this JObject o, out string type) + { + type = default; + return o != null && o.TryGetProperty(PROPERTY_TYPE, out type); + } + + internal static bool TryResourceName(this JObject o, out string name) + { + name = default; + return o != null && o.TryGetProperty(PROPERTY_NAME, out name); + } + + internal static bool TryResourceNameAndType(this JObject o, out string name, out string type) + { + name = default; + type = default; + return o != null && o.TryResourceName(out name) && o.TryResourceType(out type); + } + + /// + /// Read the scope from a specified scope property. + /// + /// The resource object. + /// A valid context to resolve properties. + /// The scope if set. + /// Returns true if the scope property was set on the resource. + internal static bool TryResourceScope(this JObject resource, ITemplateContext context, out string scopeId) + { + scopeId = default; + if (resource == null) + return false; + + return TryExplicitScope(resource, context, out scopeId) + || TryExplicitSubscriptionResourceGroupScope(resource, context, out scopeId) + || TryParentScope(resource, context, out scopeId) + || TryDeploymentScope(context, out scopeId); + } + + private static bool TryExplicitScope(JObject resource, ITemplateContext context, out string scopeId) + { + scopeId = context.ExpandProperty(resource, PROPERTY_SCOPE); + if (string.IsNullOrEmpty(scopeId)) + return false; + + // Check for full scope. + ResourceHelper.ResourceIdComponents(scopeId, out var tenant, out var managementGroup, out var subscriptionId, out var resourceGroup, out var resourceType, out var resourceName); + if (tenant != null || managementGroup != null || subscriptionId != null) + return true; + + scopeId = ResourceHelper.ResourceId(resourceType, resourceName, scopeId: context.ScopeId); + return true; + } + + /// + /// Get the scope from subscriptionId and resourceGroup properties set on the resource. + /// + private static bool TryExplicitSubscriptionResourceGroupScope(JObject resource, ITemplateContext context, out string scopeId) + { + var subscriptionId = context.ExpandProperty(resource, PROPERTY_SUBSCRIPTION_ID); + var resourceGroup = context.ExpandProperty(resource, PROPERTY_RESOURCE_GROUP); + + // Fill subscriptionId if resourceGroup is specified. + if (!string.IsNullOrEmpty(resourceGroup) && string.IsNullOrEmpty(subscriptionId)) + subscriptionId = context.Subscription.SubscriptionId; + + scopeId = !string.IsNullOrEmpty(subscriptionId) ? ResourceHelper.ResourceId(scopeTenant: null, scopeManagementGroup: null, scopeSubscriptionId: subscriptionId, scopeResourceGroup: resourceGroup, resourceType: null, resourceName: null) : null; + return scopeId != null; + } + + /// + /// Read the scope from the name and type properties if this is a sub-resource. + /// For example: A sub-resource may use name segments such as vnet-1/subnet-1. + /// + /// The resource object. + /// A valid context to resolve properties. + /// The calculated scope. + /// Returns true if the scope could be calculated from name segments. + private static bool TryParentScope(JObject resource, ITemplateContext context, out string scopeId) + { + scopeId = null; + var name = context.ExpandProperty(resource, PROPERTY_NAME); + var type = context.ExpandProperty(resource, PROPERTY_TYPE); + + if (string.IsNullOrEmpty(name) || + string.IsNullOrEmpty(type) || + !ResourceHelper.TryResourceIdComponents(type, name, out var resourceTypeComponents, out var nameComponents) || + resourceTypeComponents.Length == 1) + return false; + + scopeId = ResourceHelper.GetParentResourceId(context.Subscription.SubscriptionId, context.ResourceGroup.Name, resourceTypeComponents, nameComponents); + return true; + } + + /// + /// Get the scope of the resource based on the scope of the deployment. + /// + /// A valid context to resolve the deployment scope. + /// The scope of the deployment. + /// Returns true if a deployment scope was found. + private static bool TryDeploymentScope(ITemplateContext context, out string scopeId) + { + scopeId = context.Deployment?.Scope; + return scopeId != null; + } +} diff --git a/src/PSRule.Rules.Azure/Pipeline/AccessToken.cs b/src/PSRule.Rules.Azure/Pipeline/AccessToken.cs index 4921e9f347d..bb22067a30b 100644 --- a/src/PSRule.Rules.Azure/Pipeline/AccessToken.cs +++ b/src/PSRule.Rules.Azure/Pipeline/AccessToken.cs @@ -3,54 +3,53 @@ using System; -namespace PSRule.Rules.Azure.Pipeline +namespace PSRule.Rules.Azure.Pipeline; + +/// +/// An OAuth2 access token. +/// +public sealed class AccessToken { /// - /// An OAuth2 access token. + /// Create an instance of an access token. + /// + /// The base64 encoded token. + /// An offset for when the token expires. + /// A unique identifier for the Azure AD tenent associated to the token. + public AccessToken(string token, DateTimeOffset expiry, string tenantId) + { + Token = token; + Expiry = expiry.DateTime; + TenantId = tenantId; + } + + internal AccessToken(string tenantId) + { + Token = null; + Expiry = DateTime.MinValue; + TenantId = tenantId; + } + + /// + /// The base64 encoded token. + /// + public string Token { get; } + + /// + /// An offset for when the token expires. + /// + public DateTime Expiry { get; } + + /// + /// A unique identifier for the Azure AD tenent associated to the token. + /// + public string TenantId { get; } + + /// + /// Determine if the access token should be refreshed. /// - public sealed class AccessToken + internal bool ShouldRefresh() { - /// - /// Create an instance of an access token. - /// - /// The base64 encoded token. - /// An offset for when the token expires. - /// A unique identifier for the Azure AD tenent associated to the token. - public AccessToken(string token, DateTimeOffset expiry, string tenantId) - { - Token = token; - Expiry = expiry.DateTime; - TenantId = tenantId; - } - - internal AccessToken(string tenantId) - { - Token = null; - Expiry = DateTime.MinValue; - TenantId = tenantId; - } - - /// - /// The base64 encoded token. - /// - public string Token { get; } - - /// - /// An offset for when the token expires. - /// - public DateTime Expiry { get; } - - /// - /// A unique identifier for the Azure AD tenent associated to the token. - /// - public string TenantId { get; } - - /// - /// Determine if the access token should be refreshed. - /// - internal bool ShouldRefresh() - { - return DateTime.UtcNow.AddSeconds(180) > Expiry; - } + return DateTime.UtcNow.AddSeconds(180) > Expiry; } } diff --git a/src/PSRule.Rules.Azure/Pipeline/Exceptions.cs b/src/PSRule.Rules.Azure/Pipeline/Exceptions.cs index ab46f03cc04..fb9b523eb0a 100644 --- a/src/PSRule.Rules.Azure/Pipeline/Exceptions.cs +++ b/src/PSRule.Rules.Azure/Pipeline/Exceptions.cs @@ -5,275 +5,274 @@ using System.Runtime.Serialization; using System.Security.Permissions; -namespace PSRule.Rules.Azure.Pipeline +namespace PSRule.Rules.Azure.Pipeline; + +/// +/// A base class for all pipeline exceptions. +/// +public abstract class PipelineException : Exception +{ + /// + /// Creates a pipeline exception. + /// + protected PipelineException() + { + } + + /// + /// Creates a pipeline exception. + /// + /// The detail of the exception. + protected PipelineException(string message) + : base(message) { } + + /// + /// Creates a pipeline exception. + /// + /// The detail of the exception. + /// A nested exception that caused the issue. + protected PipelineException(string message, Exception innerException) + : base(message, innerException) { } + + /// + /// Create the exception from serialized data. + /// + protected PipelineException(SerializationInfo info, StreamingContext context) + : base(info, context) { } +} + +/// +/// A serialization exception. +/// +[Serializable] +public sealed class PipelineSerializationException : PipelineException +{ + /// + /// Creates a serialization exception. + /// + public PipelineSerializationException() + { + } + + /// + /// Creates a serialization exception. + /// + /// The detail of the exception. + public PipelineSerializationException(string message) + : base(message) { } + + /// + /// Creates a serialization exception. + /// + /// The detail of the exception. + /// A nested exception that caused the issue. + public PipelineSerializationException(string message, Exception innerException) + : base(message, innerException) { } + + /// + /// Create the exception from serialized data. + /// + private PipelineSerializationException(SerializationInfo info, StreamingContext context) + : base(info, context) { } + + /// + /// Serialize the exception. + /// + [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) + throw new ArgumentNullException(nameof(info)); + + base.GetObjectData(info, context); + } +} + +/// +/// An exception related to reading templates. +/// +[Serializable] +public sealed class TemplateReadException : PipelineException +{ + /// + /// Creates a template read exception. + /// + public TemplateReadException() + { + } + + /// + /// Creates a template read exception. + /// + /// The detail of the exception. + public TemplateReadException(string message) + : base(message) { } + + /// + /// Creates a template read exception. + /// + /// The detail of the exception. + /// A nested exception that caused the issue. + public TemplateReadException(string message, Exception innerException) + : base(message, innerException) { } + + /// + /// Creates a template read exception. + /// + /// The detail of the exception. + /// A nested exception that caused the issue. + /// The path to the ARM template file that is being used. + /// The path to the ARM template parameter file that is being used. + internal TemplateReadException(string message, Exception innerException, string templateFile, string parameterFile) + : this(message, innerException) + { + TemplateFile = templateFile; + ParameterFile = parameterFile; + } + + /// + /// Create the exception from serialized data. + /// + private TemplateReadException(SerializationInfo info, StreamingContext context) + : base(info, context) { } + + /// + /// The file path to an Azure Template. + /// + public string TemplateFile { get; } + + /// + /// The file path to an Azure Template parameter file. + /// + public string ParameterFile { get; } + + /// + /// Serialize the exception. + /// + [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) + throw new ArgumentNullException(nameof(info)); + + base.GetObjectData(info, context); + } +} + +/// +/// An exception related to template linking. +/// +[Serializable] +public sealed class InvalidTemplateLinkException : PipelineException { /// - /// A base class for all pipeline exceptions. + /// Creates a template linking exception. /// - public abstract class PipelineException : Exception + public InvalidTemplateLinkException() { - /// - /// Creates a pipeline exception. - /// - protected PipelineException() - { - } - - /// - /// Creates a pipeline exception. - /// - /// The detail of the exception. - protected PipelineException(string message) - : base(message) { } - - /// - /// Creates a pipeline exception. - /// - /// The detail of the exception. - /// A nested exception that caused the issue. - protected PipelineException(string message, Exception innerException) - : base(message, innerException) { } - - /// - /// Create the exception from serialized data. - /// - protected PipelineException(SerializationInfo info, StreamingContext context) - : base(info, context) { } } /// - /// A serialization exception. + /// Creates a template linking exception. + /// + /// The detail of the exception. + public InvalidTemplateLinkException(string message) + : base(message) { } + + /// + /// Creates a template linking exception. /// - [Serializable] - public sealed class PipelineSerializationException : PipelineException + /// The detail of the exception. + /// A nested exception that caused the issue. + public InvalidTemplateLinkException(string message, Exception innerException) + : base(message, innerException) { } + + /// + /// Create the exception from serialized data. + /// + private InvalidTemplateLinkException(SerializationInfo info, StreamingContext context) + : base(info, context) { } + + /// + /// Serialize the exception. + /// + [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] + public override void GetObjectData(SerializationInfo info, StreamingContext context) { - /// - /// Creates a serialization exception. - /// - public PipelineSerializationException() - { - } - - /// - /// Creates a serialization exception. - /// - /// The detail of the exception. - public PipelineSerializationException(string message) - : base(message) { } - - /// - /// Creates a serialization exception. - /// - /// The detail of the exception. - /// A nested exception that caused the issue. - public PipelineSerializationException(string message, Exception innerException) - : base(message, innerException) { } - - /// - /// Create the exception from serialized data. - /// - private PipelineSerializationException(SerializationInfo info, StreamingContext context) - : base(info, context) { } - - /// - /// Serialize the exception. - /// - [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - if (info == null) - throw new ArgumentNullException(nameof(info)); - - base.GetObjectData(info, context); - } + if (info == null) + throw new ArgumentNullException(nameof(info)); + + base.GetObjectData(info, context); } +} +/// +/// An exception related to compiling Bicep source files. +/// +[Serializable] +public sealed class BicepCompileException : PipelineException +{ /// - /// An exception related to reading templates. + /// Creates a Bicep compile exception. /// - [Serializable] - public sealed class TemplateReadException : PipelineException + public BicepCompileException() { - /// - /// Creates a template read exception. - /// - public TemplateReadException() - { - } - - /// - /// Creates a template read exception. - /// - /// The detail of the exception. - public TemplateReadException(string message) - : base(message) { } - - /// - /// Creates a template read exception. - /// - /// The detail of the exception. - /// A nested exception that caused the issue. - public TemplateReadException(string message, Exception innerException) - : base(message, innerException) { } - - /// - /// Creates a template read exception. - /// - /// The detail of the exception. - /// A nested exception that caused the issue. - /// The path to the ARM template file that is being used. - /// The path to the ARM template parameter file that is being used. - internal TemplateReadException(string message, Exception innerException, string templateFile, string parameterFile) - : this(message, innerException) - { - TemplateFile = templateFile; - ParameterFile = parameterFile; - } - - /// - /// Create the exception from serialized data. - /// - private TemplateReadException(SerializationInfo info, StreamingContext context) - : base(info, context) { } - - /// - /// The file path to an Azure Template. - /// - public string TemplateFile { get; } - - /// - /// The file path to an Azure Template parameter file. - /// - public string ParameterFile { get; } - - /// - /// Serialize the exception. - /// - [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - if (info == null) - throw new ArgumentNullException(nameof(info)); - - base.GetObjectData(info, context); - } } /// - /// An exception related to template linking. + /// Creates a Bicep compile exception. + /// + /// The detail of the exception. + public BicepCompileException(string message) + : base(message) { } + + /// + /// Creates a Bicep compile exception. /// - [Serializable] - public sealed class InvalidTemplateLinkException : PipelineException + /// The detail of the exception. + /// A nested exception that caused the issue. + public BicepCompileException(string message, Exception innerException) + : base(message, innerException) { } + + /// + /// Creates a Bicep compile exception. + /// + /// The detail of the exception. + /// A nested exception that caused the issue. + /// The path to the Bicep source file. + /// Specifies the version of Bicep runtime being used. + internal BicepCompileException(string message, Exception innerException, string sourceFile, string version) + : base(message, innerException) { - /// - /// Creates a template linking exception. - /// - public InvalidTemplateLinkException() - { - } - - /// - /// Creates a template linking exception. - /// - /// The detail of the exception. - public InvalidTemplateLinkException(string message) - : base(message) { } - - /// - /// Creates a template linking exception. - /// - /// The detail of the exception. - /// A nested exception that caused the issue. - public InvalidTemplateLinkException(string message, Exception innerException) - : base(message, innerException) { } - - /// - /// Create the exception from serialized data. - /// - private InvalidTemplateLinkException(SerializationInfo info, StreamingContext context) - : base(info, context) { } - - /// - /// Serialize the exception. - /// - [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - if (info == null) - throw new ArgumentNullException(nameof(info)); - - base.GetObjectData(info, context); - } + SourceFile = sourceFile; + Version = version; } /// - /// An exception related to compiling Bicep source files. + /// Create the exception from serialized data. + /// + private BicepCompileException(SerializationInfo info, StreamingContext context) + : base(info, context) { } + + /// + /// The file path to an Azure Bicep source file. + /// + public string SourceFile { get; } + + /// + /// The version of the Bicep binary. + /// + public string Version { get; } + + /// + /// Serialize the exception. /// - [Serializable] - public sealed class BicepCompileException : PipelineException + [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] + public override void GetObjectData(SerializationInfo info, StreamingContext context) { - /// - /// Creates a Bicep compile exception. - /// - public BicepCompileException() - { - } - - /// - /// Creates a Bicep compile exception. - /// - /// The detail of the exception. - public BicepCompileException(string message) - : base(message) { } - - /// - /// Creates a Bicep compile exception. - /// - /// The detail of the exception. - /// A nested exception that caused the issue. - public BicepCompileException(string message, Exception innerException) - : base(message, innerException) { } - - /// - /// Creates a Bicep compile exception. - /// - /// The detail of the exception. - /// A nested exception that caused the issue. - /// The path to the Bicep source file. - /// Specifies the version of Bicep runtime being used. - internal BicepCompileException(string message, Exception innerException, string sourceFile, string version) - : base(message, innerException) - { - SourceFile = sourceFile; - Version = version; - } - - /// - /// Create the exception from serialized data. - /// - private BicepCompileException(SerializationInfo info, StreamingContext context) - : base(info, context) { } - - /// - /// The file path to an Azure Bicep source file. - /// - public string SourceFile { get; } - - /// - /// The version of the Bicep binary. - /// - public string Version { get; } - - /// - /// Serialize the exception. - /// - [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - if (info == null) - throw new ArgumentNullException(nameof(info)); - - base.GetObjectData(info, context); - } + if (info == null) + throw new ArgumentNullException(nameof(info)); + + base.GetObjectData(info, context); } } diff --git a/src/PSRule.Rules.Azure/Pipeline/Export/AccessTokenCache.cs b/src/PSRule.Rules.Azure/Pipeline/Export/AccessTokenCache.cs index e3a6e8270d3..a4b7319e0d7 100644 --- a/src/PSRule.Rules.Azure/Pipeline/Export/AccessTokenCache.cs +++ b/src/PSRule.Rules.Azure/Pipeline/Export/AccessTokenCache.cs @@ -5,116 +5,115 @@ using System.Collections.Concurrent; using System.Threading; -namespace PSRule.Rules.Azure.Pipeline.Export +namespace PSRule.Rules.Azure.Pipeline.Export; + +/// +/// Define a cache for storing and refreshing tokens. +/// +internal sealed class AccessTokenCache : IDisposable { + private readonly GetAccessTokenFn _GetToken; + private readonly CancellationTokenSource _Cancel; + private readonly ConcurrentDictionary _Cache; + + private bool _Disposed; + /// - /// Define a cache for storing and refreshing tokens. + /// Create an instance of a token cache. /// - internal sealed class AccessTokenCache : IDisposable + /// A delegate method to get a token for a tenant. + public AccessTokenCache(GetAccessTokenFn getToken) { - private readonly GetAccessTokenFn _GetToken; - private readonly CancellationTokenSource _Cancel; - private readonly ConcurrentDictionary _Cache; - - private bool _Disposed; - - /// - /// Create an instance of a token cache. - /// - /// A delegate method to get a token for a tenant. - public AccessTokenCache(GetAccessTokenFn getToken) - { - _GetToken = getToken; - _Cache = new ConcurrentDictionary(); - _Cancel = new CancellationTokenSource(); - } + _GetToken = getToken; + _Cache = new ConcurrentDictionary(); + _Cancel = new CancellationTokenSource(); + } - /// - /// Check and refresh all tokens as required. - /// - internal void RefreshAll() + /// + /// Check and refresh all tokens as required. + /// + internal void RefreshAll() + { + var tokens = _Cache.ToArray(); + for (var i = 0; i < tokens.Length; i++) { - var tokens = _Cache.ToArray(); - for (var i = 0; i < tokens.Length; i++) + if (tokens[i].Value.ShouldRefresh()) { - if (tokens[i].Value.ShouldRefresh()) - { - var token = _GetToken.Invoke(tokens[i].Value.TenantId); - if (token != null) - _Cache.TryUpdate(tokens[i].Key, token, tokens[i].Value); - } + var token = _GetToken.Invoke(tokens[i].Value.TenantId); + if (token != null) + _Cache.TryUpdate(tokens[i].Key, token, tokens[i].Value); } } + } - /// - /// Refresh a token for the specified tenant. - /// - /// The tenant Id of the specific tenant to refresh the token for. - internal void RefreshToken(string tenantId) + /// + /// Refresh a token for the specified tenant. + /// + /// The tenant Id of the specific tenant to refresh the token for. + internal void RefreshToken(string tenantId) + { + if (!_Cache.TryGetValue(tenantId, out var oldToken)) { - if (!_Cache.TryGetValue(tenantId, out var oldToken)) - { - var newToken = _GetToken.Invoke(tenantId); - if (newToken != null) - _Cache.TryAdd(tenantId, newToken); - } - else if (oldToken.ShouldRefresh()) - { - var newToken = _GetToken.Invoke(tenantId); - if (newToken != null) - _Cache.TryUpdate(tenantId, newToken, oldToken); - } + var newToken = _GetToken.Invoke(tenantId); + if (newToken != null) + _Cache.TryAdd(tenantId, newToken); } + else if (oldToken.ShouldRefresh()) + { + var newToken = _GetToken.Invoke(tenantId); + if (newToken != null) + _Cache.TryUpdate(tenantId, newToken, oldToken); + } + } - /// - /// Get an access token for the specified tenant. - /// The token will be synchronized to the main thread. - /// - /// The tenant Id of the specific tenant to get a token for. - /// An access token or null. - internal string GetToken(string tenantId) + /// + /// Get an access token for the specified tenant. + /// The token will be synchronized to the main thread. + /// + /// The tenant Id of the specific tenant to get a token for. + /// An access token or null. + internal string GetToken(string tenantId) + { + while (!_Cancel.IsCancellationRequested) { - while (!_Cancel.IsCancellationRequested) + if (!_Cache.TryGetValue(tenantId, out var token)) { - if (!_Cache.TryGetValue(tenantId, out var token)) - { - _Cache.TryAdd(tenantId, new AccessToken(tenantId)); - } - else if (!token.ShouldRefresh()) - { - return token.Token; - } - Thread.Sleep(100); + _Cache.TryAdd(tenantId, new AccessToken(tenantId)); } - return null; + else if (!token.ShouldRefresh()) + { + return token.Token; + } + Thread.Sleep(100); } + return null; + } - internal void Cancel() - { - _Cancel.Cancel(); - } + internal void Cancel() + { + _Cancel.Cancel(); + } - #region IDisposable + #region IDisposable - private void Dispose(bool disposing) + private void Dispose(bool disposing) + { + if (!_Disposed) { - if (!_Disposed) + if (disposing) { - if (disposing) - { - _Cancel.Cancel(); - _Cancel.Dispose(); - } - _Disposed = true; + _Cancel.Cancel(); + _Cancel.Dispose(); } + _Disposed = true; } + } - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - #endregion IDisposable + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); } + + #endregion IDisposable } diff --git a/src/PSRule.Rules.Azure/Pipeline/Export/ExportDataContext.cs b/src/PSRule.Rules.Azure/Pipeline/Export/ExportDataContext.cs index 1c856104f54..4867c26cc16 100644 --- a/src/PSRule.Rules.Azure/Pipeline/Export/ExportDataContext.cs +++ b/src/PSRule.Rules.Azure/Pipeline/Export/ExportDataContext.cs @@ -14,242 +14,241 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace PSRule.Rules.Azure.Pipeline.Export +namespace PSRule.Rules.Azure.Pipeline.Export; + +/// +/// A base context to export data from Azure. +/// +internal abstract class ExportDataContext : IDisposable, ILogger { - /// - /// A base context to export data from Azure. - /// - internal abstract class ExportDataContext : IDisposable, ILogger - { - protected const string RESOURCE_MANAGER_URL = "https://management.azure.com"; + protected const string RESOURCE_MANAGER_URL = "https://management.azure.com"; - private const string HEADERS_MEDIATYPE_JSON = "application/json"; - private const string HEADERS_AUTHORIZATION_BEARER = "Bearer"; - private const string HEADERS_CORRELATION_ID = "x-ms-correlation-request-id"; + private const string HEADERS_MEDIATYPE_JSON = "application/json"; + private const string HEADERS_AUTHORIZATION_BEARER = "Bearer"; + private const string HEADERS_CORRELATION_ID = "x-ms-correlation-request-id"; - private const string PROPERTY_VALUE = "value"; + private const string PROPERTY_VALUE = "value"; - private readonly AccessTokenCache _TokenCache; - private readonly CancellationTokenSource _Cancel; - private readonly int _RetryCount; - private readonly TimeSpan _RetryInterval; - private readonly PipelineContext _Context; - private readonly ConcurrentQueue _Logger; - private readonly string[] _CorrelationId; + private readonly AccessTokenCache _TokenCache; + private readonly CancellationTokenSource _Cancel; + private readonly int _RetryCount; + private readonly TimeSpan _RetryInterval; + private readonly PipelineContext _Context; + private readonly ConcurrentQueue _Logger; + private readonly string[] _CorrelationId; - private bool _Disposed; + private bool _Disposed; - public ExportDataContext(PipelineContext context, AccessTokenCache tokenCache, int retryCount = 3, int retryInterval = 10) - { - _TokenCache = tokenCache; - _Cancel = new CancellationTokenSource(); - _RetryCount = retryCount; - _RetryInterval = TimeSpan.FromSeconds(retryInterval); - _Context = context; - _Logger = new ConcurrentQueue(); - _CorrelationId = new string[] { Guid.NewGuid().ToString() }; - } + public ExportDataContext(PipelineContext context, AccessTokenCache tokenCache, int retryCount = 3, int retryInterval = 10) + { + _TokenCache = tokenCache; + _Cancel = new CancellationTokenSource(); + _RetryCount = retryCount; + _RetryInterval = TimeSpan.FromSeconds(retryInterval); + _Context = context; + _Logger = new ConcurrentQueue(); + _CorrelationId = [Guid.NewGuid().ToString()]; + } - private readonly struct Message + private readonly struct Message + { + public Message(TraceLevel level, string text) { - public Message(TraceLevel level, string text) - { - Level = level; - Text = text; - } + Level = level; + Text = text; + } - public TraceLevel Level { get; } + public TraceLevel Level { get; } - public string Text { get; } - } + public string Text { get; } + } - protected void RefreshToken(string tenantId) - { - _TokenCache.RefreshToken(tenantId); - } + protected void RefreshToken(string tenantId) + { + _TokenCache.RefreshToken(tenantId); + } - protected void RefreshAll() - { - _TokenCache.RefreshAll(); - } + protected void RefreshAll() + { + _TokenCache.RefreshAll(); + } - protected string GetToken(string tenantId) - { - return _TokenCache.GetToken(tenantId); - } + protected string GetToken(string tenantId) + { + return _TokenCache.GetToken(tenantId); + } - private HttpClient GetClient(string tenantId) - { - var client = new HttpClient - { - Timeout = TimeSpan.FromSeconds(90) - }; - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(HEADERS_AUTHORIZATION_BEARER, GetToken(tenantId)); - client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(HEADERS_MEDIATYPE_JSON)); - client.DefaultRequestHeaders.Add(HEADERS_CORRELATION_ID, _CorrelationId); - return client; - } + private HttpClient GetClient(string tenantId) + { + var client = new HttpClient + { + Timeout = TimeSpan.FromSeconds(90) + }; + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(HEADERS_AUTHORIZATION_BEARER, GetToken(tenantId)); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(HEADERS_MEDIATYPE_JSON)); + client.DefaultRequestHeaders.Add(HEADERS_CORRELATION_ID, _CorrelationId); + return client; + } - protected static string GetEndpointUri(string baseEndpoint, string requestUri, string apiVersion) - { - return string.Concat(baseEndpoint, "/", requestUri, "?api-version=", apiVersion); - } + protected static string GetEndpointUri(string baseEndpoint, string requestUri, string apiVersion) + { + return string.Concat(baseEndpoint, "/", requestUri, "?api-version=", apiVersion); + } - protected async Task ListAsync(string tenantId, string uri, bool ignoreNotFound) + protected async Task ListAsync(string tenantId, string uri, bool ignoreNotFound) + { + var results = new List(); + var json = await GetRequestAsync(tenantId, uri, ignoreNotFound); + while (!string.IsNullOrEmpty(json)) { - var results = new List(); - var json = await GetRequestAsync(tenantId, uri, ignoreNotFound); - while (!string.IsNullOrEmpty(json)) - { - var payload = JsonConvert.DeserializeObject(json); - if (payload.TryGetProperty(PROPERTY_VALUE, out JArray data)) - results.AddRange(data.Values()); + var payload = JsonConvert.DeserializeObject(json); + if (payload.TryGetProperty(PROPERTY_VALUE, out JArray data)) + results.AddRange(data.Values()); - json = payload.TryGetProperty("nextLink", out var nextLink) && - !string.IsNullOrEmpty(nextLink) ? await GetRequestAsync(tenantId, nextLink, ignoreNotFound) : null; - } - return results.ToArray(); + json = payload.TryGetProperty("nextLink", out var nextLink) && + !string.IsNullOrEmpty(nextLink) ? await GetRequestAsync(tenantId, nextLink, ignoreNotFound) : null; } + return [.. results]; + } - protected async Task GetAsync(string tenantId, string uri) - { - var json = await GetRequestAsync(tenantId, uri, ignoreNotFound: false); - if (string.IsNullOrEmpty(json)) - return null; + protected async Task GetAsync(string tenantId, string uri) + { + var json = await GetRequestAsync(tenantId, uri, ignoreNotFound: false); + if (string.IsNullOrEmpty(json)) + return null; - var result = JsonConvert.DeserializeObject(json); - return result; - } + var result = JsonConvert.DeserializeObject(json); + return result; + } - private async Task GetRequestAsync(string tenantId, string uri, bool ignoreNotFound) + private async Task GetRequestAsync(string tenantId, string uri, bool ignoreNotFound) + { + var attempt = 0; + using var client = GetClient(tenantId); + try { - var attempt = 0; - using var client = GetClient(tenantId); - try + do { - do + attempt++; + + var response = await client.GetAsync(uri, _Cancel.Token); + if (response.IsSuccessStatusCode) { - attempt++; - - var response = await client.GetAsync(uri, _Cancel.Token); - if (response.IsSuccessStatusCode) - { - var json = await response.Content.ReadAsStringAsync(); - return json; - } - else if (response.StatusCode == HttpStatusCode.NotFound) - { - if (!ignoreNotFound) - this.WarnFailedToGet(uri, response.StatusCode, _CorrelationId[0], await response.Content.ReadAsStringAsync()); - - return null; - } - else if (!ShouldRetry(response.StatusCode)) - { - this.WarnFailedToGet(uri, response.StatusCode, _CorrelationId[0], await response.Content.ReadAsStringAsync()); - return null; - } - else - { + var json = await response.Content.ReadAsStringAsync(); + return json; + } + else if (response.StatusCode == HttpStatusCode.NotFound) + { + if (!ignoreNotFound) this.WarnFailedToGet(uri, response.StatusCode, _CorrelationId[0], await response.Content.ReadAsStringAsync()); - var retry = response.Headers.RetryAfter.Delta.GetValueOrDefault(_RetryInterval); - this.VerboseRetryIn(uri, retry, attempt); - Thread.Sleep(retry); - } - } while (attempt <= _RetryCount); - } - finally - { - // Do nothing - } - return null; - } + return null; + } + else if (!ShouldRetry(response.StatusCode)) + { + this.WarnFailedToGet(uri, response.StatusCode, _CorrelationId[0], await response.Content.ReadAsStringAsync()); + return null; + } + else + { + this.WarnFailedToGet(uri, response.StatusCode, _CorrelationId[0], await response.Content.ReadAsStringAsync()); + var retry = response.Headers.RetryAfter.Delta.GetValueOrDefault(_RetryInterval); + this.VerboseRetryIn(uri, retry, attempt); + Thread.Sleep(retry); + } - private static bool ShouldRetry(HttpStatusCode statusCode) + } while (attempt <= _RetryCount); + } + finally { - return statusCode == HttpStatusCode.RequestTimeout || - statusCode == HttpStatusCode.BadGateway || - statusCode == HttpStatusCode.ServiceUnavailable || - statusCode == HttpStatusCode.GatewayTimeout || - (int)statusCode == 429 /* Too many Requests */; + // Do nothing } + return null; + } + + private static bool ShouldRetry(HttpStatusCode statusCode) + { + return statusCode == HttpStatusCode.RequestTimeout || + statusCode == HttpStatusCode.BadGateway || + statusCode == HttpStatusCode.ServiceUnavailable || + statusCode == HttpStatusCode.GatewayTimeout || + (int)statusCode == 429 /* Too many Requests */; + } - protected virtual void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) + { + if (!_Disposed) { - if (!_Disposed) + if (disposing) { - if (disposing) - { - _Cancel.Cancel(); - _Cancel.Dispose(); - } - _Disposed = true; + _Cancel.Cancel(); + _Cancel.Dispose(); } + _Disposed = true; } + } - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - #region ILogger + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } - void ILogger.WriteVerbose(string message) - { - if (_Context == null) - return; + #region ILogger - _Logger.Enqueue(new Message(TraceLevel.Verbose, message)); - } + void ILogger.WriteVerbose(string message) + { + if (_Context == null) + return; - void ILogger.WriteVerbose(string format, params object[] args) - { - if (_Context == null) - return; + _Logger.Enqueue(new Message(TraceLevel.Verbose, message)); + } - _Logger.Enqueue(new Message(TraceLevel.Verbose, string.Format(Thread.CurrentThread.CurrentCulture, format, args))); - } + void ILogger.WriteVerbose(string format, params object[] args) + { + if (_Context == null) + return; - internal void WriteDiagnostics() - { - if (_Context == null || _Logger.IsEmpty) - return; + _Logger.Enqueue(new Message(TraceLevel.Verbose, string.Format(Thread.CurrentThread.CurrentCulture, format, args))); + } - while (!_Logger.IsEmpty && _Logger.TryDequeue(out var message)) - { - if (message.Level == TraceLevel.Verbose) - _Context.Writer.WriteVerbose(message.Text); - else if (message.Level == TraceLevel.Warning) - _Context.Writer.WriteWarning(message.Text); - } - } + internal void WriteDiagnostics() + { + if (_Context == null || _Logger.IsEmpty) + return; - void ILogger.WriteWarning(string message) + while (!_Logger.IsEmpty && _Logger.TryDequeue(out var message)) { - if (_Context == null) - return; - - _Logger.Enqueue(new Message(TraceLevel.Warning, message)); + if (message.Level == TraceLevel.Verbose) + _Context.Writer.WriteVerbose(message.Text); + else if (message.Level == TraceLevel.Warning) + _Context.Writer.WriteWarning(message.Text); } + } - void ILogger.WriteWarning(string format, params object[] args) - { - if (_Context == null) - return; + void ILogger.WriteWarning(string message) + { + if (_Context == null) + return; - _Logger.Enqueue(new Message(TraceLevel.Warning, string.Format(Thread.CurrentThread.CurrentCulture, format, args))); - } + _Logger.Enqueue(new Message(TraceLevel.Warning, message)); + } - public void WriteError(Exception exception, string errorId, ErrorCategory errorCategory, object targetObject) - { - if (_Context == null) - return; + void ILogger.WriteWarning(string format, params object[] args) + { + if (_Context == null) + return; - _Logger.Enqueue(new Message(TraceLevel.Warning, exception.Message)); - } + _Logger.Enqueue(new Message(TraceLevel.Warning, string.Format(Thread.CurrentThread.CurrentCulture, format, args))); + } - #endregion ILogger + public void WriteError(Exception exception, string errorId, ErrorCategory errorCategory, object targetObject) + { + if (_Context == null) + return; + + _Logger.Enqueue(new Message(TraceLevel.Warning, exception.Message)); } + + #endregion ILogger } diff --git a/src/PSRule.Rules.Azure/Pipeline/Export/IResourceExportContext.cs b/src/PSRule.Rules.Azure/Pipeline/Export/IResourceExportContext.cs new file mode 100644 index 00000000000..37e2631fe1f --- /dev/null +++ b/src/PSRule.Rules.Azure/Pipeline/Export/IResourceExportContext.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +namespace PSRule.Rules.Azure.Pipeline.Export; + +internal interface IResourceExportContext : ILogger +{ + /// + /// Get a resource. + /// + /// The tenant Id for the request. + /// The resource URI. + /// The apiVersion of the resource provider. + Task GetAsync(string tenantId, string requestUri, string apiVersion); + + /// + /// List resources. + /// + /// The tenant Id for the request. + /// The resource URI. + /// The apiVersion of the resource provider. + /// Determines if not found status is ignored. Otherwise a warning is logged. + Task ListAsync(string tenantId, string requestUri, string apiVersion, bool ignoreNotFound); +} diff --git a/src/PSRule.Rules.Azure/Pipeline/Export/ISubscriptionExportContext.cs b/src/PSRule.Rules.Azure/Pipeline/Export/ISubscriptionExportContext.cs new file mode 100644 index 00000000000..5d74e6dcbdb --- /dev/null +++ b/src/PSRule.Rules.Azure/Pipeline/Export/ISubscriptionExportContext.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +namespace PSRule.Rules.Azure.Pipeline.Export; + +internal interface ISubscriptionExportContext +{ + /// + /// Get resources from an Azure subscription. + /// + Task GetResourcesAsync(); + + /// + /// Get resource groups from an Azure subscription. + /// + Task GetResourceGroupsAsync(); + + /// + /// Get a resource for the Azure subscription. + /// + Task GetSubscriptionAsync(); + + /// + /// Get a specified Azure resource from a subscription. + /// + Task GetResourceAsync(string resourceId); + + /// + /// The subscription Id of the context subscription. + /// + string SubscriptionId { get; } + + /// + /// The tenant Id of the context tenant. + /// + string TenantId { get; } +} diff --git a/src/PSRule.Rules.Azure/Pipeline/Export/ResourceExportContext.cs b/src/PSRule.Rules.Azure/Pipeline/Export/ResourceExportContext.cs index 5abe3762bf8..a970bb109e8 100644 --- a/src/PSRule.Rules.Azure/Pipeline/Export/ResourceExportContext.cs +++ b/src/PSRule.Rules.Azure/Pipeline/Export/ResourceExportContext.cs @@ -6,84 +6,63 @@ using System.Threading.Tasks; using Newtonsoft.Json.Linq; -namespace PSRule.Rules.Azure.Pipeline.Export -{ - internal interface IResourceExportContext : ILogger - { - /// - /// Get a resource. - /// - /// The tenant Id for the request. - /// The resource URI. - /// The apiVersion of the resource provider. - Task GetAsync(string tenantId, string requestUri, string apiVersion); - - /// - /// List resources. - /// - /// The tenant Id for the request. - /// The resource URI. - /// The apiVersion of the resource provider. - /// Determines if not found status is ignored. Otherwise a warning is logged. - Task ListAsync(string tenantId, string requestUri, string apiVersion, bool ignoreNotFound); - } +namespace PSRule.Rules.Azure.Pipeline.Export; - /// - /// A context to export an Azure resource. - /// - internal class ResourceExportContext : ExportDataContext, IResourceExportContext - { - private readonly ConcurrentQueue _Resources; +/// +/// A context to export an Azure resource. +/// +internal class ResourceExportContext : ExportDataContext, IResourceExportContext +{ + private readonly ConcurrentQueue _Resources; - private bool _Disposed; + private bool _Disposed; - public ResourceExportContext(PipelineContext context, AccessTokenCache tokenCache) - : base(context, tokenCache) { } + public ResourceExportContext(PipelineContext context, AccessTokenCache tokenCache) + : base(context, tokenCache) { } - public ResourceExportContext(PipelineContext context, ConcurrentQueue resources, AccessTokenCache tokenCache, int retryCount, int retryInterval) - : base(context, tokenCache, retryCount, retryInterval) - { - _Resources = resources; - } + public ResourceExportContext(PipelineContext context, ConcurrentQueue resources, AccessTokenCache tokenCache, int retryCount, int retryInterval) + : base(context, tokenCache, retryCount, retryInterval) + { + _Resources = resources; + } - /// - public async Task GetAsync(string tenantId, string requestUri, string apiVersion) - { - return await GetAsync(tenantId, GetEndpointUri(RESOURCE_MANAGER_URL, requestUri, apiVersion)); - } + /// + public async Task GetAsync(string tenantId, string requestUri, string apiVersion) + { + return await GetAsync(tenantId, GetEndpointUri(RESOURCE_MANAGER_URL, requestUri, apiVersion)); + } - /// - public async Task ListAsync(string tenantId, string requestUri, string apiVersion, bool ignoreNotFound) - { - return await ListAsync(tenantId, GetEndpointUri(RESOURCE_MANAGER_URL, requestUri, apiVersion), ignoreNotFound); - } + /// + public async Task ListAsync(string tenantId, string requestUri, string apiVersion, bool ignoreNotFound) + { + return await ListAsync(tenantId, GetEndpointUri(RESOURCE_MANAGER_URL, requestUri, apiVersion), ignoreNotFound); + } - protected override void Dispose(bool disposing) + protected override void Dispose(bool disposing) + { + if (!_Disposed) { - if (!_Disposed) + if (disposing) { - if (disposing) - { - // Do nothing. - } - _Disposed = true; + // Do nothing. } - base.Dispose(disposing); + _Disposed = true; } + base.Dispose(disposing); + } - internal void Flush() - { - WriteDiagnostics(); - } + internal void Flush() + { + WriteDiagnostics(); + } - internal void Wait() + internal void Wait() + { + while (!_Resources.IsEmpty) { - while (!_Resources.IsEmpty) - { - WriteDiagnostics(); - RefreshAll(); - Thread.Sleep(100); - } + WriteDiagnostics(); + RefreshAll(); + Thread.Sleep(100); } } } diff --git a/src/PSRule.Rules.Azure/Pipeline/Export/ResourceExportVisitor.cs b/src/PSRule.Rules.Azure/Pipeline/Export/ResourceExportVisitor.cs index f361d374444..0387fec3f65 100644 --- a/src/PSRule.Rules.Azure/Pipeline/Export/ResourceExportVisitor.cs +++ b/src/PSRule.Rules.Azure/Pipeline/Export/ResourceExportVisitor.cs @@ -7,795 +7,794 @@ using Newtonsoft.Json.Linq; using PSRule.Rules.Azure.Data; -namespace PSRule.Rules.Azure.Pipeline.Export -{ - /// - /// Defines a class that gets and sets additional properties and sub-resources of a resource from an Azure subscription. - /// - internal sealed class ResourceExportVisitor - { - private const string PROPERTY_ID = "id"; - private const string PROPERTY_TYPE = "type"; - private const string PROPERTY_PROPERTIES = "properties"; - private const string PROPERTY_ZONES = "zones"; - private const string PROPERTY_RESOURCES = "resources"; - private const string PROPERTY_SUBSCRIPTIONID = "subscriptionId"; - private const string PROPERTY_RESOURCEGROUPNAME = "resourceGroupName"; - private const string PROPERTY_KIND = "kind"; - private const string PROPERTY_SHAREDKEY = "sharedKey"; - private const string PROPERTY_NETWORKPROFILE = "networkProfile"; - private const string PROPERTY_NETWORKINTERFACES = "networkInterfaces"; - private const string PROPERTY_NETOWORKPLUGIN = "networkPlugin"; - private const string PROPERTY_AGENTPOOLPROFILES = "agentPoolProfiles"; - private const string PROPERTY_TENANTID = "tenantId"; - private const string PROPERTY_POLICIES = "policies"; - private const string PROPERTY_FIREWALLRULES = "firewallRules"; - private const string PROPERTY_SECURITYALERTPOLICIES = "securityAlertPolicies"; - private const string PROPERTY_CONFIGURATIONS = "configurations"; - private const string PROPERTY_ADMINISTRATORS = "administrators"; - private const string PROPERTY_VULNERABILITYASSESSMENTS = "vulnerabilityAssessments"; - private const string PROPERTY_AUDITINGSETTINGS = "auditingSettings"; - private const string PROPERTY_CUSTOMDOMAINS = "customDomains"; - private const string PROPERTY_WEBHOOKS = "webhooks"; - private const string PROPERTY_ORIGINGROUPS = "originGroups"; - private const string PROPERTY_REPLICATIONS = "replications"; - private const string PROPERTY_TASKS = "tasks"; - private const string PROPERTY_SECURITYPOLICIES = "securityPolicies"; - private const string PROPERTY_CONTAINERS = "containers"; - private const string PROPERTY_SHARES = "shares"; - private const string PROPERTY_TOPICS = "topics"; - private const string PROPERTY_MAINTENANCECONFIGURATIONS = "maintenanceConfigurations"; - - private const string TYPE_CONTAINERSERVICE_MANAGEDCLUSTERS = "Microsoft.ContainerService/managedClusters"; - private const string TYPE_CONTAINERREGISTRY_REGISTRIES = "Microsoft.ContainerRegistry/registries"; - private const string TYPE_CONTAINERREGISTRY_REGISTRIES_LISTUSAGES = "Microsoft.ContainerRegistry/registries/listUsages"; - private const string TYPE_CDN_PROFILES = "Microsoft.Cdn/profiles"; - private const string TYPE_CDN_PROFILES_ENDPOINTS = "Microsoft.Cdn/profiles/endpoints"; - private const string TYPE_CDN_PROFILES_AFDENDPOINTS = "Microsoft.Cdn/profiles/afdEndpoints"; - private const string TYPE_AUTOMATION_ACCOUNTS = "Microsoft.Automation/automationAccounts"; - private const string TYPE_APIMANAGEMENT_SERVICE = "Microsoft.ApiManagement/service"; - private const string TYPE_SQL_SERVERS = "Microsoft.Sql/servers"; - private const string TYPE_SQL_DATABASES = "Microsoft.Sql/servers/databases"; - private const string TYPE_POSTGRESQL_SERVERS = "Microsoft.DBforPostgreSQL/servers"; - private const string TYPE_POSTGRESQL_FLEXABLESERVERS = "Microsoft.DBforPostgreSQL/flexibleServers"; - private const string TYPE_MYSQL_SERVERS = "Microsoft.DBforMySQL/servers"; - private const string TYPE_MYSQL_FLEXABLESERVERS = "Microsoft.DBforMySQL/flexibleServers"; - private const string TYPE_STORAGE_ACCOUNTS = "Microsoft.Storage/storageAccounts"; - private const string TYPE_WEB_APP = "Microsoft.Web/sites"; - private const string TYPE_WEB_APPSLOT = "Microsoft.Web/sites/slots"; - private const string TYPE_RECOVERYSERVICES_VAULT = "Microsoft.RecoveryServices/vaults"; - private const string TYPE_COMPUTER_VIRTUALMACHINE = "Microsoft.Compute/virtualMachines"; - private const string TYPE_KEYVAULT_VAULT = "Microsoft.KeyVault/vaults"; - private const string TYPE_NETWORK_FRONTDOOR = "Microsoft.Network/frontDoors"; - private const string TYPE_NETWORK_CONNECTION = "Microsoft.Network/connections"; - private const string TYPE_SUBSCRIPTION = "Microsoft.Subscription"; - private const string TYPE_RESOURCES_RESOURCEGROUP = "Microsoft.Resources/resourceGroups"; - private const string TYPE_KUSTO_CLUSTER = "Microsoft.Kusto/Clusters"; - private const string TYPE_EVENTHUB_NAMESPACE = "Microsoft.EventHub/namespaces"; - private const string TYPE_SERVICEBUS_NAMESPACE = "Microsoft.ServiceBus/namespaces"; - private const string TYPE_VISUALSTUDIO_ACCOUNT = "Microsoft.VisualStudio/account"; - private const string TYPE_DEVCENTER_PROJECT = "Microsoft.DevCenter/projects"; - private const string TYPE_NETWORK_FIREWALLPOLICY = "Microsoft.Network/firewallPolicies"; - private const string TYPE_NETWORK_VIRTUALHUB = "Microsoft.Network/virtualHubs"; - private const string TYPE_EVENTGRID_TOPIC = "Microsoft.EventGrid/topics"; - private const string TYPE_EVENTGRID_DOMAIN = "Microsoft.EventGrid/domains"; - private const string TYPE_EVENTGRID_NAMESPACE = "Microsoft.EventGrid/namespaces"; - - private const string PROVIDERTYPE_DIAGNOSTICSETTINGS = "/providers/microsoft.insights/diagnosticSettings"; - private const string PROVIDERTYPE_ROLEASSIGNMENTS = "/providers/Microsoft.Authorization/roleAssignments"; - private const string PROVIDERTYPE_RESOURCELOCKS = "/providers/Microsoft.Authorization/locks"; - private const string PROVIDERTYPE_AUTOPROVISIONING = "/providers/Microsoft.Security/autoProvisioningSettings"; - private const string PROVIDERTYPE_SECURITYCONTACTS = "/providers/Microsoft.Security/securityContacts"; - private const string PROVIDERTYPE_SECURITYPRICINGS = "/providers/Microsoft.Security/pricings"; - private const string PROVIDERTYPE_POLICYASSIGNMENTS = "/providers/Microsoft.Authorization/policyAssignments"; - private const string PROVIDERTYPE_CLASSICADMINISTRATORS = "/providers/Microsoft.Authorization/classicAdministrators"; - private const string PROVIDERTYPE_APICOLLECTIONS = "/providers/Microsoft.Security/apiCollections"; - private const string PROVIDERTYPE_DEFENDERFORSTORAGESETTINGS = "/providers/Microsoft.Security/DefenderForStorageSettings"; - - private const string MASKED_VALUE = "*** MASKED ***"; - - private const string APIVERSION_2014_04_01 = "2014-04-01"; - private const string APIVERSION_2016_09_01 = "2016-09-01"; - private const string APIVERSION_2017_12_01 = "2017-12-01"; - private const string APIVERSION_2021_05_01_PREVIEW = "2021-05-01-preview"; - private const string APIVERSION_2021_06_01_PREVIEW = "2021-06-01-preview"; - private const string APIVERSION_2021_08_27 = "2021-08-27"; - private const string APIVERSION_2021_11_01 = "2021-11-01"; - private const string APIVERSION_2022_07_01 = "2022-07-01"; - private const string APIVERSION_2022_08_01 = "2022-08-01"; - private const string APIVERSION_2022_11_20_PREVIEW = "2022-11-20-preview"; - private const string APIVERSION_2022_04_01 = "2022-04-01"; - private const string APIVERSION_2022_09_01 = "2022-09-01"; - private const string APIVERSION_2022_09_10 = "2022-09-10"; - private const string APIVERSION_2023_01_01 = "2023-01-01"; - private const string APIVERSION_2023_01_01_PREVIEW = "2023-01-01-preview"; - private const string APIVERSION_2023_03_01_PREVIEW = "2023-03-01-preview"; - private const string APIVERSION_2023_04_01 = "2023-04-01"; - private const string APIVERSION_2023_05_01 = "2023-05-01"; - private const string APIVERSION_2023_06_30 = "2023-06-30"; - private const string APIVERSION_2023_09_01 = "2023-09-01"; - private const string APIVERSION_2023_12_15_PREVIEW = "2023-12-15-preview"; - private const string APIVERSION_2024_03_02_PREVIEW = "2024-03-02-preview"; - - private readonly ProviderData _ProviderData; - - public ResourceExportVisitor() - { - _ProviderData = new ProviderData(); - } +namespace PSRule.Rules.Azure.Pipeline.Export; - private sealed class ResourceContext - { - private readonly IResourceExportContext _Context; - - public ResourceContext(IResourceExportContext context, string tenantId) - { - _Context = context; - TenantId = tenantId; - } - - public string TenantId { get; } +/// +/// Defines a class that gets and sets additional properties and sub-resources of a resource from an Azure subscription. +/// +internal sealed class ResourceExportVisitor +{ + private const string PROPERTY_ID = "id"; + private const string PROPERTY_TYPE = "type"; + private const string PROPERTY_PROPERTIES = "properties"; + private const string PROPERTY_ZONES = "zones"; + private const string PROPERTY_RESOURCES = "resources"; + private const string PROPERTY_SUBSCRIPTIONID = "subscriptionId"; + private const string PROPERTY_RESOURCEGROUPNAME = "resourceGroupName"; + private const string PROPERTY_KIND = "kind"; + private const string PROPERTY_SHAREDKEY = "sharedKey"; + private const string PROPERTY_NETWORKPROFILE = "networkProfile"; + private const string PROPERTY_NETWORKINTERFACES = "networkInterfaces"; + private const string PROPERTY_NETOWORKPLUGIN = "networkPlugin"; + private const string PROPERTY_AGENTPOOLPROFILES = "agentPoolProfiles"; + private const string PROPERTY_TENANTID = "tenantId"; + private const string PROPERTY_POLICIES = "policies"; + private const string PROPERTY_FIREWALLRULES = "firewallRules"; + private const string PROPERTY_SECURITYALERTPOLICIES = "securityAlertPolicies"; + private const string PROPERTY_CONFIGURATIONS = "configurations"; + private const string PROPERTY_ADMINISTRATORS = "administrators"; + private const string PROPERTY_VULNERABILITYASSESSMENTS = "vulnerabilityAssessments"; + private const string PROPERTY_AUDITINGSETTINGS = "auditingSettings"; + private const string PROPERTY_CUSTOMDOMAINS = "customDomains"; + private const string PROPERTY_WEBHOOKS = "webhooks"; + private const string PROPERTY_ORIGINGROUPS = "originGroups"; + private const string PROPERTY_REPLICATIONS = "replications"; + private const string PROPERTY_TASKS = "tasks"; + private const string PROPERTY_SECURITYPOLICIES = "securityPolicies"; + private const string PROPERTY_CONTAINERS = "containers"; + private const string PROPERTY_SHARES = "shares"; + private const string PROPERTY_TOPICS = "topics"; + private const string PROPERTY_MAINTENANCECONFIGURATIONS = "maintenanceConfigurations"; + + private const string TYPE_CONTAINERSERVICE_MANAGEDCLUSTERS = "Microsoft.ContainerService/managedClusters"; + private const string TYPE_CONTAINERREGISTRY_REGISTRIES = "Microsoft.ContainerRegistry/registries"; + private const string TYPE_CONTAINERREGISTRY_REGISTRIES_LISTUSAGES = "Microsoft.ContainerRegistry/registries/listUsages"; + private const string TYPE_CDN_PROFILES = "Microsoft.Cdn/profiles"; + private const string TYPE_CDN_PROFILES_ENDPOINTS = "Microsoft.Cdn/profiles/endpoints"; + private const string TYPE_CDN_PROFILES_AFDENDPOINTS = "Microsoft.Cdn/profiles/afdEndpoints"; + private const string TYPE_AUTOMATION_ACCOUNTS = "Microsoft.Automation/automationAccounts"; + private const string TYPE_APIMANAGEMENT_SERVICE = "Microsoft.ApiManagement/service"; + private const string TYPE_SQL_SERVERS = "Microsoft.Sql/servers"; + private const string TYPE_SQL_DATABASES = "Microsoft.Sql/servers/databases"; + private const string TYPE_POSTGRESQL_SERVERS = "Microsoft.DBforPostgreSQL/servers"; + private const string TYPE_POSTGRESQL_FLEXABLESERVERS = "Microsoft.DBforPostgreSQL/flexibleServers"; + private const string TYPE_MYSQL_SERVERS = "Microsoft.DBforMySQL/servers"; + private const string TYPE_MYSQL_FLEXABLESERVERS = "Microsoft.DBforMySQL/flexibleServers"; + private const string TYPE_STORAGE_ACCOUNTS = "Microsoft.Storage/storageAccounts"; + private const string TYPE_WEB_APP = "Microsoft.Web/sites"; + private const string TYPE_WEB_APPSLOT = "Microsoft.Web/sites/slots"; + private const string TYPE_RECOVERYSERVICES_VAULT = "Microsoft.RecoveryServices/vaults"; + private const string TYPE_COMPUTER_VIRTUALMACHINE = "Microsoft.Compute/virtualMachines"; + private const string TYPE_KEYVAULT_VAULT = "Microsoft.KeyVault/vaults"; + private const string TYPE_NETWORK_FRONTDOOR = "Microsoft.Network/frontDoors"; + private const string TYPE_NETWORK_CONNECTION = "Microsoft.Network/connections"; + private const string TYPE_SUBSCRIPTION = "Microsoft.Subscription"; + private const string TYPE_RESOURCES_RESOURCEGROUP = "Microsoft.Resources/resourceGroups"; + private const string TYPE_KUSTO_CLUSTER = "Microsoft.Kusto/Clusters"; + private const string TYPE_EVENTHUB_NAMESPACE = "Microsoft.EventHub/namespaces"; + private const string TYPE_SERVICEBUS_NAMESPACE = "Microsoft.ServiceBus/namespaces"; + private const string TYPE_VISUALSTUDIO_ACCOUNT = "Microsoft.VisualStudio/account"; + private const string TYPE_DEVCENTER_PROJECT = "Microsoft.DevCenter/projects"; + private const string TYPE_NETWORK_FIREWALLPOLICY = "Microsoft.Network/firewallPolicies"; + private const string TYPE_NETWORK_VIRTUALHUB = "Microsoft.Network/virtualHubs"; + private const string TYPE_EVENTGRID_TOPIC = "Microsoft.EventGrid/topics"; + private const string TYPE_EVENTGRID_DOMAIN = "Microsoft.EventGrid/domains"; + private const string TYPE_EVENTGRID_NAMESPACE = "Microsoft.EventGrid/namespaces"; + + private const string PROVIDERTYPE_DIAGNOSTICSETTINGS = "/providers/microsoft.insights/diagnosticSettings"; + private const string PROVIDERTYPE_ROLEASSIGNMENTS = "/providers/Microsoft.Authorization/roleAssignments"; + private const string PROVIDERTYPE_RESOURCELOCKS = "/providers/Microsoft.Authorization/locks"; + private const string PROVIDERTYPE_AUTOPROVISIONING = "/providers/Microsoft.Security/autoProvisioningSettings"; + private const string PROVIDERTYPE_SECURITYCONTACTS = "/providers/Microsoft.Security/securityContacts"; + private const string PROVIDERTYPE_SECURITYPRICINGS = "/providers/Microsoft.Security/pricings"; + private const string PROVIDERTYPE_POLICYASSIGNMENTS = "/providers/Microsoft.Authorization/policyAssignments"; + private const string PROVIDERTYPE_CLASSICADMINISTRATORS = "/providers/Microsoft.Authorization/classicAdministrators"; + private const string PROVIDERTYPE_APICOLLECTIONS = "/providers/Microsoft.Security/apiCollections"; + private const string PROVIDERTYPE_DEFENDERFORSTORAGESETTINGS = "/providers/Microsoft.Security/DefenderForStorageSettings"; + + private const string MASKED_VALUE = "*** MASKED ***"; + + private const string APIVERSION_2014_04_01 = "2014-04-01"; + private const string APIVERSION_2016_09_01 = "2016-09-01"; + private const string APIVERSION_2017_12_01 = "2017-12-01"; + private const string APIVERSION_2021_05_01_PREVIEW = "2021-05-01-preview"; + private const string APIVERSION_2021_06_01_PREVIEW = "2021-06-01-preview"; + private const string APIVERSION_2021_08_27 = "2021-08-27"; + private const string APIVERSION_2021_11_01 = "2021-11-01"; + private const string APIVERSION_2022_07_01 = "2022-07-01"; + private const string APIVERSION_2022_08_01 = "2022-08-01"; + private const string APIVERSION_2022_11_20_PREVIEW = "2022-11-20-preview"; + private const string APIVERSION_2022_04_01 = "2022-04-01"; + private const string APIVERSION_2022_09_01 = "2022-09-01"; + private const string APIVERSION_2022_09_10 = "2022-09-10"; + private const string APIVERSION_2023_01_01 = "2023-01-01"; + private const string APIVERSION_2023_01_01_PREVIEW = "2023-01-01-preview"; + private const string APIVERSION_2023_03_01_PREVIEW = "2023-03-01-preview"; + private const string APIVERSION_2023_04_01 = "2023-04-01"; + private const string APIVERSION_2023_05_01 = "2023-05-01"; + private const string APIVERSION_2023_06_30 = "2023-06-30"; + private const string APIVERSION_2023_09_01 = "2023-09-01"; + private const string APIVERSION_2023_12_15_PREVIEW = "2023-12-15-preview"; + private const string APIVERSION_2024_03_02_PREVIEW = "2024-03-02-preview"; + + private readonly ProviderData _ProviderData; + + public ResourceExportVisitor() + { + _ProviderData = new ProviderData(); + } - internal async Task GetAsync(string resourceId, string apiVersion) - { - return await _Context.GetAsync(TenantId, resourceId, apiVersion); - } + private sealed class ResourceContext + { + private readonly IResourceExportContext _Context; - internal async Task ListAsync(string resourceId, string apiVersion, bool ignoreNotFound) - { - return await _Context.ListAsync(TenantId, resourceId, apiVersion, ignoreNotFound); - } + public ResourceContext(IResourceExportContext context, string tenantId) + { + _Context = context; + TenantId = tenantId; } - public async Task VisitAsync(IResourceExportContext context, JObject resource) + public string TenantId { get; } + + internal async Task GetAsync(string resourceId, string apiVersion) { - await ExpandResource(context, resource); + return await _Context.GetAsync(TenantId, resourceId, apiVersion); } - private async Task ExpandResource(IResourceExportContext context, JObject resource) + internal async Task ListAsync(string resourceId, string apiVersion, bool ignoreNotFound) { - if (resource == null || - !resource.TryStringProperty(PROPERTY_TYPE, out var resourceType) || - string.IsNullOrWhiteSpace(resourceType) || - !resource.TryGetProperty(PROPERTY_ID, out var resourceId) || - !resource.TryGetProperty(PROPERTY_TENANTID, out var tenantId)) - return false; - - var resourceContext = new ResourceContext(context, tenantId); - - // Set subscriptionId and resourceGroupName. - SetResourceIdentifiers(resource, resourceType, resourceId); - - // Ignore expand of these. - if (string.Equals(resourceType, TYPE_VISUALSTUDIO_ACCOUNT, StringComparison.OrdinalIgnoreCase)) - return true; - - // Expand properties for the resource. - await GetProperties(resourceContext, resource, resourceType, resourceId); - - // Expand sub-resources for the resource. - return await VisitResourceGroup(resourceContext, resource, resourceType, resourceId) || - await VisitAPIManagement(resourceContext, resource, resourceType, resourceId) || - await VisitAutomationAccount(resourceContext, resource, resourceType, resourceId) || - await VisitCDNEndpoint(resourceContext, resource, resourceType, resourceId) || - await VisitCDNProfile(resourceContext, resource, resourceType, resourceId) || - await VisitFrontDoorEndpoint(resourceContext, resource, resourceType, resourceId) || - await VisitContainerRegistry(resourceContext, resource, resourceType, resourceId) || - await VisitAKSCluster(resourceContext, resource, resourceType, resourceId) || - await VisitSqlServers(resourceContext, resource, resourceType, resourceId) || - await VisitSqlDatabase(resourceContext, resource, resourceType, resourceId) || - await VisitPostgreSqlServer(resourceContext, resource, resourceType, resourceId) || - await VisitPostgreSqlFlexibleServer(resourceContext, resource, resourceType, resourceId) || - await VisitMySqlServer(resourceContext, resource, resourceType, resourceId) || - await VisitMySqlFlexibleServer(resourceContext, resource, resourceType, resourceId) || - await VisitStorageAccount(resourceContext, resource, resourceType, resourceId) || - await VisitWebApp(resourceContext, resource, resourceType, resourceId) || - await VisitRecoveryServicesVault(resourceContext, resource, resourceType, resourceId) || - await VisitVirtualMachine(resourceContext, resource, resourceType, resourceId) || - await VisitKeyVault(resourceContext, resource, resourceType, resourceId) || - await VisitFrontDoorClassic(resourceContext, resource, resourceType, resourceId) || - await VisitSubscription(resourceContext, resource, resourceType, resourceId) || - await VisitDataExplorerCluster(resourceContext, resource, resourceType, resourceId) || - await VisitEventHubNamespace(resourceContext, resource, resourceType, resourceId) || - await VisitServiceBusNamespace(resourceContext, resource, resourceType, resourceId) || - await VisitEventGridTopic(resourceContext, resource, resourceType, resourceId) || - await VisitEventGridDomain(resourceContext, resource, resourceType, resourceId) || - await VisitEventGridNamespace(resourceContext, resource, resourceType, resourceId) || - await VisitDevCenterProject(resourceContext, resource, resourceType, resourceId) || - await VisitFirewallPolicy(resourceContext, resource, resourceType, resourceId) || - await VisitVirtualHub(resourceContext, resource, resourceType, resourceId) || - VisitNetworkConnection(resource, resourceType); + return await _Context.ListAsync(TenantId, resourceId, apiVersion, ignoreNotFound); } + } - /// - /// Get the properties property for a resource if it hasn't been already expanded. - /// - private async Task GetProperties(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (string.Equals(resourceType, TYPE_SUBSCRIPTION, StringComparison.OrdinalIgnoreCase) || - string.Equals(resourceType, TYPE_RESOURCES_RESOURCEGROUP, StringComparison.OrdinalIgnoreCase) || - resource.ContainsKeyInsensitive(PROPERTY_PROPERTIES) || - !TryGetLatestAPIVersion(resourceType, out var apiVersion)) - return; + public async Task VisitAsync(IResourceExportContext context, JObject resource) + { + await ExpandResource(context, resource); + } - var full = await GetResource(context, resourceId, apiVersion); - if (full == null) - return; + private async Task ExpandResource(IResourceExportContext context, JObject resource) + { + if (resource == null || + !resource.TryStringProperty(PROPERTY_TYPE, out var resourceType) || + string.IsNullOrWhiteSpace(resourceType) || + !resource.TryGetProperty(PROPERTY_ID, out var resourceId) || + !resource.TryGetProperty(PROPERTY_TENANTID, out var tenantId)) + return false; + + var resourceContext = new ResourceContext(context, tenantId); + + // Set subscriptionId and resourceGroupName. + SetResourceIdentifiers(resource, resourceType, resourceId); + + // Ignore expand of these. + if (string.Equals(resourceType, TYPE_VISUALSTUDIO_ACCOUNT, StringComparison.OrdinalIgnoreCase)) + return true; + + // Expand properties for the resource. + await GetProperties(resourceContext, resource, resourceType, resourceId); + + // Expand sub-resources for the resource. + return await VisitResourceGroup(resourceContext, resource, resourceType, resourceId) || + await VisitAPIManagement(resourceContext, resource, resourceType, resourceId) || + await VisitAutomationAccount(resourceContext, resource, resourceType, resourceId) || + await VisitCDNEndpoint(resourceContext, resource, resourceType, resourceId) || + await VisitCDNProfile(resourceContext, resource, resourceType, resourceId) || + await VisitFrontDoorEndpoint(resourceContext, resource, resourceType, resourceId) || + await VisitContainerRegistry(resourceContext, resource, resourceType, resourceId) || + await VisitAKSCluster(resourceContext, resource, resourceType, resourceId) || + await VisitSqlServers(resourceContext, resource, resourceType, resourceId) || + await VisitSqlDatabase(resourceContext, resource, resourceType, resourceId) || + await VisitPostgreSqlServer(resourceContext, resource, resourceType, resourceId) || + await VisitPostgreSqlFlexibleServer(resourceContext, resource, resourceType, resourceId) || + await VisitMySqlServer(resourceContext, resource, resourceType, resourceId) || + await VisitMySqlFlexibleServer(resourceContext, resource, resourceType, resourceId) || + await VisitStorageAccount(resourceContext, resource, resourceType, resourceId) || + await VisitWebApp(resourceContext, resource, resourceType, resourceId) || + await VisitRecoveryServicesVault(resourceContext, resource, resourceType, resourceId) || + await VisitVirtualMachine(resourceContext, resource, resourceType, resourceId) || + await VisitKeyVault(resourceContext, resource, resourceType, resourceId) || + await VisitFrontDoorClassic(resourceContext, resource, resourceType, resourceId) || + await VisitSubscription(resourceContext, resource, resourceType, resourceId) || + await VisitDataExplorerCluster(resourceContext, resource, resourceType, resourceId) || + await VisitEventHubNamespace(resourceContext, resource, resourceType, resourceId) || + await VisitServiceBusNamespace(resourceContext, resource, resourceType, resourceId) || + await VisitEventGridTopic(resourceContext, resource, resourceType, resourceId) || + await VisitEventGridDomain(resourceContext, resource, resourceType, resourceId) || + await VisitEventGridNamespace(resourceContext, resource, resourceType, resourceId) || + await VisitDevCenterProject(resourceContext, resource, resourceType, resourceId) || + await VisitFirewallPolicy(resourceContext, resource, resourceType, resourceId) || + await VisitVirtualHub(resourceContext, resource, resourceType, resourceId) || + VisitNetworkConnection(resource, resourceType); + } - if (full.TryGetProperty(PROPERTY_PROPERTIES, out JObject properties)) - resource[PROPERTY_PROPERTIES] = properties; + /// + /// Get the properties property for a resource if it hasn't been already expanded. + /// + private async Task GetProperties(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (string.Equals(resourceType, TYPE_SUBSCRIPTION, StringComparison.OrdinalIgnoreCase) || + string.Equals(resourceType, TYPE_RESOURCES_RESOURCEGROUP, StringComparison.OrdinalIgnoreCase) || + resource.ContainsKeyInsensitive(PROPERTY_PROPERTIES) || + !TryGetLatestAPIVersion(resourceType, out var apiVersion)) + return; - if (full.TryGetProperty(PROPERTY_ZONES, out JArray zones)) - resource[PROPERTY_ZONES] = zones; - } + var full = await GetResource(context, resourceId, apiVersion); + if (full == null) + return; - /// - /// Set subscriptionId and resourceGroupName on the resource based on the provided resourceId. - /// - private static void SetResourceIdentifiers(JObject resource, string resourceType, string resourceId) - { - if (ResourceHelper.TryResourceGroup(resourceId, out var subscriptionId, out var resourceGroupName) && - !string.Equals(resourceType, TYPE_RESOURCES_RESOURCEGROUP, StringComparison.OrdinalIgnoreCase)) - resource.Add(PROPERTY_RESOURCEGROUPNAME, resourceGroupName); + if (full.TryGetProperty(PROPERTY_PROPERTIES, out JObject properties)) + resource[PROPERTY_PROPERTIES] = properties; - if (!string.IsNullOrEmpty(subscriptionId)) - resource.Add(PROPERTY_SUBSCRIPTIONID, subscriptionId); - } + if (full.TryGetProperty(PROPERTY_ZONES, out JArray zones)) + resource[PROPERTY_ZONES] = zones; + } - private bool TryGetLatestAPIVersion(string resourceType, out string apiVersion) - { - apiVersion = null; - if (!_ProviderData.TryResourceType(resourceType, out var data) || - data.ApiVersions == null || - data.ApiVersions.Length == 0) - return false; + /// + /// Set subscriptionId and resourceGroupName on the resource based on the provided resourceId. + /// + private static void SetResourceIdentifiers(JObject resource, string resourceType, string resourceId) + { + if (ResourceHelper.TryResourceGroup(resourceId, out var subscriptionId, out var resourceGroupName) && + !string.Equals(resourceType, TYPE_RESOURCES_RESOURCEGROUP, StringComparison.OrdinalIgnoreCase)) + resource.Add(PROPERTY_RESOURCEGROUPNAME, resourceGroupName); - apiVersion = data.ApiVersions[0]; - return true; - } + if (!string.IsNullOrEmpty(subscriptionId)) + resource.Add(PROPERTY_SUBSCRIPTIONID, subscriptionId); + } - private static async Task VisitServiceBusNamespace(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_SERVICEBUS_NAMESPACE, StringComparison.OrdinalIgnoreCase)) - return false; + private bool TryGetLatestAPIVersion(string resourceType, out string apiVersion) + { + apiVersion = null; + if (!_ProviderData.TryResourceType(resourceType, out var data) || + data.ApiVersions == null || + data.ApiVersions.Length == 0) + return false; + + apiVersion = data.ApiVersions[0]; + return true; + } - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "queues", APIVERSION_2021_06_01_PREVIEW)); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_TOPICS, APIVERSION_2021_06_01_PREVIEW)); - return true; - } + private static async Task VisitServiceBusNamespace(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_SERVICEBUS_NAMESPACE, StringComparison.OrdinalIgnoreCase)) + return false; - private static async Task VisitEventHubNamespace(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_EVENTHUB_NAMESPACE, StringComparison.OrdinalIgnoreCase)) - return false; + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "queues", APIVERSION_2021_06_01_PREVIEW)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_TOPICS, APIVERSION_2021_06_01_PREVIEW)); + return true; + } - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "eventhubs", APIVERSION_2021_11_01)); - return true; - } + private static async Task VisitEventHubNamespace(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_EVENTHUB_NAMESPACE, StringComparison.OrdinalIgnoreCase)) + return false; - private static async Task VisitEventGridTopic(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_EVENTGRID_TOPIC, StringComparison.OrdinalIgnoreCase)) - return false; + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "eventhubs", APIVERSION_2021_11_01)); + return true; + } - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "eventSubscriptions", APIVERSION_2023_12_15_PREVIEW)); - return true; - } + private static async Task VisitEventGridTopic(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_EVENTGRID_TOPIC, StringComparison.OrdinalIgnoreCase)) + return false; - private static async Task VisitEventGridDomain(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_EVENTGRID_DOMAIN, StringComparison.OrdinalIgnoreCase)) - return false; + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "eventSubscriptions", APIVERSION_2023_12_15_PREVIEW)); + return true; + } - var topics = await GetSubResourcesByType(context, resourceId, PROPERTY_TOPICS, APIVERSION_2023_12_15_PREVIEW); - foreach (var topic in topics) - { - if (topic.TryStringProperty(PROPERTY_ID, out var topicId)) - AddSubResource(topic, await GetSubResourcesByType(context, topicId, "eventSubscriptions", APIVERSION_2023_12_15_PREVIEW)); - } + private static async Task VisitEventGridDomain(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_EVENTGRID_DOMAIN, StringComparison.OrdinalIgnoreCase)) + return false; - AddSubResource(resource, topics); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "eventSubscriptions", APIVERSION_2023_12_15_PREVIEW)); - return true; + var topics = await GetSubResourcesByType(context, resourceId, PROPERTY_TOPICS, APIVERSION_2023_12_15_PREVIEW); + foreach (var topic in topics) + { + if (topic.TryStringProperty(PROPERTY_ID, out var topicId)) + AddSubResource(topic, await GetSubResourcesByType(context, topicId, "eventSubscriptions", APIVERSION_2023_12_15_PREVIEW)); } - private static async Task VisitEventGridNamespace(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_EVENTGRID_NAMESPACE, StringComparison.OrdinalIgnoreCase)) - return false; + AddSubResource(resource, topics); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "eventSubscriptions", APIVERSION_2023_12_15_PREVIEW)); + return true; + } - var topics = await GetSubResourcesByType(context, resourceId, PROPERTY_TOPICS, APIVERSION_2023_12_15_PREVIEW); - foreach (var topic in topics) - { - if (topic.TryStringProperty(PROPERTY_ID, out var topicId)) - AddSubResource(topic, await GetSubResourcesByType(context, topicId, "eventSubscriptions", APIVERSION_2023_12_15_PREVIEW)); - } + private static async Task VisitEventGridNamespace(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_EVENTGRID_NAMESPACE, StringComparison.OrdinalIgnoreCase)) + return false; - AddSubResource(resource, topics); - return true; + var topics = await GetSubResourcesByType(context, resourceId, PROPERTY_TOPICS, APIVERSION_2023_12_15_PREVIEW); + foreach (var topic in topics) + { + if (topic.TryStringProperty(PROPERTY_ID, out var topicId)) + AddSubResource(topic, await GetSubResourcesByType(context, topicId, "eventSubscriptions", APIVERSION_2023_12_15_PREVIEW)); } - private static async Task VisitDevCenterProject(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_DEVCENTER_PROJECT, StringComparison.OrdinalIgnoreCase)) - return false; + AddSubResource(resource, topics); + return true; + } - var pools = await GetSubResourcesByType(context, resourceId, "pools", APIVERSION_2023_04_01); - foreach (var pool in pools) - { - if (pool.TryStringProperty(PROPERTY_ID, out var poolId)) - AddSubResource(pool, await GetSubResourcesByType(context, poolId, "schedules", APIVERSION_2023_04_01)); - } + private static async Task VisitDevCenterProject(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_DEVCENTER_PROJECT, StringComparison.OrdinalIgnoreCase)) + return false; - AddSubResource(resource, pools); - return true; + var pools = await GetSubResourcesByType(context, resourceId, "pools", APIVERSION_2023_04_01); + foreach (var pool in pools) + { + if (pool.TryStringProperty(PROPERTY_ID, out var poolId)) + AddSubResource(pool, await GetSubResourcesByType(context, poolId, "schedules", APIVERSION_2023_04_01)); } - private static async Task VisitFirewallPolicy(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_NETWORK_FIREWALLPOLICY, StringComparison.OrdinalIgnoreCase)) - return false; + AddSubResource(resource, pools); + return true; + } - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "ruleCollectionGroups", APIVERSION_2023_09_01)); + private static async Task VisitFirewallPolicy(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_NETWORK_FIREWALLPOLICY, StringComparison.OrdinalIgnoreCase)) + return false; - // Add signature overrides for premium firewall policies. - if (resource.TryGetProperty(PROPERTY_PROPERTIES, out JObject properties) && - properties.TryGetProperty("sku", out JObject sku) && - sku.TryStringProperty("tier", out var tier) && - string.Equals(tier, "Premium", StringComparison.OrdinalIgnoreCase)) - { - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "signatureOverrides", APIVERSION_2023_09_01)); - } - return true; - } + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "ruleCollectionGroups", APIVERSION_2023_09_01)); - private static async Task VisitVirtualHub(ResourceContext context, JObject resource, string resourceType, string resourceId) + // Add signature overrides for premium firewall policies. + if (resource.TryGetProperty(PROPERTY_PROPERTIES, out JObject properties) && + properties.TryGetProperty("sku", out JObject sku) && + sku.TryStringProperty("tier", out var tier) && + string.Equals(tier, "Premium", StringComparison.OrdinalIgnoreCase)) { - if (!string.Equals(resourceType, TYPE_NETWORK_VIRTUALHUB, StringComparison.OrdinalIgnoreCase)) - return false; - - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "routingIntent", APIVERSION_2023_04_01)); - return true; + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "signatureOverrides", APIVERSION_2023_09_01)); } + return true; + } - private static async Task VisitDataExplorerCluster(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_KUSTO_CLUSTER, StringComparison.OrdinalIgnoreCase)) - return false; + private static async Task VisitVirtualHub(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_NETWORK_VIRTUALHUB, StringComparison.OrdinalIgnoreCase)) + return false; - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "databases", APIVERSION_2021_08_27)); - return true; - } + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "routingIntent", APIVERSION_2023_04_01)); + return true; + } - private static async Task VisitResourceGroup(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_RESOURCES_RESOURCEGROUP, StringComparison.OrdinalIgnoreCase)) - return false; + private static async Task VisitDataExplorerCluster(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_KUSTO_CLUSTER, StringComparison.OrdinalIgnoreCase)) + return false; - await GetRoleAssignments(context, resource, resourceId); - await GetResourceLocks(context, resource, resourceId); - return true; - } + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "databases", APIVERSION_2021_08_27)); + return true; + } - private static async Task VisitSubscription(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_SUBSCRIPTION, StringComparison.OrdinalIgnoreCase)) - return false; - - await GetRoleAssignments(context, resource, resourceId); - AddSubResource(resource, await GetResource(context, string.Concat(resourceId, PROVIDERTYPE_CLASSICADMINISTRATORS), "2015-07-01")); - AddSubResource(resource, await GetResource(context, string.Concat(resourceId, PROVIDERTYPE_AUTOPROVISIONING), "2017-08-01-preview")); - AddSubResource(resource, await GetResource(context, string.Concat(resourceId, PROVIDERTYPE_SECURITYCONTACTS), "2017-08-01-preview")); - AddSubResource(resource, await GetResource(context, string.Concat(resourceId, PROVIDERTYPE_SECURITYPRICINGS), "2018-06-01")); - AddSubResource(resource, await GetResource(context, string.Concat(resourceId, PROVIDERTYPE_POLICYASSIGNMENTS), "2019-06-01")); - return true; - } + private static async Task VisitResourceGroup(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_RESOURCES_RESOURCEGROUP, StringComparison.OrdinalIgnoreCase)) + return false; - private static bool VisitNetworkConnection(JObject resource, string resourceType) - { - if (!string.Equals(resourceType, TYPE_NETWORK_CONNECTION, StringComparison.OrdinalIgnoreCase)) - return false; + await GetRoleAssignments(context, resource, resourceId); + await GetResourceLocks(context, resource, resourceId); + return true; + } - if (resource.TryGetProperty(PROPERTY_PROPERTIES, out JObject properties)) - properties.ReplaceProperty(PROPERTY_SHAREDKEY, JValue.CreateString(MASKED_VALUE)); + private static async Task VisitSubscription(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_SUBSCRIPTION, StringComparison.OrdinalIgnoreCase)) + return false; + + await GetRoleAssignments(context, resource, resourceId); + AddSubResource(resource, await GetResource(context, string.Concat(resourceId, PROVIDERTYPE_CLASSICADMINISTRATORS), "2015-07-01")); + AddSubResource(resource, await GetResource(context, string.Concat(resourceId, PROVIDERTYPE_AUTOPROVISIONING), "2017-08-01-preview")); + AddSubResource(resource, await GetResource(context, string.Concat(resourceId, PROVIDERTYPE_SECURITYCONTACTS), "2017-08-01-preview")); + AddSubResource(resource, await GetResource(context, string.Concat(resourceId, PROVIDERTYPE_SECURITYPRICINGS), "2018-06-01")); + AddSubResource(resource, await GetResource(context, string.Concat(resourceId, PROVIDERTYPE_POLICYASSIGNMENTS), "2019-06-01")); + return true; + } - return true; - } + private static bool VisitNetworkConnection(JObject resource, string resourceType) + { + if (!string.Equals(resourceType, TYPE_NETWORK_CONNECTION, StringComparison.OrdinalIgnoreCase)) + return false; - private static async Task VisitFrontDoorClassic(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_NETWORK_FRONTDOOR, StringComparison.OrdinalIgnoreCase)) - return false; + if (resource.TryGetProperty(PROPERTY_PROPERTIES, out JObject properties)) + properties.ReplaceProperty(PROPERTY_SHAREDKEY, JValue.CreateString(MASKED_VALUE)); - await GetDiagnosticSettings(context, resource, resourceId); - return true; - } + return true; + } - private static async Task VisitFrontDoorEndpoint(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_CDN_PROFILES_AFDENDPOINTS, StringComparison.OrdinalIgnoreCase)) - return false; + private static async Task VisitFrontDoorClassic(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_NETWORK_FRONTDOOR, StringComparison.OrdinalIgnoreCase)) + return false; - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "routes", APIVERSION_2023_05_01)); - return true; - } + await GetDiagnosticSettings(context, resource, resourceId); + return true; + } - private static async Task VisitKeyVault(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_KEYVAULT_VAULT, StringComparison.OrdinalIgnoreCase)) - return false; + private static async Task VisitFrontDoorEndpoint(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_CDN_PROFILES_AFDENDPOINTS, StringComparison.OrdinalIgnoreCase)) + return false; - await GetDiagnosticSettings(context, resource, resourceId); - if (resource.TryGetProperty(PROPERTY_PROPERTIES, out JObject properties) && - properties.TryGetProperty(PROPERTY_TENANTID, out var tenantId) && - string.Equals(tenantId, context.TenantId)) - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "keys", APIVERSION_2022_07_01)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "routes", APIVERSION_2023_05_01)); + return true; + } - return true; - } + private static async Task VisitKeyVault(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_KEYVAULT_VAULT, StringComparison.OrdinalIgnoreCase)) + return false; - private static async Task VisitVirtualMachine(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_COMPUTER_VIRTUALMACHINE, StringComparison.OrdinalIgnoreCase)) - return false; + await GetDiagnosticSettings(context, resource, resourceId); + if (resource.TryGetProperty(PROPERTY_PROPERTIES, out JObject properties) && + properties.TryGetProperty(PROPERTY_TENANTID, out var tenantId) && + string.Equals(tenantId, context.TenantId)) + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "keys", APIVERSION_2022_07_01)); - if (resource.TryGetProperty(PROPERTY_PROPERTIES, out JObject properties) && - properties.TryGetProperty(PROPERTY_NETWORKPROFILE, out JObject networkProfile) && - networkProfile.TryGetProperty(PROPERTY_NETWORKINTERFACES, out JArray networkInterfaces)) + return true; + } + + private static async Task VisitVirtualMachine(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_COMPUTER_VIRTUALMACHINE, StringComparison.OrdinalIgnoreCase)) + return false; + + if (resource.TryGetProperty(PROPERTY_PROPERTIES, out JObject properties) && + properties.TryGetProperty(PROPERTY_NETWORKPROFILE, out JObject networkProfile) && + networkProfile.TryGetProperty(PROPERTY_NETWORKINTERFACES, out JArray networkInterfaces)) + { + foreach (var netif in networkInterfaces.Values()) { - foreach (var netif in networkInterfaces.Values()) - { - if (netif.TryGetProperty(PROPERTY_ID, out var id)) - AddSubResource(resource, await GetResource(context, id, APIVERSION_2022_07_01)); - } + if (netif.TryGetProperty(PROPERTY_ID, out var id)) + AddSubResource(resource, await GetResource(context, id, APIVERSION_2022_07_01)); } + } - var response = await context.GetAsync($"{resourceId}/instanceView", APIVERSION_2021_11_01); - resource.Add("PowerState", response["statuses"].FirstOrDefault(status => status["code"].Value().StartsWith("PowerState/"))["code"].Value()); + var response = await context.GetAsync($"{resourceId}/instanceView", APIVERSION_2021_11_01); + resource.Add("PowerState", response["statuses"].FirstOrDefault(status => status["code"].Value().StartsWith("PowerState/"))["code"].Value()); - return true; - } + return true; + } - private static async Task VisitRecoveryServicesVault(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_RECOVERYSERVICES_VAULT, StringComparison.OrdinalIgnoreCase)) - return false; + private static async Task VisitRecoveryServicesVault(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_RECOVERYSERVICES_VAULT, StringComparison.OrdinalIgnoreCase)) + return false; - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "replicationRecoveryPlans", APIVERSION_2022_09_10)); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "replicationAlertSettings", APIVERSION_2022_09_10)); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "backupstorageconfig/vaultstorageconfig", "2022-09-01-preview")); - return true; - } + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "replicationRecoveryPlans", APIVERSION_2022_09_10)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "replicationAlertSettings", APIVERSION_2022_09_10)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "backupstorageconfig/vaultstorageconfig", "2022-09-01-preview")); + return true; + } - private static async Task VisitWebApp(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_WEB_APP, StringComparison.OrdinalIgnoreCase) && - !string.Equals(resourceType, TYPE_WEB_APPSLOT, StringComparison.OrdinalIgnoreCase)) - return false; + private static async Task VisitWebApp(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_WEB_APP, StringComparison.OrdinalIgnoreCase) && + !string.Equals(resourceType, TYPE_WEB_APPSLOT, StringComparison.OrdinalIgnoreCase)) + return false; - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "config", APIVERSION_2022_09_01)); - return true; - } + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "config", APIVERSION_2022_09_01)); + return true; + } - private static async Task VisitStorageAccount(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_STORAGE_ACCOUNTS, StringComparison.OrdinalIgnoreCase)) - return false; + private static async Task VisitStorageAccount(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_STORAGE_ACCOUNTS, StringComparison.OrdinalIgnoreCase)) + return false; - // Get blob services. - if (resource.TryGetProperty(PROPERTY_KIND, out var kind) && - !string.Equals(kind, "FileStorage", StringComparison.OrdinalIgnoreCase)) + // Get blob services. + if (resource.TryGetProperty(PROPERTY_KIND, out var kind) && + !string.Equals(kind, "FileStorage", StringComparison.OrdinalIgnoreCase)) + { + var blobServices = await GetSubResourcesByType(context, resourceId, "blobServices", APIVERSION_2023_01_01); + AddSubResource(resource, blobServices); + foreach (var blobService in blobServices) { - var blobServices = await GetSubResourcesByType(context, resourceId, "blobServices", APIVERSION_2023_01_01); - AddSubResource(resource, blobServices); - foreach (var blobService in blobServices) - { - AddSubResource(resource, await GetSubResourcesByType(context, blobService[PROPERTY_ID].Value(), PROPERTY_CONTAINERS, APIVERSION_2023_01_01)); - } + AddSubResource(resource, await GetSubResourcesByType(context, blobService[PROPERTY_ID].Value(), PROPERTY_CONTAINERS, APIVERSION_2023_01_01)); } + } - // Get file services. - else if (kind != null && - !string.Equals(kind, "BlobStorage", StringComparison.OrdinalIgnoreCase) && - !string.Equals(kind, "BlockBlobStorage", StringComparison.OrdinalIgnoreCase)) + // Get file services. + else if (kind != null && + !string.Equals(kind, "BlobStorage", StringComparison.OrdinalIgnoreCase) && + !string.Equals(kind, "BlockBlobStorage", StringComparison.OrdinalIgnoreCase)) + { + var blobServices = await GetSubResourcesByType(context, resourceId, "fileServices", APIVERSION_2023_01_01); + AddSubResource(resource, blobServices); + foreach (var blobService in blobServices) { - var blobServices = await GetSubResourcesByType(context, resourceId, "fileServices", APIVERSION_2023_01_01); - AddSubResource(resource, blobServices); - foreach (var blobService in blobServices) - { - AddSubResource(resource, await GetSubResourcesByType(context, blobService[PROPERTY_ID].Value(), PROPERTY_SHARES, APIVERSION_2023_01_01)); - } + AddSubResource(resource, await GetSubResourcesByType(context, blobService[PROPERTY_ID].Value(), PROPERTY_SHARES, APIVERSION_2023_01_01)); } - - AddSubResource(resource, await GetSubResourcesByProvider(context, resourceId, PROVIDERTYPE_DEFENDERFORSTORAGESETTINGS, "2022-12-01-preview", ignoreNotFound: true)); - return true; } - private static async Task VisitMySqlServer(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_MYSQL_SERVERS, StringComparison.OrdinalIgnoreCase)) - return false; - - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_ADMINISTRATORS, APIVERSION_2017_12_01)); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_FIREWALLRULES, APIVERSION_2017_12_01)); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_SECURITYALERTPOLICIES, APIVERSION_2017_12_01)); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_CONFIGURATIONS, APIVERSION_2017_12_01)); - return true; - } + AddSubResource(resource, await GetSubResourcesByProvider(context, resourceId, PROVIDERTYPE_DEFENDERFORSTORAGESETTINGS, "2022-12-01-preview", ignoreNotFound: true)); + return true; + } - private static async Task VisitMySqlFlexibleServer(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_MYSQL_FLEXABLESERVERS, StringComparison.OrdinalIgnoreCase)) - return false; + private static async Task VisitMySqlServer(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_MYSQL_SERVERS, StringComparison.OrdinalIgnoreCase)) + return false; + + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_ADMINISTRATORS, APIVERSION_2017_12_01)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_FIREWALLRULES, APIVERSION_2017_12_01)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_SECURITYALERTPOLICIES, APIVERSION_2017_12_01)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_CONFIGURATIONS, APIVERSION_2017_12_01)); + return true; + } - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_ADMINISTRATORS, APIVERSION_2023_06_30)); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_FIREWALLRULES, APIVERSION_2023_06_30)); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_CONFIGURATIONS, APIVERSION_2023_06_30)); - return true; - } + private static async Task VisitMySqlFlexibleServer(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_MYSQL_FLEXABLESERVERS, StringComparison.OrdinalIgnoreCase)) + return false; - private static async Task VisitPostgreSqlServer(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_POSTGRESQL_SERVERS, StringComparison.OrdinalIgnoreCase)) - return false; + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_ADMINISTRATORS, APIVERSION_2023_06_30)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_FIREWALLRULES, APIVERSION_2023_06_30)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_CONFIGURATIONS, APIVERSION_2023_06_30)); + return true; + } - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_ADMINISTRATORS, APIVERSION_2017_12_01)); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_FIREWALLRULES, APIVERSION_2017_12_01)); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_SECURITYALERTPOLICIES, APIVERSION_2017_12_01)); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_CONFIGURATIONS, APIVERSION_2017_12_01)); - return true; - } + private static async Task VisitPostgreSqlServer(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_POSTGRESQL_SERVERS, StringComparison.OrdinalIgnoreCase)) + return false; + + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_ADMINISTRATORS, APIVERSION_2017_12_01)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_FIREWALLRULES, APIVERSION_2017_12_01)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_SECURITYALERTPOLICIES, APIVERSION_2017_12_01)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_CONFIGURATIONS, APIVERSION_2017_12_01)); + return true; + } - private static async Task VisitPostgreSqlFlexibleServer(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_POSTGRESQL_FLEXABLESERVERS, StringComparison.OrdinalIgnoreCase)) - return false; + private static async Task VisitPostgreSqlFlexibleServer(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_POSTGRESQL_FLEXABLESERVERS, StringComparison.OrdinalIgnoreCase)) + return false; - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_ADMINISTRATORS, APIVERSION_2023_03_01_PREVIEW)); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_FIREWALLRULES, APIVERSION_2023_03_01_PREVIEW)); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_CONFIGURATIONS, APIVERSION_2023_03_01_PREVIEW)); - return true; - } + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_ADMINISTRATORS, APIVERSION_2023_03_01_PREVIEW)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_FIREWALLRULES, APIVERSION_2023_03_01_PREVIEW)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_CONFIGURATIONS, APIVERSION_2023_03_01_PREVIEW)); + return true; + } - private static async Task VisitSqlDatabase(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_SQL_DATABASES, StringComparison.OrdinalIgnoreCase)) - return false; - - var lowerId = resourceId.ToLower(); - AddSubResource(resource, await GetSubResourcesByType(context, lowerId, "dataMaskingPolicies", APIVERSION_2014_04_01)); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "transparentDataEncryption", APIVERSION_2021_11_01)); - AddSubResource(resource, await GetSubResourcesByType(context, lowerId, "connectionPolicies", APIVERSION_2014_04_01)); - AddSubResource(resource, await GetSubResourcesByType(context, lowerId, "geoBackupPolicies", APIVERSION_2014_04_01)); - return true; - } + private static async Task VisitSqlDatabase(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_SQL_DATABASES, StringComparison.OrdinalIgnoreCase)) + return false; + + var lowerId = resourceId.ToLower(); + AddSubResource(resource, await GetSubResourcesByType(context, lowerId, "dataMaskingPolicies", APIVERSION_2014_04_01)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "transparentDataEncryption", APIVERSION_2021_11_01)); + AddSubResource(resource, await GetSubResourcesByType(context, lowerId, "connectionPolicies", APIVERSION_2014_04_01)); + AddSubResource(resource, await GetSubResourcesByType(context, lowerId, "geoBackupPolicies", APIVERSION_2014_04_01)); + return true; + } - private static async Task VisitSqlServers(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_SQL_SERVERS, StringComparison.OrdinalIgnoreCase)) - return false; - - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_FIREWALLRULES, APIVERSION_2021_11_01)); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_ADMINISTRATORS, APIVERSION_2021_11_01)); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_SECURITYALERTPOLICIES, APIVERSION_2021_11_01)); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_VULNERABILITYASSESSMENTS, APIVERSION_2021_11_01)); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_AUDITINGSETTINGS, APIVERSION_2021_11_01)); - return true; - } + private static async Task VisitSqlServers(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_SQL_SERVERS, StringComparison.OrdinalIgnoreCase)) + return false; + + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_FIREWALLRULES, APIVERSION_2021_11_01)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_ADMINISTRATORS, APIVERSION_2021_11_01)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_SECURITYALERTPOLICIES, APIVERSION_2021_11_01)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_VULNERABILITYASSESSMENTS, APIVERSION_2021_11_01)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_AUDITINGSETTINGS, APIVERSION_2021_11_01)); + return true; + } - private static async Task VisitAKSCluster(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_CONTAINERSERVICE_MANAGEDCLUSTERS, StringComparison.OrdinalIgnoreCase)) - return false; - - // Get related VNET - if (resource.TryGetProperty(PROPERTY_PROPERTIES, out JObject properties) && - properties.TryGetProperty(PROPERTY_NETWORKPROFILE, out JObject networkProfile) && - networkProfile.TryGetProperty(PROPERTY_NETOWORKPLUGIN, out var networkPlugin) && - string.Equals(networkPlugin, "azure", StringComparison.OrdinalIgnoreCase) && - properties.TryArrayProperty(PROPERTY_AGENTPOOLPROFILES, out var agentPoolProfiles) && - agentPoolProfiles.Count > 0) + private static async Task VisitAKSCluster(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_CONTAINERSERVICE_MANAGEDCLUSTERS, StringComparison.OrdinalIgnoreCase)) + return false; + + // Get related VNET + if (resource.TryGetProperty(PROPERTY_PROPERTIES, out JObject properties) && + properties.TryGetProperty(PROPERTY_NETWORKPROFILE, out JObject networkProfile) && + networkProfile.TryGetProperty(PROPERTY_NETOWORKPLUGIN, out var networkPlugin) && + string.Equals(networkPlugin, "azure", StringComparison.OrdinalIgnoreCase) && + properties.TryArrayProperty(PROPERTY_AGENTPOOLPROFILES, out var agentPoolProfiles) && + agentPoolProfiles.Count > 0) + { + for (var i = 0; i < agentPoolProfiles.Count; i++) { - for (var i = 0; i < agentPoolProfiles.Count; i++) + if (agentPoolProfiles[i] is JObject profile && profile.TryGetProperty("vnetSubnetID", out var vnetSubnetID)) { - if (agentPoolProfiles[i] is JObject profile && profile.TryGetProperty("vnetSubnetID", out var vnetSubnetID)) - { - // Get VNET. - AddSubResource(resource, await GetResource(context, vnetSubnetID, APIVERSION_2022_07_01)); - } + // Get VNET. + AddSubResource(resource, await GetResource(context, vnetSubnetID, APIVERSION_2022_07_01)); } } + } - // Get maintenance configurations - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_MAINTENANCECONFIGURATIONS, APIVERSION_2024_03_02_PREVIEW)); + // Get maintenance configurations + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_MAINTENANCECONFIGURATIONS, APIVERSION_2024_03_02_PREVIEW)); - // Get diagnostic settings - await GetDiagnosticSettings(context, resource, resourceId); - return true; - } + // Get diagnostic settings + await GetDiagnosticSettings(context, resource, resourceId); + return true; + } - private static async Task VisitContainerRegistry(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_CONTAINERREGISTRY_REGISTRIES, StringComparison.OrdinalIgnoreCase)) - return false; + private static async Task VisitContainerRegistry(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_CONTAINERREGISTRY_REGISTRIES, StringComparison.OrdinalIgnoreCase)) + return false; - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_REPLICATIONS, APIVERSION_2023_01_01_PREVIEW)); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_WEBHOOKS, APIVERSION_2023_01_01_PREVIEW)); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_TASKS, "2019-04-01")); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_REPLICATIONS, APIVERSION_2023_01_01_PREVIEW)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_WEBHOOKS, APIVERSION_2023_01_01_PREVIEW)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_TASKS, "2019-04-01")); - // Handle usage information that does not include a strong type. - foreach (var usage in await GetSubResourcesByType(context, resourceId, "listUsages", APIVERSION_2023_01_01_PREVIEW)) - { - usage[PROPERTY_TYPE] = TYPE_CONTAINERREGISTRY_REGISTRIES_LISTUSAGES; - AddSubResource(resource, usage); - } - return true; + // Handle usage information that does not include a strong type. + foreach (var usage in await GetSubResourcesByType(context, resourceId, "listUsages", APIVERSION_2023_01_01_PREVIEW)) + { + usage[PROPERTY_TYPE] = TYPE_CONTAINERREGISTRY_REGISTRIES_LISTUSAGES; + AddSubResource(resource, usage); } + return true; + } - private static async Task VisitCDNEndpoint(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_CDN_PROFILES_ENDPOINTS, StringComparison.OrdinalIgnoreCase)) - return false; + private static async Task VisitCDNEndpoint(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_CDN_PROFILES_ENDPOINTS, StringComparison.OrdinalIgnoreCase)) + return false; - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_CUSTOMDOMAINS, APIVERSION_2023_05_01)); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_ORIGINGROUPS, APIVERSION_2023_05_01)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_CUSTOMDOMAINS, APIVERSION_2023_05_01)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_ORIGINGROUPS, APIVERSION_2023_05_01)); - await GetDiagnosticSettings(context, resource, resourceId); - return true; - } + await GetDiagnosticSettings(context, resource, resourceId); + return true; + } - private static async Task VisitCDNProfile(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_CDN_PROFILES, StringComparison.OrdinalIgnoreCase)) - return false; - - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_CUSTOMDOMAINS, APIVERSION_2023_05_01)); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_ORIGINGROUPS, APIVERSION_2023_05_01)); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "ruleSets", APIVERSION_2023_05_01)); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "secrets", APIVERSION_2023_05_01)); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_SECURITYPOLICIES, APIVERSION_2023_05_01)); - return true; - } + private static async Task VisitCDNProfile(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_CDN_PROFILES, StringComparison.OrdinalIgnoreCase)) + return false; + + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_CUSTOMDOMAINS, APIVERSION_2023_05_01)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_ORIGINGROUPS, APIVERSION_2023_05_01)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "ruleSets", APIVERSION_2023_05_01)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "secrets", APIVERSION_2023_05_01)); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_SECURITYPOLICIES, APIVERSION_2023_05_01)); + return true; + } - private static async Task VisitAutomationAccount(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_AUTOMATION_ACCOUNTS, StringComparison.OrdinalIgnoreCase)) - return false; + private static async Task VisitAutomationAccount(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_AUTOMATION_ACCOUNTS, StringComparison.OrdinalIgnoreCase)) + return false; - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "variables", "2022-08-08")); - AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_WEBHOOKS, "2015-10-31")); - return true; - } + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "variables", "2022-08-08")); + AddSubResource(resource, await GetSubResourcesByType(context, resourceId, PROPERTY_WEBHOOKS, "2015-10-31")); + return true; + } - private static async Task VisitAPIManagement(ResourceContext context, JObject resource, string resourceType, string resourceId) - { - if (!string.Equals(resourceType, TYPE_APIMANAGEMENT_SERVICE, StringComparison.OrdinalIgnoreCase)) - return false; + private static async Task VisitAPIManagement(ResourceContext context, JObject resource, string resourceType, string resourceId) + { + if (!string.Equals(resourceType, TYPE_APIMANAGEMENT_SERVICE, StringComparison.OrdinalIgnoreCase)) + return false; - // APIs - var apis = await GetSubResourcesByType(context, resourceId, "apis", APIVERSION_2022_08_01); - AddSubResource(resource, apis); - foreach (var api in apis) - { - var apiResourceId = api[PROPERTY_ID].Value(); - var apiType = api[PROPERTY_TYPE].Value(); - var isGraphQL = string.Equals(apiType, "graphql"); + // APIs + var apis = await GetSubResourcesByType(context, resourceId, "apis", APIVERSION_2022_08_01); + AddSubResource(resource, apis); + foreach (var api in apis) + { + var apiResourceId = api[PROPERTY_ID].Value(); + var apiType = api[PROPERTY_TYPE].Value(); + var isGraphQL = string.Equals(apiType, "graphql"); - // Get policies for each API - AddSubResource(resource, await GetSubResourcesByType(context, apiResourceId, PROPERTY_POLICIES, APIVERSION_2022_08_01)); + // Get policies for each API + AddSubResource(resource, await GetSubResourcesByType(context, apiResourceId, PROPERTY_POLICIES, APIVERSION_2022_08_01)); - if (!isGraphQL) + if (!isGraphQL) + { + // Get each operation + var operations = await GetSubResourcesByType(context, apiResourceId, "operations", APIVERSION_2022_08_01); + foreach (var operation in operations) { - // Get each operation - var operations = await GetSubResourcesByType(context, apiResourceId, "operations", APIVERSION_2022_08_01); - foreach (var operation in operations) - { - AddSubResource(resource, await GetSubResourcesByType(context, operation[PROPERTY_ID].Value(), PROPERTY_POLICIES, APIVERSION_2022_08_01)); - } + AddSubResource(resource, await GetSubResourcesByType(context, operation[PROPERTY_ID].Value(), PROPERTY_POLICIES, APIVERSION_2022_08_01)); } + } - // Get each resolver - if (isGraphQL) + // Get each resolver + if (isGraphQL) + { + var resolvers = await GetSubResourcesByType(context, apiResourceId, "resolvers", APIVERSION_2022_08_01); + foreach (var resolver in resolvers) { - var resolvers = await GetSubResourcesByType(context, apiResourceId, "resolvers", APIVERSION_2022_08_01); - foreach (var resolver in resolvers) - { - AddSubResource(resource, await GetSubResourcesByType(context, resolver[PROPERTY_ID].Value(), PROPERTY_POLICIES, APIVERSION_2022_08_01)); - } + AddSubResource(resource, await GetSubResourcesByType(context, resolver[PROPERTY_ID].Value(), PROPERTY_POLICIES, APIVERSION_2022_08_01)); } } + } - var backends = await GetSubResourcesByType(context, resourceId, "backends", APIVERSION_2022_08_01); - AddSubResource(resource, backends); + var backends = await GetSubResourcesByType(context, resourceId, "backends", APIVERSION_2022_08_01); + AddSubResource(resource, backends); - var products = await GetSubResourcesByType(context, resourceId, "products", APIVERSION_2022_08_01); - AddSubResource(resource, products); - foreach (var product in products) - { - // Get policies for each product - AddSubResource(resource, await GetSubResourcesByType(context, product[PROPERTY_ID].Value(), PROPERTY_POLICIES, APIVERSION_2022_08_01)); - } + var products = await GetSubResourcesByType(context, resourceId, "products", APIVERSION_2022_08_01); + AddSubResource(resource, products); + foreach (var product in products) + { + // Get policies for each product + AddSubResource(resource, await GetSubResourcesByType(context, product[PROPERTY_ID].Value(), PROPERTY_POLICIES, APIVERSION_2022_08_01)); + } - var policies = await GetSubResourcesByType(context, resourceId, PROPERTY_POLICIES, APIVERSION_2022_08_01); - AddSubResource(resource, policies); + var policies = await GetSubResourcesByType(context, resourceId, PROPERTY_POLICIES, APIVERSION_2022_08_01); + AddSubResource(resource, policies); - var identityProviders = await GetSubResourcesByType(context, resourceId, "identityProviders", APIVERSION_2022_08_01); - AddSubResource(resource, identityProviders); + var identityProviders = await GetSubResourcesByType(context, resourceId, "identityProviders", APIVERSION_2022_08_01); + AddSubResource(resource, identityProviders); - var diagnostics = await GetSubResourcesByType(context, resourceId, "diagnostics", APIVERSION_2022_08_01); - AddSubResource(resource, diagnostics); + var diagnostics = await GetSubResourcesByType(context, resourceId, "diagnostics", APIVERSION_2022_08_01); + AddSubResource(resource, diagnostics); - var loggers = await GetSubResourcesByType(context, resourceId, "loggers", APIVERSION_2022_08_01); - AddSubResource(resource, loggers); + var loggers = await GetSubResourcesByType(context, resourceId, "loggers", APIVERSION_2022_08_01); + AddSubResource(resource, loggers); - var certificates = await GetSubResourcesByType(context, resourceId, "certificates", APIVERSION_2022_08_01); - AddSubResource(resource, certificates); + var certificates = await GetSubResourcesByType(context, resourceId, "certificates", APIVERSION_2022_08_01); + AddSubResource(resource, certificates); - var namedValues = await GetSubResourcesByType(context, resourceId, "namedValues", APIVERSION_2022_08_01); - AddSubResource(resource, namedValues); + var namedValues = await GetSubResourcesByType(context, resourceId, "namedValues", APIVERSION_2022_08_01); + AddSubResource(resource, namedValues); - var authorizationServers = await GetSubResourcesByType(context, resourceId, "authorizationServers", APIVERSION_2022_08_01); - AddSubResource(resource, authorizationServers); + var authorizationServers = await GetSubResourcesByType(context, resourceId, "authorizationServers", APIVERSION_2022_08_01); + AddSubResource(resource, authorizationServers); - var portalSettings = await GetSubResourcesByType(context, resourceId, "portalsettings", APIVERSION_2022_08_01); - AddSubResource(resource, portalSettings); + var portalSettings = await GetSubResourcesByType(context, resourceId, "portalsettings", APIVERSION_2022_08_01); + AddSubResource(resource, portalSettings); - var apiCollections = await GetSubResourcesByProvider(context, resourceId, PROVIDERTYPE_APICOLLECTIONS, APIVERSION_2022_11_20_PREVIEW); - AddSubResource(resource, apiCollections); + var apiCollections = await GetSubResourcesByProvider(context, resourceId, PROVIDERTYPE_APICOLLECTIONS, APIVERSION_2022_11_20_PREVIEW); + AddSubResource(resource, apiCollections); - return true; - } + return true; + } - /// - /// Get diagnostics for the specified resource type. - /// - private static async Task GetDiagnosticSettings(ResourceContext context, JObject resource, string resourceId) - { - AddSubResource(resource, await GetSubResourcesByProvider(context, resourceId, PROVIDERTYPE_DIAGNOSTICSETTINGS, APIVERSION_2021_05_01_PREVIEW)); - } + /// + /// Get diagnostics for the specified resource type. + /// + private static async Task GetDiagnosticSettings(ResourceContext context, JObject resource, string resourceId) + { + AddSubResource(resource, await GetSubResourcesByProvider(context, resourceId, PROVIDERTYPE_DIAGNOSTICSETTINGS, APIVERSION_2021_05_01_PREVIEW)); + } - private static async Task GetRoleAssignments(ResourceContext context, JObject resource, string resourceId) - { - AddSubResource(resource, await GetSubResourcesByProvider(context, resourceId, PROVIDERTYPE_ROLEASSIGNMENTS, APIVERSION_2022_04_01)); - } + private static async Task GetRoleAssignments(ResourceContext context, JObject resource, string resourceId) + { + AddSubResource(resource, await GetSubResourcesByProvider(context, resourceId, PROVIDERTYPE_ROLEASSIGNMENTS, APIVERSION_2022_04_01)); + } - private static async Task GetResourceLocks(ResourceContext context, JObject resource, string resourceId) - { - AddSubResource(resource, await GetSubResourcesByProvider(context, resourceId, PROVIDERTYPE_RESOURCELOCKS, APIVERSION_2016_09_01)); - } + private static async Task GetResourceLocks(ResourceContext context, JObject resource, string resourceId) + { + AddSubResource(resource, await GetSubResourcesByProvider(context, resourceId, PROVIDERTYPE_RESOURCELOCKS, APIVERSION_2016_09_01)); + } - private static void AddSubResource(JObject parent, JObject child) - { - if (child == null) - return; + private static void AddSubResource(JObject parent, JObject child) + { + if (child == null) + return; - parent.UseProperty(PROPERTY_RESOURCES, out var resources); - resources.Add(child); - } + parent.UseProperty(PROPERTY_RESOURCES, out var resources); + resources.Add(child); + } - private static void AddSubResource(JObject parent, JObject[] children) - { - if (children == null || children.Length == 0) - return; + private static void AddSubResource(JObject parent, JObject[] children) + { + if (children == null || children.Length == 0) + return; - parent.UseProperty(PROPERTY_RESOURCES, out var resources); - for (var i = 0; i < children.Length; i++) - resources.Add(children[i]); - } + parent.UseProperty(PROPERTY_RESOURCES, out var resources); + for (var i = 0; i < children.Length; i++) + resources.Add(children[i]); + } - private static async Task GetResource(ResourceContext context, string resourceId, string apiVersion) - { - return await context.GetAsync(resourceId, apiVersion); - } + private static async Task GetResource(ResourceContext context, string resourceId, string apiVersion) + { + return await context.GetAsync(resourceId, apiVersion); + } - private static async Task GetSubResourcesByType(ResourceContext context, string resourceId, string type, string apiVersion, bool ignoreNotFound = false) - { - return await context.ListAsync(string.Concat(resourceId, '/', type), apiVersion, ignoreNotFound); - } + private static async Task GetSubResourcesByType(ResourceContext context, string resourceId, string type, string apiVersion, bool ignoreNotFound = false) + { + return await context.ListAsync(string.Concat(resourceId, '/', type), apiVersion, ignoreNotFound); + } - private static async Task GetSubResourcesByProvider(ResourceContext context, string resourceId, string type, string apiVersion, bool ignoreNotFound = false) - { - return await context.ListAsync(string.Concat(resourceId, type), apiVersion, ignoreNotFound); - } + private static async Task GetSubResourcesByProvider(ResourceContext context, string resourceId, string type, string apiVersion, bool ignoreNotFound = false) + { + return await context.ListAsync(string.Concat(resourceId, type), apiVersion, ignoreNotFound); } } diff --git a/src/PSRule.Rules.Azure/Pipeline/Export/SubscriptionExportContext.cs b/src/PSRule.Rules.Azure/Pipeline/Export/SubscriptionExportContext.cs index b7ef4956c43..3eb7e904ce7 100644 --- a/src/PSRule.Rules.Azure/Pipeline/Export/SubscriptionExportContext.cs +++ b/src/PSRule.Rules.Azure/Pipeline/Export/SubscriptionExportContext.cs @@ -7,134 +7,100 @@ using Newtonsoft.Json.Linq; using PSRule.Rules.Azure.Data; -namespace PSRule.Rules.Azure.Pipeline.Export +namespace PSRule.Rules.Azure.Pipeline.Export; + +/// +/// A context to export an Azure subscription. +/// +internal class SubscriptionExportContext : ExportDataContext, ISubscriptionExportContext { - internal interface ISubscriptionExportContext - { - /// - /// Get resources from an Azure subscription. - /// - Task GetResourcesAsync(); - - /// - /// Get resource groups from an Azure subscription. - /// - Task GetResourceGroupsAsync(); - - /// - /// Get a resource for the Azure subscription. - /// - Task GetSubscriptionAsync(); - - /// - /// Get a specified Azure resource from a subscription. - /// - Task GetResourceAsync(string resourceId); - - /// - /// The subscription Id of the context subscription. - /// - string SubscriptionId { get; } - - /// - /// The tenant Id of the context tenant. - /// - string TenantId { get; } - } + private readonly string _ResourceEndpoint; + private readonly string _ResourceGroupEndpoint; + private readonly string _SubscriptionEndpoint; + private ProviderData _ProviderData; /// - /// A context to export an Azure subscription. + /// Create a context to export an Azure subscription. /// - internal class SubscriptionExportContext : ExportDataContext, ISubscriptionExportContext + public SubscriptionExportContext(PipelineContext context, ExportSubscriptionScope scope, AccessTokenCache tokenCache, Hashtable tag) + : base(context, tokenCache) { - private readonly string _ResourceEndpoint; - private readonly string _ResourceGroupEndpoint; - private readonly string _SubscriptionEndpoint; - private ProviderData _ProviderData; - - /// - /// Create a context to export an Azure subscription. - /// - public SubscriptionExportContext(PipelineContext context, ExportSubscriptionScope scope, AccessTokenCache tokenCache, Hashtable tag) - : base(context, tokenCache) - { - _ResourceEndpoint = $"{RESOURCE_MANAGER_URL}/subscriptions/{scope.SubscriptionId}/resources?api-version=2021-04-01{GetFilter(tag)}"; - _ResourceGroupEndpoint = $"{RESOURCE_MANAGER_URL}/subscriptions/{scope.SubscriptionId}/resourcegroups?api-version=2021-04-01{GetFilter(tag)}"; - _SubscriptionEndpoint = $"{RESOURCE_MANAGER_URL}/subscriptions/{scope.SubscriptionId}"; - SubscriptionId = scope.SubscriptionId; - TenantId = scope.TenantId; - RefreshToken(scope.TenantId); - } + _ResourceEndpoint = $"{RESOURCE_MANAGER_URL}/subscriptions/{scope.SubscriptionId}/resources?api-version=2021-04-01{GetFilter(tag)}"; + _ResourceGroupEndpoint = $"{RESOURCE_MANAGER_URL}/subscriptions/{scope.SubscriptionId}/resourcegroups?api-version=2021-04-01{GetFilter(tag)}"; + _SubscriptionEndpoint = $"{RESOURCE_MANAGER_URL}/subscriptions/{scope.SubscriptionId}"; + SubscriptionId = scope.SubscriptionId; + TenantId = scope.TenantId; + RefreshToken(scope.TenantId); + } - /// - public string SubscriptionId { get; } + /// + public string SubscriptionId { get; } - /// - public string TenantId { get; } + /// + public string TenantId { get; } - /// - public async Task GetResourcesAsync() - { - return await ListAsync(TenantId, _ResourceEndpoint, ignoreNotFound: false); - } + /// + public async Task GetResourcesAsync() + { + return await ListAsync(TenantId, _ResourceEndpoint, ignoreNotFound: false); + } - /// - public async Task GetResourceGroupsAsync() - { - return await ListAsync(TenantId, _ResourceGroupEndpoint, ignoreNotFound: false); - } + /// + public async Task GetResourceGroupsAsync() + { + return await ListAsync(TenantId, _ResourceGroupEndpoint, ignoreNotFound: false); + } - /// - public async Task GetSubscriptionAsync() - { - return await GetAsync(TenantId, _SubscriptionEndpoint); - } + /// + public async Task GetSubscriptionAsync() + { + return await GetAsync(TenantId, _SubscriptionEndpoint); + } - /// - public async Task GetResourceAsync(string resourceId) - { - if (!TryGetLatestAPIVersion(ResourceHelper.GetResourceType(resourceId), out var apiVersion)) - return null; + /// + public async Task GetResourceAsync(string resourceId) + { + if (!TryGetLatestAPIVersion(ResourceHelper.GetResourceType(resourceId), out var apiVersion)) + return null; - return await GetAsync(TenantId, $"{RESOURCE_MANAGER_URL}{resourceId}?api-version={apiVersion}"); - } + return await GetAsync(TenantId, $"{RESOURCE_MANAGER_URL}{resourceId}?api-version={apiVersion}"); + } - private static string GetFilter(Hashtable tag) - { - if (tag == null || tag.Count == 0) - return string.Empty; - - var first = true; - var sb = new StringBuilder("&$filter="); - foreach (DictionaryEntry kv in tag) - { - if (!first) - sb.Append(" and "); - - sb.Append("tagName eq '"); - sb.Append(kv.Key); - sb.Append("' and tagValue eq '"); - sb.Append(kv.Value); - sb.Append("'"); - first = false; - } - return sb.ToString(); - } + private static string GetFilter(Hashtable tag) + { + if (tag == null || tag.Count == 0) + return string.Empty; - private bool TryGetLatestAPIVersion(string resourceType, out string apiVersion) + var first = true; + var sb = new StringBuilder("&$filter="); + foreach (DictionaryEntry kv in tag) { - apiVersion = null; - if (string.IsNullOrEmpty(resourceType)) - return false; - - _ProviderData ??= new ProviderData(); - if (!_ProviderData.TryResourceType(resourceType, out var data) || - data.ApiVersions == null || - data.ApiVersions.Length == 0) - return false; - - apiVersion = data.ApiVersions[0]; - return true; + if (!first) + sb.Append(" and "); + + sb.Append("tagName eq '"); + sb.Append(kv.Key); + sb.Append("' and tagValue eq '"); + sb.Append(kv.Value); + sb.Append("'"); + first = false; } + return sb.ToString(); + } + + private bool TryGetLatestAPIVersion(string resourceType, out string apiVersion) + { + apiVersion = null; + if (string.IsNullOrEmpty(resourceType)) + return false; + + _ProviderData ??= new ProviderData(); + if (!_ProviderData.TryResourceType(resourceType, out var data) || + data.ApiVersions == null || + data.ApiVersions.Length == 0) + return false; + + apiVersion = data.ApiVersions[0]; + return true; } } diff --git a/src/PSRule.Rules.Azure/Pipeline/ExportDataPipeline.cs b/src/PSRule.Rules.Azure/Pipeline/ExportDataPipeline.cs index 4cda73def96..eec72ba31cf 100644 --- a/src/PSRule.Rules.Azure/Pipeline/ExportDataPipeline.cs +++ b/src/PSRule.Rules.Azure/Pipeline/ExportDataPipeline.cs @@ -3,40 +3,39 @@ using PSRule.Rules.Azure.Pipeline.Export; -namespace PSRule.Rules.Azure.Pipeline +namespace PSRule.Rules.Azure.Pipeline; + +/// +/// A base class for a pipeline that exports data from Azure. +/// +internal abstract class ExportDataPipeline : PipelineBase { - /// - /// A base class for a pipeline that exports data from Azure. - /// - internal abstract class ExportDataPipeline : PipelineBase - { - private bool _Disposed; + private bool _Disposed; - protected ExportDataPipeline(PipelineContext context, GetAccessTokenFn getToken) - : base(context) - { - PoolSize = 10; - TokenCache = new AccessTokenCache(getToken); - } + protected ExportDataPipeline(PipelineContext context, GetAccessTokenFn getToken) + : base(context) + { + PoolSize = 10; + TokenCache = new AccessTokenCache(getToken); + } - /// - /// The size of the thread pool for the pipeline. - /// - protected int PoolSize { get; } + /// + /// The size of the thread pool for the pipeline. + /// + protected int PoolSize { get; } - protected AccessTokenCache TokenCache { get; } + protected AccessTokenCache TokenCache { get; } - protected override void Dispose(bool disposing) + protected override void Dispose(bool disposing) + { + if (!_Disposed) { - if (!_Disposed) + if (disposing) { - if (disposing) - { - TokenCache.Dispose(); - } - _Disposed = true; + TokenCache.Dispose(); } - base.Dispose(disposing); + _Disposed = true; } + base.Dispose(disposing); } } diff --git a/src/PSRule.Rules.Azure/Pipeline/ExportDataPipelineBuilder.cs b/src/PSRule.Rules.Azure/Pipeline/ExportDataPipelineBuilder.cs index bd6f1792322..d238e0496a8 100644 --- a/src/PSRule.Rules.Azure/Pipeline/ExportDataPipelineBuilder.cs +++ b/src/PSRule.Rules.Azure/Pipeline/ExportDataPipelineBuilder.cs @@ -1,39 +1,38 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace PSRule.Rules.Azure.Pipeline -{ - /// - /// A delegate that returns an Access Token. - /// - public delegate AccessToken GetAccessTokenFn(string tenantId); +namespace PSRule.Rules.Azure.Pipeline; - /// - /// A helper to build a pipeline to export data from Azure. - /// - internal abstract class ExportDataPipelineBuilder : PipelineBuilderBase, IExportDataPipelineBuilder - { - protected int _RetryCount; - protected int _RetryInterval; +/// +/// A delegate that returns an Access Token. +/// +public delegate AccessToken GetAccessTokenFn(string tenantId); - protected GetAccessTokenFn _AccessToken; +/// +/// A helper to build a pipeline to export data from Azure. +/// +internal abstract class ExportDataPipelineBuilder : PipelineBuilderBase, IExportDataPipelineBuilder +{ + protected int _RetryCount; + protected int _RetryInterval; - /// - public void AccessToken(GetAccessTokenFn fn) - { - _AccessToken = fn; - } + protected GetAccessTokenFn _AccessToken; - /// - public void RetryCount(int retryCount) - { - _RetryCount = retryCount; - } + /// + public void AccessToken(GetAccessTokenFn fn) + { + _AccessToken = fn; + } - /// - public void RetryInterval(int seconds) - { - _RetryInterval = seconds; - } + /// + public void RetryCount(int retryCount) + { + _RetryCount = retryCount; + } + + /// + public void RetryInterval(int seconds) + { + _RetryInterval = seconds; } } diff --git a/src/PSRule.Rules.Azure/Pipeline/ExportSubscriptionScope.cs b/src/PSRule.Rules.Azure/Pipeline/ExportSubscriptionScope.cs index 152d1c19d0a..9e5c8f9dfd5 100644 --- a/src/PSRule.Rules.Azure/Pipeline/ExportSubscriptionScope.cs +++ b/src/PSRule.Rules.Azure/Pipeline/ExportSubscriptionScope.cs @@ -3,40 +3,39 @@ using System; -namespace PSRule.Rules.Azure.Pipeline +namespace PSRule.Rules.Azure.Pipeline; + +/// +/// The details for a subscription to export. +/// +public sealed class ExportSubscriptionScope { /// - /// The details for a subscription to export. + /// Create a scope structure containing details for the subscription to export. /// - public sealed class ExportSubscriptionScope + /// The subscription Id for the subscription to export. + /// The tenant Id associated to the subscription being exported. + public ExportSubscriptionScope(string subscriptionId, string tenantId) { - /// - /// Create a scope structure containing details for the subscription to export. - /// - /// The subscription Id for the subscription to export. - /// The tenant Id associated to the subscription being exported. - public ExportSubscriptionScope(string subscriptionId, string tenantId) - { - if (string.IsNullOrEmpty(subscriptionId)) - throw new ArgumentOutOfRangeException(nameof(subscriptionId)); + if (string.IsNullOrEmpty(subscriptionId)) + throw new ArgumentOutOfRangeException(nameof(subscriptionId)); - if (string.IsNullOrEmpty(tenantId)) - throw new ArgumentOutOfRangeException(nameof(tenantId)); + if (string.IsNullOrEmpty(tenantId)) + throw new ArgumentOutOfRangeException(nameof(tenantId)); - SubscriptionId = subscriptionId; - TenantId = tenantId; - } + SubscriptionId = subscriptionId; + TenantId = tenantId; + } - /// - /// The subscription Id for the subscription to export. - /// This is a identifier. - /// - public string SubscriptionId { get; } + /// + /// The subscription Id for the subscription to export. + /// This is a identifier. + /// + public string SubscriptionId { get; } - /// - /// The tenant Id associated to the subscription being exported. - /// This is a identifier. - /// - public string TenantId { get; } - } + /// + /// The tenant Id associated to the subscription being exported. + /// This is a identifier. + /// + public string TenantId { get; } } diff --git a/src/PSRule.Rules.Azure/Pipeline/IPipeline.cs b/src/PSRule.Rules.Azure/Pipeline/IPipeline.cs new file mode 100644 index 00000000000..4f38466acd3 --- /dev/null +++ b/src/PSRule.Rules.Azure/Pipeline/IPipeline.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; + +namespace PSRule.Rules.Azure.Pipeline; + +/// +/// An instance of a PSRule for Azure pipeline. +/// +public interface IPipeline +{ + /// + /// Initialize the pipeline and results. Call this method once prior to calling Process. + /// + void Begin(); + + /// + /// Process an object through the pipeline. + /// + /// The object to process. + void Process(PSObject sourceObject); + + /// + /// Clean up and flush pipeline results. Call this method once after processing any objects through the pipeline. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "Matches PowerShell pipeline.")] + void End(); +} diff --git a/src/PSRule.Rules.Azure/Pipeline/IPipelineBuilder.cs b/src/PSRule.Rules.Azure/Pipeline/IPipelineBuilder.cs new file mode 100644 index 00000000000..b90a7113cc7 --- /dev/null +++ b/src/PSRule.Rules.Azure/Pipeline/IPipelineBuilder.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; +using PSRule.Rules.Azure.Configuration; + +namespace PSRule.Rules.Azure.Pipeline; + +/// +/// A helper to build a PSRule for Azure pipeline. +/// +public interface IPipelineBuilder +{ + /// + /// Configure the pipeline with cmdlet runtime information. + /// + void UseCommandRuntime(PSCmdlet commandRuntime); + + /// + /// Configure the pipeline with a PowerShell execution context. + /// + void UseExecutionContext(EngineIntrinsics executionContext); + + /// + /// Configure the pipeline with options. + /// + /// Options that configure PSRule for Azure. + /// + IPipelineBuilder Configure(PSRuleOption option); + + /// + /// Build the pipeline. + /// + /// An instance of a configured pipeline. + IPipeline Build(); +} diff --git a/src/PSRule.Rules.Azure/Pipeline/IPolicyAssignmentSearchPipelineBuilder.cs b/src/PSRule.Rules.Azure/Pipeline/IPolicyAssignmentSearchPipelineBuilder.cs new file mode 100644 index 00000000000..4b331cf1655 --- /dev/null +++ b/src/PSRule.Rules.Azure/Pipeline/IPolicyAssignmentSearchPipelineBuilder.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Rules.Azure.Pipeline; + +/// +/// A builder for a policy to rule generation pipeline. +/// +public interface IPolicyAssignmentSearchPipelineBuilder : IPipelineBuilder +{ + +} diff --git a/src/PSRule.Rules.Azure/Pipeline/ITemplateLinkPipelineBuilder.cs b/src/PSRule.Rules.Azure/Pipeline/ITemplateLinkPipelineBuilder.cs new file mode 100644 index 00000000000..5a5cce60b78 --- /dev/null +++ b/src/PSRule.Rules.Azure/Pipeline/ITemplateLinkPipelineBuilder.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Rules.Azure.Pipeline; + +/// +/// A helper to build a template link pipeline. +/// +public interface ITemplateLinkPipelineBuilder : IPipelineBuilder +{ + /// + /// Determines if unlinked parameter files are skipped or error. + /// + void SkipUnlinked(bool skipUnlinked); +} diff --git a/src/PSRule.Rules.Azure/Pipeline/ITemplatePipelineBuilder.cs b/src/PSRule.Rules.Azure/Pipeline/ITemplatePipelineBuilder.cs new file mode 100644 index 00000000000..2ae2d52c203 --- /dev/null +++ b/src/PSRule.Rules.Azure/Pipeline/ITemplatePipelineBuilder.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using PSRule.Rules.Azure.Configuration; + +namespace PSRule.Rules.Azure.Pipeline; + +/// +/// A helper for building a template expansion pipeline. +/// +public interface ITemplatePipelineBuilder : IPipelineBuilder +{ + /// + /// Configures the name of the deployment. + /// + void Deployment(string deploymentName); + + /// + /// Configures properties of the resource group. + /// + void ResourceGroup(ResourceGroupOption resourceGroup); + + /// + /// Configures properties of the subscription. + /// + void Subscription(SubscriptionOption subscription); + + /// + /// Determines if expanded resources are passed through to the pipeline. + /// + void PassThru(bool passThru); +} diff --git a/src/PSRule.Rules.Azure/Pipeline/LoggingExtensions.cs b/src/PSRule.Rules.Azure/Pipeline/LoggingExtensions.cs index 567acaeee55..0b6103e48c9 100644 --- a/src/PSRule.Rules.Azure/Pipeline/LoggingExtensions.cs +++ b/src/PSRule.Rules.Azure/Pipeline/LoggingExtensions.cs @@ -6,125 +6,124 @@ using System.Net; using PSRule.Rules.Azure.Resources; -namespace PSRule.Rules.Azure.Pipeline +namespace PSRule.Rules.Azure.Pipeline; + +/// +/// Extensions for logging to the pipeline. +/// +internal static class LoggingExtensions { + internal static void VerboseFindFiles(this ILogger logger, string path) + { + if (logger == null) + return; + + logger.WriteVerbose(Diagnostics.VerboseFindFiles, path); + } + + internal static void VerboseFoundFile(this ILogger logger, string path) + { + if (logger == null) + return; + + logger.WriteVerbose(Diagnostics.VerboseFoundFile, path); + } + + internal static void VerboseMetadataNotFound(this ILogger logger, string path) + { + if (logger == null) + return; + + logger.WriteVerbose(Diagnostics.VerboseMetadataNotFound, path); + } + + internal static void VerboseTemplateLinkNotFound(this ILogger logger, string path) + { + if (logger == null) + return; + + logger.WriteVerbose(Diagnostics.VerboseTemplateLinkNotFound, path); + } + + internal static void VerboseTemplateFileNotFound(this ILogger logger, string path) + { + if (logger == null) + return; + + logger.WriteVerbose(Diagnostics.VerboseTemplateFileNotFound, path); + } + + /// + /// Getting resources from subscription: {0} + /// + internal static void VerboseGetResources(this ILogger logger, string subscriptionId) + { + if (logger == null) + return; + + logger.WriteVerbose(Diagnostics.VerboseGetResources, subscriptionId); + } + + /// + /// Added {0} resources from subscription '{1}'. + /// + internal static void VerboseGetResourcesResult(this ILogger logger, int count, string subscriptionId) + { + if (logger == null) + return; + + logger.WriteVerbose(Diagnostics.VerboseGetResourcesResult, count, subscriptionId); + } + + /// + /// Getting resource: {0} + /// + internal static void VerboseGetResource(this ILogger logger, string resourceId) + { + if (logger == null) + return; + + logger.WriteVerbose(Diagnostics.VerboseGetResource, resourceId); + } + + /// + /// Expanding resource: {0} + /// + internal static void VerboseExpandingResource(this ILogger logger, string resourceId) + { + if (logger == null) + return; + + logger.WriteVerbose(Diagnostics.VerboseExpandingResource, resourceId); + } + + /// + /// Retrying '{0}' in {1}, attept {2}. + /// + internal static void VerboseRetryIn(this ILogger logger, string uri, TimeSpan retry, int attempt) + { + if (logger == null) + return; + + logger.WriteVerbose(Diagnostics.VerboseRetryIn, uri, retry, attempt); + } + /// - /// Extensions for logging to the pipeline. + /// Failed to get '{0}': status={1}, correlation-id={2}, {3} /// - internal static class LoggingExtensions + internal static void WarnFailedToGet(this ILogger logger, string uri, HttpStatusCode statusCode, string correlationId, string body) { - internal static void VerboseFindFiles(this ILogger logger, string path) - { - if (logger == null) - return; - - logger.WriteVerbose(Diagnostics.VerboseFindFiles, path); - } - - internal static void VerboseFoundFile(this ILogger logger, string path) - { - if (logger == null) - return; - - logger.WriteVerbose(Diagnostics.VerboseFoundFile, path); - } - - internal static void VerboseMetadataNotFound(this ILogger logger, string path) - { - if (logger == null) - return; - - logger.WriteVerbose(Diagnostics.VerboseMetadataNotFound, path); - } - - internal static void VerboseTemplateLinkNotFound(this ILogger logger, string path) - { - if (logger == null) - return; - - logger.WriteVerbose(Diagnostics.VerboseTemplateLinkNotFound, path); - } - - internal static void VerboseTemplateFileNotFound(this ILogger logger, string path) - { - if (logger == null) - return; - - logger.WriteVerbose(Diagnostics.VerboseTemplateFileNotFound, path); - } - - /// - /// Getting resources from subscription: {0} - /// - internal static void VerboseGetResources(this ILogger logger, string subscriptionId) - { - if (logger == null) - return; - - logger.WriteVerbose(Diagnostics.VerboseGetResources, subscriptionId); - } - - /// - /// Added {0} resources from subscription '{1}'. - /// - internal static void VerboseGetResourcesResult(this ILogger logger, int count, string subscriptionId) - { - if (logger == null) - return; - - logger.WriteVerbose(Diagnostics.VerboseGetResourcesResult, count, subscriptionId); - } - - /// - /// Getting resource: {0} - /// - internal static void VerboseGetResource(this ILogger logger, string resourceId) - { - if (logger == null) - return; - - logger.WriteVerbose(Diagnostics.VerboseGetResource, resourceId); - } - - /// - /// Expanding resource: {0} - /// - internal static void VerboseExpandingResource(this ILogger logger, string resourceId) - { - if (logger == null) - return; - - logger.WriteVerbose(Diagnostics.VerboseExpandingResource, resourceId); - } - - /// - /// Retrying '{0}' in {1}, attept {2}. - /// - internal static void VerboseRetryIn(this ILogger logger, string uri, TimeSpan retry, int attempt) - { - if (logger == null) - return; - - logger.WriteVerbose(Diagnostics.VerboseRetryIn, uri, retry, attempt); - } - - /// - /// Failed to get '{0}': status={1}, correlation-id={2}, {3} - /// - internal static void WarnFailedToGet(this ILogger logger, string uri, HttpStatusCode statusCode, string correlationId, string body) - { - if (logger == null) - return; - - logger.WriteWarning(Diagnostics.WarnFailedToGet, uri, (int)statusCode, correlationId, body); - } - - internal static void Error(this ILogger logger, Exception exception, string errorId, ErrorCategory errorCategory = ErrorCategory.NotSpecified, object targetObject = null) - { - if (logger == null) - return; - - logger.WriteError(exception, errorId, errorCategory, targetObject); - } + if (logger == null) + return; + + logger.WriteWarning(Diagnostics.WarnFailedToGet, uri, (int)statusCode, correlationId, body); + } + + internal static void Error(this ILogger logger, Exception exception, string errorId, ErrorCategory errorCategory = ErrorCategory.NotSpecified, object targetObject = null) + { + if (logger == null) + return; + + logger.WriteError(exception, errorId, errorCategory, targetObject); } } diff --git a/src/PSRule.Rules.Azure/Pipeline/PathBuilder.cs b/src/PSRule.Rules.Azure/Pipeline/PathBuilder.cs index ab352cacf24..4457e954db5 100644 --- a/src/PSRule.Rules.Azure/Pipeline/PathBuilder.cs +++ b/src/PSRule.Rules.Azure/Pipeline/PathBuilder.cs @@ -7,160 +7,159 @@ using System.IO; using PSRule.Rules.Azure.Configuration; -namespace PSRule.Rules.Azure.Pipeline +namespace PSRule.Rules.Azure.Pipeline; + +/// +/// A helper to build a list of rule sources for discovery. +/// +internal sealed class PathBuilder { - /// - /// A helper to build a list of rule sources for discovery. - /// - internal sealed class PathBuilder - { - // Path separators - private const char Slash = '/'; - private const char BackSlash = '\\'; + // Path separators + private const char Slash = '/'; + private const char BackSlash = '\\'; - private const char Dot = '.'; + private const char Dot = '.'; - private static readonly char[] PathLiteralStopCharacters = new char[] { '*', '[', '?' }; - private static readonly char[] PathSeparatorCharacters = new char[] { '\\', '/' }; + private static readonly char[] PathLiteralStopCharacters = ['*', '[', '?']; + private static readonly char[] PathSeparatorCharacters = ['\\', '/']; - private readonly ILogger _Logger; - private readonly List _Source; - private readonly string _BasePath; - private readonly string _DefaultSearchPattern; - private readonly HashSet _Existing; + private readonly ILogger _Logger; + private readonly List _Source; + private readonly string _BasePath; + private readonly string _DefaultSearchPattern; + private readonly HashSet _Existing; - internal PathBuilder(ILogger logger, string basePath, string searchPattern) - { - _Logger = logger; - _Source = new List(); - _BasePath = PSRuleOption.GetRootedBasePath(basePath); - _DefaultSearchPattern = searchPattern; - _Existing = new HashSet(StringComparer.Ordinal); - } + internal PathBuilder(ILogger logger, string basePath, string searchPattern) + { + _Logger = logger; + _Source = []; + _BasePath = PSRuleOption.GetRootedBasePath(basePath); + _DefaultSearchPattern = searchPattern; + _Existing = new HashSet(StringComparer.Ordinal); + } - public void Add(string[] path) - { - if (path == null || path.Length == 0) - return; + public void Add(string[] path) + { + if (path == null || path.Length == 0) + return; - for (var i = 0; i < path.Length; i++) - Add(path[i]); - } + for (var i = 0; i < path.Length; i++) + Add(path[i]); + } - public void Add(string path) - { - if (string.IsNullOrEmpty(path)) - return; + public void Add(string path) + { + if (string.IsNullOrEmpty(path)) + return; - FindFiles(path); - } + FindFiles(path); + } - public FileInfo[] Build() + public FileInfo[] Build() + { + try { - try - { - return _Source.ToArray(); - } - finally - { - _Source.Clear(); - _Existing.Clear(); - } + return [.. _Source]; } - - private void FindFiles(string path) + finally { - _Logger.VerboseFindFiles(path); - if (TryAddFile(path)) - return; + _Source.Clear(); + _Existing.Clear(); + } + } - var pathLiteral = GetSearchParameters(path, out var searchPattern, out var searchOption); - if (TryAddFile(pathLiteral)) - return; + private void FindFiles(string path) + { + _Logger.VerboseFindFiles(path); + if (TryAddFile(path)) + return; - var files = Directory.EnumerateFiles(pathLiteral, searchPattern, searchOption); - foreach (var file in files) - AddFile(file); - } + var pathLiteral = GetSearchParameters(path, out var searchPattern, out var searchOption); + if (TryAddFile(pathLiteral)) + return; - private bool TryAddFile(string path) - { - if (path.IndexOfAny(PathLiteralStopCharacters) > -1) - return false; + var files = Directory.EnumerateFiles(pathLiteral, searchPattern, searchOption); + foreach (var file in files) + AddFile(file); + } - var rootedPath = GetRootedPath(path); - if (!File.Exists(rootedPath)) - return false; + private bool TryAddFile(string path) + { + if (path.IndexOfAny(PathLiteralStopCharacters) > -1) + return false; - AddFile(rootedPath); - return true; - } + var rootedPath = GetRootedPath(path); + if (!File.Exists(rootedPath)) + return false; - private void AddFile(string path) - { - // Avoid adding duplicates. - if (_Existing.Contains(path)) - return; + AddFile(rootedPath); + return true; + } - _Logger.VerboseFoundFile(path); - _Source.Add(new FileInfo(path)); - _Existing.Add(path); - } + private void AddFile(string path) + { + // Avoid adding duplicates. + if (_Existing.Contains(path)) + return; - private string GetRootedPath(string path) - { - return Path.IsPathRooted(path) ? path : Path.GetFullPath(Path.Combine(_BasePath, path)); - } + _Logger.VerboseFoundFile(path); + _Source.Add(new FileInfo(path)); + _Existing.Add(path); + } - /// - /// Split a search path into components based on wildcards. - /// - private string GetSearchParameters(string path, out string searchPattern, out SearchOption searchOption) - { - searchOption = SearchOption.AllDirectories; - var pathLiteral = SplitSearchPath(TrimPath(path, out var relativeAnchor), out searchPattern); - if (string.IsNullOrEmpty(searchPattern)) - searchPattern = _DefaultSearchPattern; + private string GetRootedPath(string path) + { + return Path.IsPathRooted(path) ? path : Path.GetFullPath(Path.Combine(_BasePath, path)); + } - // If a path separator is within the pattern use a resursive search - if (relativeAnchor || !string.IsNullOrEmpty(pathLiteral)) - searchOption = SearchOption.TopDirectoryOnly; + /// + /// Split a search path into components based on wildcards. + /// + private string GetSearchParameters(string path, out string searchPattern, out SearchOption searchOption) + { + searchOption = SearchOption.AllDirectories; + var pathLiteral = SplitSearchPath(TrimPath(path, out var relativeAnchor), out searchPattern); + if (string.IsNullOrEmpty(searchPattern)) + searchPattern = _DefaultSearchPattern; - return GetRootedPath(pathLiteral); - } + // If a path separator is within the pattern use a resursive search + if (relativeAnchor || !string.IsNullOrEmpty(pathLiteral)) + searchOption = SearchOption.TopDirectoryOnly; - private static string SplitSearchPath(string path, out string searchPattern) - { - // Find the index of the first expression character i.e. out/modules/**/file - var stopIndex = path.IndexOfAny(PathLiteralStopCharacters); + return GetRootedPath(pathLiteral); + } - // Track back to the separator before any expression characters - var literalSeparator = stopIndex > -1 ? path.LastIndexOfAny(PathSeparatorCharacters, stopIndex) + 1 : path.LastIndexOfAny(PathSeparatorCharacters) + 1; - searchPattern = path.Substring(literalSeparator, path.Length - literalSeparator); - return path.Substring(0, literalSeparator); - } + private static string SplitSearchPath(string path, out string searchPattern) + { + // Find the index of the first expression character i.e. out/modules/**/file + var stopIndex = path.IndexOfAny(PathLiteralStopCharacters); - /// - /// Remove leading ./ or .\ characters indicating a relative path anchor. - /// - /// The path to trim. - /// Returns true when a relative path anchor was present. - /// Return a clean path without the relative path anchor. - private static string TrimPath(string path, out bool relativeAnchor) - { - relativeAnchor = false; - if (path.Length >= 2 && path[0] == Dot && IsSeparator(path[1])) - { - relativeAnchor = true; - return path.Remove(0, 2); - } - return path; - } + // Track back to the separator before any expression characters + var literalSeparator = stopIndex > -1 ? path.LastIndexOfAny(PathSeparatorCharacters, stopIndex) + 1 : path.LastIndexOfAny(PathSeparatorCharacters) + 1; + searchPattern = path.Substring(literalSeparator, path.Length - literalSeparator); + return path.Substring(0, literalSeparator); + } - [DebuggerStepThrough] - private static bool IsSeparator(char c) + /// + /// Remove leading ./ or .\ characters indicating a relative path anchor. + /// + /// The path to trim. + /// Returns true when a relative path anchor was present. + /// Return a clean path without the relative path anchor. + private static string TrimPath(string path, out bool relativeAnchor) + { + relativeAnchor = false; + if (path.Length >= 2 && path[0] == Dot && IsSeparator(path[1])) { - return c is Slash or BackSlash; + relativeAnchor = true; + return path.Remove(0, 2); } + return path; + } + + [DebuggerStepThrough] + private static bool IsSeparator(char c) + { + return c is Slash or BackSlash; } } diff --git a/src/PSRule.Rules.Azure/Pipeline/PipelineBase.cs b/src/PSRule.Rules.Azure/Pipeline/PipelineBase.cs new file mode 100644 index 00000000000..de3783024d6 --- /dev/null +++ b/src/PSRule.Rules.Azure/Pipeline/PipelineBase.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Management.Automation; + +namespace PSRule.Rules.Azure.Pipeline; + +internal abstract class PipelineBase : IDisposable, IPipeline +{ + protected readonly PipelineContext Context; + + // Track whether Dispose has been called. + private bool _Disposed; + + + protected PipelineBase(PipelineContext context) + { + Context = context; + } + + #region IPipeline + + /// + public virtual void Begin() + { + // Do nothing + } + + /// + public virtual void Process(PSObject sourceObject) + { + // Do nothing + } + + /// + public virtual void End() + { + Context.Writer.End(); + } + + #endregion IPipeline + + #region IDisposable + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_Disposed) + { + if (disposing) + { + //Context.Dispose(); + } + _Disposed = true; + } + } + + #endregion IDisposable +} diff --git a/src/PSRule.Rules.Azure/Pipeline/PipelineBuilder.cs b/src/PSRule.Rules.Azure/Pipeline/PipelineBuilder.cs index 7df7d81e158..6a87eb69b54 100644 --- a/src/PSRule.Rules.Azure/Pipeline/PipelineBuilder.cs +++ b/src/PSRule.Rules.Azure/Pipeline/PipelineBuilder.cs @@ -1,240 +1,64 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.Management.Automation; using PSRule.Rules.Azure.Configuration; -using PSRule.Rules.Azure.Pipeline.Output; -namespace PSRule.Rules.Azure.Pipeline -{ - internal delegate bool ShouldProcess(string target, string action); +namespace PSRule.Rules.Azure.Pipeline; + +internal delegate bool ShouldProcess(string target, string action); +/// +/// A helper class for building a pipeline from PowerShell. +/// +public static class PipelineBuilder +{ /// - /// A helper class for building a pipeline from PowerShell. + /// Create a builder for a template expanding pipeline. /// - public static class PipelineBuilder + /// Options that configure PSRule for Azure. + /// A builder object to configure the pipeline. + public static ITemplatePipelineBuilder Template(PSRuleOption option) { - /// - /// Create a builder for a template expanding pipeline. - /// - /// Options that configure PSRule for Azure. - /// A builder object to configure the pipeline. - public static ITemplatePipelineBuilder Template(PSRuleOption option) - { - return new TemplatePipelineBuilder(option); - } - - /// - /// Create a builder for a template link discovery pipeline. - /// - /// The base path to search from. - /// A builder object to configure the pipeline. - public static ITemplateLinkPipelineBuilder TemplateLink(string path) - { - return new TemplateLinkPipelineBuilder(path); - } - - /// - /// Create a builder for a policy assignment export pipeline. - /// - /// Options that configure PSRule for Azure. - /// A builder object to configure the pipeline. - public static IPolicyAssignmentPipelineBuilder Assignment(PSRuleOption option) - { - return new PolicyAssignmentPipelineBuilder(option); - } - - /// - /// Create a builder for a policy to rule generation pipeline. - /// - /// The base path to search from. - /// A builder object to configure the pipeline. - public static IPolicyAssignmentSearchPipelineBuilder AssignmentSearch(string path) - { - return new PolicyAssignmentSearchPipelineBuilder(path); - } - - /// - /// Create a builder for creating a pipeline to exporting resource data from Azure. - /// - /// Options that configure PSRule for Azure. - /// A builder object to configure the pipeline. - public static IResourceDataPipelineBuilder ResourceData(PSRuleOption option) - { - return new ResourceDataPipelineBuilder(option); - } + return new TemplatePipelineBuilder(option); } /// - /// A helper to build a PSRule for Azure pipeline. + /// Create a builder for a template link discovery pipeline. /// - public interface IPipelineBuilder + /// The base path to search from. + /// A builder object to configure the pipeline. + public static ITemplateLinkPipelineBuilder TemplateLink(string path) { - /// - /// Configure the pipeline with cmdlet runtime information. - /// - void UseCommandRuntime(PSCmdlet commandRuntime); - - /// - /// Configure the pipeline with a PowerShell execution context. - /// - void UseExecutionContext(EngineIntrinsics executionContext); - - /// - /// Configure the pipeline with options. - /// - /// Options that configure PSRule for Azure. - /// - IPipelineBuilder Configure(PSRuleOption option); - - /// - /// Build the pipeline. - /// - /// An instance of a configured pipeline. - IPipeline Build(); + return new TemplateLinkPipelineBuilder(path); } /// - /// An instance of a PSRule for Azure pipeline. + /// Create a builder for a policy assignment export pipeline. /// - public interface IPipeline + /// Options that configure PSRule for Azure. + /// A builder object to configure the pipeline. + public static IPolicyAssignmentPipelineBuilder Assignment(PSRuleOption option) { - /// - /// Initalize the pipeline and results. Call this method once prior to calling Process. - /// - void Begin(); - - /// - /// Process an object through the pipeline. - /// - /// The object to process. - void Process(PSObject sourceObject); - - /// - /// Clean up and flush pipeline results. Call this method once after processing any objects through the pipeline. - /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "Matches PowerShell pipeline.")] - void End(); + return new PolicyAssignmentPipelineBuilder(option); } - internal abstract class PipelineBuilderBase : IPipelineBuilder + /// + /// Create a builder for a policy to rule generation pipeline. + /// + /// The base path to search from. + /// A builder object to configure the pipeline. + public static IPolicyAssignmentSearchPipelineBuilder AssignmentSearch(string path) { - private readonly PSPipelineWriter _Output; - - protected readonly PSRuleOption Option; - - protected PSCmdlet CmdletContext; - protected EngineIntrinsics ExecutionContext; - - protected PipelineBuilderBase() - { - Option = new PSRuleOption(); - _Output = new PSPipelineWriter(Option); - } - - /// - public virtual void UseCommandRuntime(PSCmdlet commandRuntime) - { - CmdletContext = commandRuntime; - _Output.UseCommandRuntime(commandRuntime); - } - - /// - public void UseExecutionContext(EngineIntrinsics executionContext) - { - ExecutionContext = executionContext; - _Output.UseExecutionContext(executionContext); - } - - /// - public virtual IPipelineBuilder Configure(PSRuleOption option) - { - if (option == null) - return this; - - Option.Output = new OutputOption(option.Output); - Option.Configuration = new ConfigurationOption(option.Configuration); - return this; - } - - /// - public abstract IPipeline Build(); - - protected PipelineContext PrepareContext() - { - return new PipelineContext(Option, PrepareWriter()); - } - - protected virtual PipelineWriter PrepareWriter() - { - var writer = new PSPipelineWriter(Option); - writer.UseCommandRuntime(CmdletContext); - writer.UseExecutionContext(ExecutionContext); - return writer; - } - - protected virtual PipelineWriter GetOutput() - { - return _Output; - } + return new PolicyAssignmentSearchPipelineBuilder(path); } - internal abstract class PipelineBase : IDisposable, IPipeline + /// + /// Create a builder for creating a pipeline to exporting resource data from Azure. + /// + /// Options that configure PSRule for Azure. + /// A builder object to configure the pipeline. + public static IResourceDataPipelineBuilder ResourceData(PSRuleOption option) { - protected readonly PipelineContext Context; - - // Track whether Dispose has been called. - private bool _Disposed; - - - protected PipelineBase(PipelineContext context) - { - Context = context; - } - - #region IPipeline - - /// - public virtual void Begin() - { - // Do nothing - } - - /// - public virtual void Process(PSObject sourceObject) - { - // Do nothing - } - - /// - public virtual void End() - { - Context.Writer.End(); - } - - #endregion IPipeline - - #region IDisposable - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (!_Disposed) - { - if (disposing) - { - //Context.Dispose(); - } - _Disposed = true; - } - } - - #endregion IDisposable + return new ResourceDataPipelineBuilder(option); } } diff --git a/src/PSRule.Rules.Azure/Pipeline/PipelineBuilderBase.cs b/src/PSRule.Rules.Azure/Pipeline/PipelineBuilderBase.cs new file mode 100644 index 00000000000..275b6367c9c --- /dev/null +++ b/src/PSRule.Rules.Azure/Pipeline/PipelineBuilderBase.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; +using PSRule.Rules.Azure.Configuration; +using PSRule.Rules.Azure.Pipeline.Output; + +namespace PSRule.Rules.Azure.Pipeline; + +internal abstract class PipelineBuilderBase : IPipelineBuilder +{ + private readonly PSPipelineWriter _Output; + + protected readonly PSRuleOption Option; + + protected PSCmdlet CmdletContext; + protected EngineIntrinsics ExecutionContext; + + protected PipelineBuilderBase() + { + Option = new PSRuleOption(); + _Output = new PSPipelineWriter(Option); + } + + /// + public virtual void UseCommandRuntime(PSCmdlet commandRuntime) + { + CmdletContext = commandRuntime; + _Output.UseCommandRuntime(commandRuntime); + } + + /// + public void UseExecutionContext(EngineIntrinsics executionContext) + { + ExecutionContext = executionContext; + _Output.UseExecutionContext(executionContext); + } + + /// + public virtual IPipelineBuilder Configure(PSRuleOption option) + { + if (option == null) + return this; + + Option.Output = new OutputOption(option.Output); + Option.Configuration = new ConfigurationOption(option.Configuration); + return this; + } + + /// + public abstract IPipeline Build(); + + protected PipelineContext PrepareContext() + { + return new PipelineContext(Option, PrepareWriter()); + } + + protected virtual PipelineWriter PrepareWriter() + { + var writer = new PSPipelineWriter(Option); + writer.UseCommandRuntime(CmdletContext); + writer.UseExecutionContext(ExecutionContext); + return writer; + } + + protected virtual PipelineWriter GetOutput() + { + return _Output; + } +} diff --git a/src/PSRule.Rules.Azure/Pipeline/PipelineWriter.cs b/src/PSRule.Rules.Azure/Pipeline/PipelineWriter.cs index fb4695e50f6..c21d61bb4c6 100644 --- a/src/PSRule.Rules.Azure/Pipeline/PipelineWriter.cs +++ b/src/PSRule.Rules.Azure/Pipeline/PipelineWriter.cs @@ -6,160 +6,159 @@ using System.Threading; using PSRule.Rules.Azure.Configuration; -namespace PSRule.Rules.Azure.Pipeline +namespace PSRule.Rules.Azure.Pipeline; + +internal abstract class PipelineWriter : ILogger { - internal abstract class PipelineWriter : ILogger + protected const string ErrorPreference = "ErrorActionPreference"; + protected const string WarningPreference = "WarningPreference"; + protected const string VerbosePreference = "VerbosePreference"; + protected const string InformationPreference = "InformationPreference"; + protected const string DebugPreference = "DebugPreference"; + + private readonly PipelineWriter _Writer; + + protected readonly PSRuleOption Option; + + protected PipelineWriter(PipelineWriter inner, PSRuleOption option) + { + _Writer = inner; + Option = option; + } + + public virtual void Begin() + { + if (_Writer == null) + return; + + _Writer.Begin(); + } + + public virtual void WriteObject(object sendToPipeline, bool enumerateCollection) + { + if (_Writer == null || sendToPipeline == null) + return; + + _Writer.WriteObject(sendToPipeline, enumerateCollection); + } + + public virtual void End() + { + if (_Writer == null) + return; + + _Writer.End(); + } + + public void WriteVerbose(string format, params object[] args) { - protected const string ErrorPreference = "ErrorActionPreference"; - protected const string WarningPreference = "WarningPreference"; - protected const string VerbosePreference = "VerbosePreference"; - protected const string InformationPreference = "InformationPreference"; - protected const string DebugPreference = "DebugPreference"; - - private readonly PipelineWriter _Writer; + if (!ShouldWriteVerbose()) + return; - protected readonly PSRuleOption Option; - - protected PipelineWriter(PipelineWriter inner, PSRuleOption option) - { - _Writer = inner; - Option = option; - } + WriteVerbose(string.Format(Thread.CurrentThread.CurrentCulture, format, args)); + } - public virtual void Begin() - { - if (_Writer == null) - return; + public virtual void WriteVerbose(string message) + { + if (_Writer == null || string.IsNullOrEmpty(message)) + return; - _Writer.Begin(); - } + _Writer.WriteVerbose(message); + } - public virtual void WriteObject(object sendToPipeline, bool enumerateCollection) - { - if (_Writer == null || sendToPipeline == null) - return; + public virtual bool ShouldWriteVerbose() + { + return _Writer != null && _Writer.ShouldWriteVerbose(); + } - _Writer.WriteObject(sendToPipeline, enumerateCollection); - } + public void WriteWarning(string format, params object[] args) + { + if (!ShouldWriteWarning()) + return; - public virtual void End() - { - if (_Writer == null) - return; + WriteWarning(string.Format(Thread.CurrentThread.CurrentCulture, format, args)); + } - _Writer.End(); - } - - public void WriteVerbose(string format, params object[] args) - { - if (!ShouldWriteVerbose()) - return; - - WriteVerbose(string.Format(Thread.CurrentThread.CurrentCulture, format, args)); - } - - public virtual void WriteVerbose(string message) - { - if (_Writer == null || string.IsNullOrEmpty(message)) - return; - - _Writer.WriteVerbose(message); - } - - public virtual bool ShouldWriteVerbose() - { - return _Writer != null && _Writer.ShouldWriteVerbose(); - } - - public void WriteWarning(string format, params object[] args) - { - if (!ShouldWriteWarning()) - return; - - WriteWarning(string.Format(Thread.CurrentThread.CurrentCulture, format, args)); - } - - public virtual void WriteWarning(string message) - { - if (_Writer == null || string.IsNullOrEmpty(message)) - return; - - _Writer.WriteWarning(message); - } - - public virtual bool ShouldWriteWarning() - { - return _Writer != null && _Writer.ShouldWriteWarning(); - } - - public void WriteError(Exception exception, string errorId, ErrorCategory errorCategory, object targetObject) - { - if (!ShouldWriteError()) - return; - - WriteError(new ErrorRecord(exception, errorId, errorCategory, targetObject)); - } - - public virtual void WriteError(ErrorRecord errorRecord) - { - if (_Writer == null || errorRecord == null) - return; - - _Writer.WriteError(errorRecord); - } - - public virtual bool ShouldWriteError() - { - return _Writer != null && _Writer.ShouldWriteError(); - } - - public virtual void WriteInformation(InformationRecord informationRecord) - { - if (_Writer == null || informationRecord == null) - return; - - _Writer.WriteInformation(informationRecord); - } - - public virtual void WriteHost(HostInformationMessage info) - { - if (_Writer == null) - return; - - _Writer.WriteHost(info); - } - - public virtual bool ShouldWriteInformation() - { - return _Writer != null && _Writer.ShouldWriteInformation(); - } - - public virtual void WriteDebug(DebugRecord debugRecord) - { - if (_Writer == null || debugRecord == null) - return; - - _Writer.WriteDebug(debugRecord); - } - - public void WriteDebug(string format, params object[] args) - { - if (_Writer == null || string.IsNullOrEmpty(format)) - return; - - var message = args == null || args.Length == 0 ? format : string.Format(format, args); - var debugRecord = new DebugRecord(message); - _Writer.WriteDebug(debugRecord); - } - - public virtual bool ShouldWriteDebug() - { - return _Writer != null && _Writer.ShouldWriteDebug(); - } - - protected static ActionPreference GetPreferenceVariable(SessionState sessionState, string variableName) - { - return (ActionPreference)sessionState.PSVariable.GetValue(variableName); - } + public virtual void WriteWarning(string message) + { + if (_Writer == null || string.IsNullOrEmpty(message)) + return; + + _Writer.WriteWarning(message); + } + + public virtual bool ShouldWriteWarning() + { + return _Writer != null && _Writer.ShouldWriteWarning(); + } + + public void WriteError(Exception exception, string errorId, ErrorCategory errorCategory, object targetObject) + { + if (!ShouldWriteError()) + return; + + WriteError(new ErrorRecord(exception, errorId, errorCategory, targetObject)); + } + + public virtual void WriteError(ErrorRecord errorRecord) + { + if (_Writer == null || errorRecord == null) + return; + + _Writer.WriteError(errorRecord); + } + + public virtual bool ShouldWriteError() + { + return _Writer != null && _Writer.ShouldWriteError(); + } + + public virtual void WriteInformation(InformationRecord informationRecord) + { + if (_Writer == null || informationRecord == null) + return; + + _Writer.WriteInformation(informationRecord); + } + + public virtual void WriteHost(HostInformationMessage info) + { + if (_Writer == null) + return; + + _Writer.WriteHost(info); + } + + public virtual bool ShouldWriteInformation() + { + return _Writer != null && _Writer.ShouldWriteInformation(); + } + + public virtual void WriteDebug(DebugRecord debugRecord) + { + if (_Writer == null || debugRecord == null) + return; + + _Writer.WriteDebug(debugRecord); + } + + public void WriteDebug(string format, params object[] args) + { + if (_Writer == null || string.IsNullOrEmpty(format)) + return; + + var message = args == null || args.Length == 0 ? format : string.Format(format, args); + var debugRecord = new DebugRecord(message); + _Writer.WriteDebug(debugRecord); + } + + public virtual bool ShouldWriteDebug() + { + return _Writer != null && _Writer.ShouldWriteDebug(); + } + + protected static ActionPreference GetPreferenceVariable(SessionState sessionState, string variableName) + { + return (ActionPreference)sessionState.PSVariable.GetValue(variableName); } } diff --git a/src/PSRule.Rules.Azure/Pipeline/PolicyAssignmentPipeline.cs b/src/PSRule.Rules.Azure/Pipeline/PolicyAssignmentPipeline.cs index 034767d06c6..c6258b1c2ef 100644 --- a/src/PSRule.Rules.Azure/Pipeline/PolicyAssignmentPipeline.cs +++ b/src/PSRule.Rules.Azure/Pipeline/PolicyAssignmentPipeline.cs @@ -4,57 +4,56 @@ using System.Management.Automation; using PSRule.Rules.Azure.Data.Policy; -namespace PSRule.Rules.Azure.Pipeline +namespace PSRule.Rules.Azure.Pipeline; + +internal sealed class PolicyAssignmentPipeline : PipelineBase { - internal sealed class PolicyAssignmentPipeline : PipelineBase + private readonly PolicyAssignmentHelper _PolicyAssignmentHelper; + + internal PolicyAssignmentPipeline(PipelineContext context, bool keepDuplicates) + : base(context) { - private readonly PolicyAssignmentHelper _PolicyAssignmentHelper; + _PolicyAssignmentHelper = new PolicyAssignmentHelper(context, keepDuplicates); + } - internal PolicyAssignmentPipeline(PipelineContext context, bool keepDuplicates) - : base(context) - { - _PolicyAssignmentHelper = new PolicyAssignmentHelper(context, keepDuplicates); - } + /// + public override void Process(PSObject sourceObject) + { + if (sourceObject == null || sourceObject.BaseObject is not PolicyAssignmentSource source) + return; - /// - public override void Process(PSObject sourceObject) - { - if (sourceObject == null || sourceObject.BaseObject is not PolicyAssignmentSource source) - return; + ProcessCatch(source.AssignmentFile); + } - ProcessCatch(source.AssignmentFile); - } + public override void End() + { + Context.Writer.WriteObject(_PolicyAssignmentHelper.Context.GetDefinitions(), true); + Context.Writer.WriteObject(_PolicyAssignmentHelper.Context.GenerateBaseline(), false); + base.End(); + } - public override void End() + private void ProcessCatch(string assignmentFile) + { + try { - Context.Writer.WriteObject(_PolicyAssignmentHelper.Context.GetDefinitions(), true); - Context.Writer.WriteObject(_PolicyAssignmentHelper.Context.GenerateBaseline(), false); - base.End(); + ProcessAssignment(assignmentFile); } - - private void ProcessCatch(string assignmentFile) + catch (PipelineException ex) { - try - { - ProcessAssignment(assignmentFile); - } - catch (PipelineException ex) - { - Context.Writer.WriteError( - ex, - nameof(PipelineException), - ErrorCategory.InvalidData, - assignmentFile); - } - catch - { - throw; - } + Context.Writer.WriteError( + ex, + nameof(PipelineException), + ErrorCategory.InvalidData, + assignmentFile); } - - private void ProcessAssignment(string assignmentFile) + catch { - _PolicyAssignmentHelper.ProcessAssignment(assignmentFile); + throw; } } + + private void ProcessAssignment(string assignmentFile) + { + _PolicyAssignmentHelper.ProcessAssignment(assignmentFile); + } } diff --git a/src/PSRule.Rules.Azure/Pipeline/PolicyAssignmentPipelineBuilder.cs b/src/PSRule.Rules.Azure/Pipeline/PolicyAssignmentPipelineBuilder.cs index 3d4b7da4f04..cbab6e6695f 100644 --- a/src/PSRule.Rules.Azure/Pipeline/PolicyAssignmentPipelineBuilder.cs +++ b/src/PSRule.Rules.Azure/Pipeline/PolicyAssignmentPipelineBuilder.cs @@ -5,83 +5,82 @@ using PSRule.Rules.Azure.Configuration; using PSRule.Rules.Azure.Pipeline.Output; -namespace PSRule.Rules.Azure.Pipeline +namespace PSRule.Rules.Azure.Pipeline; + +internal sealed class PolicyAssignmentPipelineBuilder : PipelineBuilderBase, IPolicyAssignmentPipelineBuilder { - internal sealed class PolicyAssignmentPipelineBuilder : PipelineBuilderBase, IPolicyAssignmentPipelineBuilder - { - private bool _PassThru; - private bool _KeepDuplicates; - private const string OUTPUTFILE_PREFIX = "definitions-"; - private const string OUTPUTFILE_EXTENSION = ".Rule.jsonc"; - private const string ASSIGNMENTNAME_PREFIX = "export-"; - private string _AssignmentName; + private bool _PassThru; + private bool _KeepDuplicates; + private const string OUTPUTFILE_PREFIX = "definitions-"; + private const string OUTPUTFILE_EXTENSION = ".Rule.jsonc"; + private const string ASSIGNMENTNAME_PREFIX = "export-"; + private string _AssignmentName; - internal PolicyAssignmentPipelineBuilder(PSRuleOption option) - { - _AssignmentName = string.Concat(ASSIGNMENTNAME_PREFIX, Guid.NewGuid().ToString().Substring(0, 8)); - Configure(option); - } + internal PolicyAssignmentPipelineBuilder(PSRuleOption option) + { + _AssignmentName = string.Concat(ASSIGNMENTNAME_PREFIX, Guid.NewGuid().ToString().Substring(0, 8)); + Configure(option); + } - public void Assignment(string assignmentName) - { - if (string.IsNullOrEmpty(assignmentName)) - return; + public void Assignment(string assignmentName) + { + if (string.IsNullOrEmpty(assignmentName)) + return; - _AssignmentName = assignmentName; - } + _AssignmentName = assignmentName; + } - public void ResourceGroup(ResourceGroupOption resourceGroup) - { - if (resourceGroup == null) - return; + public void ResourceGroup(ResourceGroupOption resourceGroup) + { + if (resourceGroup == null) + return; - Option.Configuration.ResourceGroup = ResourceGroupOption.Combine(resourceGroup, Option.Configuration.ResourceGroup); - } + Option.Configuration.ResourceGroup = ResourceGroupOption.Combine(resourceGroup, Option.Configuration.ResourceGroup); + } - public void Subscription(SubscriptionOption subscription) - { - if (subscription == null) - return; + public void Subscription(SubscriptionOption subscription) + { + if (subscription == null) + return; - Option.Configuration.Subscription = SubscriptionOption.Combine(subscription, Option.Configuration.Subscription); - } + Option.Configuration.Subscription = SubscriptionOption.Combine(subscription, Option.Configuration.Subscription); + } - /// - public void PassThru(bool passThru) - { - _PassThru = passThru; - } + /// + public void PassThru(bool passThru) + { + _PassThru = passThru; + } - /// - public void KeepDuplicates(bool keepDuplicates) - { - _KeepDuplicates = keepDuplicates; - } + /// + public void KeepDuplicates(bool keepDuplicates) + { + _KeepDuplicates = keepDuplicates; + } - protected override PipelineWriter GetOutput() - { - // Redirect to file instead - return !string.IsNullOrEmpty(Option.Output.Path) - ? new FileOutputWriter( - inner: base.GetOutput(), - option: Option, - encoding: Option.Output.Encoding.GetEncoding(), - path: Option.Output.Path, - defaultFile: string.Concat(OUTPUTFILE_PREFIX, _AssignmentName, OUTPUTFILE_EXTENSION), - shouldProcess: CmdletContext.ShouldProcess - ) - : base.GetOutput(); - } + protected override PipelineWriter GetOutput() + { + // Redirect to file instead + return !string.IsNullOrEmpty(Option.Output.Path) + ? new FileOutputWriter( + inner: base.GetOutput(), + option: Option, + encoding: Option.Output.Encoding.GetEncoding(), + path: Option.Output.Path, + defaultFile: string.Concat(OUTPUTFILE_PREFIX, _AssignmentName, OUTPUTFILE_EXTENSION), + shouldProcess: CmdletContext.ShouldProcess + ) + : base.GetOutput(); + } - protected override PipelineWriter PrepareWriter() - { - return _PassThru ? base.PrepareWriter() : new JsonOutputWriter(GetOutput(), Option); - } + protected override PipelineWriter PrepareWriter() + { + return _PassThru ? base.PrepareWriter() : new JsonOutputWriter(GetOutput(), Option); + } - /// - public override IPipeline Build() - { - return new PolicyAssignmentPipeline(PrepareContext(), _KeepDuplicates); - } + /// + public override IPipeline Build() + { + return new PolicyAssignmentPipeline(PrepareContext(), _KeepDuplicates); } } diff --git a/src/PSRule.Rules.Azure/Pipeline/PolicyAssignmentSearchPipeline.cs b/src/PSRule.Rules.Azure/Pipeline/PolicyAssignmentSearchPipeline.cs index 1fbe0ed566e..f3bba102103 100644 --- a/src/PSRule.Rules.Azure/Pipeline/PolicyAssignmentSearchPipeline.cs +++ b/src/PSRule.Rules.Azure/Pipeline/PolicyAssignmentSearchPipeline.cs @@ -3,35 +3,34 @@ using System.Management.Automation; -namespace PSRule.Rules.Azure.Pipeline +namespace PSRule.Rules.Azure.Pipeline; + +internal sealed class PolicyAssignmentSearchPipeline : PipelineBase { - internal sealed class PolicyAssignmentSearchPipeline : PipelineBase - { - private const string DEFAULT_ASSIGNMENTSEARCH_PATTERN = "*.assignment.json"; - private readonly PathBuilder _PathBuilder; + private const string DEFAULT_ASSIGNMENTSEARCH_PATTERN = "*.assignment.json"; + private readonly PathBuilder _PathBuilder; - internal PolicyAssignmentSearchPipeline(PipelineContext context, string basePath) - : base(context) - { - _PathBuilder = new PathBuilder(context.Writer, basePath, DEFAULT_ASSIGNMENTSEARCH_PATTERN); - } + internal PolicyAssignmentSearchPipeline(PipelineContext context, string basePath) + : base(context) + { + _PathBuilder = new PathBuilder(context.Writer, basePath, DEFAULT_ASSIGNMENTSEARCH_PATTERN); + } - /// - public override void Process(PSObject sourceObject) - { - if (sourceObject == null || !sourceObject.GetPath(out var path)) - return; + /// + public override void Process(PSObject sourceObject) + { + if (sourceObject == null || !sourceObject.GetPath(out var path)) + return; - _PathBuilder.Add(path); - var fileInfos = _PathBuilder.Build(); - foreach (var info in fileInfos) - ProcessAssignmentFile(info.FullName); - } + _PathBuilder.Add(path); + var fileInfos = _PathBuilder.Build(); + foreach (var info in fileInfos) + ProcessAssignmentFile(info.FullName); + } - private void ProcessAssignmentFile(string assignmentFile) - { - var assignmentSource = new PolicyAssignmentSource(assignmentFile); - Context.Writer.WriteObject(assignmentSource, false); - } + private void ProcessAssignmentFile(string assignmentFile) + { + var assignmentSource = new PolicyAssignmentSource(assignmentFile); + Context.Writer.WriteObject(assignmentSource, false); } } diff --git a/src/PSRule.Rules.Azure/Pipeline/PolicyAssignmentSearchPipelineBuilder.cs b/src/PSRule.Rules.Azure/Pipeline/PolicyAssignmentSearchPipelineBuilder.cs index 8bed50af2b1..d35f3aef8ef 100644 --- a/src/PSRule.Rules.Azure/Pipeline/PolicyAssignmentSearchPipelineBuilder.cs +++ b/src/PSRule.Rules.Azure/Pipeline/PolicyAssignmentSearchPipelineBuilder.cs @@ -1,29 +1,20 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace PSRule.Rules.Azure.Pipeline +namespace PSRule.Rules.Azure.Pipeline; + +internal sealed class PolicyAssignmentSearchPipelineBuilder : PipelineBuilderBase, IPolicyAssignmentSearchPipelineBuilder { - /// - /// A builder for a policy to rule generation pipeline. - /// - public interface IPolicyAssignmentSearchPipelineBuilder : IPipelineBuilder - { + private readonly string _BasePath; + internal PolicyAssignmentSearchPipelineBuilder(string basePath) + { + _BasePath = basePath; } - internal sealed class PolicyAssignmentSearchPipelineBuilder : PipelineBuilderBase, IPolicyAssignmentSearchPipelineBuilder + /// + public override IPipeline Build() { - private readonly string _BasePath; - - internal PolicyAssignmentSearchPipelineBuilder(string basePath) - { - _BasePath = basePath; - } - - /// - public override IPipeline Build() - { - return new PolicyAssignmentSearchPipeline(PrepareContext(), _BasePath); - } + return new PolicyAssignmentSearchPipeline(PrepareContext(), _BasePath); } } diff --git a/src/PSRule.Rules.Azure/Pipeline/PolicyAssignmentSource.cs b/src/PSRule.Rules.Azure/Pipeline/PolicyAssignmentSource.cs index 93adede1941..aaa4299fb4d 100644 --- a/src/PSRule.Rules.Azure/Pipeline/PolicyAssignmentSource.cs +++ b/src/PSRule.Rules.Azure/Pipeline/PolicyAssignmentSource.cs @@ -1,25 +1,24 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace PSRule.Rules.Azure.Pipeline +namespace PSRule.Rules.Azure.Pipeline; + +/// +/// A policy assignment source file. +/// +public sealed class PolicyAssignmentSource { /// - /// A policy assignment source file. + /// The assignment source file. /// - public sealed class PolicyAssignmentSource - { - /// - /// The assignment source file. - /// - public string AssignmentFile { get; } + public string AssignmentFile { get; } - /// - /// Create an assignment instance. - /// - /// The assignment source file. - public PolicyAssignmentSource(string assignmentFile) - { - AssignmentFile = assignmentFile; - } + /// + /// Create an assignment instance. + /// + /// The assignment source file. + public PolicyAssignmentSource(string assignmentFile) + { + AssignmentFile = assignmentFile; } } diff --git a/src/PSRule.Rules.Azure/Pipeline/ResourceDataPipeline.cs b/src/PSRule.Rules.Azure/Pipeline/ResourceDataPipeline.cs index 231ec9f0249..b333e47e363 100644 --- a/src/PSRule.Rules.Azure/Pipeline/ResourceDataPipeline.cs +++ b/src/PSRule.Rules.Azure/Pipeline/ResourceDataPipeline.cs @@ -14,250 +14,249 @@ using Newtonsoft.Json.Linq; using PSRule.Rules.Azure.Pipeline.Export; -namespace PSRule.Rules.Azure.Pipeline +namespace PSRule.Rules.Azure.Pipeline; + +/// +/// A pipeline to export resource data from Azure. +/// +internal sealed class ResourceDataPipeline : ExportDataPipeline { - /// - /// A pipeline to export resource data from Azure. - /// - internal sealed class ResourceDataPipeline : ExportDataPipeline - { - private const string PROPERTY_ID = "id"; - private const string PROPERTY_TYPE = "type"; - private const string PROPERTY_NAME = "name"; - private const string PROPERTY_SUBSCRIPTIONID = "subscriptionId"; - private const string PROPERTY_DISPLAYNAME = "displayName"; - private const string PROPERTY_TENANTID = "tenantId"; + private const string PROPERTY_ID = "id"; + private const string PROPERTY_TYPE = "type"; + private const string PROPERTY_NAME = "name"; + private const string PROPERTY_SUBSCRIPTIONID = "subscriptionId"; + private const string PROPERTY_DISPLAYNAME = "displayName"; + private const string PROPERTY_TENANTID = "tenantId"; - private const string ERRORID_RESOURCEDATAEXPAND = "PSRule.Rules.Azure.ResourceDataExpand"; + private const string ERRORID_RESOURCEDATAEXPAND = "PSRule.Rules.Azure.ResourceDataExpand"; - private readonly ConcurrentQueue _Resources; - private readonly ConcurrentQueue _Output; - private readonly int _ExpandThreadCount; - private readonly ExportSubscriptionScope[] _Subscription; - private readonly HashSet _ResourceGroup; - private readonly Hashtable _Tag; - private readonly int _RetryCount; - private readonly int _RetryInterval; - private readonly string _OutputPath; - private readonly string _TenantId; + private readonly ConcurrentQueue _Resources; + private readonly ConcurrentQueue _Output; + private readonly int _ExpandThreadCount; + private readonly ExportSubscriptionScope[] _Subscription; + private readonly HashSet _ResourceGroup; + private readonly Hashtable _Tag; + private readonly int _RetryCount; + private readonly int _RetryInterval; + private readonly string _OutputPath; + private readonly string _TenantId; - public ResourceDataPipeline(PipelineContext context, ExportSubscriptionScope[] subscription, string[] resourceGroup, GetAccessTokenFn getToken, Hashtable tag, int retryCount, int retryInterval, string outputPath, string tenantId) - : base(context, getToken) - { - _Subscription = subscription; - _ResourceGroup = resourceGroup != null && resourceGroup.Length > 0 ? new HashSet(resourceGroup, StringComparer.OrdinalIgnoreCase) : null; - _Tag = tag; - _RetryCount = retryCount; - _RetryInterval = retryInterval; - _OutputPath = outputPath; - _TenantId = tenantId; - _Resources = new ConcurrentQueue(); - _Output = new ConcurrentQueue(); - _ExpandThreadCount = Environment.ProcessorCount > 0 ? Environment.ProcessorCount * 4 : 20; - } + public ResourceDataPipeline(PipelineContext context, ExportSubscriptionScope[] subscription, string[] resourceGroup, GetAccessTokenFn getToken, Hashtable tag, int retryCount, int retryInterval, string outputPath, string tenantId) + : base(context, getToken) + { + _Subscription = subscription; + _ResourceGroup = resourceGroup != null && resourceGroup.Length > 0 ? new HashSet(resourceGroup, StringComparer.OrdinalIgnoreCase) : null; + _Tag = tag; + _RetryCount = retryCount; + _RetryInterval = retryInterval; + _OutputPath = outputPath; + _TenantId = tenantId; + _Resources = new ConcurrentQueue(); + _Output = new ConcurrentQueue(); + _ExpandThreadCount = Environment.ProcessorCount > 0 ? Environment.ProcessorCount * 4 : 20; + } - /// - public override void Begin() - { - // Process each subscription - for (var i = 0; _Subscription != null && i < _Subscription.Length; i++) - GetResourceBySubscription(_Subscription[i]); - } + /// + public override void Begin() + { + // Process each subscription + for (var i = 0; _Subscription != null && i < _Subscription.Length; i++) + GetResourceBySubscription(_Subscription[i]); + } - /// - public override void Process(PSObject sourceObject) - { - if (sourceObject != null && - sourceObject.BaseObject is string resourceId && - !string.IsNullOrEmpty(resourceId)) - GetResourceById(resourceId); - } + /// + public override void Process(PSObject sourceObject) + { + if (sourceObject != null && + sourceObject.BaseObject is string resourceId && + !string.IsNullOrEmpty(resourceId)) + GetResourceById(resourceId); + } - /// - public override void End() + /// + public override void End() + { + ExpandResource(_ExpandThreadCount); + WriteOutput(); + base.End(); + } + + /// + /// Write output to file or pipeline. + /// + private void WriteOutput() + { + var output = _Output.ToArray(); + if (_OutputPath == null) { - ExpandResource(_ExpandThreadCount); - WriteOutput(); - base.End(); + // Pass through results to pipeline + Context.Writer.WriteObject(JsonConvert.SerializeObject(output), enumerateCollection: false); } - - /// - /// Write output to file or pipeline. - /// - private void WriteOutput() + else { - var output = _Output.ToArray(); - if (_OutputPath == null) - { - // Pass through results to pipeline - Context.Writer.WriteObject(JsonConvert.SerializeObject(output), enumerateCollection: false); - } - else + // Group results into subscriptions a write each to a new file. + foreach (var group in output.GroupBy((r) => r[PROPERTY_SUBSCRIPTIONID].Value().ToLowerInvariant())) { - // Group results into subscriptions a write each to a new file. - foreach (var group in output.GroupBy((r) => r[PROPERTY_SUBSCRIPTIONID].Value().ToLowerInvariant())) - { - var filePath = Path.Combine(_OutputPath, string.Concat(group.Key, ".json")); - File.WriteAllText(filePath, JsonConvert.SerializeObject(group.ToArray()), Encoding.UTF8); - Context.Writer.WriteObject(new FileInfo(filePath), enumerateCollection: false); - } + var filePath = Path.Combine(_OutputPath, string.Concat(group.Key, ".json")); + File.WriteAllText(filePath, JsonConvert.SerializeObject(group.ToArray()), Encoding.UTF8); + Context.Writer.WriteObject(new FileInfo(filePath), enumerateCollection: false); } } + } - #region Get resources + #region Get resources - /// - /// Get a resource for expansion by resource Id. - /// - /// The specified resource Id. - private void GetResourceById(string resourceId) - { - if (!ResourceHelper.TrySubscriptionId(resourceId, out var subscriptionId)) - return; + /// + /// Get a resource for expansion by resource Id. + /// + /// The specified resource Id. + private void GetResourceById(string resourceId) + { + if (!ResourceHelper.TrySubscriptionId(resourceId, out var subscriptionId)) + return; - Context.Writer.VerboseGetResource(resourceId); - var scope = new ExportSubscriptionScope(subscriptionId, _TenantId); - var context = new SubscriptionExportContext(Context, scope, TokenCache, _Tag); + Context.Writer.VerboseGetResource(resourceId); + var scope = new ExportSubscriptionScope(subscriptionId, _TenantId); + var context = new SubscriptionExportContext(Context, scope, TokenCache, _Tag); - // Get results from ARM - GetResourceAsync(context, resourceId).Wait(); - } + // Get results from ARM + GetResourceAsync(context, resourceId).Wait(); + } - /// - /// Get a specific resource by Id. - /// - private async Task GetResourceAsync(ISubscriptionExportContext context, string resourceId) + /// + /// Get a specific resource by Id. + /// + private async Task GetResourceAsync(ISubscriptionExportContext context, string resourceId) + { + var added = 0; + var r = await context.GetResourceAsync(resourceId); + if (r != null) { - var added = 0; - var r = await context.GetResourceAsync(resourceId); - if (r != null) - { - added++; - r.Add(PROPERTY_TENANTID, context.TenantId); - _Resources.Enqueue(r); - added++; - } - return added; + added++; + r.Add(PROPERTY_TENANTID, context.TenantId); + _Resources.Enqueue(r); + added++; } + return added; + } - /// - /// Get resources, resource groups and the specified subscription within the subscription scope. - /// Resources are then queued for expansion. - /// - /// The subscription scope. - private void GetResourceBySubscription(ExportSubscriptionScope scope) - { - Context.Writer.VerboseGetResources(scope.SubscriptionId); - var context = new SubscriptionExportContext(Context, scope, TokenCache, _Tag); - var pool = new Task[3]; + /// + /// Get resources, resource groups and the specified subscription within the subscription scope. + /// Resources are then queued for expansion. + /// + /// The subscription scope. + private void GetResourceBySubscription(ExportSubscriptionScope scope) + { + Context.Writer.VerboseGetResources(scope.SubscriptionId); + var context = new SubscriptionExportContext(Context, scope, TokenCache, _Tag); + var pool = new Task[3]; - // Get results from ARM - pool[0] = GetResourcesAsync(context); - pool[1] = GetResourceGroupsAsync(context); - pool[2] = GetSubscriptionAsync(context); + // Get results from ARM + pool[0] = GetResourcesAsync(context); + pool[1] = GetResourceGroupsAsync(context); + pool[2] = GetSubscriptionAsync(context); - Task.WaitAll(pool); - var count = pool[0].Result + pool[1].Result + pool[2].Result; - Context.Writer.VerboseGetResourcesResult(count, scope.SubscriptionId); - } + Task.WaitAll(pool); + var count = pool[0].Result + pool[1].Result + pool[2].Result; + Context.Writer.VerboseGetResourcesResult(count, scope.SubscriptionId); + } - /// - /// Get a subscription resource. - /// - private async Task GetSubscriptionAsync(ISubscriptionExportContext context) + /// + /// Get a subscription resource. + /// + private async Task GetSubscriptionAsync(ISubscriptionExportContext context) + { + var added = 0; + var r = await context.GetSubscriptionAsync(); + if (r != null) { - var added = 0; - var r = await context.GetSubscriptionAsync(); - if (r != null) - { - r.Add(PROPERTY_TYPE, "Microsoft.Subscription"); - if (r.TryGetProperty(PROPERTY_DISPLAYNAME, out var displayName)) - r.Add(PROPERTY_NAME, displayName); + r.Add(PROPERTY_TYPE, "Microsoft.Subscription"); + if (r.TryGetProperty(PROPERTY_DISPLAYNAME, out var displayName)) + r.Add(PROPERTY_NAME, displayName); - _Resources.Enqueue(r); - added++; - } - return added; + _Resources.Enqueue(r); + added++; } + return added; + } - /// - /// Get a list of resource groups for a subscription. - /// - private async Task GetResourceGroupsAsync(ISubscriptionExportContext context) + /// + /// Get a list of resource groups for a subscription. + /// + private async Task GetResourceGroupsAsync(ISubscriptionExportContext context) + { + var added = 0; + foreach (var r in await context.GetResourceGroupsAsync()) { - var added = 0; - foreach (var r in await context.GetResourceGroupsAsync()) + // If resource group filtering is specified only queue resources in the specified resource group + if (_ResourceGroup == null || (ResourceHelper.TryResourceGroup(r[PROPERTY_ID].Value(), out _, out var resourceGroupName) && + _ResourceGroup.Contains(resourceGroupName))) { - // If resource group filtering is specified only queue resources in the specified resource group - if (_ResourceGroup == null || (ResourceHelper.TryResourceGroup(r[PROPERTY_ID].Value(), out _, out var resourceGroupName) && - _ResourceGroup.Contains(resourceGroupName))) - { - r.Add(PROPERTY_TENANTID, context.TenantId); - _Resources.Enqueue(r); - added++; - } + r.Add(PROPERTY_TENANTID, context.TenantId); + _Resources.Enqueue(r); + added++; } - return added; } + return added; + } - /// - /// Get a list of resources for a subscription. - /// - private async Task GetResourcesAsync(ISubscriptionExportContext context) + /// + /// Get a list of resources for a subscription. + /// + private async Task GetResourcesAsync(ISubscriptionExportContext context) + { + var added = 0; + foreach (var r in await context.GetResourcesAsync()) { - var added = 0; - foreach (var r in await context.GetResourcesAsync()) + // If resource group filtering is specified only queue resources in the specified resource group + if (_ResourceGroup == null || (ResourceHelper.TryResourceGroup(r[PROPERTY_ID].Value(), out _, out var resourceGroupName) && + _ResourceGroup.Contains(resourceGroupName))) { - // If resource group filtering is specified only queue resources in the specified resource group - if (_ResourceGroup == null || (ResourceHelper.TryResourceGroup(r[PROPERTY_ID].Value(), out _, out var resourceGroupName) && - _ResourceGroup.Contains(resourceGroupName))) - { - r.Add(PROPERTY_TENANTID, context.TenantId); - _Resources.Enqueue(r); - added++; - } + r.Add(PROPERTY_TENANTID, context.TenantId); + _Resources.Enqueue(r); + added++; } - return added; } + return added; + } - #endregion Get resources + #endregion Get resources - #region Expand resources + #region Expand resources - /// - /// Expand resources from the queue. - /// - /// The size of the thread pool to use. - private void ExpandResource(int poolSize) + /// + /// Expand resources from the queue. + /// + /// The size of the thread pool to use. + private void ExpandResource(int poolSize) + { + var context = new ResourceExportContext(Context, _Resources, TokenCache, _RetryCount, _RetryInterval); + var visitor = new ResourceExportVisitor(); + var pool = new Task[poolSize]; + for (var i = 0; i < poolSize; i++) { - var context = new ResourceExportContext(Context, _Resources, TokenCache, _RetryCount, _RetryInterval); - var visitor = new ResourceExportVisitor(); - var pool = new Task[poolSize]; - for (var i = 0; i < poolSize; i++) + pool[i] = Task.Run(async () => { - pool[i] = Task.Run(async () => + while (!_Resources.IsEmpty && _Resources.TryDequeue(out var r)) { - while (!_Resources.IsEmpty && _Resources.TryDequeue(out var r)) + context.VerboseExpandingResource(r[PROPERTY_ID].Value()); + try { - context.VerboseExpandingResource(r[PROPERTY_ID].Value()); - try - { - await visitor.VisitAsync(context, r); - _Output.Enqueue(r); - } - catch (Exception ex) - { - context.Error(ex, ERRORID_RESOURCEDATAEXPAND); - } + await visitor.VisitAsync(context, r); + _Output.Enqueue(r); } - }); - } - context.Wait(); - Task.WaitAll(pool); - context.Flush(); - pool.DisposeAll(); + catch (Exception ex) + { + context.Error(ex, ERRORID_RESOURCEDATAEXPAND); + } + } + }); } - - #endregion Expand resources + context.Wait(); + Task.WaitAll(pool); + context.Flush(); + pool.DisposeAll(); } + + #endregion Expand resources } diff --git a/src/PSRule.Rules.Azure/Pipeline/ResourceDataPipelineBuilder.cs b/src/PSRule.Rules.Azure/Pipeline/ResourceDataPipelineBuilder.cs index 87ae18ee2c2..071eb6a6568 100644 --- a/src/PSRule.Rules.Azure/Pipeline/ResourceDataPipelineBuilder.cs +++ b/src/PSRule.Rules.Azure/Pipeline/ResourceDataPipelineBuilder.cs @@ -4,104 +4,103 @@ using System.Collections; using PSRule.Rules.Azure.Configuration; -namespace PSRule.Rules.Azure.Pipeline +namespace PSRule.Rules.Azure.Pipeline; + +/// +/// A builder to configure the pipeline to export Azure resource data. +/// +public interface IResourceDataPipelineBuilder : IExportDataPipelineBuilder { /// - /// A builder to configure the pipeline to export Azure resource data. + /// Specifies any resource group filters to be used. /// - public interface IResourceDataPipelineBuilder : IExportDataPipelineBuilder - { - /// - /// Specifies any resource group filters to be used. - /// - /// If any are specified, only these resource groups will be included in results. - void ResourceGroup(string[] resourceGroup); - - /// - /// Specifies any subscriptions filters to be used. - /// - /// If any are specified, only these subscriptions will be included in results. - void Subscription(ExportSubscriptionScope[] scope); - - /// - /// Specifies additional tags to filter resources by. - /// - /// A hashtable of tags to use for filtering resources. - void Tag(Hashtable tag); - - /// - /// Specifies the path to write output. - /// - /// The directory path to write output. - void OutputPath(string path); - - /// - /// Specifies the default tenant to use when the tenant is unknown. - /// - /// The tenant Id of the default tenant. - void Tenant(string tenantId); - } + /// If any are specified, only these resource groups will be included in results. + void ResourceGroup(string[] resourceGroup); + + /// + /// Specifies any subscriptions filters to be used. + /// + /// If any are specified, only these subscriptions will be included in results. + void Subscription(ExportSubscriptionScope[] scope); + + /// + /// Specifies additional tags to filter resources by. + /// + /// A hashtable of tags to use for filtering resources. + void Tag(Hashtable tag); /// - /// A helper to build a pipeline that exports resource data from Azure. + /// Specifies the path to write output. /// - internal sealed class ResourceDataPipelineBuilder : ExportDataPipelineBuilder, IResourceDataPipelineBuilder + /// The directory path to write output. + void OutputPath(string path); + + /// + /// Specifies the default tenant to use when the tenant is unknown. + /// + /// The tenant Id of the default tenant. + void Tenant(string tenantId); +} + +/// +/// A helper to build a pipeline that exports resource data from Azure. +/// +internal sealed class ResourceDataPipelineBuilder : ExportDataPipelineBuilder, IResourceDataPipelineBuilder +{ + private string[] _ResourceGroup; + private ExportSubscriptionScope[] _Subscription; + private Hashtable _Tag; + private string _OutputPath; + private string _TenantId; + + internal ResourceDataPipelineBuilder(PSRuleOption option) + : base() + { + Configure(option); + } + + /// + public void ResourceGroup(string[] resourceGroup) + { + if (resourceGroup == null || resourceGroup.Length == 0) + return; + + _ResourceGroup = resourceGroup; + } + + /// + public void Subscription(ExportSubscriptionScope[] scope) + { + if (scope == null || scope.Length == 0) + return; + + _Subscription = scope; + } + + /// + public void Tag(Hashtable tag) + { + if (tag == null || tag.Count == 0) + return; + + _Tag = tag; + } + + /// + public void OutputPath(string path) + { + _OutputPath = path; + } + + /// + public void Tenant(string tenantId) + { + _TenantId = tenantId; + } + + /// + public override IPipeline Build() { - private string[] _ResourceGroup; - private ExportSubscriptionScope[] _Subscription; - private Hashtable _Tag; - private string _OutputPath; - private string _TenantId; - - internal ResourceDataPipelineBuilder(PSRuleOption option) - : base() - { - Configure(option); - } - - /// - public void ResourceGroup(string[] resourceGroup) - { - if (resourceGroup == null || resourceGroup.Length == 0) - return; - - _ResourceGroup = resourceGroup; - } - - /// - public void Subscription(ExportSubscriptionScope[] scope) - { - if (scope == null || scope.Length == 0) - return; - - _Subscription = scope; - } - - /// - public void Tag(Hashtable tag) - { - if (tag == null || tag.Count == 0) - return; - - _Tag = tag; - } - - /// - public void OutputPath(string path) - { - _OutputPath = path; - } - - /// - public void Tenant(string tenantId) - { - _TenantId = tenantId; - } - - /// - public override IPipeline Build() - { - return new ResourceDataPipeline(PrepareContext(), _Subscription, _ResourceGroup, _AccessToken, _Tag, _RetryCount, _RetryInterval, _OutputPath, _TenantId); - } + return new ResourceDataPipeline(PrepareContext(), _Subscription, _ResourceGroup, _AccessToken, _Tag, _RetryCount, _RetryInterval, _OutputPath, _TenantId); } } diff --git a/src/PSRule.Rules.Azure/Pipeline/TemplateLinkPipeline.cs b/src/PSRule.Rules.Azure/Pipeline/TemplateLinkPipeline.cs index bdf943f9da7..f3ec5f92b11 100644 --- a/src/PSRule.Rules.Azure/Pipeline/TemplateLinkPipeline.cs +++ b/src/PSRule.Rules.Azure/Pipeline/TemplateLinkPipeline.cs @@ -7,55 +7,54 @@ using PSRule.Rules.Azure.Configuration; using PSRule.Rules.Azure.Data.Template; -namespace PSRule.Rules.Azure.Pipeline +namespace PSRule.Rules.Azure.Pipeline; + +internal sealed class TemplateLinkPipeline : PipelineBase { - internal sealed class TemplateLinkPipeline : PipelineBase + private const string DEFAULT_TEMPLATESEARCH_PATTERN = "*.parameters.json"; + + private readonly string _BasePath; + private readonly PathBuilder _PathBuilder; + private readonly TemplateLinkHelper _TemplateHelper; + + internal TemplateLinkPipeline(PipelineContext context, string basePath, bool skipUnlinked) + : base(context) { - private const string DEFAULT_TEMPLATESEARCH_PATTERN = "*.parameters.json"; + _BasePath = PSRuleOption.GetRootedBasePath(basePath); + _PathBuilder = new PathBuilder(context.Writer, basePath, DEFAULT_TEMPLATESEARCH_PATTERN); + _TemplateHelper = new TemplateLinkHelper(context, _BasePath, skipUnlinked); + } - private readonly string _BasePath; - private readonly PathBuilder _PathBuilder; - private readonly TemplateLinkHelper _TemplateHelper; + /// + public override void Process(PSObject sourceObject) + { + if (sourceObject == null || !sourceObject.GetPath(out var path)) + return; + + _PathBuilder.Add(path); + var fileInfos = _PathBuilder.Build(); + foreach (var info in fileInfos) + ProcessParameterFile(info.FullName); + } - internal TemplateLinkPipeline(PipelineContext context, string basePath, bool skipUnlinked) - : base(context) + private void ProcessParameterFile(string parameterFile) + { + try { - _BasePath = PSRuleOption.GetRootedBasePath(basePath); - _PathBuilder = new PathBuilder(context.Writer, basePath, DEFAULT_TEMPLATESEARCH_PATTERN); - _TemplateHelper = new TemplateLinkHelper(context, _BasePath, skipUnlinked); + var templateLink = _TemplateHelper.ProcessParameterFile(parameterFile); + Context.Writer.WriteObject(templateLink, false); } - - /// - public override void Process(PSObject sourceObject) + catch (InvalidOperationException ex) { - if (sourceObject == null || !sourceObject.GetPath(out var path)) - return; - - _PathBuilder.Add(path); - var fileInfos = _PathBuilder.Build(); - foreach (var info in fileInfos) - ProcessParameterFile(info.FullName); + Context.Writer.WriteError(ex, nameof(InvalidOperationException), ErrorCategory.InvalidOperation, parameterFile); } - - private void ProcessParameterFile(string parameterFile) + catch (FileNotFoundException ex) + { + Context.Writer.WriteError(ex, nameof(FileNotFoundException), ErrorCategory.ObjectNotFound, parameterFile); + } + catch (PipelineException ex) { - try - { - var templateLink = _TemplateHelper.ProcessParameterFile(parameterFile); - Context.Writer.WriteObject(templateLink, false); - } - catch (InvalidOperationException ex) - { - Context.Writer.WriteError(ex, nameof(InvalidOperationException), ErrorCategory.InvalidOperation, parameterFile); - } - catch (FileNotFoundException ex) - { - Context.Writer.WriteError(ex, nameof(FileNotFoundException), ErrorCategory.ObjectNotFound, parameterFile); - } - catch (PipelineException ex) - { - Context.Writer.WriteError(ex, nameof(PipelineException), ErrorCategory.WriteError, parameterFile); - } + Context.Writer.WriteError(ex, nameof(PipelineException), ErrorCategory.WriteError, parameterFile); } } } diff --git a/src/PSRule.Rules.Azure/Pipeline/TemplateLinkPipelineBuilder.cs b/src/PSRule.Rules.Azure/Pipeline/TemplateLinkPipelineBuilder.cs index 4c826950b07..1c36fdf7132 100644 --- a/src/PSRule.Rules.Azure/Pipeline/TemplateLinkPipelineBuilder.cs +++ b/src/PSRule.Rules.Azure/Pipeline/TemplateLinkPipelineBuilder.cs @@ -1,40 +1,28 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace PSRule.Rules.Azure.Pipeline +namespace PSRule.Rules.Azure.Pipeline; + +internal sealed class TemplateLinkPipelineBuilder : PipelineBuilderBase, ITemplateLinkPipelineBuilder { - /// - /// A helper to build a template link pipeline. - /// - public interface ITemplateLinkPipelineBuilder : IPipelineBuilder + private readonly string _BasePath; + + private bool _SkipUnlinked; + + internal TemplateLinkPipelineBuilder(string basePath) { - /// - /// Determines if unlinked parameter files are skipped or error. - /// - void SkipUnlinked(bool skipUnlinked); + _BasePath = basePath; } - internal sealed class TemplateLinkPipelineBuilder : PipelineBuilderBase, ITemplateLinkPipelineBuilder + /// + public void SkipUnlinked(bool skipUnlinked) { - private readonly string _BasePath; - - private bool _SkipUnlinked; - - internal TemplateLinkPipelineBuilder(string basePath) - { - _BasePath = basePath; - } - - /// - public void SkipUnlinked(bool skipUnlinked) - { - _SkipUnlinked = skipUnlinked; - } + _SkipUnlinked = skipUnlinked; + } - /// - public override IPipeline Build() - { - return new TemplateLinkPipeline(PrepareContext(), _BasePath, _SkipUnlinked); - } + /// + public override IPipeline Build() + { + return new TemplateLinkPipeline(PrepareContext(), _BasePath, _SkipUnlinked); } } diff --git a/src/PSRule.Rules.Azure/Pipeline/TemplatePipeline.cs b/src/PSRule.Rules.Azure/Pipeline/TemplatePipeline.cs index 1502cdf37e0..22b46b67857 100644 --- a/src/PSRule.Rules.Azure/Pipeline/TemplatePipeline.cs +++ b/src/PSRule.Rules.Azure/Pipeline/TemplatePipeline.cs @@ -7,82 +7,81 @@ using PSRule.Rules.Azure.Data.Template; using PSRule.Rules.Azure.Runtime; -namespace PSRule.Rules.Azure.Pipeline +namespace PSRule.Rules.Azure.Pipeline; + +internal sealed class TemplatePipeline : PipelineBase { - internal sealed class TemplatePipeline : PipelineBase - { - private TemplateHelper _TemplateHelper; - private BicepHelper _BicepHelper; + private TemplateHelper _TemplateHelper; + private BicepHelper _BicepHelper; - internal TemplatePipeline(PipelineContext context) - : base(context) { } + internal TemplatePipeline(PipelineContext context) + : base(context) { } - /// - public override void Process(PSObject sourceObject) - { - if (sourceObject == null || sourceObject.BaseObject is not TemplateSource source) - return; + /// + public override void Process(PSObject sourceObject) + { + if (sourceObject == null || sourceObject.BaseObject is not TemplateSource source) + return; - if (source.ParametersFile == null || source.ParametersFile.Length == 0) - ProcessCatch(source.TemplateFile, null, source.Kind); - else - for (var i = 0; i < source.ParametersFile.Length; i++) - ProcessCatch(source.TemplateFile, source.ParametersFile[i], source.Kind); - } + if (source.ParametersFile == null || source.ParametersFile.Length == 0) + ProcessCatch(source.TemplateFile, null, source.Kind); + else + for (var i = 0; i < source.ParametersFile.Length; i++) + ProcessCatch(source.TemplateFile, source.ParametersFile[i], source.Kind); + } - private void ProcessCatch(string templateFile, string parameterFile, TemplateSourceKind kind) + private void ProcessCatch(string templateFile, string parameterFile, TemplateSourceKind kind) + { + try { - try + if (kind == TemplateSourceKind.Bicep) { - if (kind == TemplateSourceKind.Bicep) - { - Context.Writer.WriteObject(ProcessBicep(templateFile, parameterFile), true); - } - else if (kind == TemplateSourceKind.BicepParam) - { - Context.Writer.WriteObject(ProcessBicepParam(templateFile), true); - } - else - { - Context.Writer.WriteObject(ProcessTemplate(templateFile, parameterFile), true); - } + Context.Writer.WriteObject(ProcessBicep(templateFile, parameterFile), true); } - catch (PipelineException ex) + else if (kind == TemplateSourceKind.BicepParam) { - Context.Writer.WriteError(ex, nameof(PipelineException), ErrorCategory.InvalidData, parameterFile); + Context.Writer.WriteObject(ProcessBicepParam(templateFile), true); } - catch + else { - throw; + Context.Writer.WriteObject(ProcessTemplate(templateFile, parameterFile), true); } } - - private PSObject[] ProcessTemplate(string templateFile, string parameterFile) + catch (PipelineException ex) { - return GetTemplateHelper().ProcessTemplate(templateFile, parameterFile, out _); + Context.Writer.WriteError(ex, nameof(PipelineException), ErrorCategory.InvalidData, parameterFile); } - - private PSObject[] ProcessBicep(string templateFile, string parameterFile) + catch { - return GetBicepHelper().ProcessFile(templateFile, parameterFile); + throw; } + } - private PSObject[] ProcessBicepParam(string parameterFile) - { - return GetBicepHelper().ProcessParamFile(parameterFile); - } + private PSObject[] ProcessTemplate(string templateFile, string parameterFile) + { + return GetTemplateHelper().ProcessTemplate(templateFile, parameterFile, out _); + } - private TemplateHelper GetTemplateHelper() - { - return _TemplateHelper ??= new TemplateHelper(Context); - } + private PSObject[] ProcessBicep(string templateFile, string parameterFile) + { + return GetBicepHelper().ProcessFile(templateFile, parameterFile); + } - private BicepHelper GetBicepHelper() - { - return _BicepHelper ??= new BicepHelper(Context, new RuntimeService( - minimum: Context.Option.Configuration.BicepMinimumVersion ?? ConfigurationOption.Default.BicepMinimumVersion, - timeout: Context.Option.Configuration.BicepFileExpansionTimeout.GetValueOrDefault(ConfigurationOption.Default.BicepFileExpansionTimeout.Value) - )); - } + private PSObject[] ProcessBicepParam(string parameterFile) + { + return GetBicepHelper().ProcessParamFile(parameterFile); + } + + private TemplateHelper GetTemplateHelper() + { + return _TemplateHelper ??= new TemplateHelper(Context); + } + + private BicepHelper GetBicepHelper() + { + return _BicepHelper ??= new BicepHelper(Context, new RuntimeService( + minimum: Context.Option.Configuration.BicepMinimumVersion ?? ConfigurationOption.Default.BicepMinimumVersion, + timeout: Context.Option.Configuration.BicepFileExpansionTimeout.GetValueOrDefault(ConfigurationOption.Default.BicepFileExpansionTimeout.Value) + )); } } diff --git a/src/PSRule.Rules.Azure/Pipeline/TemplatePipelineBuilder.cs b/src/PSRule.Rules.Azure/Pipeline/TemplatePipelineBuilder.cs index 550eb709a8b..9a1ae01907c 100644 --- a/src/PSRule.Rules.Azure/Pipeline/TemplatePipelineBuilder.cs +++ b/src/PSRule.Rules.Azure/Pipeline/TemplatePipelineBuilder.cs @@ -5,110 +5,83 @@ using PSRule.Rules.Azure.Configuration; using PSRule.Rules.Azure.Pipeline.Output; -namespace PSRule.Rules.Azure.Pipeline -{ - /// - /// A helper for building a template expansion pipeline. - /// - public interface ITemplatePipelineBuilder : IPipelineBuilder - { - /// - /// Configures the name of the deployment. - /// - void Deployment(string deploymentName); +namespace PSRule.Rules.Azure.Pipeline; - /// - /// Configures properties of the resource group. - /// - void ResourceGroup(ResourceGroupOption resourceGroup); +internal sealed class TemplatePipelineBuilder : PipelineBuilderBase, ITemplatePipelineBuilder +{ + private const string OUTPUTFILE_PREFIX = "resources-"; + private const string OUTPUTFILE_EXTENSION = ".json"; + private const string DEPLOYMENTNAME_PREFIX = "export-"; - /// - /// Configures properties of the subscription. - /// - void Subscription(SubscriptionOption subscription); + private string _DeploymentName; + private bool _PassThru; - /// - /// Determines if expanded resources are passed through to the pipeline. - /// - void PassThru(bool passThru); + internal TemplatePipelineBuilder(PSRuleOption option) + : base() + { + _DeploymentName = string.Concat(DEPLOYMENTNAME_PREFIX, Guid.NewGuid().ToString().Substring(0, 8)); + Configure(option); } - internal sealed class TemplatePipelineBuilder : PipelineBuilderBase, ITemplatePipelineBuilder + /// + public void Deployment(string deploymentName) { - private const string OUTPUTFILE_PREFIX = "resources-"; - private const string OUTPUTFILE_EXTENSION = ".json"; - private const string DEPLOYMENTNAME_PREFIX = "export-"; - - private string _DeploymentName; - private bool _PassThru; + if (string.IsNullOrEmpty(deploymentName)) + return; - internal TemplatePipelineBuilder(PSRuleOption option) - : base() - { - _DeploymentName = string.Concat(DEPLOYMENTNAME_PREFIX, Guid.NewGuid().ToString().Substring(0, 8)); - Configure(option); - } - - /// - public void Deployment(string deploymentName) - { - if (string.IsNullOrEmpty(deploymentName)) - return; - - Option.Configuration.Deployment = new DeploymentOption(deploymentName); - _DeploymentName = deploymentName; - } + Option.Configuration.Deployment = new DeploymentOption(deploymentName); + _DeploymentName = deploymentName; + } - /// - public void ResourceGroup(ResourceGroupOption resourceGroup) - { - if (resourceGroup == null) - return; + /// + public void ResourceGroup(ResourceGroupOption resourceGroup) + { + if (resourceGroup == null) + return; - Option.Configuration.ResourceGroup = ResourceGroupOption.Combine(resourceGroup, Option.Configuration.ResourceGroup); - } + Option.Configuration.ResourceGroup = ResourceGroupOption.Combine(resourceGroup, Option.Configuration.ResourceGroup); + } - /// - public void Subscription(SubscriptionOption subscription) - { - if (subscription == null) - return; + /// + public void Subscription(SubscriptionOption subscription) + { + if (subscription == null) + return; - Option.Configuration.Subscription = SubscriptionOption.Combine(subscription, Option.Configuration.Subscription); - } + Option.Configuration.Subscription = SubscriptionOption.Combine(subscription, Option.Configuration.Subscription); + } - /// - public void PassThru(bool passThru) - { - _PassThru = passThru; - } + /// + public void PassThru(bool passThru) + { + _PassThru = passThru; + } - /// - protected override PipelineWriter GetOutput() - { - // Redirect to file instead - return !string.IsNullOrEmpty(Option.Output.Path) - ? new FileOutputWriter( - inner: base.GetOutput(), - option: Option, - encoding: Option.Output.Encoding.GetEncoding(), - path: Option.Output.Path, - defaultFile: string.Concat(OUTPUTFILE_PREFIX, _DeploymentName, OUTPUTFILE_EXTENSION), - shouldProcess: CmdletContext.ShouldProcess - ) - : base.GetOutput(); - } + /// + protected override PipelineWriter GetOutput() + { + // Redirect to file instead + return !string.IsNullOrEmpty(Option.Output.Path) + ? new FileOutputWriter( + inner: base.GetOutput(), + option: Option, + encoding: Option.Output.Encoding.GetEncoding(), + path: Option.Output.Path, + defaultFile: string.Concat(OUTPUTFILE_PREFIX, _DeploymentName, OUTPUTFILE_EXTENSION), + shouldProcess: CmdletContext.ShouldProcess + ) + : base.GetOutput(); + } - /// - protected override PipelineWriter PrepareWriter() - { - return _PassThru ? base.PrepareWriter() : new JsonOutputWriter(GetOutput(), Option); - } + /// + protected override PipelineWriter PrepareWriter() + { + return _PassThru ? base.PrepareWriter() : new JsonOutputWriter(GetOutput(), Option); + } - /// - public override IPipeline Build() - { - return new TemplatePipeline(PrepareContext()); - } + /// + public override IPipeline Build() + { + return new TemplatePipeline(PrepareContext()); } } diff --git a/src/PSRule.Rules.Azure/Pipeline/TemplateSource.cs b/src/PSRule.Rules.Azure/Pipeline/TemplateSource.cs index d1800ff9c1d..6c164f434ca 100644 --- a/src/PSRule.Rules.Azure/Pipeline/TemplateSource.cs +++ b/src/PSRule.Rules.Azure/Pipeline/TemplateSource.cs @@ -3,48 +3,39 @@ using System; -namespace PSRule.Rules.Azure.Pipeline +namespace PSRule.Rules.Azure.Pipeline; + +/// +/// A source for template expansion. +/// +public sealed class TemplateSource { - internal enum TemplateSourceKind + internal readonly string TemplateFile; + internal readonly string[] ParametersFile; + internal readonly TemplateSourceKind Kind; + + /// + /// Create a source. + /// + public TemplateSource(string templateFile, string[] parametersFile) { - None = 0, - Template = 1, - Bicep = 2, - BicepParam = 3, + TemplateFile = templateFile; + ParametersFile = parametersFile; + Kind = TemplateSourceKind.Template; } /// - /// A source for template expansion. + /// Create a source. /// - public sealed class TemplateSource + public TemplateSource(string sourceFile) { - internal readonly string TemplateFile; - internal readonly string[] ParametersFile; - internal readonly TemplateSourceKind Kind; - - /// - /// Create a source. - /// - public TemplateSource(string templateFile, string[] parametersFile) - { - TemplateFile = templateFile; - ParametersFile = parametersFile; - Kind = TemplateSourceKind.Template; - } - - /// - /// Create a source. - /// - public TemplateSource(string sourceFile) - { - if (string.IsNullOrEmpty(sourceFile)) - throw new ArgumentNullException(nameof(sourceFile)); + if (string.IsNullOrEmpty(sourceFile)) + throw new ArgumentNullException(nameof(sourceFile)); - TemplateFile = sourceFile; - if (TemplateFile.EndsWith(".bicep", StringComparison.OrdinalIgnoreCase)) - Kind = TemplateSourceKind.Bicep; - else if (TemplateFile.EndsWith(".bicepparam", StringComparison.OrdinalIgnoreCase)) - Kind = TemplateSourceKind.BicepParam; - } + TemplateFile = sourceFile; + if (TemplateFile.EndsWith(".bicep", StringComparison.OrdinalIgnoreCase)) + Kind = TemplateSourceKind.Bicep; + else if (TemplateFile.EndsWith(".bicepparam", StringComparison.OrdinalIgnoreCase)) + Kind = TemplateSourceKind.BicepParam; } } diff --git a/src/PSRule.Rules.Azure/Pipeline/TemplateSourceKind.cs b/src/PSRule.Rules.Azure/Pipeline/TemplateSourceKind.cs new file mode 100644 index 00000000000..3fc3fc18dec --- /dev/null +++ b/src/PSRule.Rules.Azure/Pipeline/TemplateSourceKind.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Rules.Azure.Pipeline; + +internal enum TemplateSourceKind +{ + None = 0, + Template = 1, + Bicep = 2, + BicepParam = 3, +} diff --git a/src/PSRule.Rules.Azure/Runtime/Helper.cs b/src/PSRule.Rules.Azure/Runtime/Helper.cs index c82a9446cdf..860f7e43e79 100644 --- a/src/PSRule.Rules.Azure/Runtime/Helper.cs +++ b/src/PSRule.Rules.Azure/Runtime/Helper.cs @@ -13,222 +13,221 @@ using PSRule.Rules.Azure.Pipeline; using PSRule.Rules.Azure.Pipeline.Output; -namespace PSRule.Rules.Azure.Runtime +namespace PSRule.Rules.Azure.Runtime; + +/// +/// Helper methods exposed to PowerShell. +/// +public static class Helper { /// - /// Helper methods exposed to PowerShell. - /// - public static class Helper - { - /// - /// Create a singleton context for running within PSRule. - /// - public static IRuntimeService CreateService(string minimum, int timeout) - { - return new RuntimeService(minimum, timeout); - } - - /// - /// Get information about the installed Bicep version. - /// - public static string GetBicepVersion(IService service) - { - var s = service as RuntimeService; - var context = GetContext(s); - var bicep = new BicepHelper( - context, - s - ); - return bicep.Version; - } - - /// - /// Parse and reformat the expression by removing whitespace. - /// - public static string CompressExpression(string expression) - { - return !IsTemplateExpression(expression) ? expression : ExpressionParser.Parse(expression).AsString(); - } - - /// - /// Look for literals and variable usage. - /// - public static bool HasLiteralValue(string expression) - { - return !IsTemplateExpression(expression) || - TokenStreamValidator.HasLiteralValue(ExpressionParser.Parse(expression)); - } - - /// - /// Get the name of parameters that would be assigned as values. - /// - internal static string[] GetParameterTokenValue(string expression) - { - return !IsTemplateExpression(expression) - ? Array.Empty() - : TokenStreamValidator.GetParameterTokenValue(ExpressionParser.Parse(expression)); - } - - /// - /// Returns true if an expression contains a call to the listKeys function. - /// - internal static bool UsesListKeysFunction(string expression) - { - return IsTemplateExpression(expression) && - TokenStreamValidator.UsesListKeysFunction(ExpressionParser.Parse(expression)); - } - - /// - /// Returns true if an expression contains a call to the reference function. - /// - internal static bool UsesReferenceFunction(string expression) - { - return IsTemplateExpression(expression) && - TokenStreamValidator.UsesReferenceFunction(ExpressionParser.Parse(expression)); - } - - /// - /// Checks if the value of the expression is secure, whether by using secure parameters, references to KeyVault, or the ListKeys function. - /// - public static bool HasSecureValue(string expression, string[] secureParameters) - { - if ((!string.IsNullOrEmpty(expression) && expression.StartsWith("{{Secret", StringComparison.OrdinalIgnoreCase)) || - UsesListKeysFunction(expression) || - UsesReferenceFunction(expression)) - return true; - - var parameterNamesInExpression = GetParameterTokenValue(expression); - - return parameterNamesInExpression != null && - parameterNamesInExpression.Length > 0 && - parameterNamesInExpression.Intersect(secureParameters, StringComparer.OrdinalIgnoreCase).Count() == parameterNamesInExpression.Length; - } - - /// - /// Check it the string is an expression. - /// - public static bool IsTemplateExpression(string expression) - { - return ExpressionParser.IsExpression(expression); - } - - /// - /// Expand resources from a parameter file and linked template/ bicep files. - /// - public static PSObject[] GetResources(IService service, string parameterFile) - { - var context = GetContext(service as RuntimeService); - var linkHelper = new TemplateLinkHelper(context, PSRuleOption.GetWorkingPath(), true); - var link = linkHelper.ProcessParameterFile(parameterFile); - if (link == null) - return null; - - return IsBicep(link.TemplateFile) ? - GetBicepResources(service as RuntimeService, link.TemplateFile, link.ParameterFile) : - GetTemplateResources(link.TemplateFile, link.ParameterFile, context); - } - - /// - /// Expand resources from a bicep file. - /// - public static PSObject[] GetBicepResources(IService service, string bicepFile) - { - return GetBicepResources(service as RuntimeService, bicepFile, null); - } - - /// - /// Expand resources from a bicep param file. - /// - public static PSObject[] GetBicepParamResources(IService service, string bicepFile) - { - return GetBicepParamResources(service as RuntimeService, bicepFile); - } - - /// - /// Get the linked template path. - /// - public static string GetMetadataLinkPath(string parameterFile, string templateFile) - { - return TemplateLinkHelper.GetMetadataLinkPath(parameterFile, templateFile); - } - - /// - /// Evaluate NSG rules. - /// - public static INetworkSecurityGroupEvaluator GetNetworkSecurityGroup(PSObject[] securityRules) - { - var builder = new NetworkSecurityGroupEvaluator(); - builder.With(securityRules); - return builder; - } - - /// - /// Get resource type information. - /// - public static ResourceProviderType[] GetResourceType(string providerNamespace, string resourceType) - { - var resourceProviderHelper = new ResourceProviderHelper(); - return resourceProviderHelper.GetResourceType(providerNamespace, resourceType); - } - - /// - /// Get the last element in the sub-resource name by splitting the name by '/' separator. - /// - /// The sub-resource name to split. - /// The name of the sub-resource. - public static string GetSubResourceName(string resourceName) - { - if (string.IsNullOrEmpty(resourceName)) - return resourceName; - - var parts = resourceName.Split('/'); - return parts[parts.Length - 1]; - } - - #region Helper methods - - private static PSObject[] GetTemplateResources(string templateFile, string parameterFile, PipelineContext context) - { - var helper = new TemplateHelper( - context - ); - return helper.ProcessTemplate(templateFile, parameterFile, out _); - } - - private static bool IsBicep(string path) - { - return Path.GetExtension(path) == ".bicep"; - } - - private static PSObject[] GetBicepResources(RuntimeService service, string templateFile, string parameterFile) - { - var context = GetContext(service); - var bicep = new BicepHelper( - context, - service - ); - return bicep.ProcessFile(templateFile, parameterFile); - } - - private static PSObject[] GetBicepParamResources(RuntimeService service, string parameterFile) - { - var context = GetContext(service); - var bicep = new BicepHelper( - context, - service - ); - return bicep.ProcessParamFile(parameterFile); - } - - private static PipelineContext GetContext(RuntimeService service) - { - PSCmdlet commandRuntime = null; - - var option = service.ToPSRuleOption(); - var context = new PipelineContext(option, commandRuntime != null ? new PSPipelineWriter(option, commandRuntime) : null); - return context; - } - - #endregion Helper methods + /// Create a singleton context for running within PSRule. + /// + public static IRuntimeService CreateService(string minimum, int timeout) + { + return new RuntimeService(minimum, timeout); + } + + /// + /// Get information about the installed Bicep version. + /// + public static string GetBicepVersion(IService service) + { + var s = service as RuntimeService; + var context = GetContext(s); + var bicep = new BicepHelper( + context, + s + ); + return bicep.Version; + } + + /// + /// Parse and reformat the expression by removing whitespace. + /// + public static string CompressExpression(string expression) + { + return !IsTemplateExpression(expression) ? expression : ExpressionParser.Parse(expression).AsString(); + } + + /// + /// Look for literals and variable usage. + /// + public static bool HasLiteralValue(string expression) + { + return !IsTemplateExpression(expression) || + TokenStreamValidator.HasLiteralValue(ExpressionParser.Parse(expression)); + } + + /// + /// Get the name of parameters that would be assigned as values. + /// + internal static string[] GetParameterTokenValue(string expression) + { + return !IsTemplateExpression(expression) + ? Array.Empty() + : TokenStreamValidator.GetParameterTokenValue(ExpressionParser.Parse(expression)); + } + + /// + /// Returns true if an expression contains a call to the listKeys function. + /// + internal static bool UsesListKeysFunction(string expression) + { + return IsTemplateExpression(expression) && + TokenStreamValidator.UsesListKeysFunction(ExpressionParser.Parse(expression)); + } + + /// + /// Returns true if an expression contains a call to the reference function. + /// + internal static bool UsesReferenceFunction(string expression) + { + return IsTemplateExpression(expression) && + TokenStreamValidator.UsesReferenceFunction(ExpressionParser.Parse(expression)); + } + + /// + /// Checks if the value of the expression is secure, whether by using secure parameters, references to KeyVault, or the ListKeys function. + /// + public static bool HasSecureValue(string expression, string[] secureParameters) + { + if ((!string.IsNullOrEmpty(expression) && expression.StartsWith("{{Secret", StringComparison.OrdinalIgnoreCase)) || + UsesListKeysFunction(expression) || + UsesReferenceFunction(expression)) + return true; + + var parameterNamesInExpression = GetParameterTokenValue(expression); + + return parameterNamesInExpression != null && + parameterNamesInExpression.Length > 0 && + parameterNamesInExpression.Intersect(secureParameters, StringComparer.OrdinalIgnoreCase).Count() == parameterNamesInExpression.Length; + } + + /// + /// Check it the string is an expression. + /// + public static bool IsTemplateExpression(string expression) + { + return ExpressionParser.IsExpression(expression); + } + + /// + /// Expand resources from a parameter file and linked template/ bicep files. + /// + public static PSObject[] GetResources(IService service, string parameterFile) + { + var context = GetContext(service as RuntimeService); + var linkHelper = new TemplateLinkHelper(context, PSRuleOption.GetWorkingPath(), true); + var link = linkHelper.ProcessParameterFile(parameterFile); + if (link == null) + return null; + + return IsBicep(link.TemplateFile) ? + GetBicepResources(service as RuntimeService, link.TemplateFile, link.ParameterFile) : + GetTemplateResources(link.TemplateFile, link.ParameterFile, context); } + + /// + /// Expand resources from a bicep file. + /// + public static PSObject[] GetBicepResources(IService service, string bicepFile) + { + return GetBicepResources(service as RuntimeService, bicepFile, null); + } + + /// + /// Expand resources from a bicep param file. + /// + public static PSObject[] GetBicepParamResources(IService service, string bicepFile) + { + return GetBicepParamResources(service as RuntimeService, bicepFile); + } + + /// + /// Get the linked template path. + /// + public static string GetMetadataLinkPath(string parameterFile, string templateFile) + { + return TemplateLinkHelper.GetMetadataLinkPath(parameterFile, templateFile); + } + + /// + /// Evaluate NSG rules. + /// + public static INetworkSecurityGroupEvaluator GetNetworkSecurityGroup(PSObject[] securityRules) + { + var builder = new NetworkSecurityGroupEvaluator(); + builder.With(securityRules); + return builder; + } + + /// + /// Get resource type information. + /// + public static ResourceProviderType[] GetResourceType(string providerNamespace, string resourceType) + { + var resourceProviderHelper = new ResourceProviderHelper(); + return resourceProviderHelper.GetResourceType(providerNamespace, resourceType); + } + + /// + /// Get the last element in the sub-resource name by splitting the name by '/' separator. + /// + /// The sub-resource name to split. + /// The name of the sub-resource. + public static string GetSubResourceName(string resourceName) + { + if (string.IsNullOrEmpty(resourceName)) + return resourceName; + + var parts = resourceName.Split('/'); + return parts[parts.Length - 1]; + } + + #region Helper methods + + private static PSObject[] GetTemplateResources(string templateFile, string parameterFile, PipelineContext context) + { + var helper = new TemplateHelper( + context + ); + return helper.ProcessTemplate(templateFile, parameterFile, out _); + } + + private static bool IsBicep(string path) + { + return Path.GetExtension(path) == ".bicep"; + } + + private static PSObject[] GetBicepResources(RuntimeService service, string templateFile, string parameterFile) + { + var context = GetContext(service); + var bicep = new BicepHelper( + context, + service + ); + return bicep.ProcessFile(templateFile, parameterFile); + } + + private static PSObject[] GetBicepParamResources(RuntimeService service, string parameterFile) + { + var context = GetContext(service); + var bicep = new BicepHelper( + context, + service + ); + return bicep.ProcessParamFile(parameterFile); + } + + private static PipelineContext GetContext(RuntimeService service) + { + PSCmdlet commandRuntime = null; + + var option = service.ToPSRuleOption(); + var context = new PipelineContext(option, commandRuntime != null ? new PSPipelineWriter(option, commandRuntime) : null); + return context; + } + + #endregion Helper methods } diff --git a/src/PSRule.Rules.Azure/Runtime/RuntimeService.cs b/src/PSRule.Rules.Azure/Runtime/RuntimeService.cs index b56303ae3ea..5d909d52865 100644 --- a/src/PSRule.Rules.Azure/Runtime/RuntimeService.cs +++ b/src/PSRule.Rules.Azure/Runtime/RuntimeService.cs @@ -8,166 +8,165 @@ #nullable enable -namespace PSRule.Rules.Azure.Runtime +namespace PSRule.Rules.Azure.Runtime; + +/// +/// A singleton context when running is PSRule. +/// +internal sealed class RuntimeService : IRuntimeService { + private const int BICEP_TIMEOUT_MIN = 1; + private const int BICEP_TIMEOUT_MAX = 120; + + private bool _Disposed; + private HashSet? _AllowedLocations; + private DeploymentOption? _AzureDeployment; + private ResourceGroupOption? _AzureResourceGroup; + private SubscriptionOption? _AzureSubscription; + private TenantOption? _AzureTenant; + private ManagementGroupOption? _AzureManagementGroup; + private ParameterDefaultsOption? _ParameterDefaults; + /// - /// A singleton context when running is PSRule. + /// Create a runtime service. /// - internal sealed class RuntimeService : IRuntimeService + /// The minimum version of Bicep. + /// The timeout in seconds for expansion. + public RuntimeService(string minimum, int timeout) { - private const int BICEP_TIMEOUT_MIN = 1; - private const int BICEP_TIMEOUT_MAX = 120; - - private bool _Disposed; - private HashSet? _AllowedLocations; - private DeploymentOption? _AzureDeployment; - private ResourceGroupOption? _AzureResourceGroup; - private SubscriptionOption? _AzureSubscription; - private TenantOption? _AzureTenant; - private ManagementGroupOption? _AzureManagementGroup; - private ParameterDefaultsOption? _ParameterDefaults; - - /// - /// Create a runtime service. - /// - /// The minimum version of Bicep. - /// The timeout in seconds for expansion. - public RuntimeService(string minimum, int timeout) - { - Minimum = minimum; - Timeout = timeout < BICEP_TIMEOUT_MIN ? BICEP_TIMEOUT_MIN : timeout; - if (Timeout > BICEP_TIMEOUT_MAX) - Timeout = BICEP_TIMEOUT_MAX; - } - - /// - public int Timeout { get; } + Minimum = minimum; + Timeout = timeout < BICEP_TIMEOUT_MIN ? BICEP_TIMEOUT_MIN : timeout; + if (Timeout > BICEP_TIMEOUT_MAX) + Timeout = BICEP_TIMEOUT_MAX; + } - /// - public string Minimum { get; } + /// + public int Timeout { get; } - /// - public BicepHelper.BicepInfo? Bicep { get; internal set; } + /// + public string Minimum { get; } - /// - public void WithAllowedLocations(string[] locations) - { - if (locations == null || locations.Length == 0) - return; + /// + public BicepHelper.BicepInfo? Bicep { get; internal set; } - _AllowedLocations = new HashSet(LocationHelper.Comparer); - for (var i = 0; i < locations.Length; i++) - { - if (!string.IsNullOrEmpty(locations[i])) - _AllowedLocations.Add(locations[i]); - } - } + /// + public void WithAllowedLocations(string[] locations) + { + if (locations == null || locations.Length == 0) + return; - /// - public bool IsAllowedLocation(string location) + _AllowedLocations = new HashSet(LocationHelper.Comparer); + for (var i = 0; i < locations.Length; i++) { - return location == null || - _AllowedLocations == null || - _AllowedLocations.Count == 0 || - _AllowedLocations.Contains(location); + if (!string.IsNullOrEmpty(locations[i])) + _AllowedLocations.Add(locations[i]); } + } - /// - public void WithAzureDeployment(PSObject? pso) - { - if (pso == null) - return; + /// + public bool IsAllowedLocation(string location) + { + return location == null || + _AllowedLocations == null || + _AllowedLocations.Count == 0 || + _AllowedLocations.Contains(location); + } - _AzureDeployment = DeploymentOption.FromHashtable(pso.ToHashtable()); - } + /// + public void WithAzureDeployment(PSObject? pso) + { + if (pso == null) + return; - /// - public void WithAzureResourceGroup(PSObject? pso) - { - if (pso == null) - return; + _AzureDeployment = DeploymentOption.FromHashtable(pso.ToHashtable()); + } - _AzureResourceGroup = ResourceGroupOption.FromHashtable(pso.ToHashtable()); - } + /// + public void WithAzureResourceGroup(PSObject? pso) + { + if (pso == null) + return; - /// - public void WithAzureSubscription(PSObject? pso) - { - if (pso == null) - return; + _AzureResourceGroup = ResourceGroupOption.FromHashtable(pso.ToHashtable()); + } - _AzureSubscription = SubscriptionOption.FromHashtable(pso.ToHashtable()); - } + /// + public void WithAzureSubscription(PSObject? pso) + { + if (pso == null) + return; - /// - public void WithAzureTenant(PSObject? pso) - { - if (pso == null) - return; + _AzureSubscription = SubscriptionOption.FromHashtable(pso.ToHashtable()); + } - _AzureTenant = TenantOption.FromHashtable(pso.ToHashtable()); - } + /// + public void WithAzureTenant(PSObject? pso) + { + if (pso == null) + return; - /// - public void WithAzureManagementGroup(PSObject? pso) - { - if (pso == null) - return; + _AzureTenant = TenantOption.FromHashtable(pso.ToHashtable()); + } - _AzureManagementGroup = ManagementGroupOption.FromHashtable(pso.ToHashtable()); - } + /// + public void WithAzureManagementGroup(PSObject? pso) + { + if (pso == null) + return; - /// - public void WithParameterDefaults(PSObject? pso) - { - if (pso == null) - return; + _AzureManagementGroup = ManagementGroupOption.FromHashtable(pso.ToHashtable()); + } - _ParameterDefaults = ParameterDefaultsOption.FromHashtable(pso.ToHashtable()); - } + /// + public void WithParameterDefaults(PSObject? pso) + { + if (pso == null) + return; - /// - /// Get options for the current runtime state. - /// - internal PSRuleOption ToPSRuleOption() - { - var option = PSRuleOption.FromFileOrDefault(PSRuleOption.GetWorkingPath()); - - option.Configuration.Deployment = DeploymentOption.Combine(_AzureDeployment, option.Configuration.Deployment); - option.Configuration.ResourceGroup = ResourceGroupOption.Combine(_AzureResourceGroup, option.Configuration.ResourceGroup); - option.Configuration.Subscription = SubscriptionOption.Combine(_AzureSubscription, option.Configuration.Subscription); - option.Configuration.Tenant = TenantOption.Combine(_AzureTenant, option.Configuration.Tenant); - option.Configuration.ManagementGroup = ManagementGroupOption.Combine(_AzureManagementGroup, option.Configuration.ManagementGroup); - option.Configuration.ParameterDefaults = ParameterDefaultsOption.Combine(_ParameterDefaults, option.Configuration.ParameterDefaults); - option.Configuration.BicepFileExpansionTimeout = Timeout; - option.Configuration.BicepMinimumVersion = Minimum; - - return option; - } + _ParameterDefaults = ParameterDefaultsOption.FromHashtable(pso.ToHashtable()); + } - #region IDisposable + /// + /// Get options for the current runtime state. + /// + internal PSRuleOption ToPSRuleOption() + { + var option = PSRuleOption.FromFileOrDefault(PSRuleOption.GetWorkingPath()); + + option.Configuration.Deployment = DeploymentOption.Combine(_AzureDeployment, option.Configuration.Deployment); + option.Configuration.ResourceGroup = ResourceGroupOption.Combine(_AzureResourceGroup, option.Configuration.ResourceGroup); + option.Configuration.Subscription = SubscriptionOption.Combine(_AzureSubscription, option.Configuration.Subscription); + option.Configuration.Tenant = TenantOption.Combine(_AzureTenant, option.Configuration.Tenant); + option.Configuration.ManagementGroup = ManagementGroupOption.Combine(_AzureManagementGroup, option.Configuration.ManagementGroup); + option.Configuration.ParameterDefaults = ParameterDefaultsOption.Combine(_ParameterDefaults, option.Configuration.ParameterDefaults); + option.Configuration.BicepFileExpansionTimeout = Timeout; + option.Configuration.BicepMinimumVersion = Minimum; + + return option; + } + + #region IDisposable - private void Dispose(bool disposing) + private void Dispose(bool disposing) + { + if (!_Disposed) { - if (!_Disposed) + if (disposing) { - if (disposing) - { - Bicep = null; - } - _Disposed = true; + Bicep = null; } + _Disposed = true; } + } - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - System.GC.SuppressFinalize(this); - } - - #endregion IDisposable + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + System.GC.SuppressFinalize(this); } + + #endregion IDisposable } #nullable restore diff --git a/src/PSRule.Rules.Azure/TaskExtensions.cs b/src/PSRule.Rules.Azure/TaskExtensions.cs new file mode 100644 index 00000000000..db9806abfd1 --- /dev/null +++ b/src/PSRule.Rules.Azure/TaskExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; + +namespace PSRule.Rules.Azure; + +/// +/// Helpers for instances. +/// +internal static class TaskExtensions +{ + /// + /// Call the Dispose method all the specified tasks. + /// + public static void DisposeAll(this Task[] tasks) + { + for (var i = 0; tasks != null && i < tasks.Length; i++) + tasks[i].Dispose(); + } +} diff --git a/src/PSRule.Rules.Azure/TypeExtensions.cs b/src/PSRule.Rules.Azure/TypeExtensions.cs new file mode 100644 index 00000000000..90c15cf3ebd --- /dev/null +++ b/src/PSRule.Rules.Azure/TypeExtensions.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Reflection; + +namespace PSRule.Rules.Azure; + +/// +/// Extensions for types. +/// +internal static class TypeExtensions +{ + public static bool TryGetValue(this Type type, object instance, string propertyOrField, out object value) + { + value = null; + var property = type.GetProperty(propertyOrField, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.Public); + if (property != null) + { + value = property.GetValue(instance); + return true; + } + + // Try field + var field = type.GetField(propertyOrField, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.GetField | BindingFlags.Public); + if (field != null) + { + value = field.GetValue(instance); + return true; + } + return false; + } +} diff --git a/tests/PSRule.Rules.Azure.Tests/ResourceHelperTests.cs b/tests/PSRule.Rules.Azure.Tests/ResourceHelperTests.cs index 14fda895781..bd72ed562e8 100644 --- a/tests/PSRule.Rules.Azure.Tests/ResourceHelperTests.cs +++ b/tests/PSRule.Rules.Azure.Tests/ResourceHelperTests.cs @@ -173,4 +173,3 @@ public void ResourceIdDepth(string[] resourceType, string[] resourceName, int de } #nullable restore -