From d82c70484503077f7a7fe37770ccd7fed3fb0a3d Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sat, 21 Dec 2024 13:17:24 +1000 Subject: [PATCH] Refactoring conventions (#2679) --- .vscode/settings.json | 1 + .../Baselines/BaselineExtensions.cs | 61 +++ .../Conventions/ConventionComparer.cs | 10 +- .../Conventions/ConventionExtensions.cs | 62 +++ .../ModuleConfigs/ModuleConfigExtensions.cs | 33 ++ .../Definitions/Rules/RuleExtensions.cs | 231 ++++++++ .../Rules/SeverityLevelExtensions.cs | 6 + .../Selectors/SelectorExtensions.cs | 76 +++ .../SuppressionGroupExtentions.cs} | 24 +- src/PSRule/Host/HostHelper.cs | 510 +++--------------- src/PSRule/Pipeline/GetRuleHelpPipeline.cs | 2 +- src/PSRule/Pipeline/GetRulePipeline.cs | 2 +- .../Pipeline/IResourceCacheExtensions.cs | 33 ++ src/PSRule/Pipeline/InvokeRulePipeline.cs | 2 +- src/PSRule/Pipeline/PipelineContext.cs | 12 +- src/PSRule/Pipeline/ResourceCache.cs | 11 + src/PSRule/Pipeline/RulePipeline.cs | 2 +- src/PSRule/Runtime/LanguageScriptBlock.cs | 30 +- src/PSRule/Runtime/RunspaceContext.cs | 47 +- tests/PSRule.Tests/ConventionTests.cs | 14 +- tests/PSRule.Tests/JsonRulesTests.cs | 178 ++++++ tests/PSRule.Tests/ResourceValidatorTests.cs | 30 +- tests/PSRule.Tests/SuppressionFilterTests.cs | 2 +- .../{RulesTests.cs => YamlRulesTests.cs} | 182 ++----- 24 files changed, 886 insertions(+), 675 deletions(-) create mode 100644 src/PSRule/Definitions/Baselines/BaselineExtensions.cs create mode 100644 src/PSRule/Definitions/Conventions/ConventionExtensions.cs create mode 100644 src/PSRule/Definitions/ModuleConfigs/ModuleConfigExtensions.cs create mode 100644 src/PSRule/Definitions/Rules/RuleExtensions.cs create mode 100644 src/PSRule/Definitions/Selectors/SelectorExtensions.cs rename src/PSRule/Definitions/{IResourceExtensions.cs => SuppressionGroups/SuppressionGroupExtentions.cs} (53%) create mode 100644 src/PSRule/Pipeline/IResourceCacheExtensions.cs create mode 100644 tests/PSRule.Tests/JsonRulesTests.cs rename tests/PSRule.Tests/{RulesTests.cs => YamlRulesTests.cs} (62%) diff --git a/.vscode/settings.json b/.vscode/settings.json index 9f0f157542..e8055205fd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -90,6 +90,7 @@ "cSpell.words": [ "APPSERVICEMININSTANCECOUNT", "Arity", + "Authenticode", "CLIXML", "cmdlet", "cmdlets", diff --git a/src/PSRule/Definitions/Baselines/BaselineExtensions.cs b/src/PSRule/Definitions/Baselines/BaselineExtensions.cs new file mode 100644 index 0000000000..7d8afdbe18 --- /dev/null +++ b/src/PSRule/Definitions/Baselines/BaselineExtensions.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using PSRule.Runtime; + +namespace PSRule.Definitions.Baselines; + +#nullable enable + +/// +/// Extensions methods for baselines. +/// +internal static class BaselineExtensions +{ + /// + /// Convert any baseline language blocks into resources. + /// + public static Baseline[] ToBaselineV1(this IEnumerable blocks, RunspaceContext context) + { + if (blocks == null) return []; + + // Index baselines by BaselineId + var results = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var block in blocks.OfType().ToArray()) + { + context.EnterLanguageScope(block.Source); + try + { + // Ignore baselines that don't match + if (!Match(context, block)) + continue; + + if (!results.ContainsKey(block.BaselineId)) + results[block.BaselineId] = block; + + } + finally + { + context.ExitLanguageScope(block.Source); + } + } + return [.. results.Values]; + } + + private static bool Match(RunspaceContext context, Baseline resource) + { + try + { + context.EnterLanguageScope(resource.Source); + var filter = context.LanguageScope!.GetFilter(ResourceKind.Baseline); + return filter == null || filter.Match(resource); + } + finally + { + context.ExitLanguageScope(resource.Source); + } + } +} + +#nullable restore diff --git a/src/PSRule/Definitions/Conventions/ConventionComparer.cs b/src/PSRule/Definitions/Conventions/ConventionComparer.cs index a92125ccdc..73fbea2eff 100644 --- a/src/PSRule/Definitions/Conventions/ConventionComparer.cs +++ b/src/PSRule/Definitions/Conventions/ConventionComparer.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using PSRule.Runtime; - namespace PSRule.Definitions.Conventions; /// @@ -10,15 +8,15 @@ namespace PSRule.Definitions.Conventions; /// internal sealed class ConventionComparer : IComparer { - private readonly RunspaceContext _Context; + private readonly Func _GetOrder; - internal ConventionComparer(RunspaceContext context) + internal ConventionComparer(Func getOrder) { - _Context = context; + _GetOrder = getOrder; } public int Compare(IConventionV1 x, IConventionV1 y) { - return _Context.Pipeline.GetConventionOrder(x) - _Context.Pipeline.GetConventionOrder(y); + return _GetOrder(x) - _GetOrder(y); } } diff --git a/src/PSRule/Definitions/Conventions/ConventionExtensions.cs b/src/PSRule/Definitions/Conventions/ConventionExtensions.cs new file mode 100644 index 0000000000..cd45cfcd27 --- /dev/null +++ b/src/PSRule/Definitions/Conventions/ConventionExtensions.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using PSRule.Runtime; + +namespace PSRule.Definitions.Conventions; + +#nullable enable + +/// +/// Extensions for conventions. +/// +internal static class ConventionExtensions +{ + /// + /// Convert any convention language blocks into resources. + /// + public static IConventionV1[] ToConventionsV1(this IEnumerable blocks, RunspaceContext context) + { + if (blocks == null) return []; + + // Index by Id. + var index = new HashSet(StringComparer.OrdinalIgnoreCase); + var results = new List(); + + foreach (var block in blocks.OfType().ToArray()) + { + context.EnterLanguageScope(block.Source); + try + { + // Ignore blocks that don't match. + if (!Match(context, block)) + continue; + + if (!index.Contains(block.Id.Value)) + results.Add(block); + + } + finally + { + context.ExitLanguageScope(block.Source); + } + } + return [.. results]; + } + + private static bool Match(RunspaceContext context, ScriptBlockConvention block) + { + try + { + context.EnterLanguageScope(block.Source); + var filter = context.LanguageScope.GetFilter(ResourceKind.Convention); + return filter == null || filter.Match(block); + } + finally + { + context.ExitLanguageScope(block.Source); + } + } +} + +#nullable restore diff --git a/src/PSRule/Definitions/ModuleConfigs/ModuleConfigExtensions.cs b/src/PSRule/Definitions/ModuleConfigs/ModuleConfigExtensions.cs new file mode 100644 index 0000000000..b1a4d58039 --- /dev/null +++ b/src/PSRule/Definitions/ModuleConfigs/ModuleConfigExtensions.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using PSRule.Runtime; + +namespace PSRule.Definitions.ModuleConfigs; + +#nullable enable + +/// +/// Extensions methods for module configurations. +/// +internal static class ModuleConfigExtensions +{ + /// + /// Convert any selector language blocks into resources. + /// + public static ModuleConfigV1[] ToModuleConfigV1(this IEnumerable blocks, RunspaceContext context) + { + if (blocks == null) return []; + + // Index configurations by Name. + var results = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var block in blocks.OfType().ToArray()) + { + if (!results.ContainsKey(block.Name)) + results[block.Name] = block; + } + return [.. results.Values]; + } +} + +#nullable restore diff --git a/src/PSRule/Definitions/Rules/RuleExtensions.cs b/src/PSRule/Definitions/Rules/RuleExtensions.cs new file mode 100644 index 0000000000..a73a41a82d --- /dev/null +++ b/src/PSRule/Definitions/Rules/RuleExtensions.cs @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections; +using PSRule.Host; +using PSRule.Rules; +using PSRule.Runtime; + +namespace PSRule.Definitions.Rules; + +#nullable enable + +/// +/// Extensions methods for rules. +/// +internal static class RuleExtensions +{ + /// + /// Convert any rule language blocks to . + /// + public static IRuleV1[] ToRuleV1(this IEnumerable blocks, RunspaceContext context) + { + if (blocks == null) return []; + + var results = new List(); + + foreach (var block in blocks.OfType()) + { + results.Add(block); + } + + // Process from YAML/ JSON + foreach (var block in blocks.OfType()) + { + var ruleName = block.Name; + + context.EnterLanguageScope(block.Source); + context.LanguageScope!.TryGetOverride(block.Id, out var propertyOverride); + try + { + var info = GetRuleHelpInfo(context, block) ?? new RuleHelpInfo( + ruleName, + ruleName, + block.Source.Module, + synopsis: new InfoString(block.Synopsis) + ); + MergeAnnotations(info, block.Metadata); + + results.Add(new RuleBlock + ( + source: block.Source, + id: block.Id, + @ref: block.Ref, + @default: new RuleProperties + { + Level = block.Level + }, + @override: propertyOverride, + info: info, + condition: new RuleVisitor(context, block.Id, block.Source, block.Spec), + alias: block.Alias, + tag: block.Metadata.Tags, + dependsOn: null, // No support for DependsOn yet + configuration: null, // No support for rule configuration use module or workspace config + extent: null, + flags: block.Flags, + labels: block.Metadata.Labels + )); + } + finally + { + context.ExitLanguageScope(block.Source); + } + } + return [.. results]; + } + + + public static RuleHelpInfo[] ToRuleHelp(this IEnumerable blocks, RunspaceContext context) + { + if (blocks == null) return []; + + // Index rules by RuleId + var results = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var block in blocks.OfType()) + { + context.EnterLanguageScope(block.Source); + try + { + // Ignore rule blocks that don't match + if (!Match(context, block)) + continue; + + if (!results.ContainsKey(block.Id.Value)) + results[block.Id.Value] = block.Info; + } + finally + { + context.ExitLanguageScope(block.Source); + } + + } + return [.. results.Values]; + } + + public static DependencyTargetCollection ToRuleDependencyTargetCollection(this IEnumerable blocks, RunspaceContext context, bool skipDuplicateName) + { + // Index rules by RuleId + var results = new DependencyTargetCollection(); + if (blocks == null) return results; + + // Keep track of rule names and ids that have been added + var knownRuleNames = new HashSet(StringComparer.OrdinalIgnoreCase); + var knownRuleIds = new HashSet(ResourceIdEqualityComparer.Default); + + // Process from PowerShell + foreach (var block in blocks.OfType()) + { + if (knownRuleIds.ContainsIds(block.Id, block.Ref, block.Alias, out var duplicateId)) + { + context.DuplicateResourceId(block.Id, duplicateId.Value); + continue; + } + if (knownRuleNames.ContainsNames(block.Id, block.Ref, block.Alias, out var duplicateName)) + { + context.WarnDuplicateRuleName(duplicateName); + if (skipDuplicateName) + continue; + } + + results.TryAdd(block); + knownRuleNames.AddNames(block.Id, block.Ref, block.Alias); + knownRuleIds.AddIds(block.Id, block.Ref, block.Alias); + } + + // Process from YAML/ JSON + foreach (var block in blocks.OfType()) + { + var ruleName = block.Name; + if (knownRuleIds.ContainsIds(block.Id, block.Ref, block.Alias, out var duplicateId)) + { + context.DuplicateResourceId(block.Id, duplicateId.Value); + continue; + } + if (knownRuleNames.ContainsNames(block.Id, block.Ref, block.Alias, out var duplicateName)) + { + context.WarnDuplicateRuleName(duplicateName); + if (skipDuplicateName) + continue; + } + + context.EnterLanguageScope(block.Source); + context.LanguageScope.TryGetOverride(block.Id, out var propertyOverride); + try + { + var info = GetRuleHelpInfo(context, block) ?? new RuleHelpInfo( + ruleName, + ruleName, + block.Source.Module, + synopsis: new InfoString(block.Synopsis) + ); + MergeAnnotations(info, block.Metadata); + + results.TryAdd(new RuleBlock + ( + source: block.Source, + id: block.Id, + @ref: block.Ref, + @default: new RuleProperties + { + Level = block.Level + }, + @override: propertyOverride, + info: info, + condition: new RuleVisitor(context, block.Id, block.Source, block.Spec), + alias: block.Alias, + tag: block.Metadata.Tags, + dependsOn: null, // No support for DependsOn yet + configuration: null, // No support for rule configuration use module or workspace config + extent: block.Extent, + flags: block.Flags, + labels: block.Metadata.Labels + )); + knownRuleNames.AddNames(block.Id, block.Ref, block.Alias); + knownRuleIds.AddIds(block.Id, block.Ref, block.Alias); + } + finally + { + context.ExitLanguageScope(block.Source); + } + } + return results; + } + + private static void MergeAnnotations(RuleHelpInfo info, ResourceMetadata metadata) + { + if (info == null || metadata == null || metadata.Annotations == null || metadata.Annotations.Count == 0) + return; + + info.Annotations ??= new Hashtable(StringComparer.OrdinalIgnoreCase); + foreach (var kv in metadata.Annotations) + { + if (!info.Annotations.ContainsKey(kv.Key)) + info.Annotations[kv.Key] = kv.Value; + } + if (!info.HasOnlineHelp()) + info.SetOnlineHelpUrl(metadata.Link); + } + + private static bool Match(RunspaceContext context, RuleBlock resource) + { + try + { + context.EnterLanguageScope(resource.Source); + var filter = context.LanguageScope!.GetFilter(ResourceKind.Rule); + return filter == null || filter.Match(resource); + } + finally + { + context.ExitLanguageScope(resource.Source); + } + } + + private static RuleHelpInfo GetRuleHelpInfo(RunspaceContext context, IRuleV1 rule) + { + return HostHelper.GetRuleHelpInfo(context, rule.Name, rule.Synopsis, rule.Info.DisplayName, rule.Info.Description, rule.Recommendation); + } +} + +#nullable restore diff --git a/src/PSRule/Definitions/Rules/SeverityLevelExtensions.cs b/src/PSRule/Definitions/Rules/SeverityLevelExtensions.cs index 84cc7ae613..8f615f928b 100644 --- a/src/PSRule/Definitions/Rules/SeverityLevelExtensions.cs +++ b/src/PSRule/Definitions/Rules/SeverityLevelExtensions.cs @@ -3,8 +3,14 @@ namespace PSRule.Definitions.Rules; +/// +/// Extensions for . +/// internal static class SeverityLevelExtensions { + /// + /// Get the worst case , such that Error > Warning > Information > None. + /// public static SeverityLevel GetWorstCase(this SeverityLevel o1, SeverityLevel o2) { if (o2 == SeverityLevel.Error || o1 == SeverityLevel.Error) diff --git a/src/PSRule/Definitions/Selectors/SelectorExtensions.cs b/src/PSRule/Definitions/Selectors/SelectorExtensions.cs new file mode 100644 index 0000000000..a72e9b16d9 --- /dev/null +++ b/src/PSRule/Definitions/Selectors/SelectorExtensions.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using PSRule.Runtime; + +namespace PSRule.Definitions.Selectors; + +#nullable enable + +/// +/// Extensions methods for selectors. +/// +internal static class SelectorExtensions +{ + /// + /// Convert a selector into a selector visitor. + /// + /// The selector resource. + /// A valid runspace context. + /// An instance of a . + public static SelectorVisitor ToSelectorVisitor(this SelectorV1 resource, RunspaceContext runspaceContext) + { + return new SelectorVisitor( + runspaceContext, + resource.Id, + resource.Source, + resource.Spec.If + ); + } + + /// + /// Convert any selector language blocks into resources. + /// + public static SelectorV1[] ToSelectorV1(this IEnumerable blocks, RunspaceContext context) + { + if (blocks == null) return []; + + // Index selectors by Id. + var results = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var block in blocks.OfType().ToArray()) + { + context.EnterLanguageScope(block.Source); + try + { + // Ignore selectors that don't match. + if (!Match(context, block)) + continue; + + if (!results.ContainsKey(block.Id.Value)) + results[block.Id.Value] = block; + } + finally + { + context.ExitLanguageScope(block.Source); + } + } + return [.. results.Values]; + } + + private static bool Match(RunspaceContext context, SelectorV1 resource) + { + try + { + context.EnterLanguageScope(resource.Source); + var filter = context.LanguageScope!.GetFilter(ResourceKind.Selector); + return filter == null || filter.Match(resource); + } + finally + { + context.ExitLanguageScope(resource.Source); + } + } +} + +#nullable restore diff --git a/src/PSRule/Definitions/IResourceExtensions.cs b/src/PSRule/Definitions/SuppressionGroups/SuppressionGroupExtentions.cs similarity index 53% rename from src/PSRule/Definitions/IResourceExtensions.cs rename to src/PSRule/Definitions/SuppressionGroups/SuppressionGroupExtentions.cs index ea7a5d8f0e..510a4c19c3 100644 --- a/src/PSRule/Definitions/IResourceExtensions.cs +++ b/src/PSRule/Definitions/SuppressionGroups/SuppressionGroupExtentions.cs @@ -1,18 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using PSRule.Definitions.Selectors; -using PSRule.Definitions.SuppressionGroups; using PSRule.Runtime; -namespace PSRule.Definitions; +namespace PSRule.Definitions.SuppressionGroups; #nullable enable /// -/// Extensions for resource types. +/// Extensions methods for suppression groups. /// -internal static class IResourceExtensions +internal static class SuppressionGroupExtensions { /// /// Convert a suppression group into a suppression group visitor. @@ -30,22 +28,6 @@ public static SuppressionGroupVisitor ToSuppressionGroupVisitor(this Suppression info: resource.Info ); } - - /// - /// Converts a selector into a selector visitor. - /// - /// The selector resource. - /// A valid runspace context. - /// An instance of a . - public static SelectorVisitor ToSelectorVisitor(this SelectorV1 resource, RunspaceContext runspaceContext) - { - return new SelectorVisitor( - runspaceContext, - resource.Id, - resource.Source, - resource.Spec.If - ); - } } #nullable restore diff --git a/src/PSRule/Host/HostHelper.cs b/src/PSRule/Host/HostHelper.cs index 090a521c0e..9bc33c475f 100644 --- a/src/PSRule/Host/HostHelper.cs +++ b/src/PSRule/Host/HostHelper.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Collections; using System.Collections.ObjectModel; using System.Management.Automation; using System.Text; @@ -10,7 +9,6 @@ using PSRule.Converters.Yaml; using PSRule.Definitions; using PSRule.Definitions.Baselines; -using PSRule.Definitions.Conventions; using PSRule.Definitions.ModuleConfigs; using PSRule.Definitions.Rules; using PSRule.Definitions.Selectors; @@ -23,34 +21,40 @@ using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.Serialization.NodeDeserializers; -using Rule = PSRule.Rules.Rule; namespace PSRule.Host; +#nullable enable + internal static class HostHelper { private const string Markdown_Extension = ".md"; - internal static IRuleV1[] GetRule(Source[] source, RunspaceContext runspaceContext, bool includeDependencies) + internal static IRuleV1[] GetRule(RunspaceContext context, bool includeDependencies) { - var rules = ToRuleV1(GetLanguageBlock(runspaceContext, source), runspaceContext); - var builder = new DependencyGraphBuilder(runspaceContext, includeDependencies, includeDisabled: true); - builder.Include(rules, filter: (b) => Match(runspaceContext, b)); + var rules = context.Pipeline.ResourceCache.OfType(); + var blocks = rules.ToRuleDependencyTargetCollection(context, skipDuplicateName: false); + + var builder = new DependencyGraphBuilder(context, includeDependencies, includeDisabled: true); + builder.Include(blocks, filter: (b) => Match(context, b)); return builder.GetItems(); } - internal static RuleHelpInfo[] GetRuleHelp(Source[] source, RunspaceContext context) + internal static RuleHelpInfo[] GetRuleHelp(RunspaceContext context) { - return ToRuleHelp(ToRuleBlockV1(GetLanguageBlock(context, source), context, skipDuplicateName: true).GetAll(), context); + var rules = context.Pipeline.ResourceCache.OfType(); + var blocks = rules.ToRuleDependencyTargetCollection(context, skipDuplicateName: true); + + return blocks.GetAll().ToRuleHelp(context); } - internal static DependencyGraph GetRuleBlockGraph(Source[] source, RunspaceContext context) + internal static DependencyGraph GetRuleBlockGraph(RunspaceContext context) { - var blocks = GetLanguageBlock(context, source); - var rules = ToRuleBlockV1(blocks, context, skipDuplicateName: false); - Import(GetConventions(blocks, context), context); + var rules = context.Pipeline.ResourceCache.OfType(); + var blocks = rules.ToRuleDependencyTargetCollection(context, skipDuplicateName: false); + var builder = new DependencyGraphBuilder(context, includeDependencies: true, includeDisabled: false); - builder.Include(rules, filter: (b) => Match(context, b)); + builder.Include(blocks, filter: (b) => Match(context, b)); return builder.Build(); } @@ -67,12 +71,24 @@ internal static IEnumerable GetMetaResources(Source[] source, IResourceDis return results; } + /// + /// Get PS resources which are resource defined in PowerShell. + /// + internal static IEnumerable GetPSResources(Source[] source, RunspaceContext context) where T : ILanguageBlock + { + if (source == null || source.Length == 0) return []; + + var results = new List(); + results.AddRange(GetPSLanguageBlocks(context, source).OfType()); + return results; + } + /// /// Read YAML/JSON objects and return baselines. /// internal static IEnumerable GetBaseline(Source[] source, RunspaceContext context) { - return ToBaselineV1(GetMetaResources(source, context), context); + return GetMetaResources(source, context).ToBaselineV1(context); } /// @@ -80,7 +96,7 @@ internal static IEnumerable GetBaseline(Source[] source, RunspaceConte /// internal static IEnumerable GetModuleConfigForTests(Source[] source, RunspaceContext context) { - return ToModuleConfigV1(GetMetaResources(source, context), context); + return GetMetaResources(source, context).ToModuleConfigV1(context); } /// @@ -88,7 +104,7 @@ internal static IEnumerable GetModuleConfigForTests(Source[] sou /// internal static IEnumerable GetSelectorForTests(Source[] source, RunspaceContext context) { - return ToSelectorV1(GetMetaResources(source, context), context); + return GetMetaResources(source, context).ToSelectorV1(context); } /// @@ -145,17 +161,6 @@ internal static void UnblockFile(IPipelineWriter writer, string[] publisher, str } } - /// - /// Get all the language elements. - /// - private static ILanguageBlock[] GetLanguageBlock(RunspaceContext context, Source[] sources) - { - var results = new List(); - results.AddRange(GetPSLanguageBlocks(context, sources)); - results.AddRange(GetMetaResources(sources, context)); - return [.. results]; - } - /// /// Execute PowerShell script files to get language blocks. /// @@ -420,330 +425,12 @@ public static void InvokeRuleBlock(RunspaceContext context, RuleBlock ruleBlock, //} } - /// - /// Convert matching language blocks to rules. - /// - private static DependencyTargetCollection ToRuleV1(ILanguageBlock[] blocks, RunspaceContext runspaceContext) - { - // Index rules by RuleId - var results = new DependencyTargetCollection(); - - // Keep track of rule names and ids that have been added - var knownRuleNames = new HashSet(StringComparer.OrdinalIgnoreCase); - var knownRuleIds = new HashSet(ResourceIdEqualityComparer.Default); - - - foreach (var block in blocks.OfType()) - { - if (knownRuleIds.ContainsIds(block.Id, block.Ref, block.Alias, out var duplicateId)) - { - runspaceContext.DuplicateResourceId(block.Id, duplicateId.Value); - continue; - } - - if (knownRuleNames.ContainsNames(block.Id, block.Ref, block.Alias, out var duplicateName)) - runspaceContext.WarnDuplicateRuleName(duplicateName); - - results.TryAdd(new Rule - { - Id = block.Id, - Ref = block.Ref, - Alias = block.Alias, - Source = block.Source, - Tag = block.Tag, - Level = block.Level, - Info = block.Info, - DependsOn = block.DependsOn, - Flags = block.Flags, - Extent = block.Extent, - Labels = block.Labels, - }); - knownRuleNames.AddNames(block.Id, block.Ref, block.Alias); - knownRuleIds.AddIds(block.Id, block.Ref, block.Alias); - } - - foreach (var block in blocks.OfType()) - { - if (knownRuleIds.ContainsIds(block.Id, block.Ref, block.Alias, out var duplicateId)) - { - runspaceContext.DuplicateResourceId(block.Id, duplicateId.Value); - continue; - } - - if (knownRuleNames.ContainsNames(block.Id, block.Ref, block.Alias, out var duplicateName)) - runspaceContext.WarnDuplicateRuleName(duplicateName); - - runspaceContext.EnterLanguageScope(block.Source); - try - { - var info = GetRuleHelpInfo(runspaceContext, block); - results.TryAdd(new Rule - { - Id = block.Id, - Ref = block.Ref, - Alias = block.Alias, - Source = block.Source, - Tag = block.Metadata.Tags, - Level = block.Level, - Info = info, - DependsOn = null, // TODO: No support for DependsOn yet - Flags = block.Flags, - Extent = block.Extent, - Labels = block.Metadata.Labels, - }); - knownRuleNames.AddNames(block.Id, block.Ref, block.Alias); - knownRuleIds.AddIds(block.Id, block.Ref, block.Alias); - } - finally - { - runspaceContext.ExitLanguageScope(block.Source); - } - } - return results; - } - - private static DependencyTargetCollection ToRuleBlockV1(ILanguageBlock[] blocks, RunspaceContext context, bool skipDuplicateName) - { - // Index rules by RuleId - var results = new DependencyTargetCollection(); - - // Keep track of rule names and ids that have been added - var knownRuleNames = new HashSet(StringComparer.OrdinalIgnoreCase); - var knownRuleIds = new HashSet(ResourceIdEqualityComparer.Default); - - // Process from PowerShell - foreach (var block in blocks.OfType()) - { - if (knownRuleIds.ContainsIds(block.Id, block.Ref, block.Alias, out var duplicateId)) - { - context.DuplicateResourceId(block.Id, duplicateId.Value); - continue; - } - if (knownRuleNames.ContainsNames(block.Id, block.Ref, block.Alias, out var duplicateName)) - { - context.WarnDuplicateRuleName(duplicateName); - if (skipDuplicateName) - continue; - } - - results.TryAdd(block); - knownRuleNames.AddNames(block.Id, block.Ref, block.Alias); - knownRuleIds.AddIds(block.Id, block.Ref, block.Alias); - } - - // Process from YAML/ JSON - foreach (var block in blocks.OfType()) - { - var ruleName = block.Name; - if (knownRuleIds.ContainsIds(block.Id, block.Ref, block.Alias, out var duplicateId)) - { - context.DuplicateResourceId(block.Id, duplicateId.Value); - continue; - } - if (knownRuleNames.ContainsNames(block.Id, block.Ref, block.Alias, out var duplicateName)) - { - context.WarnDuplicateRuleName(duplicateName); - if (skipDuplicateName) - continue; - } - - context.EnterLanguageScope(block.Source); - context.LanguageScope.TryGetOverride(block.Id, out var propertyOverride); - try - { - var info = GetRuleHelpInfo(context, block) ?? new RuleHelpInfo( - ruleName, - ruleName, - block.Source.Module, - synopsis: new InfoString(block.Synopsis) - ); - MergeAnnotations(info, block.Metadata); - - results.TryAdd(new RuleBlock - ( - source: block.Source, - id: block.Id, - @ref: block.Ref, - @default: new RuleProperties - { - Level = block.Level - }, - @override: propertyOverride, - info: info, - condition: new RuleVisitor(context, block.Id, block.Source, block.Spec), - alias: block.Alias, - tag: block.Metadata.Tags, - dependsOn: null, // No support for DependsOn yet - configuration: null, // No support for rule configuration use module or workspace config - extent: null, - flags: block.Flags, - labels: block.Metadata.Labels - )); - knownRuleNames.AddNames(block.Id, block.Ref, block.Alias); - knownRuleIds.AddIds(block.Id, block.Ref, block.Alias); - } - finally - { - context.ExitLanguageScope(block.Source); - } - } - return results; - } - - private static void MergeAnnotations(RuleHelpInfo info, ResourceMetadata metadata) - { - if (info == null || metadata == null || metadata.Annotations == null || metadata.Annotations.Count == 0) - return; - - info.Annotations ??= new Hashtable(StringComparer.OrdinalIgnoreCase); - foreach (var kv in metadata.Annotations) - { - if (!info.Annotations.ContainsKey(kv.Key)) - info.Annotations[kv.Key] = kv.Value; - } - if (!info.HasOnlineHelp()) - info.SetOnlineHelpUrl(metadata.Link); - } - - private static RuleHelpInfo[] ToRuleHelp(IEnumerable blocks, RunspaceContext context) - { - // Index rules by RuleId - var results = new Dictionary(StringComparer.OrdinalIgnoreCase); - - foreach (var block in blocks.OfType()) - { - context.EnterLanguageScope(block.Source); - try - { - // Ignore rule blocks that don't match - if (!Match(context, block)) - continue; - - if (!results.ContainsKey(block.Id.Value)) - results[block.Id.Value] = block.Info; - } - finally - { - context.ExitLanguageScope(block.Source); - } - - } - return [.. results.Values]; - } - - private static Baseline[] ToBaselineV1(IEnumerable blocks, RunspaceContext context) - { - if (blocks == null) - return []; - - // Index baselines by BaselineId - var results = new Dictionary(StringComparer.OrdinalIgnoreCase); - - foreach (var block in blocks.OfType().ToArray()) - { - context.EnterLanguageScope(block.Source); - try - { - // Ignore baselines that don't match - if (!Match(context, block)) - continue; - - if (!results.ContainsKey(block.BaselineId)) - results[block.BaselineId] = block; - - } - finally - { - context.ExitLanguageScope(block.Source); - } - } - return [.. results.Values]; - } - - private static ModuleConfigV1[] ToModuleConfigV1(IEnumerable blocks, RunspaceContext context) - { - if (blocks == null) return []; - - // Index configurations by Name. - var results = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var block in blocks.OfType().ToArray()) - { - if (!results.ContainsKey(block.Name)) - results[block.Name] = block; - } - return [.. results.Values]; - } - - /// - /// Get conventions. - /// - private static IConventionV1[] GetConventions(ILanguageBlock[] blocks, RunspaceContext context) - { - // Index by Id. - var index = new HashSet(StringComparer.OrdinalIgnoreCase); - var results = new List(blocks.Length); - - foreach (var block in blocks.OfType().ToArray()) - { - context.EnterLanguageScope(block.Source); - try - { - // Ignore blocks that don't match. - if (!Match(context, block)) - continue; - - if (!index.Contains(block.Id.Value)) - results.Add(block); - - } - finally - { - context.ExitLanguageScope(block.Source); - } - } - return Sort(context, [.. results]); - } - - private static SelectorV1[] ToSelectorV1(IEnumerable blocks, RunspaceContext context) - { - if (blocks == null) - return Array.Empty(); - - // Index selectors by Id. - var results = new Dictionary(StringComparer.OrdinalIgnoreCase); - - foreach (var block in blocks.OfType().ToArray()) - { - context.EnterLanguageScope(block.Source); - try - { - // Ignore selectors that don't match. - if (!Match(context, block)) - continue; - - if (!results.ContainsKey(block.Id.Value)) - results[block.Id.Value] = block; - } - finally - { - context.ExitLanguageScope(block.Source); - } - } - return [.. results.Values]; - } - - private static void Import(IConventionV1[] blocks, RunspaceContext context) - { - foreach (var resource in blocks) - context.Import(resource); - } - private static bool Match(RunspaceContext context, RuleBlock resource) { try { context.EnterLanguageScope(resource.Source); - var filter = context.LanguageScope.GetFilter(ResourceKind.Rule); + var filter = context.LanguageScope!.GetFilter(ResourceKind.Rule); return filter == null || filter.Match(resource); } finally @@ -752,66 +439,41 @@ private static bool Match(RunspaceContext context, RuleBlock resource) } } - private static bool Match(RunspaceContext context, IRuleV1 resource) + internal static void UpdateHelpInfo(IGetLocalizedPathContext context, IResource resource) { - try - { - context.EnterLanguageScope(resource.Source); - var filter = context.LanguageScope.GetFilter(ResourceKind.Rule); - return filter == null || filter.Match(resource); - } - finally - { - context.ExitLanguageScope(resource.Source); - } - } + if (context == null || resource == null || !TryHelpPath(context, resource.Name, out var path, out var culture) || !TryHelpInfo(path, culture, out var info)) + return; - private static bool Match(RunspaceContext context, Baseline resource) - { - try - { - context.EnterLanguageScope(resource.Source); - var filter = context.LanguageScope.GetFilter(ResourceKind.Baseline); - return filter == null || filter.Match(resource); - } - finally - { - context.ExitLanguageScope(resource.Source); - } + resource.Info.Update(info); } - private static bool Match(RunspaceContext context, ScriptBlockConvention block) + internal static bool TryHelpPath(IGetLocalizedPathContext context, string name, out string? path, out string? culture) { - try - { - context.EnterLanguageScope(block.Source); - var filter = context.LanguageScope.GetFilter(ResourceKind.Convention); - return filter == null || filter.Match(block); - } - finally - { - context.ExitLanguageScope(block.Source); - } - } + path = null; + culture = null; + if (string.IsNullOrEmpty(context?.Source?.HelpPath)) + return false; - private static bool Match(RunspaceContext context, SelectorV1 resource) - { - try - { - context.EnterLanguageScope(resource.Source); - var filter = context.LanguageScope.GetFilter(ResourceKind.Selector); - return filter == null || filter.Match(resource); - } - finally - { - context.ExitLanguageScope(resource.Source); - } + var helpFileName = string.Concat(name, Markdown_Extension); + path = context?.GetLocalizedPath(helpFileName, out culture); + return path != null; } - private static IConventionV1[] Sort(RunspaceContext context, IConventionV1[] conventions) + private static bool TryHelpInfo(string? path, string? culture, out IResourceHelpInfo? info) { - Array.Sort(conventions, new ConventionComparer(context)); - return conventions; + info = null; + if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(culture)) + return false; + + var markdown = File.ReadAllText(path); + if (string.IsNullOrEmpty(markdown)) + return false; + + var reader = new MarkdownReader(yamlHeaderOnly: false); + var stream = reader.Read(markdown, path); + var lexer = new ResourceHelpLexer(culture); + info = lexer.Process(stream).ToInfo(); + return info != null; } internal static RuleHelpInfo GetRuleHelpInfo(RunspaceContext context, string name, string defaultSynopsis, string defaultDisplayName, InfoString defaultDescription, InfoString defaultRecommendation) @@ -820,15 +482,15 @@ internal static RuleHelpInfo GetRuleHelpInfo(RunspaceContext context, string nam ? new RuleHelpInfo( name: name, displayName: defaultDisplayName ?? name, - moduleName: context.Source.Module, + moduleName: context.Source!.Module, synopsis: InfoString.Create(defaultSynopsis), description: defaultDescription, recommendation: defaultRecommendation ) : new RuleHelpInfo( name: name, - displayName: document.Name ?? defaultDisplayName ?? name, - moduleName: context.Source.Module, + displayName: document!.Name ?? defaultDisplayName ?? name, + moduleName: context.Source!.Module, synopsis: document.Synopsis ?? new InfoString(defaultSynopsis), description: document.Description ?? defaultDescription, recommendation: document.Recommendation ?? defaultRecommendation ?? document.Synopsis ?? InfoString.Create(defaultSynopsis) @@ -840,34 +502,12 @@ internal static RuleHelpInfo GetRuleHelpInfo(RunspaceContext context, string nam }; } - private static RuleHelpInfo GetRuleHelpInfo(RunspaceContext context, IRuleV1 rule) - { - return GetRuleHelpInfo(context, rule.Name, rule.Synopsis, rule.Info.DisplayName, rule.Info.Description, rule.Recommendation); - } - - internal static void UpdateHelpInfo(IGetLocalizedPathContext context, IResource resource) - { - if (context == null || resource == null || !TryHelpPath(context, resource.Name, out var path, out var culture) || !TryHelpInfo(path, culture, out var info)) - return; - - resource.Info.Update(info); - } - - private static bool TryHelpPath(IGetLocalizedPathContext context, string name, out string path, out string culture) + private static bool TryDocument(string? path, string? culture, out RuleDocument? document) { - path = null; - culture = null; - if (string.IsNullOrEmpty(context.Source.HelpPath)) + document = null; + if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(culture)) return false; - var helpFileName = string.Concat(name, Markdown_Extension); - path = context.GetLocalizedPath(helpFileName, out culture); - return path != null; - } - - private static bool TryDocument(string path, string culture, out RuleDocument document) - { - document = null; var markdown = File.ReadAllText(path); if (string.IsNullOrEmpty(markdown)) return false; @@ -879,21 +519,7 @@ private static bool TryDocument(string path, string culture, out RuleDocument do return document != null; } - private static bool TryHelpInfo(string path, string culture, out IResourceHelpInfo info) - { - info = null; - var markdown = File.ReadAllText(path); - if (string.IsNullOrEmpty(markdown)) - return false; - - var reader = new MarkdownReader(yamlHeaderOnly: false); - var stream = reader.Read(markdown, path); - var lexer = new ResourceHelpLexer(culture); - info = lexer.Process(stream).ToInfo(); - return info != null; - } - - private static Rules.Link[] GetLinks(Help.Link[] links) + private static Rules.Link[]? GetLinks(Help.Link[] links) { if (links == null || links.Length == 0) return null; @@ -905,3 +531,5 @@ private static Rules.Link[] GetLinks(Help.Link[] links) return result; } } + +#nullable restore diff --git a/src/PSRule/Pipeline/GetRuleHelpPipeline.cs b/src/PSRule/Pipeline/GetRuleHelpPipeline.cs index b89a30ab9e..532aa8d979 100644 --- a/src/PSRule/Pipeline/GetRuleHelpPipeline.cs +++ b/src/PSRule/Pipeline/GetRuleHelpPipeline.cs @@ -160,6 +160,6 @@ internal GetRuleHelpPipeline(PipelineContext pipeline, Source[] source, Pipeline public override void End() { - Writer.WriteObject(HostHelper.GetRuleHelp(Source, Context), true); + Writer.WriteObject(HostHelper.GetRuleHelp(Context), true); } } diff --git a/src/PSRule/Pipeline/GetRulePipeline.cs b/src/PSRule/Pipeline/GetRulePipeline.cs index cb491edfdd..eb5429c731 100644 --- a/src/PSRule/Pipeline/GetRulePipeline.cs +++ b/src/PSRule/Pipeline/GetRulePipeline.cs @@ -23,7 +23,7 @@ bool includeDependencies public override void End() { - Writer.WriteObject(HostHelper.GetRule(Source, Context, _IncludeDependencies), true); + Writer.WriteObject(HostHelper.GetRule(Context, _IncludeDependencies), true); Writer.End(Result); } } diff --git a/src/PSRule/Pipeline/IResourceCacheExtensions.cs b/src/PSRule/Pipeline/IResourceCacheExtensions.cs new file mode 100644 index 0000000000..8af167f281 --- /dev/null +++ b/src/PSRule/Pipeline/IResourceCacheExtensions.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using PSRule.Definitions; + +namespace PSRule.Pipeline; + +#nullable enable + +/// +/// Extension methods for . +/// +internal static class IResourceCacheExtensions +{ + /// + /// Import a collection of resources into a cache. + /// + /// The instance to import into. + /// The resources to import into the instance. + /// The type of resource to import. + public static void Import(this IResourceCache cache, IEnumerable? resource) + where TResource : IResource + { + if (cache == null || resource == null) return; + + foreach (var r in resource) + { + cache.Import(r); + } + } +} + +#nullable restore diff --git a/src/PSRule/Pipeline/InvokeRulePipeline.cs b/src/PSRule/Pipeline/InvokeRulePipeline.cs index 3783eca729..a2eddf1eda 100644 --- a/src/PSRule/Pipeline/InvokeRulePipeline.cs +++ b/src/PSRule/Pipeline/InvokeRulePipeline.cs @@ -28,7 +28,7 @@ internal sealed class InvokeRulePipeline : RulePipeline, IPipeline internal InvokeRulePipeline(PipelineContext context, Source[] source, IPipelineWriter writer, RuleOutcome outcome) : base(context, source, context.Reader, writer) { - _RuleGraph = HostHelper.GetRuleBlockGraph(Source, Context); + _RuleGraph = HostHelper.GetRuleBlockGraph(Context); RuleCount = _RuleGraph.Count; if (RuleCount == 0) Context.WarnRuleNotFound(); diff --git a/src/PSRule/Pipeline/PipelineContext.cs b/src/PSRule/Pipeline/PipelineContext.cs index c9b443ed04..ad0fa251fc 100644 --- a/src/PSRule/Pipeline/PipelineContext.cs +++ b/src/PSRule/Pipeline/PipelineContext.cs @@ -8,6 +8,8 @@ using System.Text; using PSRule.Configuration; using PSRule.Definitions; +using PSRule.Definitions.Conventions; +using PSRule.Definitions.Rules; using PSRule.Definitions.Selectors; using PSRule.Definitions.SuppressionGroups; using PSRule.Host; @@ -153,8 +155,16 @@ internal Runspace GetRunspace() return _Runspace; } - internal void Begin(RunspaceContext runspaceContext) + internal void Initialize(RunspaceContext runspaceContext, Source[] sources) { + // Import PS Language Blocks. + var blocks = HostHelper.GetPSResources(sources, runspaceContext); + var conventions = blocks.ToConventionsV1(runspaceContext); + var rules = blocks.ToRuleV1(runspaceContext); + + ResourceCache.Import(conventions); + ResourceCache.Import(rules); + ReportUnresolved(runspaceContext); ReportIssue(runspaceContext); diff --git a/src/PSRule/Pipeline/ResourceCache.cs b/src/PSRule/Pipeline/ResourceCache.cs index e13fd6e0d4..6d9042970e 100644 --- a/src/PSRule/Pipeline/ResourceCache.cs +++ b/src/PSRule/Pipeline/ResourceCache.cs @@ -75,6 +75,17 @@ public bool Import(IResource resource) _Resources.Add(suppressionGroup!); return true; } + else if (resource.Kind == ResourceKind.Rule) + { + _Resources.Add(resource); + return true; + } + else if (resource.Kind == ResourceKind.Convention) + { + _Resources.Add(resource); + return true; + } + return false; } diff --git a/src/PSRule/Pipeline/RulePipeline.cs b/src/PSRule/Pipeline/RulePipeline.cs index 3afe06de87..f1502b7ef2 100644 --- a/src/PSRule/Pipeline/RulePipeline.cs +++ b/src/PSRule/Pipeline/RulePipeline.cs @@ -10,7 +10,7 @@ namespace PSRule.Pipeline; internal abstract class RulePipeline : IPipeline { protected readonly PipelineContext Pipeline; - protected readonly RunspaceContext Context; + internal readonly RunspaceContext Context; protected readonly Source[] Source; protected readonly IPipelineReader Reader; protected readonly IPipelineWriter Writer; diff --git a/src/PSRule/Runtime/LanguageScriptBlock.cs b/src/PSRule/Runtime/LanguageScriptBlock.cs index 2695037dc5..47d5b524f4 100644 --- a/src/PSRule/Runtime/LanguageScriptBlock.cs +++ b/src/PSRule/Runtime/LanguageScriptBlock.cs @@ -1,24 +1,40 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Diagnostics; using System.Management.Automation; namespace PSRule.Runtime; -internal sealed class LanguageScriptBlock : IDisposable +internal sealed class LanguageScriptBlock(PowerShell block) : IDisposable { - private readonly PowerShell _Block; + private readonly PowerShell _Block = block; + private readonly Stopwatch _Stopwatch = new(); private bool _Disposed; - public LanguageScriptBlock(PowerShell block) - { - _Block = block; - } + /// + /// The number of times the block was invoked. + /// + public int Count { get; private set; } = 0; + + /// + /// The total number of milliseconds elapsed while invoking the block. + /// + public long Time => _Stopwatch.ElapsedMilliseconds; public void Invoke() { - _Block.Invoke(); + Count++; + _Stopwatch.Start(); + try + { + _Block.Invoke(); + } + finally + { + _Stopwatch.Stop(); + } } #region IDisposable diff --git a/src/PSRule/Runtime/RunspaceContext.cs b/src/PSRule/Runtime/RunspaceContext.cs index ecf0c7af17..79dc61c874 100644 --- a/src/PSRule/Runtime/RunspaceContext.cs +++ b/src/PSRule/Runtime/RunspaceContext.cs @@ -6,6 +6,7 @@ using System.Management.Automation.Language; using PSRule.Configuration; using PSRule.Definitions; +using PSRule.Definitions.Conventions; using PSRule.Options; using PSRule.Pipeline; using PSRule.Resources; @@ -62,7 +63,7 @@ internal sealed class RunspaceContext : IDisposable, ILogger, IScriptResourceDis private readonly Stopwatch _RuleTimer; private readonly List _Reason; - private readonly List _Conventions; + private IConventionV1[]? _Conventions; // Track whether Dispose has been called. private bool _Disposed; @@ -84,7 +85,6 @@ internal RunspaceContext(PipelineContext pipeline) _ObjectNumber = -1; _RuleTimer = new Stopwatch(); _Reason = []; - _Conventions = []; _Scope = new Stack(); } @@ -640,11 +640,6 @@ public void ExitRuleBlock(RuleBlock ruleBlock) ExitLanguageScope(ruleBlock.Source); } - internal void Import(IConventionV1 resource) - { - _Conventions.Add(resource); - } - internal void AddService(string id, object service) { if (LanguageScope == null) throw new InvalidOperationException("Can not call out of scope."); @@ -666,45 +661,28 @@ internal void AddService(string id, object service) private void RunConventionInitialize() { - if (IsEmptyConventions()) - return; - - for (var i = 0; i < _Conventions.Count; i++) + for (var i = 0; _Conventions != null && i < _Conventions.Length; i++) _Conventions[i].Initialize(this, null); } private void RunConventionBegin() { - if (IsEmptyConventions()) - return; - - for (var i = 0; i < _Conventions.Count; i++) + for (var i = 0; _Conventions != null && i < _Conventions.Length; i++) _Conventions[i].Begin(this, null); } private void RunConventionProcess() { - if (IsEmptyConventions()) - return; - - for (var i = 0; i < _Conventions.Count; i++) + for (var i = 0; _Conventions != null && i < _Conventions.Length; i++) _Conventions[i].Process(this, null); } private void RunConventionEnd() { - if (IsEmptyConventions()) - return; - - for (var i = 0; i < _Conventions.Count; i++) + for (var i = 0; _Conventions != null && i < _Conventions.Length; i++) _Conventions[i].End(this, null); } - private bool IsEmptyConventions() - { - return _Conventions == null || _Conventions.Count == 0; - } - internal void WriteReason(ResultReason[] reason) { for (var i = 0; reason != null && i < reason.Length; i++) @@ -742,13 +720,18 @@ public void Initialize(Source[] source) foreach (var languageScope in Pipeline.LanguageScope.Get()) Pipeline.UpdateLanguageScope(languageScope); + + Pipeline.Initialize(this, source); + + _Conventions = Pipeline.ResourceCache.OfType().ToArray(); + Array.Sort(_Conventions, new ConventionComparer(Pipeline.GetConventionOrder)); + + RunConventionInitialize(); } public void Begin() { - Pipeline.Begin(this); - - RunConventionInitialize(); + // Do nothing. } public void End(IEnumerable output) @@ -891,7 +874,7 @@ private void Dispose(bool disposing) { _RuleTimer.Stop(); _Reason.Clear(); - for (var i = 0; _Conventions != null && i < _Conventions.Count; i++) + for (var i = 0; _Conventions != null && i < _Conventions.Length; i++) { if (_Conventions[i] is IDisposable d) d.Dispose(); diff --git a/tests/PSRule.Tests/ConventionTests.cs b/tests/PSRule.Tests/ConventionTests.cs index 568c355583..6c12d18365 100644 --- a/tests/PSRule.Tests/ConventionTests.cs +++ b/tests/PSRule.Tests/ConventionTests.cs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Linq; using System.Management.Automation; using PSRule.Configuration; +using PSRule.Definitions; using PSRule.Pipeline; namespace PSRule; @@ -10,23 +12,29 @@ namespace PSRule; public sealed class ConventionTests : BaseTests { [Fact] - public void WithConventions() + public void Invoke_WithConventions_CallsConventions() { var testObject1 = new TestObject { Name = "TestObject1" }; var option = GetOption(); option.Rule.Include = ["ConventionTest"]; option.Convention.Include = ["Convention1"]; + var builder = PipelineBuilder.Invoke(GetSource(), option, null); - var pipeline = builder.Build(); + var pipeline = builder.Build() as InvokeRulePipeline; Assert.NotNull(pipeline); + + // Check conventions have been imported. + var conventions = pipeline.Context.Pipeline.ResourceCache.OfType(); + Assert.NotEmpty(conventions); + pipeline.Begin(); pipeline.Process(PSObject.AsPSObject(testObject1)); pipeline.End(); } [Fact] - public void ConventionOrder() + public void Invoke_WithConventions_CallConventionsInOrder() { var testObject1 = new TestObject { Name = "TestObject1" }; var option = GetOption(); diff --git a/tests/PSRule.Tests/JsonRulesTests.cs b/tests/PSRule.Tests/JsonRulesTests.cs new file mode 100644 index 0000000000..99faf328c0 --- /dev/null +++ b/tests/PSRule.Tests/JsonRulesTests.cs @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO; +using System.Linq; +using System.Management.Automation; +using Newtonsoft.Json; +using PSRule.Host; +using PSRule.Pipeline; +using PSRule.Rules; +using PSRule.Runtime; + +namespace PSRule; + +public sealed class JsonRulesTests : ContextBaseTests +{ + /// + /// Test that a JSON-based rule can be parsed. + /// + [Fact] + public void GetRule_FromCurrentDirectory_ShouldReturnRules() + { + var sources = GetSource("FromFile.Rule.jsonc"); + var context = new RunspaceContext(GetPipelineContext(sources: sources)); + context.Initialize(sources); + context.Begin(); + + // From current path + var rule = HostHelper.GetRule(context, includeDependencies: false); + Assert.NotNull(rule); + Assert.Equal("JsonBasicRule", rule[0].Name); + Assert.Equal(Environment.GetRootedPath(""), rule[0].Source.HelpPath); + Assert.Equal(7, rule[0].Extent.Line); + + var block = HostHelper.GetRuleBlockGraph(context).GetAll(); + var actual = block.FirstOrDefault(b => b.Name == "JsonBasicRule"); + Assert.NotNull(actual.Info.Annotations); + Assert.Equal("test123", actual.Info.Annotations["test_value"]); + Assert.Equal("Basic JSON rule", actual.Info.DisplayName); + Assert.Equal("This is a description of a basic rule.", actual.Info.Description); + Assert.Equal("A JSON rule recommendation for testing.", actual.Info.Recommendation); + Assert.Equal("https://aka.ms/ps-rule", actual.Info.GetOnlineHelpUrl()); + } + + [Fact] + public void GetRule_WithRelativePath_ShouldReturnRules() + { + var sources = GetSource("../../../FromFile.Rule.jsonc"); + var context = new RunspaceContext(GetPipelineContext(sources: sources)); + context.Initialize(sources); + context.Begin(); + + // From relative path + var rule = HostHelper.GetRule(context, includeDependencies: false); + Assert.NotNull(rule); + Assert.Equal("JsonBasicRule", rule[0].Name); + Assert.Equal(Environment.GetRootedPath("../../.."), rule[0].Source.HelpPath); + + var hashtable = rule[0].Tag.ToHashtable(); + Assert.Equal("tag", hashtable["feature"]); + + var block = HostHelper.GetRuleBlockGraph(context).GetAll(); + var actual = block.FirstOrDefault(b => b.Name == "JsonBasicRule"); + Assert.NotNull(actual.Info.Annotations); + Assert.Equal("test123", actual.Info.Annotations["test_value"]); + Assert.Equal("Basic JSON rule", actual.Info.DisplayName); + Assert.Equal("This is a description of a basic rule.", actual.Info.Description); + Assert.Equal("A JSON rule recommendation for testing.", actual.Info.Recommendation); + Assert.Equal("https://aka.ms/ps-rule", actual.Info.GetOnlineHelpUrl()); + } + + /// + /// Test that a JSON-based rule with sub-selectors can be parsed. + /// + [Fact] + public void ReadJsonSubSelectorRule() + { + var sources = GetSource("FromFileSubSelector.Rule.jsonc"); + var context = new RunspaceContext(GetPipelineContext(sources: sources, optionBuilder: GetOptionBuilder())); + context.Initialize(sources); + context.Begin(); + + // From current path + var rule = HostHelper.GetRule(context, includeDependencies: false); + Assert.NotNull(rule); + Assert.Equal("JsonRuleWithPrecondition", rule[0].Name); + Assert.Equal("JsonRuleWithSubselector", rule[1].Name); + Assert.Equal("JsonRuleWithSubselectorReordered", rule[2].Name); + Assert.Equal("JsonRuleWithQuantifier", rule[3].Name); + + context.Initialize(GetSource("FromFileSubSelector.Rule.yaml")); + context.Begin(); + var subselector1 = GetRuleVisitor(context, "JsonRuleWithPrecondition"); + var subselector2 = GetRuleVisitor(context, "JsonRuleWithSubselector"); + var subselector3 = GetRuleVisitor(context, "JsonRuleWithSubselectorReordered"); + var subselector4 = GetRuleVisitor(context, "JsonRuleWithQuantifier"); + context.EnterLanguageScope(subselector1.Source); + + var actual1 = GetObject((name: "kind", value: "test"), (name: "resources", value: new string[] { "abc", "abc" })); + var actual2 = GetObject((name: "resources", value: new string[] { "abc", "123", "abc" })); + + // JsonRuleWithPrecondition + context.EnterTargetObject(actual1); + context.EnterRuleBlock(subselector1); + Assert.True(subselector1.Condition.If().AllOf()); + + context.EnterTargetObject(actual2); + context.EnterRuleBlock(subselector1); + Assert.True(subselector1.Condition.If().Skipped()); + + // JsonRuleWithSubselector + context.EnterTargetObject(actual1); + context.EnterRuleBlock(subselector2); + Assert.True(subselector2.Condition.If().AllOf()); + + context.EnterTargetObject(actual2); + context.EnterRuleBlock(subselector2); + Assert.False(subselector2.Condition.If().AllOf()); + + // JsonRuleWithSubselectorReordered + context.EnterTargetObject(actual1); + context.EnterRuleBlock(subselector3); + Assert.True(subselector3.Condition.If().AllOf()); + + context.EnterTargetObject(actual2); + context.EnterRuleBlock(subselector3); + Assert.True(subselector3.Condition.If().AllOf()); + + // JsonRuleWithQuantifier + var fromFile = GetObjectAsTarget("ObjectFromFile3.json"); + actual1 = fromFile[0]; + actual2 = fromFile[1]; + var actual3 = fromFile[2]; + + context.EnterTargetObject(actual1); + context.EnterRuleBlock(subselector4); + Assert.True(subselector4.Condition.If().AllOf()); + + context.EnterTargetObject(actual2); + context.EnterRuleBlock(subselector4); + Assert.False(subselector4.Condition.If().AllOf()); + + context.EnterTargetObject(actual3); + context.EnterRuleBlock(subselector4); + Assert.True(subselector4.Condition.If().AllOf()); + } + + #region Helper methods + + private new static Source[] GetSource(string path) + { + var builder = new SourcePipelineBuilder(null, null); + builder.Directory(GetSourcePath(path)); + return builder.Build(); + } + + private new static TargetObject GetObject(params (string name, object value)[] properties) + { + var result = new PSObject(); + for (var i = 0; properties != null && i < properties.Length; i++) + result.Properties.Add(new PSNoteProperty(properties[i].name, properties[i].value)); + + return new TargetObject(result); + } + + private static TargetObject[] GetObjectAsTarget(string path) + { + return JsonConvert.DeserializeObject(File.ReadAllText(path)).Select(o => new TargetObject(new PSObject(o))).ToArray(); + } + + private static RuleBlock GetRuleVisitor(RunspaceContext context, string name) + { + var block = HostHelper.GetRuleBlockGraph(context).GetAll(); + return block.FirstOrDefault(s => s.Name == name); + } + + #endregion Helper methods +} diff --git a/tests/PSRule.Tests/ResourceValidatorTests.cs b/tests/PSRule.Tests/ResourceValidatorTests.cs index 1c6b6bb940..e29f7f3123 100644 --- a/tests/PSRule.Tests/ResourceValidatorTests.cs +++ b/tests/PSRule.Tests/ResourceValidatorTests.cs @@ -10,27 +10,31 @@ namespace PSRule; public sealed class ResourceValidatorTests : ContextBaseTests { - [Fact] - public void ResourceName() + [Theory] + [InlineData("FromFile.Rule.yaml")] + public void GetRule_WithValidResourceName_ShouldNotReturnError(string path) { var writer = GetTestWriter(); - var sources = GetSource(); + var sources = GetSource(path); var context = new RunspaceContext(GetPipelineContext(writer: writer, sources: sources)); // Get good rules - var rule = HostHelper.GetRule(sources, context, includeDependencies: false); + var rule = HostHelper.GetRule(context, includeDependencies: false); Assert.NotNull(rule); Assert.Empty(writer.Errors); + } - // Get invalid rule names YAML - rule = HostHelper.GetRule(GetSource("FromFileName.Rule.yaml"), context, includeDependencies: false); - Assert.NotNull(rule); - Assert.NotEmpty(writer.Errors); - Assert.Equal("PSRule.Parse.InvalidResourceName", writer.Errors[0].FullyQualifiedErrorId); + [Theory] + [InlineData("FromFileName.Rule.yaml")] + [InlineData("FromFileName.Rule.jsonc")] + public void GetRule_WithInvalidResourceName_ShouldReturnError(string path) + { + var writer = GetTestWriter(); + var sources = GetSource(path); + var context = new RunspaceContext(GetPipelineContext(writer: writer, sources: sources)); - // Get invalid rule names JSON - writer.Errors.Clear(); - rule = HostHelper.GetRule(GetSource("FromFileName.Rule.jsonc"), context, includeDependencies: false); + // Get invalid rule names. + var rule = HostHelper.GetRule(context, includeDependencies: false); Assert.NotNull(rule); Assert.NotEmpty(writer.Errors); Assert.Equal("PSRule.Parse.InvalidResourceName", writer.Errors[0].FullyQualifiedErrorId); @@ -64,7 +68,7 @@ public void IsNameValid() #region Helper methods - private static new Source[] GetSource(string path = "FromFile.Rule.yaml") + private static new Source[] GetSource(string path) { var builder = new SourcePipelineBuilder(null, null); builder.Directory(GetSourcePath(path)); diff --git a/tests/PSRule.Tests/SuppressionFilterTests.cs b/tests/PSRule.Tests/SuppressionFilterTests.cs index e8e432de39..ea0ece35b7 100644 --- a/tests/PSRule.Tests/SuppressionFilterTests.cs +++ b/tests/PSRule.Tests/SuppressionFilterTests.cs @@ -20,7 +20,7 @@ public void Match() var context = new RunspaceContext(GetPipelineContext(option: option, sources: sources)); context.Initialize(sources); context.Begin(); - var rules = HostHelper.GetRule(sources, context, includeDependencies: false); + var rules = HostHelper.GetRule(context, includeDependencies: false); var resourceIndex = new ResourceIndex(rules); var filter = new SuppressionFilter(context, option.Suppression, resourceIndex); diff --git a/tests/PSRule.Tests/RulesTests.cs b/tests/PSRule.Tests/YamlRulesTests.cs similarity index 62% rename from tests/PSRule.Tests/RulesTests.cs rename to tests/PSRule.Tests/YamlRulesTests.cs index e1556e7d33..f952556d92 100644 --- a/tests/PSRule.Tests/RulesTests.cs +++ b/tests/PSRule.Tests/YamlRulesTests.cs @@ -12,29 +12,46 @@ namespace PSRule; -public sealed class RulesTests : ContextBaseTests +public sealed class YamlRulesTests : ContextBaseTests { - #region Yaml rules - /// /// Test that a YAML-based rule can be parsed. /// [Fact] - public void ReadYamlRule() + public void GetRule_FromCurrentDirectory_ShouldReturnRules() { - var context = new RunspaceContext(GetPipelineContext(sources: GetSource())); - context.Initialize(GetSource()); + var sources = GetSource("FromFile.Rule.yaml"); + var context = new RunspaceContext(GetPipelineContext(sources: sources)); + context.Initialize(sources); context.Begin(); // From current path - var rule = HostHelper.GetRule(GetSource(), context, includeDependencies: false); + var rule = HostHelper.GetRule(context, includeDependencies: false); Assert.NotNull(rule); Assert.Equal("YamlBasicRule", rule[0].Name); Assert.Equal(Environment.GetRootedPath(""), rule[0].Source.HelpPath); Assert.Equal(10, rule[0].Extent.Line); + var block = HostHelper.GetRuleBlockGraph(context).GetAll(); + var actual = block.FirstOrDefault(b => b.Name == "YamlBasicRule"); + Assert.NotNull(actual.Info.Annotations); + Assert.Equal("test123", actual.Info.Annotations["test_value"]); + Assert.Equal("Basic YAML rule", actual.Info.DisplayName); + Assert.Equal("This is a description of a basic rule.", actual.Info.Description); + Assert.Equal("A YAML rule recommendation for testing.", actual.Info.Recommendation); + Assert.Equal("https://aka.ms/ps-rule", actual.Info.GetOnlineHelpUrl()); + } + + [Fact] + public void GetRule_WithRelativePath_ShouldReturnRules() + { + var sources = GetSource("../../../FromFile.Rule.yaml"); + var context = new RunspaceContext(GetPipelineContext(sources: sources)); + context.Initialize(sources); + context.Begin(); + // From relative path - rule = HostHelper.GetRule(GetSource("../../../FromFile.Rule.yaml"), context, includeDependencies: false); + var rule = HostHelper.GetRule(context, includeDependencies: false); Assert.NotNull(rule); Assert.Equal("YamlBasicRule", rule[0].Name); Assert.Equal(Environment.GetRootedPath("../../.."), rule[0].Source.HelpPath); @@ -42,7 +59,7 @@ public void ReadYamlRule() var hashtable = rule[0].Tag.ToHashtable(); Assert.Equal("tag", hashtable["feature"]); - var block = HostHelper.GetRuleBlockGraph(GetSource(), context).GetAll(); + var block = HostHelper.GetRuleBlockGraph(context).GetAll(); var actual = block.FirstOrDefault(b => b.Name == "YamlBasicRule"); Assert.NotNull(actual.Info.Annotations); Assert.Equal("test123", actual.Info.Annotations["test_value"]); @@ -64,7 +81,7 @@ public void ReadYamlSubSelectorRule() context.Begin(); // From current path - var rule = HostHelper.GetRule(sources, context, includeDependencies: false); + var rule = HostHelper.GetRule(context, includeDependencies: false); Assert.NotNull(rule); Assert.Equal("YamlRuleWithPrecondition", rule[0].Name); Assert.Equal("YamlRuleWithSubselector", rule[1].Name); @@ -73,10 +90,10 @@ public void ReadYamlSubSelectorRule() context.Initialize(sources); context.Begin(); - var subselector1 = GetRuleVisitor(context, "YamlRuleWithPrecondition", sources); - var subselector2 = GetRuleVisitor(context, "YamlRuleWithSubselector", sources); - var subselector3 = GetRuleVisitor(context, "YamlRuleWithSubselectorReordered", sources); - var subselector4 = GetRuleVisitor(context, "YamlRuleWithQuantifier", sources); + var subselector1 = GetRuleVisitor(context, "YamlRuleWithPrecondition"); + var subselector2 = GetRuleVisitor(context, "YamlRuleWithSubselector"); + var subselector3 = GetRuleVisitor(context, "YamlRuleWithSubselectorReordered"); + var subselector4 = GetRuleVisitor(context, "YamlRuleWithQuantifier"); context.EnterLanguageScope(subselector1.Source); var actual1 = GetObject((name: "kind", value: "test"), (name: "resources", value: new string[] { "abc", "abc" })); @@ -131,7 +148,7 @@ public void ReadYamlSubSelectorRule() [Fact] public void EvaluateYamlRule() { - var sources = GetSource(); + var sources = GetSource("FromFile.Rule.yaml"); var context = new RunspaceContext(GetPipelineContext(sources: sources, optionBuilder: GetOptionBuilder())); context.Initialize(sources); context.Begin(); @@ -195,7 +212,7 @@ public void EvaluateYamlRule() [Fact] public void RuleWithObjectPath() { - var sources = GetSource(); + var sources = GetSource("FromFile.Rule.yaml"); var context = new RunspaceContext(GetPipelineContext(sources: sources, optionBuilder: GetOptionBuilder())); context.Initialize(sources); context.Begin(); @@ -218,128 +235,9 @@ public void RuleWithObjectPath() Assert.True(yamlObjectPath.Condition.If().AllOf()); } - #endregion Yaml rules - - #region Json rules - - /// - /// Test that a JSON-based rule can be parsed. - /// - [Fact] - public void ReadJsonRule() - { - var sources = GetSource(); - var context = new RunspaceContext(GetPipelineContext()); - context.Initialize(sources); - context.Begin(); - - // From current path - var rule = HostHelper.GetRule(GetSource("FromFile.Rule.jsonc"), context, includeDependencies: false); - Assert.NotNull(rule); - Assert.Equal("JsonBasicRule", rule[0].Name); - Assert.Equal(Environment.GetRootedPath(""), rule[0].Source.HelpPath); - Assert.Equal(7, rule[0].Extent.Line); - - // From relative path - rule = HostHelper.GetRule(GetSource("../../../FromFile.Rule.jsonc"), context, includeDependencies: false); - Assert.NotNull(rule); - Assert.Equal("JsonBasicRule", rule[0].Name); - Assert.Equal(Environment.GetRootedPath("../../.."), rule[0].Source.HelpPath); - - var hashtable = rule[0].Tag.ToHashtable(); - Assert.Equal("tag", hashtable["feature"]); - - var block = HostHelper.GetRuleBlockGraph(GetSource("FromFile.Rule.jsonc"), context).GetAll(); - var actual = block.FirstOrDefault(b => b.Name == "JsonBasicRule"); - Assert.NotNull(actual.Info.Annotations); - Assert.Equal("test123", actual.Info.Annotations["test_value"]); - Assert.Equal("Basic JSON rule", actual.Info.DisplayName); - Assert.Equal("This is a description of a basic rule.", actual.Info.Description); - Assert.Equal("A JSON rule recommendation for testing.", actual.Info.Recommendation); - Assert.Equal("https://aka.ms/ps-rule", actual.Info.GetOnlineHelpUrl()); - } - - /// - /// Test that a JSON-based rule with sub-selectors can be parsed. - /// - [Fact] - public void ReadJsonSubSelectorRule() - { - var sources = GetSource("FromFileSubSelector.Rule.jsonc"); - var context = new RunspaceContext(GetPipelineContext(sources: sources, optionBuilder: GetOptionBuilder())); - context.Initialize(sources); - context.Begin(); - - // From current path - var rule = HostHelper.GetRule(sources, context, includeDependencies: false); - Assert.NotNull(rule); - Assert.Equal("JsonRuleWithPrecondition", rule[0].Name); - Assert.Equal("JsonRuleWithSubselector", rule[1].Name); - Assert.Equal("JsonRuleWithSubselectorReordered", rule[2].Name); - Assert.Equal("JsonRuleWithQuantifier", rule[3].Name); - - context.Initialize(GetSource("FromFileSubSelector.Rule.yaml")); - context.Begin(); - var subselector1 = GetRuleVisitor(context, "JsonRuleWithPrecondition", sources); - var subselector2 = GetRuleVisitor(context, "JsonRuleWithSubselector", sources); - var subselector3 = GetRuleVisitor(context, "JsonRuleWithSubselectorReordered", sources); - var subselector4 = GetRuleVisitor(context, "JsonRuleWithQuantifier", sources); - context.EnterLanguageScope(subselector1.Source); - - var actual1 = GetObject((name: "kind", value: "test"), (name: "resources", value: new string[] { "abc", "abc" })); - var actual2 = GetObject((name: "resources", value: new string[] { "abc", "123", "abc" })); - - // JsonRuleWithPrecondition - context.EnterTargetObject(actual1); - context.EnterRuleBlock(subselector1); - Assert.True(subselector1.Condition.If().AllOf()); - - context.EnterTargetObject(actual2); - context.EnterRuleBlock(subselector1); - Assert.True(subselector1.Condition.If().Skipped()); - - // JsonRuleWithSubselector - context.EnterTargetObject(actual1); - context.EnterRuleBlock(subselector2); - Assert.True(subselector2.Condition.If().AllOf()); - - context.EnterTargetObject(actual2); - context.EnterRuleBlock(subselector2); - Assert.False(subselector2.Condition.If().AllOf()); - - // JsonRuleWithSubselectorReordered - context.EnterTargetObject(actual1); - context.EnterRuleBlock(subselector3); - Assert.True(subselector3.Condition.If().AllOf()); - - context.EnterTargetObject(actual2); - context.EnterRuleBlock(subselector3); - Assert.True(subselector3.Condition.If().AllOf()); - - // JsonRuleWithQuantifier - var fromFile = GetObjectAsTarget("ObjectFromFile3.json"); - actual1 = fromFile[0]; - actual2 = fromFile[1]; - var actual3 = fromFile[2]; - - context.EnterTargetObject(actual1); - context.EnterRuleBlock(subselector4); - Assert.True(subselector4.Condition.If().AllOf()); - - context.EnterTargetObject(actual2); - context.EnterRuleBlock(subselector4); - Assert.False(subselector4.Condition.If().AllOf()); - - context.EnterTargetObject(actual3); - context.EnterRuleBlock(subselector4); - Assert.True(subselector4.Condition.If().AllOf()); - } - - #endregion Json rules - #region Helper methods - private new static Source[] GetSource(string path = "FromFile.Rule.yaml") + private new static Source[] GetSource(string path) { var builder = new SourcePipelineBuilder(null, null); builder.Directory(GetSourcePath(path)); @@ -365,19 +263,11 @@ private static TargetObject[] GetObjectAsTarget(string path) return JsonConvert.DeserializeObject(File.ReadAllText(path)).Select(o => new TargetObject(new PSObject(o))).ToArray(); } - private static RuleBlock GetRuleVisitor(RunspaceContext context, string name, Source[] source = null) + private static RuleBlock GetRuleVisitor(RunspaceContext context, string name) { - var block = HostHelper.GetRuleBlockGraph(source ?? GetSource(), context).GetAll(); + var block = HostHelper.GetRuleBlockGraph(context).GetAll(); return block.FirstOrDefault(s => s.Name == name); } - private static void ImportSelectors(RunspaceContext context, Source[] source = null) - { - var selectors = HostHelper.GetSelectorForTests(source ?? GetSource(), context).ToArray(); - var cache = context.Pipeline.ResourceCache; - foreach (var selector in selectors) - cache.Import(selector); - } - #endregion Helper methods }