From 22b98bdb7856af8a749432ba54c27931499bd407 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sat, 23 Jul 2022 16:02:00 +1000 Subject: [PATCH 001/156] Improve outcome option docs #1166 (#1178) --- docs/CHANGELOG-v2.md | 2 ++ .../concepts/PSRule/en-US/about_PSRule_Options.md | 15 ++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 8fdeb5edea..854c008601 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -21,6 +21,8 @@ What's changed since pre-release v2.3.0-B0030: - Engineering: - Added publishing support for NuGet symbol packages @BernieWhite. [#1173](https://github.com/microsoft/PSRule/issues/1173) + - Updated outcome option docs by @BernieWhite. + [#1166](https://github.com/microsoft/PSRule/issues/1166) ## v2.3.0-B0030 (pre-release) diff --git a/docs/concepts/PSRule/en-US/about_PSRule_Options.md b/docs/concepts/PSRule/en-US/about_PSRule_Options.md index f11e8f7df9..65bacf0b20 100644 --- a/docs/concepts/PSRule/en-US/about_PSRule_Options.md +++ b/docs/concepts/PSRule/en-US/about_PSRule_Options.md @@ -2003,12 +2003,17 @@ variables: Filters output to include results with the specified outcome. The following outcome options are available: -- `None` - Results for rules that did not get processed are returned. -- `Pass` - Results for rules that passed are returned. -- `Fail` - Results for rules that failed are returned. -- `Error` - Results for rules that raised an error are returned. -- `Processed` - Results for rules that either passed, failed, or raised an error are returned. +- `None` (0) - Results for rules that did not get processed are returned. + This include rules that have been suppressed or were not run against a target object. +- `Fail` (1) - Results for rules that failed are returned. +- `Pass` (2) - Results for rules that passed are returned. +- `Error` (4) - Results for rules that raised an error are returned. + +Additionally the following rollup options exist: + +- `Processed` - Results for rules with the `Fail`, `Pass`, or `Error` outcome. This is the default option. +- `Problem` - Results for rules with the `Fail`, or `Error` outcome. - `All` - All results for rules are returned. This option can be specified using: From 35e54693491275bc9736e28e45b676b45eab714b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 23 Jul 2022 17:16:56 +1000 Subject: [PATCH 002/156] Bump Sarif.Sdk from 2.4.15 to 2.4.16 (#1177) * Bump Sarif.Sdk from 2.4.15 to 2.4.16 Bumps [Sarif.Sdk](https://github.com/Microsoft/sarif-sdk) from 2.4.15 to 2.4.16. - [Release notes](https://github.com/Microsoft/sarif-sdk/releases) - [Commits](https://github.com/Microsoft/sarif-sdk/compare/v2.4.15...v2.4.16) --- updated-dependencies: - dependency-name: Sarif.Sdk dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Bump change log Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bernie White --- docs/CHANGELOG-v2.md | 2 ++ src/PSRule/PSRule.csproj | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 854c008601..e7f31ec2d6 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -23,6 +23,8 @@ What's changed since pre-release v2.3.0-B0030: [#1173](https://github.com/microsoft/PSRule/issues/1173) - Updated outcome option docs by @BernieWhite. [#1166](https://github.com/microsoft/PSRule/issues/1166) + - Bump Sarif.Sdk to v2.4.16. + [#1177](https://github.com/microsoft/PSRule/pull/1177) ## v2.3.0-B0030 (pre-release) diff --git a/src/PSRule/PSRule.csproj b/src/PSRule/PSRule.csproj index 569e2f243a..c64591b8e2 100644 --- a/src/PSRule/PSRule.csproj +++ b/src/PSRule/PSRule.csproj @@ -19,7 +19,7 @@ - + From 277ea5086340a3ecd83d3e610cca0506da7198a7 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sat, 23 Jul 2022 20:00:29 +1000 Subject: [PATCH 003/156] Pre-release v2.3.0-B0051 (#1181) --- docs/CHANGELOG-v2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index e7f31ec2d6..0ca64f3ab6 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -13,6 +13,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.3.0-B0051 (pre-release) + What's changed since pre-release v2.3.0-B0030: - General improvements: From 8dde559cb456109594815d6aad98cace727e7b71 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sun, 24 Jul 2022 17:50:18 +1000 Subject: [PATCH 004/156] Improve JSON object binding #1182 (#1183) --- docs/CHANGELOG-v2.md | 6 ++++ src/PSRule/Common/PipelineExtensions.cs | 20 +++++++++++++ src/PSRule/Configuration/PSRuleOption.cs | 3 ++ src/PSRule/Pipeline/CommandLineBuilder.cs | 23 +++++++++++++++ src/PSRule/Pipeline/InvokeRulePipeline.cs | 10 +++++++ src/PSRule/Pipeline/PipelineBuilder.cs | 20 +++++++++++++ src/PSRule/Pipeline/RulePipeline.cs | 3 ++ src/PSRule/Runtime/ObjectHelper.cs | 35 +---------------------- src/PSRule/Runtime/PSRuleMemberInfo.cs | 1 - tests/PSRule.Tests/TargetBinderTests.cs | 27 ++++++++++++++++- 10 files changed, 112 insertions(+), 36 deletions(-) create mode 100644 src/PSRule/Common/PipelineExtensions.cs diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 0ca64f3ab6..fe5a92f229 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -13,6 +13,12 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since pre-release v2.3.0-B0051: + +- General improvements: + - Added support for binding with JSON objects by @BernieWhite. + [#1182](https://github.com/microsoft/PSRule/issues/1182) + ## v2.3.0-B0051 (pre-release) What's changed since pre-release v2.3.0-B0030: diff --git a/src/PSRule/Common/PipelineExtensions.cs b/src/PSRule/Common/PipelineExtensions.cs new file mode 100644 index 0000000000..cf612d7add --- /dev/null +++ b/src/PSRule/Common/PipelineExtensions.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; +using PSRule.Pipeline; + +namespace PSRule +{ + public static class PipelineExtensions + { + /// + /// Process an object through the pipeline. Each object will be processed by rules that apply based on pre-conditions. + /// + /// The object to process. + public static void Process(this IPipeline pipeline, object sourceObject) + { + pipeline.Process(PSObject.AsPSObject(sourceObject)); + } + } +} diff --git a/src/PSRule/Configuration/PSRuleOption.cs b/src/PSRule/Configuration/PSRuleOption.cs index 91258a5f84..0e743ddfad 100644 --- a/src/PSRule/Configuration/PSRuleOption.cs +++ b/src/PSRule/Configuration/PSRuleOption.cs @@ -56,6 +56,9 @@ public sealed class PSRuleOption : IEquatable, IBaselineSpec /// private static CultureInfo _CurrentCulture = Thread.CurrentThread.CurrentCulture; + /// + /// Create an empty PSRule options object. + /// public PSRuleOption() { // Set defaults diff --git a/src/PSRule/Pipeline/CommandLineBuilder.cs b/src/PSRule/Pipeline/CommandLineBuilder.cs index 6e29c8acae..a8656d694a 100644 --- a/src/PSRule/Pipeline/CommandLineBuilder.cs +++ b/src/PSRule/Pipeline/CommandLineBuilder.cs @@ -5,8 +5,21 @@ namespace PSRule.Pipeline { + /// + /// A helper to create a PSRule pipeline that can be used via the .NET SDK. + /// public static class CommandLineBuilder { + /// + /// Create a builder for an Invoke pipeline. + /// + /// + /// Invoke piplines process objects and produce records indicating the outcome of each rule. + /// + /// The name of modules containing rules to process. + /// Options that configure PSRule. + /// An implementation of a host context that will recieve output and results. + /// A builder object to configure the pipeline. public static IInvokePipelineBuilder Invoke(string[] module, PSRuleOption option, IHostContext hostContext) { var sourcePipeline = new SourcePipelineBuilder(hostContext, option); @@ -19,6 +32,16 @@ public static IInvokePipelineBuilder Invoke(string[] module, PSRuleOption option return pipeline; } + /// + /// Create a builder for an Assert pipeline. + /// + /// + /// Assert pipelines process objects with rules and produce text-based output suitable for output to a CI pipeline. + /// + /// The name of modules containing rules to process. + /// Options that configure PSRule. + /// An implementation of a host context that will recieve output and results. + /// A builder object to configure the pipeline. public static IInvokePipelineBuilder Assert(string[] module, PSRuleOption option, IHostContext hostContext) { var sourcePipeline = new SourcePipelineBuilder(hostContext, option); diff --git a/src/PSRule/Pipeline/InvokeRulePipeline.cs b/src/PSRule/Pipeline/InvokeRulePipeline.cs index 550685f68b..189578c63a 100644 --- a/src/PSRule/Pipeline/InvokeRulePipeline.cs +++ b/src/PSRule/Pipeline/InvokeRulePipeline.cs @@ -15,8 +15,16 @@ namespace PSRule.Pipeline { public interface IInvokePipelineBuilder : IPipelineBuilder { + /// + /// Configures paths that will be scanned for input. + /// + /// An array of relative or absolute path specs to be scanned. Directories will be recursively scanned for all files not excluded matching the file path spec. void InputPath(string[] path); + /// + /// Configures a variable that will recieve all results in addition to the host context. + /// + /// The name of the variable to set. void ResultVariable(string variableName); } @@ -193,6 +201,7 @@ internal InvokeRulePipeline(PipelineContext context, Source[] source, IPipelineW public int RuleCount { get; private set; } + /// public override void Process(PSObject sourceObject) { try @@ -212,6 +221,7 @@ public override void Process(PSObject sourceObject) } } + /// public override void End() { if (_Completed.Count > 0) diff --git a/src/PSRule/Pipeline/PipelineBuilder.cs b/src/PSRule/Pipeline/PipelineBuilder.cs index af2c4c0a89..30ad181816 100644 --- a/src/PSRule/Pipeline/PipelineBuilder.cs +++ b/src/PSRule/Pipeline/PipelineBuilder.cs @@ -18,6 +18,9 @@ namespace PSRule.Pipeline { + /// + /// A helper to create a PowerShell-based pipeline for running PSRule. + /// public static class PipelineBuilder { public static IInvokePipelineBuilder Assert(Source[] source, PSRuleOption option, IHostContext hostContext) @@ -85,17 +88,34 @@ public static IGetTargetPipelineBuilder GetTarget(PSRuleOption option, IHostCont public interface IPipelineBuilder { + /// + /// Configure the pipeline with options. + /// IPipelineBuilder Configure(PSRuleOption option); + /// + /// Build the pipeline. + /// + /// Optionally specify a custom writer which will handle output processing. IPipeline Build(IPipelineWriter writer = null); } public interface IPipeline : IDisposable { + /// + /// Initalize the pipeline and results. Call this method once prior to calling Process. + /// void Begin(); + /// + /// Process an object through the pipeline. Each object will be processed by rules that apply based on pre-conditions. + /// + /// 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/Pipeline/RulePipeline.cs b/src/PSRule/Pipeline/RulePipeline.cs index c14796c025..b8b00ce61d 100644 --- a/src/PSRule/Pipeline/RulePipeline.cs +++ b/src/PSRule/Pipeline/RulePipeline.cs @@ -32,6 +32,7 @@ protected RulePipeline(PipelineContext context, Source[] source, PipelineReader #region IPipeline + /// public virtual void Begin() { Reader.Open(); @@ -39,11 +40,13 @@ public virtual void Begin() Context.Begin(); } + /// public virtual void Process(PSObject sourceObject) { // Do nothing } + /// public virtual void End() { Writer.End(); diff --git a/src/PSRule/Runtime/ObjectHelper.cs b/src/PSRule/Runtime/ObjectHelper.cs index d0a989db77..81d9726d87 100644 --- a/src/PSRule/Runtime/ObjectHelper.cs +++ b/src/PSRule/Runtime/ObjectHelper.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.Collections; using System.Diagnostics; using System.Management.Automation; using PSRule.Runtime.ObjectPath; @@ -16,9 +14,7 @@ internal static class ObjectHelper { public static bool GetPath(PSObject targetObject, string path, bool caseSensitive, out object value) { - return targetObject.BaseObject is IDictionary dictionary ? - TryDictionary(dictionary, path, caseSensitive, out value) : - TryPropertyValue(targetObject, path, caseSensitive, out value); + return GetPath(null, targetObject, path, caseSensitive, out value); } public static bool GetPath(IBindingContext bindingContext, object targetObject, string path, bool caseSensitive, out object value) @@ -33,35 +29,6 @@ public static bool GetPath(IBindingContext bindingContext, object targetObject, return expression.TryGet(targetObject, caseSensitive, out value); } - private static bool TryDictionary(IDictionary dictionary, string key, bool caseSensitive, out object value) - { - value = null; - var comparer = caseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase; - foreach (var k in dictionary.Keys) - { - if (comparer.Equals(key, k)) - { - value = dictionary[k]; - return true; - } - } - return false; - } - - private static bool TryPropertyValue(PSObject targetObject, string propertyName, bool caseSensitive, out object value) - { - value = null; - var p = targetObject.Properties[propertyName]; - if (p == null) - return false; - - if (caseSensitive && !StringComparer.Ordinal.Equals(p.Name, propertyName)) - return false; - - value = p.Value; - return true; - } - /// /// Get a token for the specified name either by creating or reading from cache. /// diff --git a/src/PSRule/Runtime/PSRuleMemberInfo.cs b/src/PSRule/Runtime/PSRuleMemberInfo.cs index 748b18ffb7..6eddb1c809 100644 --- a/src/PSRule/Runtime/PSRuleMemberInfo.cs +++ b/src/PSRule/Runtime/PSRuleMemberInfo.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Collections.Generic; using System.Diagnostics; using System.Management.Automation; diff --git a/tests/PSRule.Tests/TargetBinderTests.cs b/tests/PSRule.Tests/TargetBinderTests.cs index 6245ecfba6..ea24f23e91 100644 --- a/tests/PSRule.Tests/TargetBinderTests.cs +++ b/tests/PSRule.Tests/TargetBinderTests.cs @@ -1,7 +1,8 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Management.Automation; +using Newtonsoft.Json.Linq; using PSRule.Configuration; using PSRule.Definitions.Baselines; using PSRule.Pipeline; @@ -32,6 +33,28 @@ public void BindTargetObject() Assert.Equal("System.Management.Automation.PSCustomObject", m0.TargetType); } + [Fact] + public void BindJObject() + { + var binder = GetBinder(); + var targetObject = new TargetObject(PSObject.AsPSObject(JToken.Parse("{ \"name\": \"Name1\", \"type\": \"Type1\", \"AlternativeName\": \"Name2\", \"AlternativeType\": \"Type2\" }"))); + binder.Bind(targetObject); + + var m1 = binder.Using("Module1"); + Assert.Equal("Name1", m1.TargetName); + Assert.Equal("Type1", m1.TargetType); + + var m2 = binder.Using("Module2"); + Assert.Equal("Name2", m2.TargetName); + Assert.Equal("Type1", m2.TargetType); + + var m0 = binder.Using("."); + Assert.Equal("Name1", m0.TargetName); + Assert.Equal("Newtonsoft.Json.Linq.JObject", m0.TargetType); + } + + #region Helper methods + private TargetObject GetTargetObject() { var pso = new PSObject(); @@ -87,5 +110,7 @@ private static IBaselineSpec GetOption(string[] targetName, string[] targetType) result.Binding.TargetType = targetType; return result; } + + #endregion Helper methods } } From fbb907c7e3c143848184806564fb66c19c46def1 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sun, 24 Jul 2022 22:54:50 +1000 Subject: [PATCH 005/156] Pre-release v2.3.0-B0074 (#1184) --- docs/CHANGELOG-v2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index fe5a92f229..41654cf4e0 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -13,6 +13,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.3.0-B0074 (pre-release) + What's changed since pre-release v2.3.0-B0051: - General improvements: From 57a28f389f86f17ae985ce9bebf2f52e2390503d Mon Sep 17 00:00:00 2001 From: Bernie White Date: Tue, 26 Jul 2022 15:18:31 +1000 Subject: [PATCH 006/156] Fixed handling for JSON objects in rules #1187 (#1188) --- docs/CHANGELOG-v2.md | 6 +++++ src/PSRule/Common/JsonConverters.cs | 6 ++--- src/PSRule/Common/JsonHelper.cs | 24 +++++++++++++++++ src/PSRule/Pipeline/TargetObject.cs | 35 ++++++++++++++++--------- tests/PSRule.Tests/TargetBinderTests.cs | 2 +- 5 files changed, 55 insertions(+), 18 deletions(-) create mode 100644 src/PSRule/Common/JsonHelper.cs diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 41654cf4e0..f6893b3ef7 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -13,6 +13,12 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since pre-release v2.3.0-B0074: + +- Bug fixes: + - Fixed handling for JSON objects in rules by @BernieWhite. + [#1187](https://github.com/microsoft/PSRule/issues/1187) + ## v2.3.0-B0074 (pre-release) What's changed since pre-release v2.3.0-B0051: diff --git a/src/PSRule/Common/JsonConverters.cs b/src/PSRule/Common/JsonConverters.cs index 57b292207e..be23eb3628 100644 --- a/src/PSRule/Common/JsonConverters.cs +++ b/src/PSRule/Common/JsonConverters.cs @@ -32,7 +32,7 @@ public override bool CanConvert(Type objectType) public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { - if (!(value is PSObject obj)) + if (value is not PSObject obj) throw new ArgumentException(message: PSRuleResources.SerializeNullPSObject, paramName: nameof(value)); if (WriteFileSystemInfo(writer, value, serializer) || WriteBaseObject(writer, obj, serializer)) @@ -91,18 +91,16 @@ private void ReadObject(PSObject value, JsonReader reader) case JsonToken.StartArray: var items = new List(); reader.Read(); - var item = new PSObject(); - while (reader.TokenType != JsonToken.EndArray) { if (SkipComments(reader)) continue; + var item = new PSObject(); ReadObject(value: item, reader: reader); items.Add(item); reader.Read(); } - value.Properties.Add(new PSNoteProperty(name: name, value: items.ToArray())); break; diff --git a/src/PSRule/Common/JsonHelper.cs b/src/PSRule/Common/JsonHelper.cs new file mode 100644 index 0000000000..1606fe179d --- /dev/null +++ b/src/PSRule/Common/JsonHelper.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace PSRule +{ + internal static class JsonHelper + { + internal static PSObject ToPSObject(JToken token) + { + return token.ToObject(SingleObjectSerializer()); + } + + private static JsonSerializer SingleObjectSerializer() + { + var s = new JsonSerializer(); + s.Converters.Add(new PSObjectJsonConverter()); + return s; + } + } +} diff --git a/src/PSRule/Pipeline/TargetObject.cs b/src/PSRule/Pipeline/TargetObject.cs index 2cc5eefacb..9688fc4354 100644 --- a/src/PSRule/Pipeline/TargetObject.cs +++ b/src/PSRule/Pipeline/TargetObject.cs @@ -5,6 +5,7 @@ using System.Collections; using System.Collections.Generic; using System.Management.Automation; +using Newtonsoft.Json.Linq; using PSRule.Data; using PSRule.Definitions.Selectors; @@ -35,6 +36,9 @@ public void SetSelectorResult(SelectorVisitor selector, bool result) } } + /// + /// An object processed by PSRule. + /// public sealed class TargetObject { private readonly Dictionary _Annotations; @@ -46,12 +50,12 @@ internal TargetObject(PSObject o) internal TargetObject(PSObject o, TargetSourceCollection source) { - Value = o; - Value.ConvertTargetInfoProperty(); - Value.ConvertTargetInfoType(); - Source = ReadSourceInfo(source); - Issue = ReadIssueInfo(null); - Path = ReadPath(); + o.ConvertTargetInfoProperty(); + o.ConvertTargetInfoType(); + Source = ReadSourceInfo(o, source); + Issue = ReadIssueInfo(o, null); + Path = ReadPath(o); + Value = Convert(o); _Annotations = new Dictionary(); } @@ -84,26 +88,31 @@ internal Hashtable RequireData() return (T)value; } - private string ReadPath() + private static string ReadPath(PSObject o) { - return Value.GetTargetPath(); + return o.GetTargetPath(); } - private TargetSourceCollection ReadSourceInfo(TargetSourceCollection source) + private static TargetSourceCollection ReadSourceInfo(PSObject o, TargetSourceCollection source) { var result = source ?? new TargetSourceCollection(); - if (ExpressionHelpers.GetBaseObject(Value) is ITargetInfo targetInfo) + if (ExpressionHelpers.GetBaseObject(o) is ITargetInfo targetInfo) result.Add(targetInfo.Source); - result.AddRange(Value.GetSourceInfo()); + result.AddRange(o.GetSourceInfo()); return result; } - private TargetIssueCollection ReadIssueInfo(TargetIssueCollection issue) + private static TargetIssueCollection ReadIssueInfo(PSObject o, TargetIssueCollection issue) { var result = issue ?? new TargetIssueCollection(); - result.AddRange(Value.GetIssueInfo()); + result.AddRange(o.GetIssueInfo()); return result; } + + private static PSObject Convert(PSObject o) + { + return o.BaseObject is JToken token ? JsonHelper.ToPSObject(token) : o; + } } } diff --git a/tests/PSRule.Tests/TargetBinderTests.cs b/tests/PSRule.Tests/TargetBinderTests.cs index ea24f23e91..adaeac92c6 100644 --- a/tests/PSRule.Tests/TargetBinderTests.cs +++ b/tests/PSRule.Tests/TargetBinderTests.cs @@ -50,7 +50,7 @@ public void BindJObject() var m0 = binder.Using("."); Assert.Equal("Name1", m0.TargetName); - Assert.Equal("Newtonsoft.Json.Linq.JObject", m0.TargetType); + Assert.Equal("System.Management.Automation.PSCustomObject", m0.TargetType); } #region Helper methods From 88bc098664c321db9029b2c35170ecbb70e27d77 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Tue, 26 Jul 2022 17:31:06 +1000 Subject: [PATCH 007/156] Added comment documentation to classes #1186 (#1189) --- docs/CHANGELOG-v2.md | 3 + src/PSRule.Benchmark/PSRule.Benchmark.csproj | 1 + src/PSRule.Common.props | 1 + src/PSRule/Common/GitHelper.cs | 2 +- src/PSRule/Common/PipelineWriterExtensions.cs | 19 --- src/PSRule/Configuration/BindingOption.cs | 16 +- src/PSRule/Configuration/ConventionOption.cs | 15 +- src/PSRule/Configuration/ExecutionOption.cs | 3 + src/PSRule/Configuration/IncludeOption.cs | 12 +- src/PSRule/Configuration/InputFormat.cs | 24 ++- src/PSRule/Configuration/InputOption.cs | 10 ++ src/PSRule/Configuration/LanguageMode.cs | 13 +- src/PSRule/Configuration/LoggingOption.cs | 5 +- src/PSRule/Configuration/OutputEncoding.cs | 24 ++- src/PSRule/Configuration/OutputFormat.cs | 25 +++ src/PSRule/Configuration/OutputOption.cs | 10 ++ src/PSRule/Configuration/PSRuleOption.cs | 49 +++++- src/PSRule/Configuration/RepositoryOption.cs | 17 +- src/PSRule/Configuration/RequiresOption.cs | 14 +- src/PSRule/Configuration/RuleOption.cs | 3 + src/PSRule/Data/TargetIssueInfo.cs | 30 +++- src/PSRule/Data/TargetSourceInfo.cs | 2 + src/PSRule/Definitions/ResourceId.cs | 10 +- src/PSRule/Definitions/SourceExtent.cs | 12 ++ src/PSRule/Pipeline/Exceptions.cs | 113 +++++++++++-- src/PSRule/Pipeline/InvokePipelineBuilder.cs | 159 ++++++++++++++++++ src/PSRule/Pipeline/InvokeRulePipeline.cs | 152 ----------------- .../PipelineExtensions.cs | 10 +- .../Pipeline/PipelineWriterExtensions.cs | 10 +- src/PSRule/Rules/RuleOutcomeReason.cs | 3 +- src/PSRule/Rules/RuleRecord.cs | 15 ++ src/PSRule/Runtime/Assert.cs | 108 ++++++++++-- src/PSRule/Runtime/AssertResult.cs | 16 ++ src/PSRule/Runtime/Operand.cs | 33 ++++ src/PSRule/Runtime/PSRule.cs | 50 ++++++ src/PSRule/Runtime/Rule.cs | 8 +- src/PSRule/Runtime/ScopedItem.cs | 5 +- src/PSRule/Runtime/SemanticVersion.cs | 2 +- 38 files changed, 775 insertions(+), 229 deletions(-) delete mode 100644 src/PSRule/Common/PipelineWriterExtensions.cs create mode 100644 src/PSRule/Pipeline/InvokePipelineBuilder.cs rename src/PSRule/{Common => Pipeline}/PipelineExtensions.cs (69%) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index f6893b3ef7..a9e3c09074 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -15,6 +15,9 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers What's changed since pre-release v2.3.0-B0074: +- Engineering: + - Added comment documentation to .NET classes and interfaces by @BernieWhite. + [#1186](https://github.com/microsoft/PSRule/issues/1186) - Bug fixes: - Fixed handling for JSON objects in rules by @BernieWhite. [#1187](https://github.com/microsoft/PSRule/issues/1187) diff --git a/src/PSRule.Benchmark/PSRule.Benchmark.csproj b/src/PSRule.Benchmark/PSRule.Benchmark.csproj index 7434e330fa..d24cf224c5 100644 --- a/src/PSRule.Benchmark/PSRule.Benchmark.csproj +++ b/src/PSRule.Benchmark/PSRule.Benchmark.csproj @@ -7,6 +7,7 @@ {3ec0912f-bfc7-4b53-a1a1-0ba993c6282e} false false + false diff --git a/src/PSRule.Common.props b/src/PSRule.Common.props index ed3277cc6d..68024b2602 100644 --- a/src/PSRule.Common.props +++ b/src/PSRule.Common.props @@ -10,6 +10,7 @@ true snupkg true + true diff --git a/src/PSRule/Common/GitHelper.cs b/src/PSRule/Common/GitHelper.cs index e1b36434d8..bf90100720 100644 --- a/src/PSRule/Common/GitHelper.cs +++ b/src/PSRule/Common/GitHelper.cs @@ -76,7 +76,7 @@ public static bool TryHeadBranch(out string value, string path = null) /// /// Get the target ref. - /// public static bool TryBaseRef(out string value, string path = null) { // Try PSRule diff --git a/src/PSRule/Common/PipelineWriterExtensions.cs b/src/PSRule/Common/PipelineWriterExtensions.cs deleted file mode 100644 index 56acb6188e..0000000000 --- a/src/PSRule/Common/PipelineWriterExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Management.Automation; -using PSRule.Pipeline; - -namespace PSRule -{ - public static class PipelineWriterExtensions - { - public static void WriteDebug(this IPipelineWriter writer, DebugRecord debugRecord) - { - if (debugRecord == null) - return; - - writer.WriteDebug(debugRecord.Message); - } - } -} diff --git a/src/PSRule/Configuration/BindingOption.cs b/src/PSRule/Configuration/BindingOption.cs index c6634e38dc..4671ce62e0 100644 --- a/src/PSRule/Configuration/BindingOption.cs +++ b/src/PSRule/Configuration/BindingOption.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; @@ -24,6 +24,9 @@ public sealed class BindingOption : IEquatable UseQualifiedName = DEFAULT_USEQUALIFIEDNAME }; + /// + /// Creates an empty binding option. + /// public BindingOption() { Field = null; @@ -35,6 +38,10 @@ public BindingOption() UseQualifiedName = null; } + /// + /// Creates a binding option by copying an existing instance. + /// + /// The option instance to copy. public BindingOption(BindingOption option) { if (option == null) @@ -49,11 +56,13 @@ public BindingOption(BindingOption option) UseQualifiedName = option.UseQualifiedName; } + /// public override bool Equals(object obj) { return obj is BindingOption option && Equals(option); } + /// public bool Equals(BindingOption other) { return other != null && @@ -66,6 +75,7 @@ public bool Equals(BindingOption other) UseQualifiedName == other.UseQualifiedName; } + /// public override int GetHashCode() { unchecked // Overflow is fine @@ -122,13 +132,13 @@ internal static BindingOption Combine(BindingOption o1, BindingOption o2) public bool? PreferTargetInfo { get; set; } /// - /// One or more property names to use to bind TargetName. + /// Property names to use to bind TargetName. /// [DefaultValue(null)] public string[] TargetName { get; set; } /// - /// One or more property names to use to bind TargetType. + /// Property names to use to bind TargetType. /// [DefaultValue(null)] public string[] TargetType { get; set; } diff --git a/src/PSRule/Configuration/ConventionOption.cs b/src/PSRule/Configuration/ConventionOption.cs index 6d3d5b128e..d6e98f72a5 100644 --- a/src/PSRule/Configuration/ConventionOption.cs +++ b/src/PSRule/Configuration/ConventionOption.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; @@ -17,11 +17,18 @@ public sealed class ConventionOption : IEquatable }; + /// + /// Creates an empty convention option. + /// public ConventionOption() { Include = null; } + /// + /// Creates a convertion option by copying an existing instance. + /// + /// The option instance to copy. public ConventionOption(ConventionOption option) { if (option == null) @@ -30,17 +37,20 @@ public ConventionOption(ConventionOption option) Include = option.Include; } + /// public override bool Equals(object obj) { return obj is ConventionOption option && Equals(option); } + /// public bool Equals(ConventionOption other) { return other != null && Include == other.Include; } + /// public override int GetHashCode() { unchecked // Overflow is fine @@ -60,6 +70,9 @@ internal static ConventionOption Combine(ConventionOption o1, ConventionOption o return result; } + /// + /// Conventions by name to use when executing PSRule. + /// [DefaultValue(null)] public string[] Include { get; set; } diff --git a/src/PSRule/Configuration/ExecutionOption.cs b/src/PSRule/Configuration/ExecutionOption.cs index 7bcaade654..f595816b67 100644 --- a/src/PSRule/Configuration/ExecutionOption.cs +++ b/src/PSRule/Configuration/ExecutionOption.cs @@ -52,11 +52,13 @@ public ExecutionOption(ExecutionOption option) InvariantCultureWarning = option.InvariantCultureWarning; } + /// public override bool Equals(object obj) { return obj is ExecutionOption option && Equals(option); } + /// public bool Equals(ExecutionOption other) { return other != null && @@ -68,6 +70,7 @@ public bool Equals(ExecutionOption other) InvariantCultureWarning == other.InvariantCultureWarning; } + /// public override int GetHashCode() { unchecked // Overflow is fine diff --git a/src/PSRule/Configuration/IncludeOption.cs b/src/PSRule/Configuration/IncludeOption.cs index a9f71ab2b9..3f0eb03fbf 100644 --- a/src/PSRule/Configuration/IncludeOption.cs +++ b/src/PSRule/Configuration/IncludeOption.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; @@ -21,12 +21,19 @@ public sealed class IncludeOption : IEquatable Module = DEFAULT_MODULE, }; + /// + /// Create an empty include option. + /// public IncludeOption() { Path = null; Module = null; } + /// + /// Create an include option by copying an existing instance. + /// + /// The option instance to copy. public IncludeOption(IncludeOption option) { if (option == null) @@ -36,11 +43,13 @@ public IncludeOption(IncludeOption option) Module = option.Module; } + /// public override bool Equals(object obj) { return obj is IncludeOption option && Equals(option); } + /// public bool Equals(IncludeOption other) { return other != null && @@ -48,6 +57,7 @@ public bool Equals(IncludeOption other) Module == other.Module; } + /// public override int GetHashCode() { unchecked // Overflow is fine diff --git a/src/PSRule/Configuration/InputFormat.cs b/src/PSRule/Configuration/InputFormat.cs index 5b02619416..dc9f8e4006 100644 --- a/src/PSRule/Configuration/InputFormat.cs +++ b/src/PSRule/Configuration/InputFormat.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Newtonsoft.Json; @@ -8,22 +8,44 @@ namespace PSRule.Configuration { /// /// The formats to convert input from. + /// /// [JsonConverter(typeof(StringEnumConverter))] public enum InputFormat { + /// + /// Treat strings as plain text and do not deserialize files. + /// None = 0, + /// + /// Deserialize as one or more YAML objects. + /// Yaml = 1, + /// + /// Deserialize as one or more JSON objects. + /// Json = 2, + /// + /// Deserialize as a markdown object. + /// Markdown = 3, + /// + /// Deserialize as a PowerShell data object. + /// PowerShellData = 4, + /// + /// Files are treated as objects and not deserialized. + /// File = 5, + /// + /// Detect format based on file extension. This is the default. + /// Detect = 255 } } diff --git a/src/PSRule/Configuration/InputOption.cs b/src/PSRule/Configuration/InputOption.cs index 545d5f8296..70fad4bd02 100644 --- a/src/PSRule/Configuration/InputOption.cs +++ b/src/PSRule/Configuration/InputOption.cs @@ -31,6 +31,9 @@ public sealed class InputOption : IEquatable TargetType = DEFAULT_TARGETTYPE, }; + /// + /// Creates an empty input option. + /// public InputOption() { Format = null; @@ -42,6 +45,10 @@ public InputOption() TargetType = null; } + /// + /// Creates a input option by copying an existing instance. + /// + /// The option instance to copy. public InputOption(InputOption option) { if (option == null) @@ -56,11 +63,13 @@ public InputOption(InputOption option) TargetType = option.TargetType; } + /// public override bool Equals(object obj) { return obj is InputOption option && Equals(option); } + /// public bool Equals(InputOption other) { return other != null && @@ -73,6 +82,7 @@ public bool Equals(InputOption other) TargetType == other.TargetType; } + /// public override int GetHashCode() { unchecked // Overflow is fine diff --git a/src/PSRule/Configuration/LanguageMode.cs b/src/PSRule/Configuration/LanguageMode.cs index e9c96f288c..53a4653ec0 100644 --- a/src/PSRule/Configuration/LanguageMode.cs +++ b/src/PSRule/Configuration/LanguageMode.cs @@ -1,12 +1,23 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. namespace PSRule.Configuration { + /// + /// Configures the language mode PowerShell code executes as within PSRule runtime. + /// Does not affect YAML or JSON expressions. + /// + /// public enum LanguageMode { + /// + /// PowerShell code executes unconstrained. + /// FullLanguage = 0, + /// + /// PowerShell code executes in constrained language mode that restricts the types and methods that can be used. + /// ConstrainedLanguage = 1 } } diff --git a/src/PSRule/Configuration/LoggingOption.cs b/src/PSRule/Configuration/LoggingOption.cs index 978c04f113..2fa20903f5 100644 --- a/src/PSRule/Configuration/LoggingOption.cs +++ b/src/PSRule/Configuration/LoggingOption.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; @@ -42,11 +42,13 @@ public LoggingOption(LoggingOption option) RulePass = option.RulePass; } + /// public override bool Equals(object obj) { return obj is LoggingOption option && Equals(option); } + /// public bool Equals(LoggingOption other) { return other != null && @@ -56,6 +58,7 @@ public bool Equals(LoggingOption other) RulePass == other.RulePass; } + /// public override int GetHashCode() { unchecked // Overflow is fine diff --git a/src/PSRule/Configuration/OutputEncoding.cs b/src/PSRule/Configuration/OutputEncoding.cs index a510deaace..a24e9a3a99 100644 --- a/src/PSRule/Configuration/OutputEncoding.cs +++ b/src/PSRule/Configuration/OutputEncoding.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Newtonsoft.Json; @@ -6,19 +6,41 @@ namespace PSRule.Configuration { + /// + /// The encoding format to convert output to. + /// + /// [JsonConverter(typeof(StringEnumConverter))] public enum OutputEncoding { + /// + /// UTF-8 with Byte Order Mark (BOM). This is the default. + /// Default = 0, + /// + /// UTF-8 without Byte Order Mark (BOM). + /// UTF8, + /// + /// UTF-7. + /// UTF7, + /// + /// Unicode. Same as UTF-16. + /// Unicode, + /// + /// UTF-32. + /// UTF32, + /// + /// ASCII. + /// ASCII } } diff --git a/src/PSRule/Configuration/OutputFormat.cs b/src/PSRule/Configuration/OutputFormat.cs index 4c4c508591..e6a0a736c9 100644 --- a/src/PSRule/Configuration/OutputFormat.cs +++ b/src/PSRule/Configuration/OutputFormat.cs @@ -8,24 +8,49 @@ namespace PSRule.Configuration { /// /// The formats to return results in. + /// /// [JsonConverter(typeof(StringEnumConverter))] public enum OutputFormat { + /// + /// Output is presented as an object using PowerShell defaults. This is the default. + /// None = 0, + /// + /// Output is serialized as YAML. + /// Yaml = 1, + /// + /// Output is serialized as JSON. + /// Json = 2, + /// + /// Output is serialized as NUnit3 (XML). + /// NUnit3 = 3, + /// + /// Output is serialized as a comma-separated values (CSV). + /// Csv = 4, + /// + /// Output is presented using the wide table format, which includes reason and wraps columns. + /// Wide = 5, + /// + /// Output is serialized as Markdown. + /// Markdown = 6, + /// + /// Output is serialized as SARIF. + /// Sarif = 7 } } diff --git a/src/PSRule/Configuration/OutputOption.cs b/src/PSRule/Configuration/OutputOption.cs index 5b8c39c7af..4b3cd1e7f4 100644 --- a/src/PSRule/Configuration/OutputOption.cs +++ b/src/PSRule/Configuration/OutputOption.cs @@ -36,6 +36,9 @@ public sealed class OutputOption : IEquatable Style = DEFAULT_STYLE, }; + /// + /// Creates an empty output option. + /// public OutputOption() { As = null; @@ -50,6 +53,10 @@ public OutputOption() Style = null; } + /// + /// Creates a output option by copying an existing instance. + /// + /// The option instance to copy. public OutputOption(OutputOption option) { if (option == null) @@ -68,11 +75,13 @@ public OutputOption(OutputOption option) Style = option.Style; } + /// public override bool Equals(object obj) { return obj is OutputOption option && Equals(option); } + /// public bool Equals(OutputOption other) { return other != null && @@ -89,6 +98,7 @@ public bool Equals(OutputOption other) Style == other.Style; } + /// public override int GetHashCode() { unchecked // Overflow is fine diff --git a/src/PSRule/Configuration/PSRuleOption.cs b/src/PSRule/Configuration/PSRuleOption.cs index 0e743ddfad..5e8b4cd42e 100644 --- a/src/PSRule/Configuration/PSRuleOption.cs +++ b/src/PSRule/Configuration/PSRuleOption.cs @@ -290,6 +290,14 @@ private static PSRuleOption FromYaml(string path, string yaml) return option; } + /// + /// Read PSRule options from environment variables. + /// + /// An existing options object to set. If null an empty options object is used. + /// An options object. + /// + /// Any environment variables that are set will override options set in the specified object. + /// private static PSRuleOption FromEnvironment(PSRuleOption option) { if (option == null) @@ -309,10 +317,18 @@ private static PSRuleOption FromEnvironment(PSRuleOption option) return option; } + /// + /// Read PSRule options from a hashtable. + /// + /// A hashtable to read options from. + /// An options object. + /// + /// A null or empty hashtable will return an empty options object. + /// public static PSRuleOption FromHashtable(Hashtable hashtable) { var option = new PSRuleOption(); - if (hashtable == null) + if (hashtable == null || hashtable.Count == 0) return option; // Start loading matching values @@ -346,18 +362,29 @@ public static void UseExecutionContext(EngineIntrinsics executionContext) _GetWorkingPath = () => executionContext.SessionState.Path.CurrentFileSystemLocation.Path; } + /// + /// Configures PSRule to use the culture of the current thread at runtime. + /// [DebuggerStepThrough] public static void UseCurrentCulture() { UseCurrentCulture(Thread.CurrentThread.CurrentCulture); } + /// + /// Configures PSRule to use the specified culture at runtime. + /// + /// A valid culture. [DebuggerStepThrough] public static void UseCurrentCulture(string culture) { UseCurrentCulture(CultureInfo.CreateSpecificCulture(culture)); } + /// + /// Configures PSRule to use the specified culture at runtime. + /// + /// A valid culture. public static void UseCurrentCulture(CultureInfo culture) { _CurrentCulture = culture; @@ -376,7 +403,8 @@ public static CultureInfo GetCurrentCulture() /// /// Convert from hashtable to options by processing key values. This enables -Option @{ } from PowerShell. /// - /// + /// A hashtable to read options from. + /// An options object. public static implicit operator PSRuleOption(Hashtable hashtable) { return FromHashtable(hashtable); @@ -386,16 +414,19 @@ public static implicit operator PSRuleOption(Hashtable hashtable) /// Convert from string to options by loading the yaml file from disk. This enables -Option '.\ps-rule.yaml' from PowerShell. /// /// A file or directory to read options from. + /// An options object. public static implicit operator PSRuleOption(string path) { return FromFile(path); } + /// public override bool Equals(object obj) { return obj is PSRuleOption option && Equals(option); } + /// public bool Equals(PSRuleOption other) { return other != null && @@ -413,6 +444,7 @@ public bool Equals(PSRuleOption other) Rule == other.Rule; } + /// public override int GetHashCode() { unchecked // Overflow is fine @@ -463,8 +495,9 @@ public static string GetFilePath(string path) /// /// Get a full path instead of a relative path that may be passed from PowerShell. /// - /// - /// + /// A full or relative path. + /// When set to true the returned path uses forward slashes instead of backslashes. + /// A absolute path. internal static string GetRootedPath(string path, bool normalize = false, string basePath = null) { basePath ??= GetWorkingPath(); @@ -473,8 +506,14 @@ internal static string GetRootedPath(string path, bool normalize = false, string } /// - /// Get a full path instead of a relative path that may be passed from PowerShell. + /// Get a full base path instead of a relative path that may be passed from PowerShell. /// + /// A full or relative path. + /// When set to true the returned path uses forward slashes instead of backslashes. + /// A absolute base path. + /// + /// A base path always includes a trailing /. + /// internal static string GetRootedBasePath(string path, bool normalize = false) { var rootedPath = GetRootedPath(path); diff --git a/src/PSRule/Configuration/RepositoryOption.cs b/src/PSRule/Configuration/RepositoryOption.cs index a2a2aef368..320c6b23d1 100644 --- a/src/PSRule/Configuration/RepositoryOption.cs +++ b/src/PSRule/Configuration/RepositoryOption.cs @@ -8,7 +8,7 @@ namespace PSRule.Configuration { /// - /// Options that configure the execution sandbox. + /// Options that repository properties that are used by PSRule. /// public sealed class RepositoryOption : IEquatable { @@ -17,11 +17,18 @@ public sealed class RepositoryOption : IEquatable }; + /// + /// Create an empty repository option. + /// public RepositoryOption() { Url = null; } + /// + /// Create a repository option by copying an existing instance. + /// + /// The option instance to copy. public RepositoryOption(RepositoryOption option) { if (option == null) @@ -30,17 +37,21 @@ public RepositoryOption(RepositoryOption option) Url = option.Url; } + /// public override bool Equals(object obj) { - return obj is RepositoryOption option && Equals(option); + return obj is RepositoryOption option && + Equals(option); } + /// public bool Equals(RepositoryOption other) { return other != null && Url == other.Url; } + /// public override int GetHashCode() { unchecked // Overflow is fine @@ -61,7 +72,7 @@ internal static RepositoryOption Combine(RepositoryOption o1, RepositoryOption o } /// - /// Determines if a warning is raised when an alias to a resource is used. + /// Configures the repository URL to report in output. /// [DefaultValue(null)] public string Url { get; set; } diff --git a/src/PSRule/Configuration/RequiresOption.cs b/src/PSRule/Configuration/RequiresOption.cs index 8609e002cd..c409c459eb 100644 --- a/src/PSRule/Configuration/RequiresOption.cs +++ b/src/PSRule/Configuration/RequiresOption.cs @@ -1,10 +1,15 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Collections.Generic; namespace PSRule.Configuration { + /// + /// Specifies module version constraints for running PSRule. + /// When set, PSRule will error if a module version is used that does not satisfy the requirements. + /// + /// public sealed class RequiresOption : KeyMapDictionary { private const string ENVIRONMENT_PREFIX = "PSRULE_REQUIRES_"; @@ -12,9 +17,16 @@ public sealed class RequiresOption : KeyMapDictionary private const char UNDERSCORE = '_'; private const char DOT = '.'; + /// + /// Creates an empty requires option. + /// public RequiresOption() : base() { } + /// + /// Creates a requires option by copying an existing instance. + /// + /// The option instance to copy. internal RequiresOption(RequiresOption option) : base(option) { } diff --git a/src/PSRule/Configuration/RuleOption.cs b/src/PSRule/Configuration/RuleOption.cs index 4e4173215a..8c15f0785e 100644 --- a/src/PSRule/Configuration/RuleOption.cs +++ b/src/PSRule/Configuration/RuleOption.cs @@ -37,11 +37,13 @@ public RuleOption(RuleOption option) Tag = option.Tag; } + /// public override bool Equals(object obj) { return obj is RuleOption option && Equals(option); } + /// public bool Equals(RuleOption other) { return other != null && @@ -52,6 +54,7 @@ public bool Equals(RuleOption other) Tag == other.Tag; } + /// public override int GetHashCode() { unchecked // Overflow is fine diff --git a/src/PSRule/Data/TargetIssueInfo.cs b/src/PSRule/Data/TargetIssueInfo.cs index 22a26a5f6a..e41518fcdf 100644 --- a/src/PSRule/Data/TargetIssueInfo.cs +++ b/src/PSRule/Data/TargetIssueInfo.cs @@ -1,35 +1,55 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Management.Automation; using Newtonsoft.Json; namespace PSRule.Data { - public sealed class TargetIssueInfo + /// + /// An issue reported by a downstream tool. + /// + public sealed class TargetIssueInfo : IEquatable { private const string PROPERTY_TYPE = "type"; private const string PROPERTY_NAME = "name"; private const string PROPERTY_PATH = "path"; private const string PROPERTY_MESSAGE = "message"; + /// + /// Create an empty issue. + /// public TargetIssueInfo() { // Do nothing } + /// + /// The type of issue. + /// [JsonProperty(PropertyName = PROPERTY_TYPE)] public string Type { get; internal set; } + /// + /// The name of the issue. + /// [JsonProperty(PropertyName = PROPERTY_NAME)] public string Name { get; internal set; } + /// + /// The object path reported by the issue. + /// [JsonProperty(PropertyName = PROPERTY_PATH)] public string Path { get; internal set; } + /// + /// A localized message describing the issue. + /// [JsonProperty(PropertyName = PROPERTY_MESSAGE)] public string Message { get; internal set; } + /// public bool Equals(TargetIssueInfo other) { return other != null && @@ -39,11 +59,13 @@ public bool Equals(TargetIssueInfo other) Message == other.Message; } + /// public override bool Equals(object obj) { return obj is TargetIssueInfo info && Equals(info); } + /// public override int GetHashCode() { unchecked // Overflow is fine @@ -57,11 +79,17 @@ public override int GetHashCode() } } + /// + /// Create an issue from a structured object. + /// public static TargetIssueInfo Create(object o) { return o is PSObject pso ? Create(pso) : null; } + /// + /// Create an issue from a structured object. + /// public static TargetIssueInfo Create(PSObject o) { var result = new TargetIssueInfo(); diff --git a/src/PSRule/Data/TargetSourceInfo.cs b/src/PSRule/Data/TargetSourceInfo.cs index 8ebf4bf726..3587a1f0f1 100644 --- a/src/PSRule/Data/TargetSourceInfo.cs +++ b/src/PSRule/Data/TargetSourceInfo.cs @@ -64,11 +64,13 @@ public bool Equals(TargetSourceInfo other) Type == other.Type; } + /// public override bool Equals(object obj) { return obj is TargetSourceInfo info && Equals(info); } + /// public override int GetHashCode() { unchecked // Overflow is fine diff --git a/src/PSRule/Definitions/ResourceId.cs b/src/PSRule/Definitions/ResourceId.cs index 35028be81e..4497fe9c77 100644 --- a/src/PSRule/Definitions/ResourceId.cs +++ b/src/PSRule/Definitions/ResourceId.cs @@ -28,7 +28,7 @@ internal enum ResourceIdKind /// A unique identifier for a resource. /// [DebuggerDisplay("{Value}")] - public struct ResourceId + public struct ResourceId : IEquatable, IEquatable { private const char SCOPE_SEPARATOR = '\\'; @@ -54,22 +54,29 @@ internal ResourceId(string scope, string name, ResourceIdKind kind) internal ResourceIdKind Kind { get; } + /// + /// Converts the resource identifier to a string. + /// + /// public override string ToString() { return Value; } + /// [DebuggerStepThrough] public override int GetHashCode() { return _HashCode; } + /// public override bool Equals(object obj) { return obj is ResourceId id && Equals(id); } + /// public bool Equals(ResourceId id) { return _HashCode == id._HashCode && @@ -77,6 +84,7 @@ public bool Equals(ResourceId id) EqualOrNull(Name, id.Name); } + /// public bool Equals(string id) { return TryParse(id, out var scope, out var name) && diff --git a/src/PSRule/Definitions/SourceExtent.cs b/src/PSRule/Definitions/SourceExtent.cs index 154119b0d9..1ef6263ea6 100644 --- a/src/PSRule/Definitions/SourceExtent.cs +++ b/src/PSRule/Definitions/SourceExtent.cs @@ -3,12 +3,24 @@ namespace PSRule.Definitions { + /// + /// A source location for a PSRule expression. + /// public interface ISourceExtent { + /// + /// The source file path. + /// string File { get; } + /// + /// The first line of the expression. + /// int? Line { get; } + /// + /// The first position of the expression. + /// int? Position { get; } } diff --git a/src/PSRule/Pipeline/Exceptions.cs b/src/PSRule/Pipeline/Exceptions.cs index ed8b28a613..d8ca09e84f 100644 --- a/src/PSRule/Pipeline/Exceptions.cs +++ b/src/PSRule/Pipeline/Exceptions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; @@ -13,15 +13,27 @@ namespace PSRule.Pipeline /// public abstract class PipelineException : Exception { + /// + /// Initialize a new instance of a PSRule exception. + /// protected PipelineException() : base() { } + /// + /// Initialize a new instance of a PSRule exception. + /// protected PipelineException(string message) : base(message) { } + /// + /// Initialize a new instance of a PSRule exception. + /// protected PipelineException(string message, Exception innerException) : base(message, innerException) { } + /// + /// Initialize a new instance of a PSRule exception. + /// protected PipelineException(SerializationInfo info, StreamingContext context) : base(info, context) { } } @@ -31,15 +43,27 @@ protected PipelineException(SerializationInfo info, StreamingContext context) /// public abstract class RuntimeException : PipelineException { + /// + /// Initialize a new instance of a PSRule exception that is thrown during runtime. + /// protected RuntimeException() : base() { } + /// + /// Initialize a new instance of a PSRule exception that is thrown during runtime. + /// protected RuntimeException(string message) : base(message) { } + /// + /// Initialize a new instance of a PSRule exception that is thrown during runtime. + /// protected RuntimeException(string message, Exception innerException) : base(message, innerException) { } + /// + /// Initialize a new instance of a PSRule exception that is thrown during runtime. + /// protected RuntimeException(Exception innerException, InvocationInfo invocationInfo, string ruleId) : base(innerException?.Message, innerException) { @@ -47,11 +71,20 @@ protected RuntimeException(Exception innerException, InvocationInfo invocationIn RuleId = ruleId; } + /// + /// Initialize a new instance of a PSRule exception that is thrown during runtime. + /// protected RuntimeException(SerializationInfo info, StreamingContext context) : base(info, context) { } + /// + /// Additional information about the invocation when executing PowerShell language elements. + /// public InvocationInfo CommandInvocation { get; } + /// + /// A unique identifier for the rule that was executing if currently within the context of a rule. + /// public string RuleId { get; } } @@ -85,6 +118,7 @@ public PipelineBuilderException(string message, Exception innerException) private PipelineBuilderException(SerializationInfo info, StreamingContext context) : base(info, context) { } + /// [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] public override void GetObjectData(SerializationInfo info, StreamingContext context) { @@ -108,6 +142,9 @@ public PipelineSerializationException() { } + /// + /// Creates a serialization exception. + /// internal PipelineSerializationException(string message, string path, Exception innerException) : this(message, innerException) { @@ -131,6 +168,9 @@ public PipelineSerializationException(string message, Exception innerException) { } + /// + /// Creates a serialization exception. + /// private PipelineSerializationException(SerializationInfo info, StreamingContext context) : base(info, context) { } @@ -140,6 +180,7 @@ private PipelineSerializationException(SerializationInfo info, StreamingContext /// public string Path { get; } + /// [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] public override void GetObjectData(SerializationInfo info, StreamingContext context) { @@ -157,44 +198,59 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont public sealed class ParseException : PipelineException { /// - /// Creates a rule exception. + /// Creates a parser exception. /// public ParseException() { } + /// + /// Creates a parser exception. + /// public ParseException(string message) : base(message) { } + /// + /// Creates a parser exception. + /// public ParseException(string message, Exception innerException) : base(message, innerException) { } /// - /// Creates a rule exception. + /// Creates a parser exception. /// /// The detail of the exception. + /// A unique identifier related to the exception. internal ParseException(string message, string errorId) : base(message) { ErrorId = errorId; } /// - /// Creates a rule exception. + /// Creates a parser exception. /// /// The detail of the exception. + /// A unique identifier related to the exception. /// A nested exception that caused the issue. internal ParseException(string message, string errorId, Exception innerException) : base(message, innerException) { ErrorId = errorId; } + /// + /// Creates a parser exception. + /// private ParseException(SerializationInfo info, StreamingContext context) : base(info, context) { ErrorId = info.GetString("ErrorId"); } + /// + /// An associated identifier related to why the exception was thrown. + /// public string ErrorId { get; } + /// [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] public override void GetObjectData(SerializationInfo info, StreamingContext context) { @@ -234,17 +290,27 @@ public RuleException(string message) public RuleException(string message, Exception innerException) : base(message, innerException) { } + /// + /// Creates a rule runtime exception. + /// internal RuleException(Exception innerException, InvocationInfo invocationInfo, string ruleId, ErrorRecord errorRecord) : base(innerException, invocationInfo, ruleId) { ErrorRecord = errorRecord; } + /// + /// Creates a rule runtime exception. + /// private RuleException(SerializationInfo info, StreamingContext context) : base(info, context) { } + /// + /// An associated error record related to the exception. + /// public ErrorRecord ErrorRecord { get; } + /// [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] public override void GetObjectData(SerializationInfo info, StreamingContext context) { @@ -277,10 +343,16 @@ public PipelineConfigurationException(string optionName, string message) : base( { } + /// + /// Creates a pipeline configuration exception. + /// public PipelineConfigurationException(string message) : base(message) { } + /// + /// Creates a pipeline configuration exception. + /// public PipelineConfigurationException(string message, Exception innerException) : base(message, innerException) { } @@ -295,10 +367,12 @@ public PipelineConfigurationException(string optionName, string message, Excepti { } + /// private PipelineConfigurationException(SerializationInfo info, StreamingContext context) : base(info, context) { } + /// [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] public override void GetObjectData(SerializationInfo info, StreamingContext context) { @@ -309,32 +383,33 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont } } + /// + /// An exception thrown by PSRule when the calling PowerShell environment should fail because one or more rules failed or errored. + /// [Serializable] public sealed class FailPipelineException : PipelineException { - /// - /// Creates a rule runtime exception. - /// + /// public FailPipelineException() { } - /// - /// Creates a rule runtime exception. - /// - /// The detail of the exception. + /// public FailPipelineException(string message) : base(message) { } + /// public FailPipelineException(string message, Exception innerException) : base(message, innerException) { } + /// private FailPipelineException(SerializationInfo info, StreamingContext context) : base(info, context) { } + /// [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] public override void GetObjectData(SerializationInfo info, StreamingContext context) { @@ -345,32 +420,34 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont } } + /// + /// An exception thrown by PSRule when a runtime property or method is used outside of the intended scope. + /// Avoid using runtime variables outside of a PSRule pipeline. + /// [Serializable] public sealed class RuntimeScopeException : PipelineException { - /// - /// Creates a rule runtime exception. - /// + /// public RuntimeScopeException() { } - /// - /// Creates a rule runtime exception. - /// - /// The detail of the exception. + /// public RuntimeScopeException(string message) : base(message) { } + /// public RuntimeScopeException(string message, Exception innerException) : base(message, innerException) { } + /// private RuntimeScopeException(SerializationInfo info, StreamingContext context) : base(info, context) { } + /// [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] public override void GetObjectData(SerializationInfo info, StreamingContext context) { diff --git a/src/PSRule/Pipeline/InvokePipelineBuilder.cs b/src/PSRule/Pipeline/InvokePipelineBuilder.cs new file mode 100644 index 0000000000..80fa3ee8ba --- /dev/null +++ b/src/PSRule/Pipeline/InvokePipelineBuilder.cs @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using PSRule.Configuration; +using PSRule.Data; + +namespace PSRule.Pipeline +{ + public interface IInvokePipelineBuilder : IPipelineBuilder + { + /// + /// Configures paths that will be scanned for input. + /// + /// An array of relative or absolute path specs to be scanned. Directories will be recursively scanned for all files not excluded matching the file path spec. + void InputPath(string[] path); + + /// + /// Configures a variable that will recieve all results in addition to the host context. + /// + /// The name of the variable to set. + void ResultVariable(string variableName); + } + + internal abstract class InvokePipelineBuilderBase : PipelineBuilderBase, IInvokePipelineBuilder + { + protected InputFileInfo[] _InputPath; + protected string _ResultVariableName; + + protected InvokePipelineBuilderBase(Source[] source, IHostContext hostContext) + : base(source, hostContext) + { + _InputPath = null; + } + + public void InputPath(string[] path) + { + if (path == null || path.Length == 0) + return; + + var builder = new InputPathBuilder(GetOutput(), PSRuleOption.GetWorkingPath(), "*", GetInputFilter()); + builder.Add(path); + _InputPath = builder.Build(); + } + + public void ResultVariable(string variableName) + { + _ResultVariableName = variableName; + } + + public override IPipelineBuilder Configure(PSRuleOption option) + { + if (option == null) + return this; + + base.Configure(option); + + Option.Execution.InconclusiveWarning = option.Execution.InconclusiveWarning ?? ExecutionOption.Default.InconclusiveWarning; + Option.Execution.NotProcessedWarning = option.Execution.NotProcessedWarning ?? ExecutionOption.Default.NotProcessedWarning; + Option.Execution.SuppressedRuleWarning = option.Execution.SuppressedRuleWarning ?? ExecutionOption.Default.SuppressedRuleWarning; + Option.Execution.InvariantCultureWarning = option.Execution.InvariantCultureWarning ?? ExecutionOption.Default.InvariantCultureWarning; + + Option.Logging.RuleFail = option.Logging.RuleFail ?? LoggingOption.Default.RuleFail; + Option.Logging.RulePass = option.Logging.RulePass ?? LoggingOption.Default.RulePass; + Option.Logging.LimitVerbose = option.Logging.LimitVerbose; + Option.Logging.LimitDebug = option.Logging.LimitDebug; + + Option.Output.As = option.Output.As ?? OutputOption.Default.As; + Option.Output.Culture = GetCulture(option.Output.Culture); + Option.Output.Encoding = option.Output.Encoding ?? OutputOption.Default.Encoding; + Option.Output.Format = option.Output.Format ?? OutputOption.Default.Format; + Option.Output.Path = option.Output.Path ?? OutputOption.Default.Path; + Option.Output.JsonIndent = NormalizeJsonIndentRange(option.Output.JsonIndent); + + if (option.Rule != null) + Option.Rule = new RuleOption(option.Rule); + + if (option.Configuration != null) + Option.Configuration = new ConfigurationOption(option.Configuration); + + ConfigureBinding(option); + Option.Requires = new RequiresOption(option.Requires); + if (option.Suppression.Count > 0) + Option.Suppression = new SuppressionOption(option.Suppression); + + return this; + } + + public override IPipeline Build(IPipelineWriter writer = null) + { + return !RequireModules() || !RequireSources() + ? null + : (IPipeline)new InvokeRulePipeline(PrepareContext(BindTargetNameHook, BindTargetTypeHook, BindFieldHook), Source, writer ?? PrepareWriter(), Option.Output.Outcome.Value); + } + + protected override PipelineReader PrepareReader() + { + if (!string.IsNullOrEmpty(Option.Input.ObjectPath)) + { + AddVisitTargetObjectAction((sourceObject, next) => + { + return PipelineReceiverActions.ReadObjectPath(sourceObject, next, Option.Input.ObjectPath, true); + }); + } + + if (Option.Input.Format == InputFormat.Yaml) + { + AddVisitTargetObjectAction((sourceObject, next) => + { + return PipelineReceiverActions.ConvertFromYaml(sourceObject, next); + }); + } + else if (Option.Input.Format == InputFormat.Json) + { + AddVisitTargetObjectAction((sourceObject, next) => + { + return PipelineReceiverActions.ConvertFromJson(sourceObject, next); + }); + } + else if (Option.Input.Format == InputFormat.Markdown) + { + AddVisitTargetObjectAction((sourceObject, next) => + { + return PipelineReceiverActions.ConvertFromMarkdown(sourceObject, next); + }); + } + else if (Option.Input.Format == InputFormat.PowerShellData) + { + AddVisitTargetObjectAction((sourceObject, next) => + { + return PipelineReceiverActions.ConvertFromPowerShellData(sourceObject, next); + }); + } + else if (Option.Input.Format == InputFormat.File) + { + AddVisitTargetObjectAction((sourceObject, next) => + { + return PipelineReceiverActions.ConvertFromGitHead(sourceObject, next); + }); + } + else if (Option.Input.Format == InputFormat.Detect && _InputPath != null) + { + AddVisitTargetObjectAction((sourceObject, next) => + { + return PipelineReceiverActions.DetectInputFormat(sourceObject, next); + }); + } + return new PipelineReader(VisitTargetObject, _InputPath, GetInputObjectSourceFilter()); + } + } + + /// + /// A helper to construct the pipeline for Invoke-PSRule. + /// + internal sealed class InvokeRulePipelineBuilder : InvokePipelineBuilderBase + { + internal InvokeRulePipelineBuilder(Source[] source, IHostContext hostContext) + : base(source, hostContext) { } + } +} diff --git a/src/PSRule/Pipeline/InvokeRulePipeline.cs b/src/PSRule/Pipeline/InvokeRulePipeline.cs index 189578c63a..536326cbc1 100644 --- a/src/PSRule/Pipeline/InvokeRulePipeline.cs +++ b/src/PSRule/Pipeline/InvokeRulePipeline.cs @@ -6,164 +6,12 @@ using System.Linq; using System.Management.Automation; using PSRule.Configuration; -using PSRule.Data; using PSRule.Definitions; using PSRule.Host; using PSRule.Rules; namespace PSRule.Pipeline { - public interface IInvokePipelineBuilder : IPipelineBuilder - { - /// - /// Configures paths that will be scanned for input. - /// - /// An array of relative or absolute path specs to be scanned. Directories will be recursively scanned for all files not excluded matching the file path spec. - void InputPath(string[] path); - - /// - /// Configures a variable that will recieve all results in addition to the host context. - /// - /// The name of the variable to set. - void ResultVariable(string variableName); - } - - internal abstract class InvokePipelineBuilderBase : PipelineBuilderBase, IInvokePipelineBuilder - { - protected InputFileInfo[] _InputPath; - protected string _ResultVariableName; - - protected InvokePipelineBuilderBase(Source[] source, IHostContext hostContext) - : base(source, hostContext) - { - _InputPath = null; - } - - public void InputPath(string[] path) - { - if (path == null || path.Length == 0) - return; - - var builder = new InputPathBuilder(GetOutput(), PSRuleOption.GetWorkingPath(), "*", GetInputFilter()); - builder.Add(path); - _InputPath = builder.Build(); - } - - public void ResultVariable(string variableName) - { - _ResultVariableName = variableName; - } - - public override IPipelineBuilder Configure(PSRuleOption option) - { - if (option == null) - return this; - - base.Configure(option); - - Option.Execution.InconclusiveWarning = option.Execution.InconclusiveWarning ?? ExecutionOption.Default.InconclusiveWarning; - Option.Execution.NotProcessedWarning = option.Execution.NotProcessedWarning ?? ExecutionOption.Default.NotProcessedWarning; - Option.Execution.SuppressedRuleWarning = option.Execution.SuppressedRuleWarning ?? ExecutionOption.Default.SuppressedRuleWarning; - Option.Execution.InvariantCultureWarning = option.Execution.InvariantCultureWarning ?? ExecutionOption.Default.InvariantCultureWarning; - - Option.Logging.RuleFail = option.Logging.RuleFail ?? LoggingOption.Default.RuleFail; - Option.Logging.RulePass = option.Logging.RulePass ?? LoggingOption.Default.RulePass; - Option.Logging.LimitVerbose = option.Logging.LimitVerbose; - Option.Logging.LimitDebug = option.Logging.LimitDebug; - - Option.Output.As = option.Output.As ?? OutputOption.Default.As; - Option.Output.Culture = GetCulture(option.Output.Culture); - Option.Output.Encoding = option.Output.Encoding ?? OutputOption.Default.Encoding; - Option.Output.Format = option.Output.Format ?? OutputOption.Default.Format; - Option.Output.Path = option.Output.Path ?? OutputOption.Default.Path; - Option.Output.JsonIndent = NormalizeJsonIndentRange(option.Output.JsonIndent); - - if (option.Rule != null) - Option.Rule = new RuleOption(option.Rule); - - if (option.Configuration != null) - Option.Configuration = new ConfigurationOption(option.Configuration); - - ConfigureBinding(option); - Option.Requires = new RequiresOption(option.Requires); - if (option.Suppression.Count > 0) - Option.Suppression = new SuppressionOption(option.Suppression); - - return this; - } - - public override IPipeline Build(IPipelineWriter writer = null) - { - return !RequireModules() || !RequireSources() - ? null - : (IPipeline)new InvokeRulePipeline(PrepareContext(BindTargetNameHook, BindTargetTypeHook, BindFieldHook), Source, writer ?? PrepareWriter(), Option.Output.Outcome.Value); - } - - protected override PipelineReader PrepareReader() - { - if (!string.IsNullOrEmpty(Option.Input.ObjectPath)) - { - AddVisitTargetObjectAction((sourceObject, next) => - { - return PipelineReceiverActions.ReadObjectPath(sourceObject, next, Option.Input.ObjectPath, true); - }); - } - - if (Option.Input.Format == InputFormat.Yaml) - { - AddVisitTargetObjectAction((sourceObject, next) => - { - return PipelineReceiverActions.ConvertFromYaml(sourceObject, next); - }); - } - else if (Option.Input.Format == InputFormat.Json) - { - AddVisitTargetObjectAction((sourceObject, next) => - { - return PipelineReceiverActions.ConvertFromJson(sourceObject, next); - }); - } - else if (Option.Input.Format == InputFormat.Markdown) - { - AddVisitTargetObjectAction((sourceObject, next) => - { - return PipelineReceiverActions.ConvertFromMarkdown(sourceObject, next); - }); - } - else if (Option.Input.Format == InputFormat.PowerShellData) - { - AddVisitTargetObjectAction((sourceObject, next) => - { - return PipelineReceiverActions.ConvertFromPowerShellData(sourceObject, next); - }); - } - else if (Option.Input.Format == InputFormat.File) - { - AddVisitTargetObjectAction((sourceObject, next) => - { - return PipelineReceiverActions.ConvertFromGitHead(sourceObject, next); - }); - } - else if (Option.Input.Format == InputFormat.Detect && _InputPath != null) - { - AddVisitTargetObjectAction((sourceObject, next) => - { - return PipelineReceiverActions.DetectInputFormat(sourceObject, next); - }); - } - return new PipelineReader(VisitTargetObject, _InputPath, GetInputObjectSourceFilter()); - } - } - - /// - /// A helper to construct the pipeline for Invoke-PSRule. - /// - internal sealed class InvokeRulePipelineBuilder : InvokePipelineBuilderBase - { - internal InvokeRulePipelineBuilder(Source[] source, IHostContext hostContext) - : base(source, hostContext) { } - } - internal sealed class InvokeRulePipeline : RulePipeline, IPipeline { private readonly RuleOutcome _Outcome; diff --git a/src/PSRule/Common/PipelineExtensions.cs b/src/PSRule/Pipeline/PipelineExtensions.cs similarity index 69% rename from src/PSRule/Common/PipelineExtensions.cs rename to src/PSRule/Pipeline/PipelineExtensions.cs index cf612d7add..09fa4fd58a 100644 --- a/src/PSRule/Common/PipelineExtensions.cs +++ b/src/PSRule/Pipeline/PipelineExtensions.cs @@ -2,18 +2,24 @@ // Licensed under the MIT License. using System.Management.Automation; -using PSRule.Pipeline; -namespace PSRule +namespace PSRule.Pipeline { + /// + /// Extension methods for the PSRule pipelines. + /// public static class PipelineExtensions { /// /// Process an object through the pipeline. Each object will be processed by rules that apply based on pre-conditions. /// + /// An instance of a PSRule pipeline. /// The object to process. public static void Process(this IPipeline pipeline, object sourceObject) { + if (pipeline == null) + return; + pipeline.Process(PSObject.AsPSObject(sourceObject)); } } diff --git a/src/PSRule/Pipeline/PipelineWriterExtensions.cs b/src/PSRule/Pipeline/PipelineWriterExtensions.cs index 8509b6eb04..c8feb0cd1f 100644 --- a/src/PSRule/Pipeline/PipelineWriterExtensions.cs +++ b/src/PSRule/Pipeline/PipelineWriterExtensions.cs @@ -8,8 +8,16 @@ namespace PSRule.Pipeline { - internal static class PipelineWriterExtensions + public static class PipelineWriterExtensions { + public static void WriteDebug(this IPipelineWriter writer, DebugRecord debugRecord) + { + if (debugRecord == null) + return; + + writer.WriteDebug(debugRecord.Message); + } + internal static void DebugMessage(this IPipelineWriter logger, string message) { if (logger == null || !logger.ShouldWriteDebug()) diff --git a/src/PSRule/Rules/RuleOutcomeReason.cs b/src/PSRule/Rules/RuleOutcomeReason.cs index f1029024d9..0b7a3387ea 100644 --- a/src/PSRule/Rules/RuleOutcomeReason.cs +++ b/src/PSRule/Rules/RuleOutcomeReason.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Newtonsoft.Json; @@ -54,6 +54,7 @@ public enum RuleOutcomeReason /// /// /// This reason is used with the None outcome. + /// Suppressed = 5 } } diff --git a/src/PSRule/Rules/RuleRecord.cs b/src/PSRule/Rules/RuleRecord.cs index b6a2bd491a..928146aa58 100644 --- a/src/PSRule/Rules/RuleRecord.cs +++ b/src/PSRule/Rules/RuleRecord.cs @@ -59,6 +59,9 @@ internal RuleRecord(string runId, ResourceId ruleId, string @ref, TargetObject t /// /// A unique identifier for the rule. /// + /// + /// An additional opaque identifer may also be provided by by . + /// [JsonIgnore] [YamlIgnore] public readonly string RuleId; @@ -69,6 +72,9 @@ internal RuleRecord(string runId, ResourceId ruleId, string @ref, TargetObject t [JsonProperty(PropertyName = "ruleName")] public readonly string RuleName; + /// + /// A stable opaque unique identifier for the rule in addition to . + /// public string Ref { get; } /// @@ -77,6 +83,9 @@ internal RuleRecord(string runId, ResourceId ruleId, string @ref, TargetObject t [JsonProperty(PropertyName = "level")] public SeverityLevel Level { get; } + /// + /// A source location for the rule that executed. + /// [JsonIgnore] [YamlIgnore] public ISourceExtent Extent { get; } @@ -87,9 +96,15 @@ internal RuleRecord(string runId, ResourceId ruleId, string @ref, TargetObject t [JsonProperty(PropertyName = "outcome")] public RuleOutcome Outcome { get; internal set; } + /// + /// An additional reason code for the . + /// [JsonProperty(PropertyName = "outcomeReason")] public RuleOutcomeReason OutcomeReason { get; internal set; } + /// + /// A localized recommendation for the rule. + /// [JsonIgnore] [YamlIgnore] public string Recommendation => Info.Recommendation?.Text ?? Info.Synopsis?.Text; diff --git a/src/PSRule/Runtime/Assert.cs b/src/PSRule/Runtime/Assert.cs index dc5851977d..69bce3ea9b 100644 --- a/src/PSRule/Runtime/Assert.cs +++ b/src/PSRule/Runtime/Assert.cs @@ -17,7 +17,7 @@ namespace PSRule.Runtime { /// - /// A helper variable exposed at runtime for rules. + /// A set of assertion helpers that are exposed at runtime through the $Assert variable. /// public sealed class Assert { @@ -25,23 +25,41 @@ public sealed class Assert private const string PROPERTY_SCHEMA = "$schema"; private const string VARIABLE_NAME = "Assert"; private const string TYPENAME_STRING = "[string]"; - private const string TYPENAME_BOOL = "[bool]"; - private const string TYPENAME_ARRAY = "[array]"; private const string TYPENAME_NULL = "null"; - private const string TYPENAME_DATETIME = "[DateTime]"; #region Authoring + /// + /// Create a result based on a boolean . + /// + /// A boolean condition that passes when set to true, and fails when set to false. + /// A localized reason why the assertion failed. This parameter is ignored if the assertion passed. + /// An assertion result. public AssertResult Create(bool condition, string reason = null) { return Create(condition, reason, args: null); } + /// + /// Create a result based on a boolean . + /// + /// A boolean condition that passes when set to true, and fails when set to false. + /// An unformatted localized reason why the assertion failed. This parameter is ignored if the assertion passed. + /// A list of arguments that are inserted into the format string. + /// An assertion result. public AssertResult Create(bool condition, string reason, params object[] args) { return Create(Operand.FromTarget(), condition, reason, args); } + /// + /// Create a result based on a boolean . + /// + /// The object path that was reported by the assertion. + /// A boolean condition that passes when set to true, and fails when set to false. + /// An unformatted localized reason why the assertion failed. This parameter is ignored if the assertion passed. + /// A list of arguments that are inserted into the format string. + /// An assertion result. public AssertResult Create(string path, bool condition, string reason, params object[] args) { return Create(string.IsNullOrEmpty(path) ? Operand.FromTarget() : Operand.FromPath(path), condition, reason, args); @@ -55,6 +73,11 @@ internal AssertResult Create(IOperand operand, bool condition, string reason, pa return new AssertResult(this, operand, condition, reason, args); } + /// + /// Create a result based on issues reported downstream. + /// + /// An array of issues reported downstream. + /// An assertion result. public AssertResult Create(TargetIssueInfo[] issue) { if (issue == null || issue.Length == 0) @@ -68,31 +91,41 @@ public AssertResult Create(TargetIssueInfo[] issue) } /// - /// Pass the assertion. + /// Create a passing assertion result. /// + /// An assertion result. public AssertResult Pass() { return Create(condition: true); } /// - /// Fail the assertion. + /// Create a failed assertion result. /// + /// An assertion result. public AssertResult Fail() { return Create(condition: false, reason: null, args: null); } /// - /// Fail the assertion. + /// Create a failed assertion result. /// - /// An unformatted reason why the assertion failed. - /// Additional parameters for the reason format. + /// An unformatted localized reason why the assertion failed. + /// A list of arguments that are inserted into the format string. + /// An assertion result. public AssertResult Fail(string reason, params object[] args) { return Create(condition: false, reason: reason, args: args); } + /// + /// Create a failed assertion result. + /// + /// An operand that was reported by the assertion. + /// An unformatted localized reason why the assertion failed. + /// A list of arguments that are inserted into the format string. + /// An assertion result. public AssertResult Fail(IOperand operand, string reason, params object[] args) { return Create(operand, condition: false, reason: reason, args: args); @@ -791,8 +824,13 @@ public AssertResult TypeOf(PSObject inputObject, string field, string[] type) } /// - /// The object field value should match the version constraint. Only applies to strings. + /// The Version assertion method checks the field value is a valid semantic version. + /// A constraint can optionally be provided to require the semantic version to be within a range. + /// /// + /// + /// Only applies to strings. + /// public AssertResult Version(PSObject inputObject, string field, string constraint = null, bool includePrerelease = false) { // Guard parameters @@ -809,6 +847,11 @@ public AssertResult Version(PSObject inputObject, string field, string constrain return c != null && !c.Equals(value) ? Fail(Operand.FromPath(field), ReasonStrings.VersionContraint, value, constraint) : Pass(); } + /// + /// The Greater assertion method checks the field value is greater than the specified value. + /// The field value can either be an integer, float, array, or string. + /// + /// public AssertResult Greater(PSObject inputObject, string field, int value, bool convert = false) { // Guard parameters @@ -823,6 +866,11 @@ public AssertResult Greater(PSObject inputObject, string field, int value, bool return Fail(Operand.FromPath(field), ReasonStrings.Compare, fieldValue, value); } + /// + /// The GreaterOrEqual assertion method checks the field value is greater or equal to the specified value. + /// The field value can either be an integer, float, array, or string. + /// + /// public AssertResult GreaterOrEqual(PSObject inputObject, string field, int value, bool convert = false) { // Guard parameters @@ -837,6 +885,11 @@ public AssertResult GreaterOrEqual(PSObject inputObject, string field, int value return Fail(Operand.FromPath(field), ReasonStrings.Compare, fieldValue, value); } + /// + /// The Less assertion method checks the field value is less than the specified value. + /// The field value can either be an integer, float, array, or string. + /// + /// public AssertResult Less(PSObject inputObject, string field, int value, bool convert = false) { // Guard parameters @@ -851,6 +904,11 @@ public AssertResult Less(PSObject inputObject, string field, int value, bool con return Fail(Operand.FromPath(field), ReasonStrings.Compare, fieldValue, value); } + /// + /// The LessOrEqual assertion method checks the field value is less or equal to the specified value. + /// The field value can either be an integer, float, array, or string. + /// + /// public AssertResult LessOrEqual(PSObject inputObject, string field, int value, bool convert = false) { // Guard parameters @@ -867,6 +925,7 @@ public AssertResult LessOrEqual(PSObject inputObject, string field, int value, b /// /// The object field value must be included in the set. + /// /// public AssertResult In(PSObject inputObject, string field, Array values, bool caseSensitive = false) { @@ -887,6 +946,7 @@ public AssertResult In(PSObject inputObject, string field, Array values, bool ca /// /// The object field value must not be included in the set. + /// /// public AssertResult NotIn(PSObject inputObject, string field, Array values, bool caseSensitive = false) { @@ -913,7 +973,11 @@ public AssertResult NotIn(PSObject inputObject, string field, Array values, bool } /// - /// The object field value must include the set. + /// The Subset assertion method checks the field value includes all of the specified values. + /// The field value may also contain additional values that are not specified in the values parameter. + /// The field value must be an array or collection. + /// Specified values can be included in the field value in any order. + /// /// public AssertResult Subset(PSObject inputObject, string field, Array values, bool caseSensitive = false, bool unique = false) { @@ -933,6 +997,12 @@ public AssertResult Subset(PSObject inputObject, string field, Array values, boo return Pass(); } + /// + /// The SetOf assertion method checks the field value only includes all of the specified values. + /// The field value must be an array or collection. + /// Specified values can be included in the field value in any order. + /// + /// public AssertResult SetOf(PSObject inputObject, string field, Array values, bool caseSensitive = false) { // Guard parameters @@ -955,7 +1025,8 @@ public AssertResult SetOf(PSObject inputObject, string field, Array values, bool } /// - /// The field value must contain the specified number of items. + /// The field value must contain the specified number of items. + /// /// public AssertResult Count(PSObject inputObject, string field, int count) { @@ -971,6 +1042,7 @@ public AssertResult Count(PSObject inputObject, string field, int count) /// /// The field value must not contain the specified number of items. + /// /// public AssertResult NotCount(PSObject inputObject, string field, int count) { @@ -986,6 +1058,7 @@ public AssertResult NotCount(PSObject inputObject, string field, int count) /// /// The object field value must match the regular expression. + /// /// public AssertResult Match(PSObject inputObject, string field, string pattern, bool caseSensitive = false) { @@ -1001,6 +1074,7 @@ public AssertResult Match(PSObject inputObject, string field, string pattern, bo /// /// The object field value must not match the regular expression. + /// /// public AssertResult NotMatch(PSObject inputObject, string field, string pattern, bool caseSensitive = false) { @@ -1026,6 +1100,11 @@ public AssertResult NotMatch(PSObject inputObject, string field, string pattern, return Fail(Operand.FromPath(field), ReasonStrings.NotMatchPattern, value, pattern); } + /// + /// The FilePath assertion method checks the file exists. + /// Checks use file system case-sensitivity rules. + /// + /// public AssertResult FilePath(PSObject inputObject, string field, string[] suffix = null) { // Guard parameters @@ -1051,6 +1130,11 @@ public AssertResult FilePath(PSObject inputObject, string field, string[] suffix return reason; } + /// + /// The FileHeader assertion method checks a file for a comment header. + /// When comparing the file header, the format of line comments are automatically detected by file extension. + /// + /// public AssertResult FileHeader(PSObject inputObject, string field, string[] header, string prefix = null) { // Guard parameters diff --git a/src/PSRule/Runtime/AssertResult.cs b/src/PSRule/Runtime/AssertResult.cs index bf34d6a7aa..dbea5580d9 100644 --- a/src/PSRule/Runtime/AssertResult.cs +++ b/src/PSRule/Runtime/AssertResult.cs @@ -10,6 +10,9 @@ namespace PSRule.Runtime { + /// + /// The result of a single assertion. + /// public sealed class AssertResult : IEquatable { private readonly Assert _Assert; @@ -26,6 +29,9 @@ internal AssertResult(Assert assert, IOperand operand, bool value, string reason } } + /// + /// Convert the result into a boolean value. + /// public static explicit operator bool(AssertResult result) { return result != null && result.Result; @@ -161,21 +167,31 @@ public bool Complete() return Result; } + /// + /// Clear any reasons for this result. + /// public void Ignore() { _Reason.Clear(); } + /// public bool Equals(bool other) { return Result == other; } + /// + /// Get a formatted string of the result reasons. + /// public override string ToString() { return IsNullOrEmptyReason() ? string.Empty : string.Join(" ", _Reason.GetStrings()); } + /// + /// Convert the result into a boolean value. + /// public bool ToBoolean() { return Result; diff --git a/src/PSRule/Runtime/Operand.cs b/src/PSRule/Runtime/Operand.cs index eb27df79ca..37810ad087 100644 --- a/src/PSRule/Runtime/Operand.cs +++ b/src/PSRule/Runtime/Operand.cs @@ -5,27 +5,60 @@ namespace PSRule.Runtime { + /// + /// The type of operand that is compared with the expression. + /// public enum OperandKind { + /// + /// Unknown. + /// None = 0, + /// + /// An object path. + /// Path = 1, + /// + /// The object target type. + /// Type = 2, + /// + /// The object target name. + /// Name = 3, + /// + /// The object source information. + /// Source = 4, + /// + /// The target object itself. + /// Target = 5 } + /// + /// An operand that is compared with PSRule expressions. + /// public interface IOperand { + /// + /// The value of the operand. + /// object Value { get; } + /// + /// The type of operand. + /// OperandKind Kind { get; } + /// + /// The object path to the operand. + /// string Path { get; } } diff --git a/src/PSRule/Runtime/PSRule.cs b/src/PSRule/Runtime/PSRule.cs index dd279b6f75..0285b8a592 100644 --- a/src/PSRule/Runtime/PSRule.cs +++ b/src/PSRule/Runtime/PSRule.cs @@ -22,6 +22,9 @@ public sealed class PSRule : ScopedItem private ITargetIssueCollection _Issue; private IBadgeBuilder _BadgeBuilder; + /// + /// Create an empty instance. + /// public PSRule() { } internal PSRule(RunspaceContext context) @@ -87,6 +90,12 @@ public Hashtable Data /// /// A set of custom fields bound for the target object. /// + /// + /// This property can only be accessed from a rule or pre-condition. + /// + /// + /// Thrown when accessing this property outside of a rule or pre-condition. + /// public Hashtable Field { get @@ -96,6 +105,15 @@ public Hashtable Field } } + /// + /// An aggregated set of results from executing PSRule rules. + /// + /// + /// This property can only be accessed from an end convention block. + /// + /// + /// Thrown when accessing this property outside of an end convention block. + /// public IEnumerable Output { get @@ -105,8 +123,14 @@ public IEnumerable Output } } + /// + /// The source information for the location the target object originated from. + /// public ITargetSourceCollection Source => GetSource(); + /// + /// Any issues reported by downstream tools and annotated to the target object. + /// public ITargetIssueCollection Issue => GetIssue(); /// @@ -205,6 +229,12 @@ public object[] GetPath(object sourceObject, string path) /// /// Imports source objects into the pipeline for processing. /// + /// + /// This method can only be called from a convention begin block. + /// + /// + /// Thrown when accessing this method outside of a convention begin block. + /// public void Import(PSObject[] sourceObject) { if (sourceObject == null || sourceObject.Length == 0) @@ -220,6 +250,18 @@ public void Import(PSObject[] sourceObject) } } + /// + /// Add a reusable singleton object into PSRule runtime that can be reference across multiple rules or conventions. To retrieve the singleton call . + /// + /// A unique identifier for the object. + /// A instance of the singleton. + /// + /// If either or is null or empty the singleton is ignored. + /// This method can only be called from a convention initialize block. + /// + /// + /// Thrown when accessing this method outside of a convention initialize block. + /// public void AddService(string id, object service) { if (service == null || string.IsNullOrEmpty(id)) @@ -229,6 +271,14 @@ public void AddService(string id, object service) GetContext().AddService(id, service); } + /// + /// Retrieve a reusable singleton object from the PSRule runtime that has previously been stored with . + /// + /// The unique identifier for the object. + /// The singleton instance or null if an object with the specified was not found. + /// + /// Thrown when accessing this method outside of PSRule. + /// public object GetService(string id) { if (string.IsNullOrEmpty(id)) diff --git a/src/PSRule/Runtime/Rule.cs b/src/PSRule/Runtime/Rule.cs index b4e1dc82b7..1c57d568a4 100644 --- a/src/PSRule/Runtime/Rule.cs +++ b/src/PSRule/Runtime/Rule.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. namespace PSRule.Runtime @@ -10,8 +10,14 @@ namespace PSRule.Runtime /// public sealed class Rule { + /// + /// The name of the currently executing rule. + /// public string RuleName => RunspaceContext.CurrentThread.RuleRecord.RuleName; + /// + /// A unique identifer of the currently executing rule. + /// public string RuleId => RunspaceContext.CurrentThread.RuleRecord.RuleId; } diff --git a/src/PSRule/Runtime/ScopedItem.cs b/src/PSRule/Runtime/ScopedItem.cs index bb00dcbb0d..c9ce859c4a 100644 --- a/src/PSRule/Runtime/ScopedItem.cs +++ b/src/PSRule/Runtime/ScopedItem.cs @@ -1,10 +1,13 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using PSRule.Pipeline; namespace PSRule.Runtime { + /// + /// A base class for scoped context variables used internally by PSRule. + /// public abstract class ScopedItem { private readonly RunspaceContext _Context; diff --git a/src/PSRule/Runtime/SemanticVersion.cs b/src/PSRule/Runtime/SemanticVersion.cs index 5567a082f9..d941bbbc45 100644 --- a/src/PSRule/Runtime/SemanticVersion.cs +++ b/src/PSRule/Runtime/SemanticVersion.cs @@ -635,7 +635,7 @@ internal void Build(out string label) /// /// 1.2.3 || 3.4.5 - /// >=1.2.3 <3.4.5 + /// >=1.2.3 <3.4.5 /// internal JoinOperator GetJoin() { From 7d1ef0f15054ffd3306b2aab136ce7ff66fae66e Mon Sep 17 00:00:00 2001 From: Bernie White Date: Tue, 26 Jul 2022 20:20:50 +1000 Subject: [PATCH 008/156] Pre-release v2.3.0-B0100 (#1190) --- docs/CHANGELOG-v2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index a9e3c09074..e803dc4960 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -13,6 +13,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.3.0-B0100 (pre-release) + What's changed since pre-release v2.3.0-B0074: - Engineering: From cf9dba52f7aed16819bc5a9f85a119a74ed9721e Mon Sep 17 00:00:00 2001 From: Bernie White Date: Tue, 26 Jul 2022 20:57:16 +1000 Subject: [PATCH 009/156] Update prior release notes (#1191) --- docs/CHANGELOG-v0.md | 5 ++++- docs/CHANGELOG-v1.md | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/CHANGELOG-v0.md b/docs/CHANGELOG-v0.md index 8929d8fa3a..46dafbdb4c 100644 --- a/docs/CHANGELOG-v0.md +++ b/docs/CHANGELOG-v0.md @@ -6,7 +6,10 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers !!! Attention PSRule v0 is a prior release. - Please upgrade to the latest version. + For more information see [v2][2] release notes. + Please check out our [upgrade notes][1] to get prepared for upgrading to the latest version. + + [2]: https://microsoft.github.io/PSRule/latest/CHANGELOG-v2/ ## v0.22.0 diff --git a/docs/CHANGELOG-v1.md b/docs/CHANGELOG-v1.md index 037b63db55..7e3821e661 100644 --- a/docs/CHANGELOG-v1.md +++ b/docs/CHANGELOG-v1.md @@ -10,10 +10,10 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers - Setting the default module baseline requires a module configuration from PSRule v2. [#809](https://github.com/microsoft/PSRule/issues/809) - Resource names have naming restrictions introduced from PSRule v2. [#1012](https://github.com/microsoft/PSRule/issues/1012) -!!! Info - The next release of PSRule is available. +!!! Attention + PSRule v1 is a prior release. For more information see [v2][2] release notes. - Please check out our [upgrade notes][1] to get prepared for the next release. + Please check out our [upgrade notes][1] to get prepared for upgrading to the latest version. [2]: https://microsoft.github.io/PSRule/latest/CHANGELOG-v2/ From d169ebca4c70007cd41600bce76cfbb1594ac541 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Wed, 27 Jul 2022 11:41:38 +1000 Subject: [PATCH 010/156] Fix JSON parsing issue #1193 (#1194) --- docs/CHANGELOG-v2.md | 6 + src/PSRule/Common/JsonConverters.cs | 326 +++++++----------- src/PSRule/Pipeline/Exceptions.cs | 19 +- .../Resources/PSRuleResources.Designer.cs | 9 + src/PSRule/Resources/PSRuleResources.resx | 3 + tests/PSRule.Tests/JsonHelperTests.cs | 27 ++ 6 files changed, 192 insertions(+), 198 deletions(-) create mode 100644 tests/PSRule.Tests/JsonHelperTests.cs diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index e803dc4960..34f99fa847 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -13,6 +13,12 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since pre-release v2.3.0-B0100: + +- Bug fixes: + - Fixes JSON parsing of string array for single objects by @BernieWhite. + [#1193](https://github.com/microsoft/PSRule/issues/1193) + ## v2.3.0-B0100 (pre-release) What's changed since pre-release v2.3.0-B0074: diff --git a/src/PSRule/Common/JsonConverters.cs b/src/PSRule/Common/JsonConverters.cs index be23eb3628..cc3c8b5411 100644 --- a/src/PSRule/Common/JsonConverters.cs +++ b/src/PSRule/Common/JsonConverters.cs @@ -21,110 +21,177 @@ namespace PSRule { /// - /// A custom serializer to correctly convert PSObject properties to JSON instead of CLIXML. + /// A base PSObject converter. /// - internal sealed class PSObjectJsonConverter : JsonConverter + internal abstract class PSObjectBaseConverter : JsonConverter { - public override bool CanConvert(Type objectType) + /// + /// Skip JSON comments. + /// + protected static bool SkipComments(JsonReader reader) { - return objectType == typeof(PSObject); + var hasComments = false; + while (reader.TokenType == JsonToken.Comment && reader.Read()) + hasComments = true; + + return hasComments; } - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + protected static void ReadObject(PSObject value, JsonReader reader, bool bindTargetInfo, TargetSourceInfo sourceInfo) { - if (value is not PSObject obj) - throw new ArgumentException(message: PSRuleResources.SerializeNullPSObject, paramName: nameof(value)); + SkipComments(reader); + var path = reader.Path; + if (reader.TokenType != JsonToken.StartObject || !reader.Read()) + throw new PipelineSerializationException(PSRuleResources.ReadJsonFailedExpectedToken, Enum.GetName(typeof(JsonToken), reader.TokenType)); - if (WriteFileSystemInfo(writer, value, serializer) || WriteBaseObject(writer, obj, serializer)) - return; + string name = null; + var lineNumber = 0; + var linePosition = 0; - writer.WriteStartObject(); - foreach (var property in obj.Properties) + if (bindTargetInfo && reader is IJsonLineInfo lineInfo && lineInfo.HasLineInfo()) { - // 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; + lineNumber = lineInfo.LineNumber; + linePosition = lineInfo.LinePosition; + } - writer.WritePropertyName(property.Name); - serializer.Serialize(writer, property.Value); + // Read each token + while (reader.TokenType != JsonToken.EndObject) + { + switch (reader.TokenType) + { + case JsonToken.PropertyName: + name = reader.Value.ToString(); + if (string.IsNullOrEmpty(name)) + { + reader.Skip(); + } + else if (name == PSRuleTargetInfo.PropertyName) + { + var targetInfo = ReadInfo(reader); + if (targetInfo != null) + value.SetTargetInfo(targetInfo); + } + break; + + case JsonToken.StartObject: + var item = new PSObject(); + ReadObject(item, reader, bindTargetInfo: false, sourceInfo: null); + value.Properties.Add(new PSNoteProperty(name, value: item)); + break; + + case JsonToken.StartArray: + var items = ReadArray(reader: reader); + value.Properties.Add(new PSNoteProperty(name, value: items)); + break; + + case JsonToken.Comment: + break; + + default: + value.Properties.Add(new PSNoteProperty(name, value: reader.Value)); + break; + } + if (!reader.Read() || reader.TokenType == JsonToken.None) + throw new PipelineSerializationException(PSRuleResources.ReadJsonFailedExpectedToken, Enum.GetName(typeof(JsonToken), reader.TokenType)); + } + if (bindTargetInfo) + { + value.UseTargetInfo(out var info); + info.SetSource(sourceInfo?.File, lineNumber, linePosition); + if (string.IsNullOrEmpty(info.Path)) + info.Path = path; } - writer.WriteEndObject(); } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + protected static PSRuleTargetInfo ReadInfo(JsonReader reader) { - // Create target object based on JObject - var result = existingValue as PSObject ?? new PSObject(); + if (!reader.Read() || reader.TokenType == JsonToken.None || reader.TokenType != JsonToken.StartObject) + return null; - // Read tokens - ReadObject(value: result, reader: reader); - return result; + var s = JsonSerializer.Create(); + return s.Deserialize(reader); } - private void ReadObject(PSObject value, JsonReader reader) + protected static PSObject[] ReadArray(JsonReader reader) { SkipComments(reader); - if (reader.TokenType != JsonToken.StartObject) - throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed); + if (reader.TokenType != JsonToken.StartArray || !reader.Read()) + throw new PipelineSerializationException(PSRuleResources.ReadJsonFailedExpectedToken, Enum.GetName(typeof(JsonToken), reader.TokenType)); - reader.Read(); - string name = null; + var result = new List(); - // Read each token - while (reader.TokenType != JsonToken.EndObject) + // Read until the end of the array + while (reader.TokenType != JsonToken.EndArray) { switch (reader.TokenType) { - case JsonToken.PropertyName: - name = reader.Value.ToString(); - if (string.IsNullOrEmpty(name)) - reader.Skip(); - - break; - case JsonToken.StartObject: - var child = new PSObject(); - ReadObject(value: child, reader: reader); - value.Properties.Add(new PSNoteProperty(name, value: child)); + var item = new PSObject(); + ReadObject(item, reader, bindTargetInfo: false, sourceInfo: null); + result.Add(item); break; case JsonToken.StartArray: - var items = new List(); - reader.Read(); - while (reader.TokenType != JsonToken.EndArray) - { - if (SkipComments(reader)) - continue; + result.Add(PSObject.AsPSObject(ReadArray(reader))); + break; - var item = new PSObject(); - ReadObject(value: item, reader: reader); - items.Add(item); - reader.Read(); - } - value.Properties.Add(new PSNoteProperty(name: name, value: items.ToArray())); + case JsonToken.Null: + result.Add(null); break; case JsonToken.Comment: break; default: - value.Properties.Add(new PSNoteProperty(name: name, value: reader.Value)); + result.Add(PSObject.AsPSObject(reader.Value)); break; } - reader.Read(); + if (!reader.Read() || reader.TokenType == JsonToken.None) + throw new PipelineSerializationException(PSRuleResources.ReadJsonFailedExpectedToken, Enum.GetName(typeof(JsonToken), reader.TokenType)); } + return result.ToArray(); } + } - /// - /// Skip JSON comments. - /// - private static bool SkipComments(JsonReader reader) + /// + /// A custom serializer to correctly convert PSObject properties to JSON instead of CLIXML. + /// + internal sealed class PSObjectJsonConverter : PSObjectBaseConverter + { + public override bool CanConvert(Type objectType) { - var hasComments = false; - while (reader.TokenType == JsonToken.Comment && reader.Read()) - hasComments = true; + return objectType == typeof(PSObject); + } - return hasComments; + 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 (WriteFileSystemInfo(writer, value, serializer) || WriteBaseObject(writer, obj, 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(result, reader, bindTargetInfo: true, sourceInfo: null); + return result; } /// @@ -132,7 +199,7 @@ private static bool SkipComments(JsonReader reader) /// private static bool WriteFileSystemInfo(JsonWriter writer, object value, JsonSerializer serializer) { - if (!(value is FileSystemInfo fileSystemInfo)) + if (value is not FileSystemInfo fileSystemInfo) return false; serializer.Serialize(writer, fileSystemInfo.FullName); @@ -155,7 +222,7 @@ private static bool WriteBaseObject(JsonWriter writer, PSObject value, JsonSeria /// /// 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 + internal sealed class PSObjectArrayJsonConverter : PSObjectBaseConverter { private readonly TargetSourceInfo _SourceInfo; @@ -180,7 +247,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist { SkipComments(reader); if (reader.TokenType != JsonToken.StartObject && reader.TokenType != JsonToken.StartArray) - throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed); + throw new PipelineSerializationException(PSRuleResources.ReadJsonFailedExpectedToken, Enum.GetName(typeof(JsonToken), reader.TokenType)); var result = new List(); var isArray = reader.TokenType == JsonToken.StartArray; @@ -193,7 +260,8 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist if (SkipComments(reader)) continue; - var value = ReadObject(reader, bindTargetInfo: true, _SourceInfo); + var value = new PSObject(); + ReadObject(value, reader, bindTargetInfo: true, sourceInfo: _SourceInfo); result.Add(value); // Consume the EndObject token @@ -201,132 +269,6 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist } return result.ToArray(); } - - /// - /// Skip JSON comments. - /// - private static bool SkipComments(JsonReader reader) - { - var hasComments = false; - while (reader.TokenType == JsonToken.Comment && reader.Read()) - hasComments = true; - - return hasComments; - } - - private static PSObject ReadObject(JsonReader reader, bool bindTargetInfo, TargetSourceInfo sourceInfo) - { - SkipComments(reader); - var path = reader.Path; - if (reader.TokenType != JsonToken.StartObject || !reader.Read()) - throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed); - - var result = new PSObject(); - string name = null; - var lineNumber = 0; - var linePosition = 0; - - if (bindTargetInfo && reader is IJsonLineInfo lineInfo && lineInfo.HasLineInfo()) - { - lineNumber = lineInfo.LineNumber; - linePosition = lineInfo.LinePosition; - } - - // Read each token - while (reader.TokenType != JsonToken.EndObject) - { - switch (reader.TokenType) - { - case JsonToken.PropertyName: - name = reader.Value.ToString(); - if (string.IsNullOrEmpty(name)) - { - reader.Skip(); - } - else if (name == PSRuleTargetInfo.PropertyName) - { - var targetInfo = ReadInfo(reader); - if (targetInfo != null) - result.SetTargetInfo(targetInfo); - } - break; - - case JsonToken.StartObject: - var value = ReadObject(reader, bindTargetInfo: false, sourceInfo: null); - result.Properties.Add(new PSNoteProperty(name, value: value)); - break; - - case JsonToken.StartArray: - var items = ReadArray(reader: reader); - result.Properties.Add(new PSNoteProperty(name, value: items)); - break; - - case JsonToken.Comment: - break; - - default: - result.Properties.Add(new PSNoteProperty(name, value: reader.Value)); - break; - } - if (!reader.Read() || reader.TokenType == JsonToken.None) - throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed); - } - if (bindTargetInfo) - { - result.UseTargetInfo(out var info); - info.SetSource(sourceInfo?.File, lineNumber, linePosition); - if (string.IsNullOrEmpty(info.Path)) - info.Path = path; - } - return result; - } - - private static PSRuleTargetInfo ReadInfo(JsonReader reader) - { - if (!reader.Read() || reader.TokenType == JsonToken.None || reader.TokenType != JsonToken.StartObject) - return null; - - var s = JsonSerializer.Create(); - return s.Deserialize(reader); - } - - private static PSObject[] ReadArray(JsonReader reader) - { - SkipComments(reader); - if (reader.TokenType != JsonToken.StartArray || !reader.Read()) - throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed); - - var result = new List(); - - // Read until the end of the array - while (reader.TokenType != JsonToken.EndArray) - { - switch (reader.TokenType) - { - case JsonToken.StartObject: - result.Add(ReadObject(reader, bindTargetInfo: false, sourceInfo: null)); - break; - - case JsonToken.StartArray: - result.Add(PSObject.AsPSObject(ReadArray(reader))); - break; - - case JsonToken.Null: - result.Add(null); - break; - - case JsonToken.Comment: - break; - - default: - result.Add(PSObject.AsPSObject(reader.Value)); - break; - } - if (!reader.Read() || reader.TokenType == JsonToken.None) - throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed); - } - return result.ToArray(); - } } /// diff --git a/src/PSRule/Pipeline/Exceptions.cs b/src/PSRule/Pipeline/Exceptions.cs index d8ca09e84f..f4e5bda85c 100644 --- a/src/PSRule/Pipeline/Exceptions.cs +++ b/src/PSRule/Pipeline/Exceptions.cs @@ -5,6 +5,7 @@ using System.Management.Automation; using System.Runtime.Serialization; using System.Security.Permissions; +using System.Threading; namespace PSRule.Pipeline { @@ -159,21 +160,27 @@ public PipelineSerializationException(string message) : base(message) { } + /// + /// Creates a serialization exception. + /// + /// The detail of the exception. + /// Additional argument to add to the format string. + internal PipelineSerializationException(string message, params object[] args) + : base(string.Format(Thread.CurrentThread.CurrentCulture, message, args)) { } + /// /// 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) - { - } + public PipelineSerializationException(string message, Exception innerException) + : base(message, innerException) { } /// /// Creates a serialization exception. /// - private PipelineSerializationException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } + private PipelineSerializationException(SerializationInfo info, StreamingContext context) + : base(info, context) { } /// /// The path to the file. diff --git a/src/PSRule/Resources/PSRuleResources.Designer.cs b/src/PSRule/Resources/PSRuleResources.Designer.cs index 2f015e42e8..7028b08425 100644 --- a/src/PSRule/Resources/PSRuleResources.Designer.cs +++ b/src/PSRule/Resources/PSRuleResources.Designer.cs @@ -456,6 +456,15 @@ internal static string ReadJsonFailed { } } + /// + /// Looks up a localized string similar to Read JSON failed because the token ({0}) was not expected.. + /// + internal static string ReadJsonFailedExpectedToken { + get { + return ResourceManager.GetString("ReadJsonFailedExpectedToken", resourceCulture); + } + } + /// /// Looks up a localized string similar to The module version '{1}' for '{0}' does not match the required version '{2}'. To continue, first update the module to match the version requirement.. /// diff --git a/src/PSRule/Resources/PSRuleResources.resx b/src/PSRule/Resources/PSRuleResources.resx index fe7a05810c..6ac20172fb 100644 --- a/src/PSRule/Resources/PSRuleResources.resx +++ b/src/PSRule/Resources/PSRuleResources.resx @@ -348,4 +348,7 @@ Object path not found. + + Read JSON failed because the token ({0}) was not expected. + \ No newline at end of file diff --git a/tests/PSRule.Tests/JsonHelperTests.cs b/tests/PSRule.Tests/JsonHelperTests.cs new file mode 100644 index 0000000000..b545b22afc --- /dev/null +++ b/tests/PSRule.Tests/JsonHelperTests.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Newtonsoft.Json.Linq; +using Xunit; + +namespace PSRule +{ + public sealed class JsonHelperTests + { + [Fact] + public void JTokenToPSObject() + { + var actual = JsonHelper.ToPSObject(GetJObject()); + Assert.NotNull(actual); + } + + #region Helper methods + + private JToken GetJObject() + { + return JObject.Parse("{ \"metadata\": {}, \"parameters\": { \"sku\": { \"type\": \"string\", \"defaultValue\": \"Developer\", \"allowValues\": [ \"Developer\", \"Standard\", \"Premium\" ] } } }"); + } + + #endregion Helper methods + } +} From 5b5ed1652872a41c09ec173b5b36344fe9c4f6c9 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Wed, 27 Jul 2022 12:42:56 +1000 Subject: [PATCH 011/156] Expose online link extension method #1195 (#1196) --- docs/CHANGELOG-v2.md | 3 ++ src/PSRule/Data/RepositoryInfo.cs | 3 ++ src/PSRule/Data/TargetSourceInfo.cs | 34 ++++++++++++- src/PSRule/Definitions/IResourceHelpInfo.cs | 19 ++++++++ src/PSRule/Definitions/InfoString.cs | 15 ++++++ src/PSRule/Rules/RuleHelpInfo.cs | 54 +++++++++++++++++---- src/PSRule/Rules/RuleRecord.cs | 15 ++++++ 7 files changed, 133 insertions(+), 10 deletions(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 34f99fa847..aeb97846c7 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -15,6 +15,9 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers What's changed since pre-release v2.3.0-B0100: +- Engineering: + - Expose online link extension method by @BernieWhite. + [#1195](https://github.com/microsoft/PSRule/issues/1195) - Bug fixes: - Fixes JSON parsing of string array for single objects by @BernieWhite. [#1193](https://github.com/microsoft/PSRule/issues/1193) diff --git a/src/PSRule/Data/RepositoryInfo.cs b/src/PSRule/Data/RepositoryInfo.cs index 17bd5c18fa..9a13363d67 100644 --- a/src/PSRule/Data/RepositoryInfo.cs +++ b/src/PSRule/Data/RepositoryInfo.cs @@ -3,6 +3,9 @@ namespace PSRule.Data { + /// + /// Repository target information. + /// public sealed class RepositoryInfo : ITargetInfo { internal RepositoryInfo(string basePath, string headRef) diff --git a/src/PSRule/Data/TargetSourceInfo.cs b/src/PSRule/Data/TargetSourceInfo.cs index 3587a1f0f1..d1d4208fd9 100644 --- a/src/PSRule/Data/TargetSourceInfo.cs +++ b/src/PSRule/Data/TargetSourceInfo.cs @@ -10,7 +10,10 @@ namespace PSRule.Data { - public sealed class TargetSourceInfo + /// + /// An object source location reported by a downstream tool. + /// + public sealed class TargetSourceInfo : IEquatable { private const string PROPERTY_FILE = "file"; private const string PROPERTY_LINE = "line"; @@ -20,6 +23,9 @@ public sealed class TargetSourceInfo private const string COLON = ":"; private const string COLONSPACE = ": "; + /// + /// Creates an empty source information structure. + /// public TargetSourceInfo() { // Do nothing @@ -43,18 +49,31 @@ internal TargetSourceInfo(Uri uri) Type = PSRuleResources.FileSourceType; } + /// + /// The file path of the source file. + /// [JsonProperty(PropertyName = PROPERTY_FILE)] public string File { get; internal set; } + /// + /// The first line of the object. + /// [JsonProperty(PropertyName = PROPERTY_LINE)] public int? Line { get; internal set; } + /// + /// The first position of the object. + /// [JsonProperty(PropertyName = PROPERTY_POSITION)] public int? Position { get; internal set; } + /// + /// The type of source. + /// [JsonProperty(PropertyName = PROPERTY_TYPE)] public string Type { get; internal set; } + /// public bool Equals(TargetSourceInfo other) { return other != null && @@ -84,11 +103,18 @@ public override int GetHashCode() } } + /// public override string ToString() { return ToString(null, false); } + /// + /// Converts the souce information into a formatted string for display. + /// + /// The default type to use if the type was not specified. + /// Determine if a relative path is returned. + /// A formatted source string. public string ToString(string defaultType, bool useRelativePath) { var type = Type ?? defaultType; @@ -103,11 +129,17 @@ internal string GetPath(bool useRelativePath) return useRelativePath ? ExpressionHelpers.NormalizePath(PSRuleOption.GetWorkingPath(), File) : File; } + /// + /// Create source information from a structured object. + /// public static TargetSourceInfo Create(object o) { return o is PSObject pso ? Create(pso) : null; } + /// + /// Create source information from a structured object. + /// public static TargetSourceInfo Create(PSObject o) { var result = new TargetSourceInfo(); diff --git a/src/PSRule/Definitions/IResourceHelpInfo.cs b/src/PSRule/Definitions/IResourceHelpInfo.cs index 2a79605867..3a47686cb7 100644 --- a/src/PSRule/Definitions/IResourceHelpInfo.cs +++ b/src/PSRule/Definitions/IResourceHelpInfo.cs @@ -5,14 +5,29 @@ namespace PSRule.Definitions { + /// + /// Metadata about a PSRule resource. + /// public interface IResourceHelpInfo { + /// + /// The name of the resource. + /// string Name { get; } + /// + /// A display name of the resource if set. + /// string DisplayName { get; } + /// + /// A short description of the resource if set. + /// InfoString Synopsis { get; } + /// + /// A long description of the resource if set. + /// InfoString Description { get; } } @@ -26,15 +41,19 @@ internal ResourceHelpInfo(string name, string displayName, InfoString synopsis, Description = description; } + /// [JsonProperty(PropertyName = "name")] public string Name { get; } + /// [JsonProperty(PropertyName = "displayName")] public string DisplayName { get; } + /// [JsonProperty(PropertyName = "synopsis")] public InfoString Synopsis { get; } + /// [JsonProperty(PropertyName = "synopsis")] public InfoString Description { get; } } diff --git a/src/PSRule/Definitions/InfoString.cs b/src/PSRule/Definitions/InfoString.cs index 4361b20a84..02d23ea36e 100644 --- a/src/PSRule/Definitions/InfoString.cs +++ b/src/PSRule/Definitions/InfoString.cs @@ -1,8 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Diagnostics; + namespace PSRule.Definitions { + /// + /// A string formatted with plain text and/ or markdown. + /// + [DebuggerDisplay("{Text}")] public sealed class InfoString { private string _Text; @@ -16,11 +22,17 @@ internal InfoString(string text, string markdown = null) Markdown = markdown ?? text; } + /// + /// Determine if the information string is empty. + /// public bool HasValue { get { return Text != null || Markdown != null; } } + /// + /// A plain text representation. + /// public string Text { get { return _Text; } @@ -31,6 +43,9 @@ public string Text } } + /// + /// A markdown formatted representation if set. Otherwise this is the same as . + /// public string Markdown { get { return _Markdown; } diff --git a/src/PSRule/Rules/RuleHelpInfo.cs b/src/PSRule/Rules/RuleHelpInfo.cs index b7c2fda3ea..ebf8b1826f 100644 --- a/src/PSRule/Rules/RuleHelpInfo.cs +++ b/src/PSRule/Rules/RuleHelpInfo.cs @@ -10,37 +10,67 @@ namespace PSRule.Rules { + /// + /// A rule help information structure. + /// public interface IRuleHelpInfoV2 : IResourceHelpInfo { + /// + /// The rule recommendation. + /// InfoString Recommendation { get; } + /// + /// Additional annotations, which are string key/ value pairs. + /// Hashtable Annotations { get; } + /// + /// The name of the module where the rule was loaded from. + /// string ModuleName { get; } + /// + /// Additional online links to reference information for the rule. + /// Link[] Links { get; } } - internal static class RuleHelpInfoExtensions + /// + /// Extension methods for rule help information. + /// + public static class RuleHelpInfoExtensions { private const string ONLINE_HELP_LINK_ANNOTATION = "online version"; /// /// Get the URI for the online version of the documentation. /// - /// A URI when a valid link is set. Otherwise null is returned. + /// Returns the URI when a valid link is set, otherwise null is returned. public static Uri GetOnlineHelpUri(this IRuleHelpInfoV2 info) { - if (info.Annotations == null || !info.Annotations.ContainsKey(ONLINE_HELP_LINK_ANNOTATION)) - return null; - - if (Uri.TryCreate(info.Annotations[ONLINE_HELP_LINK_ANNOTATION].ToString(), UriKind.Absolute, out var result)) - return result; + var link = GetOnlineHelpUrl(info); + return link == null || + !Uri.TryCreate(link, UriKind.Absolute, out var result) ? + null : result; + } - return null; + /// + /// Get the URL for the online version of the documentation. + /// + /// Returns the URL when set, otherwise null is returned. + public static string GetOnlineHelpUrl(this IRuleHelpInfoV2 info) + { + return info == null || + info.Annotations == null || + !info.Annotations.ContainsKey(ONLINE_HELP_LINK_ANNOTATION) ? + null : info.Annotations[ONLINE_HELP_LINK_ANNOTATION].ToString(); } } + /// + /// An URL link to reference information. + /// public sealed class Link { internal Link(string name, string uri) @@ -49,8 +79,14 @@ internal Link(string name, string uri) Uri = uri; } + /// + /// The display name of the link. + /// public string Name { get; } + /// + /// The URL to the information, or the target link. + /// public string Uri { get; } } @@ -122,7 +158,7 @@ internal RuleHelpInfo(string name, string displayName, string moduleName, InfoSt /// Reference links for the rule. /// [JsonIgnore, YamlIgnore] - public Rules.Link[] Links { get; internal set; } + public Link[] Links { get; internal set; } /// /// Metadata annotations for the rule. diff --git a/src/PSRule/Rules/RuleRecord.cs b/src/PSRule/Rules/RuleRecord.cs index 928146aa58..5a47c23c6d 100644 --- a/src/PSRule/Rules/RuleRecord.cs +++ b/src/PSRule/Rules/RuleRecord.cs @@ -168,6 +168,9 @@ internal RuleRecord(string runId, ResourceId ruleId, string @ref, TargetObject t [JsonProperty(PropertyName = "time")] public long Time { get; internal set; } + /// + /// Additional information if the rule errored. If the rule passed or failed this value is null. + /// [DefaultValue(null)] [JsonProperty(PropertyName = "error")] public ErrorInfo Error { get; internal set; } @@ -187,16 +190,28 @@ internal RuleRecord(string runId, ResourceId ruleId, string @ref, TargetObject t [YamlMember()] public IResultDetailV2 Detail => _Detail; + /// + /// Determine if the rule is successful or skipped. + /// public bool IsSuccess() { return Outcome == RuleOutcome.Pass || Outcome == RuleOutcome.None; } + /// + /// Determine if the rule was executed and resulted in an outcome. + /// public bool IsProcessed() { return Outcome == RuleOutcome.Pass || Outcome == RuleOutcome.Fail || Outcome == RuleOutcome.Error; } + /// + /// Gets a string for output views in PowerShell. + /// + /// + /// This method is called by PowerShell. + /// public string GetReasonViewString() { return Reason == null || Reason.Length == 0 ? string.Empty : string.Join(Environment.NewLine, Reason); From 1d038e23f3371fb8f4b16670b6637454e859c936 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Wed, 27 Jul 2022 14:12:52 +1000 Subject: [PATCH 012/156] Pre-release v2.3.0-B0130 (#1197) --- docs/CHANGELOG-v2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index aeb97846c7..00c039c47c 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -13,6 +13,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.3.0-B0130 (pre-release) + What's changed since pre-release v2.3.0-B0100: - Engineering: From 42182e8cee04069a86ddef4b5e906e9358a68414 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Thu, 28 Jul 2022 15:37:29 +1000 Subject: [PATCH 013/156] Added PathPrefix metho to add path prefix #1198 (#1199) --- docs/CHANGELOG-v2.md | 6 ++ .../PSRule/en-US/about_PSRule_Assert.md | 14 +++++ src/PSRule/Definitions/ResultReason.cs | 57 +++++++++++++++++-- src/PSRule/Runtime/AssertResult.cs | 12 ++++ src/PSRule/Runtime/Operand.cs | 15 ++++- tests/PSRule.Tests/AssertTests.cs | 8 ++- tests/PSRule.Tests/FromFile.Rule.ps1 | 5 ++ tests/PSRule.Tests/PSRule.Common.Tests.ps1 | 6 +- tests/PSRule.Tests/PipelineTests.cs | 39 +++++++++++++ 9 files changed, 150 insertions(+), 12 deletions(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 00c039c47c..5125f218d6 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -13,6 +13,12 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since pre-release v2.3.0-B0130: + +- General improvements: + - Added `PathPrefix` method to add an object path prefix to assertion reasons by @BernieWhite. + [#1198](https://github.com/microsoft/PSRule/issues/1198) + ## v2.3.0-B0130 (pre-release) What's changed since pre-release v2.3.0-B0100: diff --git a/docs/concepts/PSRule/en-US/about_PSRule_Assert.md b/docs/concepts/PSRule/en-US/about_PSRule_Assert.md index 5206b0571d..c831b7c660 100644 --- a/docs/concepts/PSRule/en-US/about_PSRule_Assert.md +++ b/docs/concepts/PSRule/en-US/about_PSRule_Assert.md @@ -1663,6 +1663,9 @@ The following methods are available: This method can be chained, similar to `Reason`. - `ReasonIf( path, condition, text, params args)` - Replaces the reason if the condition is true. This method can be chained, similar to `ReasonFrom`. +- `PathPrefix( path)` - Adds a path prefix to any reasons. + This method can be chained. + For usage see examples below. - `GetReason()` - Gets any reasons currently associated with the failed result. - `Complete()` - Returns `$True` (Pass) or `$False` (Fail) to the rule record. If the assertion failed, any reasons are automatically added to the rule record. @@ -1701,6 +1704,17 @@ Rule 'Assert.HasCustomValue' { } ``` +In this example, the built-in reason has a path prefix added to any reasons. + +```powershell +Rule 'Assert.ChildHasFieldValue' { + $items = @($TargetObject.items) + for ($i = 0; $i -lt $items.Length; $i++) { + $Assert.HasFieldValue($items[$i], 'Name').PathPrefix("items[$i]") + } +} +``` + ### Downstream issues Before PSRule performs analysis external tools or rules modules may have already performed analysis. diff --git a/src/PSRule/Definitions/ResultReason.cs b/src/PSRule/Definitions/ResultReason.cs index b1b30976c0..249d046083 100644 --- a/src/PSRule/Definitions/ResultReason.cs +++ b/src/PSRule/Definitions/ResultReason.cs @@ -8,27 +8,62 @@ namespace PSRule.Definitions { internal sealed class ResultReason : IResultReasonV2 { + private string _Path; private string _Formatted; private string _Message; - private readonly IOperand _Operand; + private string _FullPath; + private readonly string _ParentPath; internal ResultReason(string parentPath, IOperand operand, string text, object[] args) { - _Operand = operand; + _ParentPath = parentPath; + Operand = operand; + _Path = Operand?.Path; Text = text; Args = args; - FullPath = ObjectPathJoin(parentPath, operand?.Path); } + internal IOperand Operand { get; } + /// /// The object path that failed. /// - public string Path => _Operand?.Path; + public string Path + { + get + { + _Path ??= GetPath(); + return _Path; + } + } + + /// + /// A prefix to add to the object path that failed. + /// + internal string Prefix + { + get { return Operand?.Prefix; } + set + { + if (Operand != null && Operand.Prefix != value) + { + Operand.Prefix = value; + _Formatted = _Path = _FullPath = null; + } + } + } /// /// The object path including the path of the parent object. /// - public string FullPath { get; } + public string FullPath + { + get + { + _FullPath ??= GetFullPath(); + return _FullPath; + } + } public string Text { get; } @@ -51,7 +86,7 @@ public override string ToString() public string Format() { _Formatted ??= string.Concat( - _Operand?.ToString(), + Operand?.ToString(), Message ); return _Formatted; @@ -64,5 +99,15 @@ private string ObjectPathJoin(string parentPath, string path) return string.IsNullOrEmpty(path) ? parentPath : string.Concat(parentPath, ".", path); } + + private string GetPath() + { + return ObjectPathJoin(Prefix, Operand?.Path); + } + + private string GetFullPath() + { + return ObjectPathJoin(_ParentPath, Path); + } } } diff --git a/src/PSRule/Runtime/AssertResult.cs b/src/PSRule/Runtime/AssertResult.cs index dbea5580d9..d8c78768f3 100644 --- a/src/PSRule/Runtime/AssertResult.cs +++ b/src/PSRule/Runtime/AssertResult.cs @@ -89,6 +89,18 @@ public AssertResult WithReason(string text, bool replace = false) return this; } + /// + /// Adds a logical path prefix on to each reason path. + /// + /// A string to prefix on each path. + public AssertResult PathPrefix(string prefix) + { + for (var i = 0; _Reason != null && i < _Reason.Count; i++) + _Reason[i].Prefix = prefix; + + return this; + } + /// /// Replace the existing reason with the supplied format string. /// diff --git a/src/PSRule/Runtime/Operand.cs b/src/PSRule/Runtime/Operand.cs index 37810ad087..f4bfc37c1e 100644 --- a/src/PSRule/Runtime/Operand.cs +++ b/src/PSRule/Runtime/Operand.cs @@ -60,6 +60,11 @@ public interface IOperand /// The object path to the operand. /// string Path { get; } + + /// + /// A logical prefix to add to the object path. + /// + string Prefix { get; set; } } internal sealed class Operand : IOperand @@ -80,6 +85,8 @@ private Operand(OperandKind kind, string path, object value) public string Path { get; } + public string Prefix { get; set; } + public OperandKind Kind { get; } internal static IOperand FromName(string name, string path) @@ -109,7 +116,13 @@ internal static IOperand FromTarget() public override string ToString() { - return string.IsNullOrEmpty(Path) || Kind == OperandKind.Target ? null : string.Concat(Enum.GetName(typeof(OperandKind), Kind), " ", Path, ": "); + return string.IsNullOrEmpty(Path) || Kind == OperandKind.Target ? null : OperandString(); + } + + private string OperandString() + { + var kind = Enum.GetName(typeof(OperandKind), Kind); + return string.IsNullOrEmpty(Prefix) ? string.Concat(kind, " ", Path, ": ") : string.Concat(kind, " ", Prefix, ".", Path, ": "); } } } diff --git a/tests/PSRule.Tests/AssertTests.cs b/tests/PSRule.Tests/AssertTests.cs index 67b78375a8..1d7fe0f6e4 100644 --- a/tests/PSRule.Tests/AssertTests.cs +++ b/tests/PSRule.Tests/AssertTests.cs @@ -79,6 +79,10 @@ public void Assertion() Assert.Equal("Path value2: New New Reason", actual1.ToString()); Assert.Equal("Path value2: New New Reason", actual1.GetReason()[0]); + // WithPathPrefix + actual1.PathPrefix("resources[0]"); + Assert.Equal("Path resources[0].value2: New New Reason", actual1.GetReason()[0]); + // Aggregate results Assert.True(assert.AnyOf(actual2, actual3).Result); Assert.True(assert.AnyOf(actual2).Result); @@ -91,8 +95,8 @@ public void Assertion() Assert.Equal("Fail reason", test1.GetReason()[0]); var test2 = assert.AllOf(actual1, actual2, actual3); - Assert.Equal("Path value2: New New Reason Fail reason", test2.ToString()); - Assert.Equal(new string[] { "Path value2: New New Reason", "Fail reason" }, test2.GetReason()); + Assert.Equal("Path resources[0].value2: New New Reason Fail reason", test2.ToString()); + Assert.Equal(new string[] { "Path resources[0].value2: New New Reason", "Fail reason" }, test2.GetReason()); Assert.True(assert.AllOf(actual2, actual2).Result); Assert.True(assert.AllOf(actual2).Result); Assert.False(assert.AllOf().Result); diff --git a/tests/PSRule.Tests/FromFile.Rule.ps1 b/tests/PSRule.Tests/FromFile.Rule.ps1 index 8dd74e9f2e..4083c539eb 100644 --- a/tests/PSRule.Tests/FromFile.Rule.ps1 +++ b/tests/PSRule.Tests/FromFile.Rule.ps1 @@ -472,3 +472,8 @@ Rule 'PS1RuleWarningLevel' -Level 'Warning' -Tag @{ test = 'Level' } { Rule 'PS1RuleInfoLevel' -Level 'Information' -Tag @{ test = 'Level' } { $Assert.HasFieldValue($TargetObject, 'name', 'TestObject1'); } + +# Synopsis: Test WithPathPrefix. +Rule 'WithPathPrefix' { + $Assert.HasFieldValue($TargetObject, 'Name', 'TestValue').PathPrefix('item'); +} diff --git a/tests/PSRule.Tests/PSRule.Common.Tests.ps1 b/tests/PSRule.Tests/PSRule.Common.Tests.ps1 index 15f8937f90..51d15f4a15 100644 --- a/tests/PSRule.Tests/PSRule.Common.Tests.ps1 +++ b/tests/PSRule.Tests/PSRule.Common.Tests.ps1 @@ -1320,7 +1320,7 @@ Describe 'Invoke-PSRule' -Tag 'Invoke-PSRule','Common' { $Null = $testObject | Invoke-PSRule @invokeParams2 -Option $option -WarningVariable outWarnings -WarningAction SilentlyContinue; $warningMessages = $outwarnings.ToArray(); - $warningMessages.Length | Should -Be 152; + $warningMessages.Length | Should -Be 154; $warningMessages | Should -BeOfType [System.Management.Automation.WarningRecord]; $warningMessages.Message | Should -MatchExactly "Rule '.\\[a-zA-Z0-9]+' was suppressed by suppression group '.\\SuppressWithTargetNameAnd(Null|Empty)Rule' for 'TestObject[1-2]'. Ignore test objects for all \((null|empty)\) rules." @@ -1364,9 +1364,9 @@ Describe 'Invoke-PSRule' -Tag 'Invoke-PSRule','Common' { $warningMessages.Length | Should -Be 2; $warningMessages[0] | Should -BeOfType [System.Management.Automation.WarningRecord]; - $warningMessages[0].Message | Should -BeExactly "76 rule/s were suppressed by suppression group '.\SuppressWithTargetNameAndNullRule' for 'TestObject1'. Ignore test objects for all (null) rules." + $warningMessages[0].Message | Should -BeExactly "77 rule/s were suppressed by suppression group '.\SuppressWithTargetNameAndNullRule' for 'TestObject1'. Ignore test objects for all (null) rules." $warningMessages[1] | Should -BeOfType [System.Management.Automation.WarningRecord]; - $warningMessages[1].Message | Should -BeExactly "76 rule/s were suppressed by suppression group '.\SuppressWithTargetNameAndEmptyRule' for 'TestObject2'. Ignore test objects for all (empty) rules." + $warningMessages[1].Message | Should -BeExactly "77 rule/s were suppressed by suppression group '.\SuppressWithTargetNameAndEmptyRule' for 'TestObject2'. Ignore test objects for all (empty) rules." } It 'No warnings' { diff --git a/tests/PSRule.Tests/PipelineTests.cs b/tests/PSRule.Tests/PipelineTests.cs index cc60544385..6857067e1e 100644 --- a/tests/PSRule.Tests/PipelineTests.cs +++ b/tests/PSRule.Tests/PipelineTests.cs @@ -86,6 +86,45 @@ public void InvokePipelineWithJObject() Assert.Equal("resources[1].Name", actual.Detail.Reason.First().FullPath); } + [Fact] + public void InvokePipelineWithPathPrefix() + { + var parent = new JObject + { + ["resources"] = new JArray(new object[] { + new JObject + { + ["Name"] = "TestValue" + }, + new JObject + { + ["Name"] = "TestValue2" + } + }) + }; + + var option = GetOption(); + option.Rule.Include = new string[] { "WithPathPrefix" }; + option.Input.Format = InputFormat.File; + var builder = PipelineBuilder.Invoke(GetSource(), option, null); + var writer = new TestWriter(option); + var pipeline = builder.Build(writer); + + Assert.NotNull(pipeline); + pipeline.Begin(); + pipeline.Process(PSObject.AsPSObject(parent["resources"][0])); + pipeline.Process(PSObject.AsPSObject(parent["resources"][1])); + pipeline.End(); + + var actual = (writer.Output[0] as InvokeResult).AsRecord().FirstOrDefault(); + Assert.Equal(RuleOutcome.Pass, actual.Outcome); + + actual = (writer.Output[1] as InvokeResult).AsRecord().FirstOrDefault(); + Assert.Equal(RuleOutcome.Fail, actual.Outcome); + Assert.Equal("item.Name", actual.Detail.Reason.First().Path); + Assert.Equal("resources[1].item.Name", actual.Detail.Reason.First().FullPath); + } + [Fact] public void BuildGetPipeline() { From a40db4014ce2278964a5be87ee5123afe287a74d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Aug 2022 07:45:37 +1000 Subject: [PATCH 014/156] Bump xunit from 2.4.1 to 2.4.2 (#1200) Bumps [xunit](https://github.com/xunit/xunit) from 2.4.1 to 2.4.2. - [Release notes](https://github.com/xunit/xunit/releases) - [Commits](https://github.com/xunit/xunit/compare/2.4.1...2.4.2) --- updated-dependencies: - dependency-name: xunit dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tests/PSRule.Tests/PSRule.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PSRule.Tests/PSRule.Tests.csproj b/tests/PSRule.Tests/PSRule.Tests.csproj index 4e37ce1ac6..5b46f25dfa 100644 --- a/tests/PSRule.Tests/PSRule.Tests.csproj +++ b/tests/PSRule.Tests/PSRule.Tests.csproj @@ -12,7 +12,7 @@ - + all runtime; build; native; contentfiles; analyzers From 9ccc521f539d9e58b89a8e5be4cf8abf4e0eeb96 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Wed, 3 Aug 2022 20:41:29 +1000 Subject: [PATCH 015/156] Pre-release v2.3.0-B0163 (#1201) --- docs/CHANGELOG-v2.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 5125f218d6..b700b45e10 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -13,11 +13,16 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.3.0-B0163 (pre-release) + What's changed since pre-release v2.3.0-B0130: - General improvements: - Added `PathPrefix` method to add an object path prefix to assertion reasons by @BernieWhite. [#1198](https://github.com/microsoft/PSRule/issues/1198) +- Engineering: + - Bump xunit to v2.4.2. + [#1200](https://github.com/microsoft/PSRule/pull/1200) ## v2.3.0-B0130 (pre-release) From 224a335602418b9d162a96157806cae5e2f22061 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Fri, 5 Aug 2022 00:06:31 +1000 Subject: [PATCH 016/156] Release v2.3.0 (#1202) --- docs/CHANGELOG-v2.md | 48 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index b700b45e10..6a2d42a8f3 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -13,6 +13,54 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.3.0 + +What's changed since v2.2.0: + +- General improvements: + - Added `PathPrefix` method to add an object path prefix to assertion reasons by @BernieWhite. + [#1198](https://github.com/microsoft/PSRule/issues/1198) + - Added support for binding with JSON objects by @BernieWhite. + [#1182](https://github.com/microsoft/PSRule/issues/1182) + - Added support for full path from JSON objects by @BernieWhite. + [#1174](https://github.com/microsoft/PSRule/issues/1174) + - Improved reporting of full object path from pre-processed results by @BernieWhite. + [#1169](https://github.com/microsoft/PSRule/issues/1169) + - Added PSRule for Azure expansion configuration to options schema by @BernieWhite. + [#1149](https://github.com/microsoft/PSRule/issues/1149) +- Engineering: + - Bump xunit to v2.4.2. + [#1200](https://github.com/microsoft/PSRule/pull/1200) + - Expose online link extension method by @BernieWhite. + [#1195](https://github.com/microsoft/PSRule/issues/1195) + - Added comment documentation to .NET classes and interfaces by @BernieWhite. + [#1186](https://github.com/microsoft/PSRule/issues/1186) + - Added publishing support for NuGet symbol packages @BernieWhite. + [#1173](https://github.com/microsoft/PSRule/issues/1173) + - Updated outcome option docs by @BernieWhite. + [#1166](https://github.com/microsoft/PSRule/issues/1166) + - Bump Sarif.Sdk to v2.4.16. + [#1177](https://github.com/microsoft/PSRule/pull/1177) + - Refactoring and updates to interfaces to allow use outside of PowerShell by @BernieWhite. + [#1152](https://github.com/microsoft/PSRule/issues/1152) +- Bug fixes: + - Fixes JSON parsing of string array for single objects by @BernieWhite. + [#1193](https://github.com/microsoft/PSRule/issues/1193) + - Fixed handling for JSON objects in rules by @BernieWhite. + [#1187](https://github.com/microsoft/PSRule/issues/1187) + - Fixed null object reference for object equity comparison by @BernieWhite. + [#1157](https://github.com/microsoft/PSRule/issues/1157) + - Fixed expression evaluation not logging debug output when using the `-Debug` switch by @BernieWhite. + [#1158](https://github.com/microsoft/PSRule/issues/1158) + - Fixed startIndex cannot be larger than length of string by @BernieWhite. + [#1160](https://github.com/microsoft/PSRule/issues/1160) + - Fixed path within SDK package causes `psd1` to compile by @BernieWhite. + [#1146](https://github.com/microsoft/PSRule/issues/1146) + +What's changed since pre-release v2.3.0-B0163: + +- No additional changes. + ## v2.3.0-B0163 (pre-release) What's changed since pre-release v2.3.0-B0130: From a5bd2fea8e8f9a70ac2ba89a48e1f027b3844b32 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 6 Aug 2022 10:14:18 +1000 Subject: [PATCH 017/156] Bump mkdocs-redirects from 1.0.4 to 1.0.5 (#1203) Bumps [mkdocs-redirects](https://github.com/datarobot/mkdocs-redirects) from 1.0.4 to 1.0.5. - [Release notes](https://github.com/datarobot/mkdocs-redirects/releases) - [Commits](https://github.com/datarobot/mkdocs-redirects/compare/v1.0.4...v1.0.5) --- updated-dependencies: - dependency-name: mkdocs-redirects dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index be3805275f..d7749a9dcf 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -5,4 +5,4 @@ mike==1.1.2 mkdocs-simple-hooks==0.1.5 mkdocs-git-revision-date-plugin==0.3.2 mdx-truly-sane-lists==1.3 -mkdocs-redirects==1.0.4 +mkdocs-redirects==1.0.5 From a9ebc6153d0521c9b72528b900d753d1c585ad1d Mon Sep 17 00:00:00 2001 From: Bernie White Date: Tue, 9 Aug 2022 01:20:49 +1000 Subject: [PATCH 018/156] Merge changes from release v2.3.1 (#1207) * Fixes object path for self identifier #1204 (#1205) * Release v2.3.1 (#1206) --- .vscode/settings.json | 3 ++- docs/CHANGELOG-v2.md | 8 ++++++++ src/PSRule/Definitions/ResultReason.cs | 12 ++---------- src/PSRule/Runtime/Operand.cs | 20 +++++++++++++++++++- tests/PSRule.Tests/AssertTests.cs | 4 ++++ 5 files changed, 35 insertions(+), 12 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 235540dbc1..13640f1761 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -89,6 +89,7 @@ "omnisharp.enableEditorConfigSupport": true, "omnisharp.enableRoslynAnalyzers": true, "git.branchProtection": [ - "main" + "main", + "release/*" ] } diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 6a2d42a8f3..0940b0424f 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -13,6 +13,14 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.3.1 + +What's changed since v2.3.0: + +- Bug fixes: + - Fixed object path join handling of self path identifier by @BernieWhite. + [#1204](https://github.com/microsoft/PSRule/issues/1204) + ## v2.3.0 What's changed since v2.2.0: diff --git a/src/PSRule/Definitions/ResultReason.cs b/src/PSRule/Definitions/ResultReason.cs index 249d046083..c6193a4c22 100644 --- a/src/PSRule/Definitions/ResultReason.cs +++ b/src/PSRule/Definitions/ResultReason.cs @@ -92,22 +92,14 @@ public string Format() return _Formatted; } - private string ObjectPathJoin(string parentPath, string path) - { - if (string.IsNullOrEmpty(parentPath)) - return path; - - return string.IsNullOrEmpty(path) ? parentPath : string.Concat(parentPath, ".", path); - } - private string GetPath() { - return ObjectPathJoin(Prefix, Operand?.Path); + return Runtime.Operand.JoinPath(Prefix, Operand?.Path); } private string GetFullPath() { - return ObjectPathJoin(_ParentPath, Path); + return Runtime.Operand.JoinPath(_ParentPath, Path); } } } diff --git a/src/PSRule/Runtime/Operand.cs b/src/PSRule/Runtime/Operand.cs index f4bfc37c1e..550c8597a7 100644 --- a/src/PSRule/Runtime/Operand.cs +++ b/src/PSRule/Runtime/Operand.cs @@ -69,6 +69,9 @@ public interface IOperand internal sealed class Operand : IOperand { + private const string Dot = "."; + private const string Space = " "; + private Operand(OperandKind kind, object value) { Kind = kind; @@ -114,6 +117,14 @@ internal static IOperand FromTarget() return new Operand(OperandKind.Target, null, null); } + internal static string JoinPath(string p1, string p2) + { + if (IsEmptyPath(p1)) + return p2; + + return IsEmptyPath(p2) ? p1 : string.Concat(p1, Dot, p2); + } + public override string ToString() { return string.IsNullOrEmpty(Path) || Kind == OperandKind.Target ? null : OperandString(); @@ -122,7 +133,14 @@ public override string ToString() private string OperandString() { var kind = Enum.GetName(typeof(OperandKind), Kind); - return string.IsNullOrEmpty(Prefix) ? string.Concat(kind, " ", Path, ": ") : string.Concat(kind, " ", Prefix, ".", Path, ": "); + return IsEmptyPath(Prefix) ? string.Concat(kind, Space, Path, ": ") : string.Concat(kind, Space, Prefix, Dot, Path, ": "); + } + + private static bool IsEmptyPath(string s) + { + return string.IsNullOrEmpty(s) || + string.IsNullOrWhiteSpace(s) || + s == Dot; } } } diff --git a/tests/PSRule.Tests/AssertTests.cs b/tests/PSRule.Tests/AssertTests.cs index 1d7fe0f6e4..9fca26166f 100644 --- a/tests/PSRule.Tests/AssertTests.cs +++ b/tests/PSRule.Tests/AssertTests.cs @@ -82,6 +82,10 @@ public void Assertion() // WithPathPrefix actual1.PathPrefix("resources[0]"); Assert.Equal("Path resources[0].value2: New New Reason", actual1.GetReason()[0]); + actual4.PathPrefix("resources[0]"); + Assert.Equal("Path resources[0].Name: Test reason", actual4.GetReason()[0]); + actual4.PathPrefix("."); + Assert.Equal("Path Name: Test reason", actual4.GetReason()[0]); // Aggregate results Assert.True(assert.AnyOf(actual2, actual3).Result); From a3232e8dc8b3764a25d92346000cf3bc8e5b2eab Mon Sep 17 00:00:00 2001 From: Bernie White Date: Wed, 10 Aug 2022 13:35:46 +1000 Subject: [PATCH 019/156] Bump support projects to .NET 6 #1209 (#1210) * Bump support projects to .NET 6 #1209 * Updtae benchmark and instructions --- docs/CHANGELOG-v2.md | 6 ++++++ docs/install-instructions.md | 8 ++++---- pipeline.build.ps1 | 11 ++--------- src/PSRule.Benchmark/PSRule.Benchmark.csproj | 10 +++------- src/PSRule.BuildTool/BadgeResource.cs | 17 +++++++++++++---- src/PSRule.BuildTool/PSRule.BuildTool.csproj | 6 +++++- tests/PSRule.Tests/PSRule.Tests.csproj | 6 +++--- 7 files changed, 36 insertions(+), 28 deletions(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 0940b0424f..4656106e76 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -13,6 +13,12 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since v2.3.1: + +- Engineering: + - Bump support projects to .NET 6 by @BernieWhite. + [#1209](https://github.com/microsoft/PSRule/issues/1209) + ## v2.3.1 What's changed since v2.3.0: diff --git a/docs/install-instructions.md b/docs/install-instructions.md index c5dc14271a..2f59c6ac17 100644 --- a/docs/install-instructions.md +++ b/docs/install-instructions.md @@ -155,9 +155,9 @@ The following PowerShell modules will be automatically install if the required v These additional modules are only required for building PSRule. -Additionally .NET Core SDK v3.1 is required. -.NET Core will not be automatically downloaded and installed. -To download and install the latest SDK see [Download .NET Core 3.1][dotnet]. +Additionally .NET SDK v6 is required. +.NET will not be automatically downloaded and installed. +To download and install the latest SDK see [Download .NET 6][dotnet]. ### Limited access networks @@ -191,4 +191,4 @@ After downloading the modules, copy the module directories to devices with restr *[CI]: continuous integration [module]: https://www.powershellgallery.com/packages/PSRule -[dotnet]: https://dotnet.microsoft.com/download/dotnet-core/3.1 +[dotnet]: https://dotnet.microsoft.com/download/dotnet/6.0 diff --git a/pipeline.build.ps1 b/pipeline.build.ps1 index 6d5cf5572c..f2fb935922 100644 --- a/pipeline.build.ps1 +++ b/pipeline.build.ps1 @@ -324,14 +324,7 @@ task Rules { task Benchmark { if ($Benchmark -or $BuildTask -eq 'Benchmark') { - dotnet run -p src/PSRule.Benchmark -f netcoreapp3.1 -c Release -- benchmark --output $PWD; - } -} - -# Synopsis: Add shipit build tag -task TagBuild { - if ($Null -ne $Env:BUILD_DEFINITIONNAME) { - Write-Host "`#`#vso[build.addbuildtag]shipit"; + dotnet run -p src/PSRule.Benchmark -f net6.0 -c Release -- benchmark --output $PWD; } } @@ -348,6 +341,6 @@ task Build Clean, BuildModule, BuildHelp, VersionModule, PackageModule task Test Build, Rules, TestDotNet, TestModule -task Release ReleaseModule, TagBuild +task Release ReleaseModule task AnalyzeRepository Build, Rules diff --git a/src/PSRule.Benchmark/PSRule.Benchmark.csproj b/src/PSRule.Benchmark/PSRule.Benchmark.csproj index d24cf224c5..7599ff38ed 100644 --- a/src/PSRule.Benchmark/PSRule.Benchmark.csproj +++ b/src/PSRule.Benchmark/PSRule.Benchmark.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6.0 AnyCPU {3ec0912f-bfc7-4b53-a1a1-0ba993c6282e} false @@ -23,12 +23,8 @@ - - - - - - + + diff --git a/src/PSRule.BuildTool/BadgeResource.cs b/src/PSRule.BuildTool/BadgeResource.cs index 7517db41ce..00652a4e07 100644 --- a/src/PSRule.BuildTool/BadgeResource.cs +++ b/src/PSRule.BuildTool/BadgeResource.cs @@ -1,10 +1,11 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; using System.CommandLine.Invocation; using System.Drawing; using System.IO; +using System.Runtime.InteropServices; using Newtonsoft.Json; namespace PSRule.BuildTool @@ -21,14 +22,19 @@ internal sealed class BadgeResource { public static void Build(BadgeResourceOption options, InvocationContext invocation) { + // Guard non-Windows platforms. + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + invocation.Console.Error.Write("This tool supports execution on Windows platforms only."); + invocation.ExitCode = 1; + return; + } + var c = GetChars(); var set = new object[c.Length][]; var padding = GetPadding(); - for (var i = 0; i < c.Length; i++) - { set[i] = new object[2] { c[i], Measure(c[i]) - padding }; - } var json = JsonConvert.SerializeObject(set); File.WriteAllText(Path.Combine(Directory.GetCurrentDirectory(), options.OutputPath ?? "en.json"), json); @@ -62,6 +68,9 @@ private static double Measure(char c) private static double Measure(string s) { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return 0; + using var font = new Font("Verdana", 11f, GraphicsUnit.Pixel); using var g = Graphics.FromHwnd(IntPtr.Zero); var size = g.MeasureString(s, font); diff --git a/src/PSRule.BuildTool/PSRule.BuildTool.csproj b/src/PSRule.BuildTool/PSRule.BuildTool.csproj index a9210b9ccf..02f5db9501 100644 --- a/src/PSRule.BuildTool/PSRule.BuildTool.csproj +++ b/src/PSRule.BuildTool/PSRule.BuildTool.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6.0 PSRule.BuildTool false false @@ -14,6 +14,10 @@ + + Windows + + CmdStrings.resx diff --git a/tests/PSRule.Tests/PSRule.Tests.csproj b/tests/PSRule.Tests/PSRule.Tests.csproj index 5b46f25dfa..fd858d8bd1 100644 --- a/tests/PSRule.Tests/PSRule.Tests.csproj +++ b/tests/PSRule.Tests/PSRule.Tests.csproj @@ -1,7 +1,7 @@ - + - netcoreapp3.1 + net6.0 {d3488ce2-779f-4474-b38a-f894a4b689f7} true false @@ -11,7 +11,7 @@ - + all From c2f2f10c252947e9e252a88d8f0a9d65456fd20f Mon Sep 17 00:00:00 2001 From: Bernie White Date: Wed, 10 Aug 2022 14:49:36 +1000 Subject: [PATCH 020/156] Improved documentation of classes #1186 (#1211) --- .editorconfig | 26 ++++++- README.md | 28 ++++---- src/PSRule.Badges/SvgBuilder.cs | 1 + src/PSRule.BuildTool/ClientBuilder.cs | 2 +- src/PSRule/Badges/BadgeBuilder.cs | 39 +++++++++-- src/PSRule/Common/KeyMapDictionary.cs | 67 +++++++++++++++++- src/PSRule/Common/ReadOnlyHashtable.cs | 5 ++ src/PSRule/Configuration/BindingOption.cs | 8 ++- .../Configuration/ConfigurationOption.cs | 32 ++++++++- src/PSRule/Configuration/ExecutionOption.cs | 13 +++- src/PSRule/Configuration/InputOption.cs | 6 +- src/PSRule/Configuration/PSRuleOption.cs | 69 ++++++++++++++----- src/PSRule/Configuration/RepositoryOption.cs | 16 ++++- src/PSRule/Configuration/RuleOption.cs | 16 ++++- src/PSRule/Configuration/SuppressionOption.cs | 55 +++++++++++++-- src/PSRule/Pipeline/Source.cs | 45 +++++++++++- 16 files changed, 376 insertions(+), 52 deletions(-) diff --git a/.editorconfig b/.editorconfig index 2ca08167a3..f7a39e14d1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,6 +7,7 @@ root = true [*] charset = utf-8 indent_style = space +insert_final_newline = true # Source code [*.{cs,ps1,psd1,psm1}] @@ -44,4 +45,27 @@ csharp_prefer_simple_default_expression = true:suggestion # Prefer "var" everywhere csharp_style_var_for_built_in_types = true:suggestion csharp_style_var_when_type_is_apparent = true:suggestion -csharp_style_var_elsewhere = true:suggestion \ No newline at end of file +csharp_style_var_elsewhere = true:suggestion + +# Define the 'private_fields' symbol group: +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private + +# Define the 'private_static_fields' symbol group +dotnet_naming_symbols.private_static_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private +dotnet_naming_symbols.private_static_fields.required_modifiers = static + +# Define the 'underscored' naming style +dotnet_naming_style.underscored.capitalization = pascal_case +dotnet_naming_style.underscored.required_prefix = _ + +# Private instance fields must use pascal case with a leading '_' +dotnet_naming_rule.private_fields_underscored.symbols = private_fields +dotnet_naming_rule.private_fields_underscored.style = underscored +dotnet_naming_rule.private_fields_underscored.severity = error + +# Exclude private static fields from underscored style +dotnet_naming_rule.private_static_fields_none.symbols = private_static_fields +dotnet_naming_rule.private_static_fields_none.style = underscored +dotnet_naming_rule.private_static_fields_none.severity = none diff --git a/README.md b/README.md index bcffca2613..d6fe505a0c 100644 --- a/README.md +++ b/README.md @@ -58,9 +58,13 @@ You can download and install the PSRule module from the PowerShell Gallery. Module | Description | Downloads / instructions ------ | ----------- | ------------------------ -PSRule | Validate infrastructure as code (IaC) and objects using PowerShell rules. | [latest][module-psrule] / [instructions][install] +PSRule | Validate infrastructure as code (IaC) and objects using PowerShell rules. | [latest][8] / [instructions][9] -For rule and integration modules see [related projects](#related-projects). +For rule and integration modules see [related projects][10]. + + [8]: https://www.powershellgallery.com/packages/PSRule + [9]: https://microsoft.github.io/PSRule/v2/install-instructions/ + [10]: https://microsoft.github.io/PSRule/v2/related-projects/ ## Getting extensions @@ -68,9 +72,13 @@ Companion extensions are available for the following platforms. Platform | Description | Downloads / instructions -------- | ----------- | ------------------------ -Azure Pipelines | Validate infrastructure as code (IaC) and DevOps repositories using Azure Pipelines. | [latest][extension-pipelines] / [instructions][install] -GitHub Actions | Validate infrastructure as code (IaC) and DevOps repositories using GitHub Actions. | [latest][extension-actions] / [instructions][install] -Visual Studio Code | Visual Studio Code extension for PSRule. | [latest][extension-vscode] / [instructions][install] +Azure Pipelines | Validate infrastructure as code (IaC) and DevOps repositories using Azure Pipelines. | [latest][11] / [instructions][9] +GitHub Actions | Validate infrastructure as code (IaC) and DevOps repositories using GitHub Actions. | [latest][12] / [instructions][9] +Visual Studio Code | Visual Studio Code extension for PSRule. | [latest][13] / [instructions][9] + + [11]: https://marketplace.visualstudio.com/items?itemName=bewhite.ps-rule + [12]: https://github.com/marketplace/actions/psrule + [13]: https://marketplace.visualstudio.com/items?itemName=bewhite.psrule-vscode ## Getting started @@ -300,12 +308,12 @@ PSRule uses the following schemas: ## Related projects -For a list of projects and integrations see [Related projects](https://microsoft.github.io/PSRule/v2/related-projects/). +For a list of projects and integrations see [Related projects][10]. ## Changes and versioning Modules in this repository use [semantic versioning](https://semver.org/) to declare breaking changes. -For a list of module changes please see the [change log](CHANGELOG.md). +For a list of module changes please see the [change log](https://aka.ms/ps-rule/changelog). > Pre-release module versions are created on major commits and can be installed from the PowerShell Gallery. > Pre-release versions should be considered experimental. @@ -330,9 +338,3 @@ or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any addi ## License This project is [licensed under the MIT License](LICENSE). - -[install]: https://microsoft.github.io/PSRule/v2/install-instructions/ -[module-psrule]: https://www.powershellgallery.com/packages/PSRule -[extension-vscode]: https://marketplace.visualstudio.com/items?itemName=bewhite.psrule-vscode -[extension-pipelines]: https://marketplace.visualstudio.com/items?itemName=bewhite.ps-rule -[extension-actions]: https://github.com/marketplace/actions/psrule diff --git a/src/PSRule.Badges/SvgBuilder.cs b/src/PSRule.Badges/SvgBuilder.cs index 8c91868cd0..3f561bc787 100644 --- a/src/PSRule.Badges/SvgBuilder.cs +++ b/src/PSRule.Badges/SvgBuilder.cs @@ -98,6 +98,7 @@ public void Begin(string text) Append($""); } + /// public override string ToString() { return _Builder.ToString(); diff --git a/src/PSRule.BuildTool/ClientBuilder.cs b/src/PSRule.BuildTool/ClientBuilder.cs index 8583dee40c..c7784de952 100644 --- a/src/PSRule.BuildTool/ClientBuilder.cs +++ b/src/PSRule.BuildTool/ClientBuilder.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.CommandLine; diff --git a/src/PSRule/Badges/BadgeBuilder.cs b/src/PSRule/Badges/BadgeBuilder.cs index 321dc1366e..00bd7e738b 100644 --- a/src/PSRule/Badges/BadgeBuilder.cs +++ b/src/PSRule/Badges/BadgeBuilder.cs @@ -11,15 +11,30 @@ namespace PSRule.Badges { + /// + /// The type of badge. + /// public enum BadgeType { + /// + /// A badge that reports an unknown state. + /// Unknown = 0, + /// + /// A badge reporting a successful state. + /// Success = 1, + /// + /// A bagde reporting a failed state. + /// Failure = 2 } + /// + /// An instance of a badge created by the badge API. + /// public interface IBadge { /// @@ -33,6 +48,9 @@ public interface IBadge void ToFile(string path); } + /// + /// A builder for the badge API. + /// public interface IBadgeBuilder { /// @@ -59,6 +77,9 @@ public interface IBadgeBuilder IBadge Create(string title, BadgeType type, string label); } + /// + /// An instance of a badge created by the Badge API. + /// internal sealed class Badge : IBadge { private readonly string _LeftText; @@ -81,11 +102,13 @@ internal Badge(string left, string right, string fill) _Fill = fill; } + /// public override string ToString() { return ToSvg(); } + /// public string ToSvg() { var w = (int)Math.Round(_LeftWidth + _RightWidth + 2 * _BorderPadding + 2 * _MidPadding); @@ -106,6 +129,7 @@ public string ToSvg() return builder.ToString(); } + /// public void ToFile(string path) { path = PSRuleOption.GetRootedPath(path); @@ -117,6 +141,9 @@ public void ToFile(string path) } } + /// + /// A badge builder that implements the Badge API within PSRule. + /// internal sealed class BadgeBuilder : IBadgeBuilder { private const string BADGE_FILL_GREEN = "#4CAF50"; @@ -125,16 +152,19 @@ internal sealed class BadgeBuilder : IBadgeBuilder #region IBadgeBuilder + /// public IBadge Create(string title, BadgeType type, string label) { return CreateCustom(title, label, GetTypeFill(type)); } + /// public IBadge Create(InvokeResult result) { return CreateInternal(GetOutcome(result)); } + /// public IBadge Create(IEnumerable result) { var worstCase = RuleOutcome.Pass; @@ -154,6 +184,8 @@ public IBadge Create(IEnumerable result) #endregion IBadgeBuilder + #region Private helper methods + private static string GetOutcomeFill(RuleOutcome outcome) { if (outcome == RuleOutcome.Pass) @@ -173,10 +205,7 @@ private static string GetOutcomeLabel(RuleOutcome outcome) if (outcome == RuleOutcome.Fail) return PSRuleResources.OutcomeFail; - if (outcome == RuleOutcome.Error) - return PSRuleResources.OutcomeError; - - return PSRuleResources.OutcomeUnknown; + return outcome == RuleOutcome.Error ? PSRuleResources.OutcomeError : PSRuleResources.OutcomeUnknown; } private static RuleOutcome GetOutcome(InvokeResult result) @@ -206,5 +235,7 @@ private static IBadge CreateInternal(RuleOutcome outcome) var outcomeFill = GetOutcomeFill(outcome); return CreateCustom("PSRule", outcomeLabel, outcomeFill); } + + #endregion Private helper methods } } diff --git a/src/PSRule/Common/KeyMapDictionary.cs b/src/PSRule/Common/KeyMapDictionary.cs index 511549d668..c4c6ed58d4 100644 --- a/src/PSRule/Common/KeyMapDictionary.cs +++ b/src/PSRule/Common/KeyMapDictionary.cs @@ -9,15 +9,27 @@ namespace PSRule { + /// + /// A dictionary of key/ value pairs indexed by a string key that is case-insensitive. + /// + /// public abstract class KeyMapDictionary : DynamicObject, IDictionary { private readonly Dictionary _Map; + /// + /// Create an empty map. + /// internal protected KeyMapDictionary() { _Map = new Dictionary(StringComparer.OrdinalIgnoreCase); } + /// + /// Create a map intially populated with values copied from an existing instance. + /// + /// An existing instance to copy key/ values from. + /// Is raised if the map is null. internal protected KeyMapDictionary(KeyMapDictionary map) { if (map == null) @@ -26,6 +38,10 @@ internal protected KeyMapDictionary(KeyMapDictionary map) _Map = new Dictionary(map._Map, StringComparer.OrdinalIgnoreCase); } + /// + /// Create a map intially populated with values copied from a dictionary. + /// + /// An existing dictionary to copy key/ values from. internal protected KeyMapDictionary(IDictionary dictionary) { _Map = dictionary == null ? @@ -33,76 +49,111 @@ internal protected KeyMapDictionary(IDictionary dictionary) new Dictionary(dictionary, StringComparer.OrdinalIgnoreCase); } + /// + /// Create a map intially populated with values copied from a hashtable. + /// + /// An existing hashtable to copy key/ values from. internal protected KeyMapDictionary(Hashtable hashtable) : this() { Load(hashtable); } + /// public TValue this[string key] { get => _Map[key]; set => _Map[key] = value; } + /// public ICollection Keys => _Map.Keys; + /// public ICollection Values => _Map.Values; + /// public int Count => _Map.Count; + /// public bool IsReadOnly => false; + /// public void Add(string key, TValue value) { _Map.Add(key, value); } + /// public void Add(KeyValuePair item) { Add(item.Key, item.Value); } + /// + /// Clear the map of all keys and values. + /// public void Clear() { _Map.Clear(); } + /// public bool Contains(KeyValuePair item) { return ((IDictionary)_Map).Contains(item); } + /// + /// Determines if a specified key exists in the map. + /// + /// The key map. + /// public bool ContainsKey(string key) { return _Map.ContainsKey(key); } + /// public void CopyTo(KeyValuePair[] array, int arrayIndex) { ((IDictionary)_Map).CopyTo(array, arrayIndex); } + /// public IEnumerator> GetEnumerator() { return _Map.GetEnumerator(); } + /// + /// Remove the key/ value from the map by key. + /// + /// The key of the key/ value to remove. + /// Returns true if the element was found and removed. public bool Remove(string key) { return _Map.Remove(key); } + /// public bool Remove(KeyValuePair item) { return ((IDictionary)_Map).Remove(item); } + /// + /// Try to get the value from the specified key. + /// + /// The specific key to find in the map. + /// The value of the specific key. + /// Returns true if the key was found and returned. public bool TryGetValue(string key, out TValue value) { return _Map.TryGetValue(key, out value); } + /// IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); @@ -111,6 +162,7 @@ IEnumerator IEnumerable.GetEnumerator() /// /// Load options from a hashtable. /// + /// Is raised if the hashtable is null. protected void Load(Hashtable hashtable) { if (hashtable == null) @@ -121,8 +173,10 @@ protected void Load(Hashtable hashtable) } /// - /// Load options from environment variables. + /// Load values from environment variables into the option. + /// Keys that appear in both will replaced by environment variable values. /// + /// Is raised if the environment helper is null. internal void Load(string prefix, EnvironmentHelper env, Func format = null) { if (env == null) @@ -141,8 +195,10 @@ internal void Load(string prefix, EnvironmentHelper env, Func fo } /// - /// Load options from a dictionary. + /// Load values from a key/ value dictionary into the option. + /// Keys that appear in both will replaced by dictionary values. /// + /// Is raised if the dictionary is null. protected void Load(string prefix, IDictionary dictionary) { if (dictionary == null) @@ -176,6 +232,13 @@ private static bool TryKeyPrefix(string key, string prefix, out string suffix) return false; } + /// + /// Get the value of a dynamic object member. + /// + /// A dynamic binder object. + /// The value of the member. + /// Returns true if the member was found and false if the member was not. + /// Is raised if the binder is null. public sealed override bool TryGetMember(GetMemberBinder binder, out object result) { if (binder == null) diff --git a/src/PSRule/Common/ReadOnlyHashtable.cs b/src/PSRule/Common/ReadOnlyHashtable.cs index 16caee6b9c..18ec3bbb3c 100644 --- a/src/PSRule/Common/ReadOnlyHashtable.cs +++ b/src/PSRule/Common/ReadOnlyHashtable.cs @@ -5,11 +5,16 @@ namespace PSRule { + /// + /// Defined a readonly hashtable. + /// public sealed class ReadOnlyHashtable : Hashtable { internal ReadOnlyHashtable(IDictionary dictionary, IEqualityComparer equalityComparer) : base(dictionary, equalityComparer) { } + + /// public override bool IsReadOnly => true; } } diff --git a/src/PSRule/Configuration/BindingOption.cs b/src/PSRule/Configuration/BindingOption.cs index 4671ce62e0..5f955b4941 100644 --- a/src/PSRule/Configuration/BindingOption.cs +++ b/src/PSRule/Configuration/BindingOption.cs @@ -7,7 +7,7 @@ namespace PSRule.Configuration { /// - /// Options tht affect property binding of TargetName and TargetType. + /// Options that affect property binding of TargetName and TargetType. /// public sealed class BindingOption : IEquatable { @@ -16,7 +16,7 @@ public sealed class BindingOption : IEquatable private const string DEFAULT_NAMESEPARATOR = "/"; private const bool DEFAULT_USEQUALIFIEDNAME = false; - internal static readonly BindingOption Default = new BindingOption + internal static readonly BindingOption Default = new() { IgnoreCase = DEFAULT_IGNORECASE, NameSeparator = DEFAULT_NAMESEPARATOR, @@ -92,6 +92,10 @@ public override int GetHashCode() } } + /// + /// Merge two option instances by repacing any unset properties from with values. + /// Values from that are set are not overridden. + /// internal static BindingOption Combine(BindingOption o1, BindingOption o2) { var result = new BindingOption(o1) diff --git a/src/PSRule/Configuration/ConfigurationOption.cs b/src/PSRule/Configuration/ConfigurationOption.cs index 3e16d7148d..308cbd37ee 100644 --- a/src/PSRule/Configuration/ConfigurationOption.cs +++ b/src/PSRule/Configuration/ConfigurationOption.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Collections; @@ -14,20 +14,38 @@ public sealed class ConfigurationOption : KeyMapDictionary private const string ENVIRONMENT_PREFIX = "PSRULE_CONFIGURATION_"; private const string DICTIONARY_PREFIX = "Configuration."; + /// + /// Creates an empty configuration option. + /// public ConfigurationOption() : base() { } + /// + /// Creates a configuration option by copying an existing instance. + /// + /// The option instance to copy. public ConfigurationOption(ConfigurationOption option) : base(option) { } + /// + /// Creates a configuration option from a hashtable. + /// private ConfigurationOption(Hashtable hashtable) : base(hashtable) { } + /// + /// Convert a hashtable (commonly used in PowerShell) to a configuration option. + /// + /// The hashtable to convert. public static implicit operator ConfigurationOption(Hashtable hashtable) { return new ConfigurationOption(hashtable); } + /// + /// Merge two option instances by repacing any unset properties from with values. + /// Values from that are set are not overridden. + /// internal static ConfigurationOption Combine(ConfigurationOption o1, ConfigurationOption o2) { var result = new ConfigurationOption(o1); @@ -35,14 +53,22 @@ internal static ConfigurationOption Combine(ConfigurationOption o1, Configuratio return result; } + /// + /// Load values from environment variables into the configuration option. + /// Keys that appear in both will replaced by environment variable values. + /// internal void Load(EnvironmentHelper env) { - base.Load(ENVIRONMENT_PREFIX, env); + Load(ENVIRONMENT_PREFIX, env); } + /// + /// Load values from a key/ value dictionary into the configuration option. + /// Keys that appear in both will replaced by dictionary values. + /// internal void Load(IDictionary dictionary) { - base.Load(DICTIONARY_PREFIX, dictionary); + Load(DICTIONARY_PREFIX, dictionary); } } } diff --git a/src/PSRule/Configuration/ExecutionOption.cs b/src/PSRule/Configuration/ExecutionOption.cs index f595816b67..a71b86f2ee 100644 --- a/src/PSRule/Configuration/ExecutionOption.cs +++ b/src/PSRule/Configuration/ExecutionOption.cs @@ -19,7 +19,7 @@ public sealed class ExecutionOption : IEquatable private const bool DEFAULT_ALIASREFERENCEWARNING = true; private const bool DEFAULT_INVARIANTCULTUREWARNING = true; - internal static readonly ExecutionOption Default = new ExecutionOption + internal static readonly ExecutionOption Default = new() { AliasReferenceWarning = DEFAULT_ALIASREFERENCEWARNING, LanguageMode = DEFAULT_LANGUAGEMODE, @@ -29,6 +29,9 @@ public sealed class ExecutionOption : IEquatable InvariantCultureWarning = DEFAULT_INVARIANTCULTUREWARNING }; + /// + /// Creates an empty execution option. + /// public ExecutionOption() { AliasReferenceWarning = null; @@ -39,6 +42,10 @@ public ExecutionOption() InvariantCultureWarning = null; } + /// + /// Creates a execution option by copying an existing instance. + /// + /// The option instance to copy. public ExecutionOption(ExecutionOption option) { if (option == null) @@ -86,6 +93,10 @@ public override int GetHashCode() } } + /// + /// Merge two option instances by repacing any unset properties from with values. + /// Values from that are set are not overridden. + /// internal static ExecutionOption Combine(ExecutionOption o1, ExecutionOption o2) { var result = new ExecutionOption(o1) diff --git a/src/PSRule/Configuration/InputOption.cs b/src/PSRule/Configuration/InputOption.cs index 70fad4bd02..3574d2829a 100644 --- a/src/PSRule/Configuration/InputOption.cs +++ b/src/PSRule/Configuration/InputOption.cs @@ -20,7 +20,7 @@ public sealed class InputOption : IEquatable private const string[] DEFAULT_PATHIGNORE = null; private const string[] DEFAULT_TARGETTYPE = null; - internal static readonly InputOption Default = new InputOption + internal static readonly InputOption Default = new() { Format = DEFAULT_FORMAT, IgnoreGitPath = DEFAULT_IGNOREGITPATH, @@ -99,6 +99,10 @@ public override int GetHashCode() } } + /// + /// Merge two option instances by repacing any unset properties from with values. + /// Values from that are set are not overridden. + /// internal static InputOption Combine(InputOption o1, InputOption o2) { var result = new InputOption(o1) diff --git a/src/PSRule/Configuration/PSRuleOption.cs b/src/PSRule/Configuration/PSRuleOption.cs index 5e8b4cd42e..9bcc1049f7 100644 --- a/src/PSRule/Configuration/PSRuleOption.cs +++ b/src/PSRule/Configuration/PSRuleOption.cs @@ -32,9 +32,12 @@ public sealed class PSRuleOption : IEquatable, IBaselineSpec private const char Backslash = '\\'; private const char Slash = '/'; - private string SourcePath; + /// + /// The original source path the options were loaded from if applicable. + /// + private string _SourcePath; - private static readonly PSRuleOption Default = new PSRuleOption + private static readonly PSRuleOption Default = new() { Binding = BindingOption.Default, Convention = ConventionOption.Default, @@ -79,7 +82,7 @@ public PSRuleOption() private PSRuleOption(string sourcePath, PSRuleOption option) { - SourcePath = sourcePath; + _SourcePath = sourcePath; // Set from existing option instance Binding = new BindingOption(option?.Binding); @@ -102,6 +105,9 @@ private PSRuleOption(string sourcePath, PSRuleOption option) /// public BindingOption Binding { get; set; } + /// + /// Allows configuration key/ values to be specified that can be used within rule definitions. + /// public ConfigurationOption Configuration { get; set; } /// @@ -134,10 +140,16 @@ private PSRuleOption(string sourcePath, PSRuleOption option) /// public OutputOption Output { get; set; } + /// + /// Configures pipeline hooks. + /// [YamlIgnore] [JsonIgnore] public PipelineHook Pipeline { get; set; } + /// + /// Options for repository properties that are used by PSRule. + /// public RepositoryOption Repository { get; set; } /// @@ -145,6 +157,9 @@ private PSRuleOption(string sourcePath, PSRuleOption option) /// public RequiresOption Requires { get; set; } + /// + /// Options for that affect which rules are executed by including and filtering discovered rules. + /// public RuleOption Rule { get; set; } /// @@ -155,31 +170,50 @@ private PSRuleOption(string sourcePath, PSRuleOption option) /// /// Return options as YAML. /// + /// PSRule options serialized as YAML. /// /// Called from PowerShell. /// public string ToYaml() { var yaml = GetYaml(); - return string.IsNullOrEmpty(SourcePath) + return string.IsNullOrEmpty(_SourcePath) ? yaml : string.Concat( string.Format( Thread.CurrentThread.CurrentCulture, PSRuleResources.OptionsSourceComment, - SourcePath), + _SourcePath), Environment.NewLine, yaml); } + /// + /// Create a new object instance with the same options set. + /// + /// A new instance. public PSRuleOption Clone() { - return new PSRuleOption(sourcePath: SourcePath, option: this); + return new PSRuleOption(sourcePath: _SourcePath, option: this); } + /// + /// Create a instance from PSRule defaults. + /// + /// A new instance. + public static PSRuleOption FromDefault() + { + return Default.Clone(); + } + + /// + /// Merge two option instances by repacing any unset properties from with values. + /// Values from that are set are not overridden. + /// + /// A new instance combining options from both instances. private static PSRuleOption Combine(PSRuleOption o1, PSRuleOption o2) { - var result = new PSRuleOption(o1?.SourcePath ?? o2?.SourcePath, o1); + var result = new PSRuleOption(o1?._SourcePath ?? o2?._SourcePath, o1); result.Binding = BindingOption.Combine(result.Binding, o2?.Binding); result.Configuration = ConfigurationOption.Combine(result.Configuration, o2?.Configuration); result.Convention = ConventionOption.Combine(result.Convention, o2?.Convention); @@ -203,11 +237,6 @@ public void ToFile(string path) File.WriteAllText(path: filePath, contents: GetYaml()); } - public static PSRuleOption FromDefault() - { - return Default.Clone(); - } - /// /// Load a YAML formatted PSRuleOption object from disk. /// @@ -271,7 +300,7 @@ public static PSRuleOption FromFileOrEmpty(PSRuleOption option, string path) if (option == null) return FromFileOrEmpty(path); - return string.IsNullOrEmpty(option.SourcePath) ? Combine(option, FromFileOrEmpty(path)) : option; + return string.IsNullOrEmpty(option._SourcePath) ? Combine(option, FromFileOrEmpty(path)) : option; } private static PSRuleOption FromYaml(string path, string yaml) @@ -286,7 +315,7 @@ private static PSRuleOption FromYaml(string path, string yaml) .Build(); var option = d.Deserialize(yaml) ?? new PSRuleOption(); - option.SourcePath = path; + option._SourcePath = path; return option; } @@ -300,8 +329,7 @@ private static PSRuleOption FromYaml(string path, string yaml) /// private static PSRuleOption FromEnvironment(PSRuleOption option) { - if (option == null) - option = new PSRuleOption(); + option ??= new PSRuleOption(); // Start loading matching values var env = EnvironmentHelper.Default; @@ -390,11 +418,19 @@ public static void UseCurrentCulture(CultureInfo culture) _CurrentCulture = culture; } + /// + /// Gets the current working path being used by PSRule. + /// + /// The current working path. public static string GetWorkingPath() { return _GetWorkingPath(); } + /// + /// Get the current culture being used by PSRule. + /// + /// The current culture. public static CultureInfo GetCurrentCulture() { return _CurrentCulture; @@ -497,6 +533,7 @@ public static string GetFilePath(string path) /// /// A full or relative path. /// When set to true the returned path uses forward slashes instead of backslashes. + /// The base path to use. When null of unspecified, the current working path will be used. /// A absolute path. internal static string GetRootedPath(string path, bool normalize = false, string basePath = null) { diff --git a/src/PSRule/Configuration/RepositoryOption.cs b/src/PSRule/Configuration/RepositoryOption.cs index 320c6b23d1..157418ddb1 100644 --- a/src/PSRule/Configuration/RepositoryOption.cs +++ b/src/PSRule/Configuration/RepositoryOption.cs @@ -8,11 +8,11 @@ namespace PSRule.Configuration { /// - /// Options that repository properties that are used by PSRule. + /// Options for repository properties that are used by PSRule. /// public sealed class RepositoryOption : IEquatable { - internal static readonly RepositoryOption Default = new RepositoryOption + internal static readonly RepositoryOption Default = new() { }; @@ -62,6 +62,10 @@ public override int GetHashCode() } } + /// + /// Merge two option instances by repacing any unset properties from with values. + /// Values from that are set are not overridden. + /// internal static RepositoryOption Combine(RepositoryOption o1, RepositoryOption o2) { var result = new RepositoryOption(o1) @@ -77,12 +81,20 @@ internal static RepositoryOption Combine(RepositoryOption o1, RepositoryOption o [DefaultValue(null)] public string Url { get; set; } + /// + /// Load options from environment variables into repository option. + /// Options that appear in both will replaced by environment variable values. + /// internal void Load(EnvironmentHelper env) { if (env.TryString("PSRULE_REPOSITORY_URL", out var url)) Url = url; } + /// + /// Load options from a key/ value dictionary into the repository options. + /// Options that appear in both will replaced by dictionary values. + /// internal void Load(Dictionary index) { if (index.TryPopString("Repository.Url", out var url)) diff --git a/src/PSRule/Configuration/RuleOption.cs b/src/PSRule/Configuration/RuleOption.cs index 8c15f0785e..cbd7b86f48 100644 --- a/src/PSRule/Configuration/RuleOption.cs +++ b/src/PSRule/Configuration/RuleOption.cs @@ -7,15 +7,21 @@ namespace PSRule.Configuration { + /// + /// Options for that affect which rules are executed by including and filtering discovered rules. + /// public sealed class RuleOption : IEquatable { private const bool DEFAULT_INCLUDELOCAL = false; - internal static readonly RuleOption Default = new RuleOption + internal static readonly RuleOption Default = new() { IncludeLocal = DEFAULT_INCLUDELOCAL }; + /// + /// Create an empty rule option. + /// public RuleOption() { Baseline = null; @@ -25,6 +31,10 @@ public RuleOption() Tag = null; } + /// + /// Create a rule option by copying an existing instance. + /// + /// The option instance to copy. public RuleOption(RuleOption option) { if (option == null) @@ -69,6 +79,10 @@ public override int GetHashCode() } } + /// + /// Merge two option instances by repacing any unset properties from with values. + /// Values from that are set are not overridden. + /// internal static RuleOption Combine(RuleOption o1, RuleOption o2) { var result = new RuleOption(o1) diff --git a/src/PSRule/Configuration/SuppressionOption.cs b/src/PSRule/Configuration/SuppressionOption.cs index 0a46d2eda9..8c78075f04 100644 --- a/src/PSRule/Configuration/SuppressionOption.cs +++ b/src/PSRule/Configuration/SuppressionOption.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; @@ -7,15 +7,25 @@ namespace PSRule.Configuration { + /// + /// Options that affect rule suppression during execution. + /// public sealed class SuppressionOption : IDictionary { private readonly Dictionary _Rules; + /// + /// Creates an empty suppression option. + /// public SuppressionOption() { _Rules = new Dictionary(StringComparer.OrdinalIgnoreCase); } + /// + /// Creates a suppression option by loading from a dictionary. + /// + /// A dictionary of . internal SuppressionOption(IDictionary rules) { _Rules = rules == null ? @@ -23,86 +33,123 @@ internal SuppressionOption(IDictionary rules) new Dictionary(rules, StringComparer.OrdinalIgnoreCase); } + /// + /// Get a indexed by rule name. + /// + /// The name of the rule. + /// A matching . public SuppressionRule this[string key] { get => _Rules[key]; set => _Rules[key] = value; } + /// public ICollection Keys => _Rules.Keys; + /// public ICollection Values => _Rules.Values; + /// public int Count => _Rules.Count; + /// public bool IsReadOnly => false; + /// + /// Add a suppression rule to the option by rule name. + /// + /// The name of the rule to apply the suppression rule to. + /// A to map to the rule. public void Add(string key, SuppressionRule value) { _Rules.Add(key, value); } + /// public void Add(KeyValuePair item) { Add(item.Key, item.Value); } + /// + /// Clear all suppression rules. + /// public void Clear() { _Rules.Clear(); } + /// public bool Contains(KeyValuePair item) { return ((IDictionary)_Rules).Contains(item); } + /// public bool ContainsKey(string key) { return _Rules.ContainsKey(key); } + /// public void CopyTo(KeyValuePair[] array, int arrayIndex) { ((IDictionary)_Rules).CopyTo(array, arrayIndex); } + /// public IEnumerator> GetEnumerator() { return _Rules.GetEnumerator(); } + /// + /// Remove a specific by rule name. + /// + /// The name of the rule. + /// Returns true if the element was found and removed. public bool Remove(string key) { return _Rules.Remove(key); } + /// public bool Remove(KeyValuePair item) { return ((IDictionary)_Rules).Remove(item); } + /// + /// Try to get a from the specified rule name. + /// + /// The name of the rule. + /// A if any match the specified rule name. + /// Returns true if the key was found and returned. public bool TryGetValue(string key, out SuppressionRule value) { return _Rules.TryGetValue(key, out value); } + /// IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } + /// + /// Convert a hashtable to suppression options. + /// + /// A hashtable of indexed by rule name. public static implicit operator SuppressionOption(Hashtable hashtable) { var option = new SuppressionOption(); - foreach (DictionaryEntry entry in hashtable) { var rule = SuppressionRule.FromObject(entry.Value); option._Rules.Add(entry.Key.ToString(), rule); } - return option; } } -} \ No newline at end of file +} diff --git a/src/PSRule/Pipeline/Source.cs b/src/PSRule/Pipeline/Source.cs index 5800d4fb95..2be5ec15d9 100644 --- a/src/PSRule/Pipeline/Source.cs +++ b/src/PSRule/Pipeline/Source.cs @@ -12,17 +12,29 @@ namespace PSRule.Pipeline { + /// + /// The type of source file. + /// public enum SourceType { + /// + /// PowerShell script file. + /// Script = 1, + /// + /// YAML file. + /// Yaml = 2, + /// + /// JSON or JSON with comments file. + /// Json = 3 } /// - /// A source file for rule definitions. + /// A source file containing resources that will be loaded and interpreted by PSRule. /// [DebuggerDisplay("{Type}: {Path}")] public sealed class SourceFile @@ -31,6 +43,13 @@ public sealed class SourceFile internal Source Source; + /// + /// Create an instance of a PSRule source. + /// + /// The file path to the source. + /// The name of the module if the source was loaded from a module. + /// The type of source file. + /// The base path to use for loading help content. public SourceFile(string path, string module, SourceType type, string helpPath) { Path = path; @@ -39,20 +58,35 @@ public SourceFile(string path, string module, SourceType type, string helpPath) HelpPath = helpPath; } + /// + /// The file path to the source. + /// [JsonProperty(PropertyName = "path")] public string Path { get; } + /// + /// The name of the module if the source was loaded from a module. + /// [JsonProperty(PropertyName = "moduleName")] public string Module { get; } + /// + /// The name of the module if the source was loaded from a module. + /// [JsonIgnore] [Obsolete("Use Module property instead.")] public string ModuleName => Module; + /// + /// The type of source file. + /// [YamlIgnore] [JsonIgnore] public SourceType Type { get; } + /// + /// The base path to use for loading help content. + /// [YamlIgnore] [JsonIgnore] public string HelpPath { get; } @@ -71,6 +105,9 @@ internal bool IsDependency() } } + /// + /// A PSRule source containing one or more source files. + /// public sealed class Source { internal bool Dependency; @@ -145,8 +182,14 @@ private static bool TryPrivateData(PSModuleInfo info, string propertyName, out H } } + /// + /// The base path of the source. + /// public string Path { get; } + /// + /// An array of source files. + /// public SourceFile[] File { get; } internal string Scope From eb849eff73bdbbafcb3eb893acdd853e5d1d9eb8 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Wed, 10 Aug 2022 15:22:53 +1000 Subject: [PATCH 021/156] Add NuGet config (#1212) --- .vscode/settings.json | 18 ++++++++++++------ NuGet.config | 7 +++++++ 2 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 NuGet.config diff --git a/.vscode/settings.json b/.vscode/settings.json index 13640f1761..e4c4f5964b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,7 +7,8 @@ "TestResults/": true }, "search.exclude": { - "out/": true + "out/": true, + "reports/": true }, "editor.insertSpaces": true, "editor.tabSize": 4, @@ -38,10 +39,12 @@ "editor.formatOnSave": true }, "[yaml]": { - "editor.tabSize": 2 + "editor.tabSize": 2, + "files.insertFinalNewline": true }, "[markdown]": { - "editor.tabSize": 2 + "editor.tabSize": 2, + "files.insertFinalNewline": true }, "[json]": { "editor.tabSize": 4, @@ -52,11 +55,13 @@ "editor.suggest.insertMode": "replace", "gitlens.codeLens.scopes": [ "document" - ] + ], + "files.insertFinalNewline": true }, "files.associations": { "**/.azure-pipelines/*.yaml": "azure-pipelines", - "**/.azure-pipelines/jobs/*.yaml": "azure-pipelines" + "**/.azure-pipelines/jobs/*.yaml": "azure-pipelines", + "**/NuGet.config": "xml" }, "cSpell.words": [ "APPSERVICEMININSTANCECOUNT", @@ -82,7 +87,8 @@ "xunit" ], "[csharp]": { - "editor.formatOnSave": true + "editor.formatOnSave": true, + "files.insertFinalNewline": true }, "omnisharp.enableImportCompletion": true, "omnisharp.organizeImportsOnFormat": true, diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 0000000000..3a9f6b3272 --- /dev/null +++ b/NuGet.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file From 0a1b39c43d3938e6b5b662ef96c197c8e7bd680a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Aug 2022 13:19:27 +1000 Subject: [PATCH 022/156] Bump Microsoft.NET.Test.Sdk from 17.2.0 to 17.3.0 (#1213) * Bump Microsoft.NET.Test.Sdk from 17.2.0 to 17.3.0 Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 17.2.0 to 17.3.0. - [Release notes](https://github.com/microsoft/vstest/releases) - [Commits](https://github.com/microsoft/vstest/compare/v17.2.0...v17.3.0) --- updated-dependencies: - dependency-name: Microsoft.NET.Test.Sdk dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Bump change log Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bernie White --- docs/CHANGELOG-v2.md | 2 ++ tests/PSRule.Tests/PSRule.Tests.csproj | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 4656106e76..b309098403 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -18,6 +18,8 @@ What's changed since v2.3.1: - Engineering: - Bump support projects to .NET 6 by @BernieWhite. [#1209](https://github.com/microsoft/PSRule/issues/1209) + - Bump Microsoft.NET.Test.Sdk to v17.3.0. + [#1213](https://github.com/microsoft/PSRule/pull/1208) ## v2.3.1 diff --git a/tests/PSRule.Tests/PSRule.Tests.csproj b/tests/PSRule.Tests/PSRule.Tests.csproj index fd858d8bd1..ccae856278 100644 --- a/tests/PSRule.Tests/PSRule.Tests.csproj +++ b/tests/PSRule.Tests/PSRule.Tests.csproj @@ -10,7 +10,7 @@ - + From 830e5ffbaba5e7595c296e6248835863078c6f67 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sat, 13 Aug 2022 14:18:48 +1000 Subject: [PATCH 023/156] Add v2.0.0 benchmark report (#1217) --- docs/scenarios/benchmark/results-v2.0.0.md | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 docs/scenarios/benchmark/results-v2.0.0.md diff --git a/docs/scenarios/benchmark/results-v2.0.0.md b/docs/scenarios/benchmark/results-v2.0.0.md new file mode 100644 index 0000000000..9ee6bb3007 --- /dev/null +++ b/docs/scenarios/benchmark/results-v2.0.0.md @@ -0,0 +1,29 @@ +``` ini + +BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000 +Intel Core i7-1065G7 CPU 1.30GHz, 1 CPU, 8 logical and 4 physical cores +.NET SDK=6.0.400 + [Host] : .NET Core 3.1.28 (CoreCLR 4.700.22.36202, CoreFX 4.700.22.36301), X64 RyuJIT + DefaultJob : .NET Core 3.1.28 (CoreCLR 4.700.22.36202, CoreFX 4.700.22.36301), X64 RyuJIT + + +``` +| Method | Mean | Error | StdDev | Median | Gen 0 | Gen 1 | Allocated | +|------------------------- |-----------------:|----------------:|-----------------:|-----------------:|-----------:|----------:|----------:| +| Invoke | 71,586,583.6 ns | 4,077,161.43 ns | 11,957,608.68 ns | 71,526,200.0 ns | 4200.0000 | 600.0000 | 17,703 KB | +| InvokeIf | 59,661,136.5 ns | 1,161,506.18 ns | 1,192,781.33 ns | 59,397,680.0 ns | 4400.0000 | 400.0000 | 19,954 KB | +| InvokeType | 55,089,186.0 ns | 1,045,769.22 ns | 1,989,684.63 ns | 54,242,620.0 ns | 4300.0000 | 500.0000 | 17,703 KB | +| InvokeSummary | 55,371,580.7 ns | 1,271,559.50 ns | 3,586,456.14 ns | 53,815,745.0 ns | 4300.0000 | 500.0000 | 17,704 KB | +| Assert | 56,146,053.8 ns | 1,122,221.08 ns | 3,053,085.20 ns | 54,841,105.0 ns | 4500.0000 | 600.0000 | 18,407 KB | +| Get | 5,701,341.2 ns | 110,332.76 ns | 158,235.95 ns | 5,665,673.8 ns | 78.1250 | 7.8125 | 365 KB | +| GetHelp | 5,750,066.9 ns | 113,595.72 ns | 170,024.73 ns | 5,720,298.0 ns | 85.9375 | 7.8125 | 366 KB | +| Within | 99,151,338.5 ns | 2,136,858.10 ns | 6,165,324.21 ns | 97,015,516.7 ns | 8333.3333 | 1333.3333 | 34,142 KB | +| WithinBulk | 147,062,385.7 ns | 2,633,709.47 ns | 2,334,714.85 ns | 146,838,450.0 ns | 14000.0000 | 3000.0000 | 61,169 KB | +| WithinLike | 120,988,346.7 ns | 2,099,294.17 ns | 1,963,681.06 ns | 120,963,000.0 ns | 11666.6667 | 1666.6667 | 48,297 KB | +| DefaultTargetNameBinding | 731,969.9 ns | 13,918.54 ns | 36,422.40 ns | 714,067.8 ns | 38.0859 | - | 156 KB | +| CustomTargetNameBinding | 1,052,297.9 ns | 44,284.38 ns | 125,627.41 ns | 1,022,416.2 ns | 85.9375 | - | 352 KB | +| NestedTargetNameBinding | 916,580.7 ns | 24,378.48 ns | 71,497.85 ns | 903,449.3 ns | 85.9375 | - | 352 KB | +| AssertHasFieldValue | 3,082,706.1 ns | 61,644.86 ns | 68,518.10 ns | 3,058,487.1 ns | 234.3750 | - | 962 KB | +| PathTokenize | 846.6 ns | 16.52 ns | 23.69 ns | 842.4 ns | 0.2632 | - | 1 KB | +| PathExpressionBuild | 548.3 ns | 10.14 ns | 11.68 ns | 547.1 ns | 0.3500 | - | 1 KB | +| PathExpressionGet | 356,089.5 ns | 7,027.54 ns | 11,348.18 ns | 351,085.5 ns | 17.0898 | - | 70 KB | From dc03d8fbabf4ad22d317d53d584c99ece196b827 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sat, 13 Aug 2022 15:14:13 +1000 Subject: [PATCH 024/156] Merge changes from patch release v2.3.2 (#1220) * Fixes object path for self identifier #1204 (#1205) * Release v2.3.1 (#1206) * Fixes lost scope for rules #1214 (#1216) * Release v2.3.2 (#1218) --- docs/CHANGELOG-v2.md | 10 +++++++++- src/PSRule/Commands/InvokeRuleBlockCommand.cs | 3 ++- src/PSRule/Commands/NewRuleDefinitionCommand.cs | 8 +++++++- src/PSRule/Definitions/Resource.cs | 11 ++++++++--- src/PSRule/Host/HostHelper.cs | 6 ++++++ src/PSRule/Runtime/RunspaceContext.cs | 12 ++++++++++-- 6 files changed, 42 insertions(+), 8 deletions(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index b309098403..11e0ccb289 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -13,7 +13,7 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased -What's changed since v2.3.1: +What's changed since v2.3.2: - Engineering: - Bump support projects to .NET 6 by @BernieWhite. @@ -21,6 +21,14 @@ What's changed since v2.3.1: - Bump Microsoft.NET.Test.Sdk to v17.3.0. [#1213](https://github.com/microsoft/PSRule/pull/1208) +## v2.3.2 + +What's changed since v2.3.1: + +- Bug fixes: + - Fixes lost scope for rules by @BernieWhite. + [#1214](https://github.com/microsoft/PSRule/issues/1214) + ## v2.3.1 What's changed since v2.3.0: diff --git a/src/PSRule/Commands/InvokeRuleBlockCommand.cs b/src/PSRule/Commands/InvokeRuleBlockCommand.cs index bd1b3d18b5..5680979384 100644 --- a/src/PSRule/Commands/InvokeRuleBlockCommand.cs +++ b/src/PSRule/Commands/InvokeRuleBlockCommand.cs @@ -3,6 +3,7 @@ using System; using System.Management.Automation; +using PSRule.Definitions; using PSRule.Pipeline; using PSRule.Resources; using PSRule.Runtime; @@ -18,7 +19,7 @@ internal sealed class InvokeRuleBlockCommand : Cmdlet public string[] Type; [Parameter()] - public string[] With; + public ResourceId[] With; [Parameter()] public ScriptBlock If; diff --git a/src/PSRule/Commands/NewRuleDefinitionCommand.cs b/src/PSRule/Commands/NewRuleDefinitionCommand.cs index df32699a61..b8543c742e 100644 --- a/src/PSRule/Commands/NewRuleDefinitionCommand.cs +++ b/src/PSRule/Commands/NewRuleDefinitionCommand.cs @@ -3,6 +3,7 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Management.Automation; using System.Threading; using PSRule.Definitions; @@ -147,7 +148,7 @@ private PowerShellCondition GetCondition(RunspaceContext context, ResourceId id, var result = context.GetPowerShell(); result.AddCommand(new CmdletInfo(CmdletName, typeof(InvokeRuleBlockCommand))); result.AddParameter(Cmdlet_TypeParameter, Type); - result.AddParameter(Cmdlet_WithParameter, With); + result.AddParameter(Cmdlet_WithParameter, GetScopedSelectors(source)); result.AddParameter(Cmdlet_IfParameter, If); result.AddParameter(Cmdlet_BodyParameter, Body); result.AddParameter(Cmdlet_SourceParameter, source); @@ -166,5 +167,10 @@ private void CheckDependsOn() )); } } + + private ResourceId[] GetScopedSelectors(SourceFile source) + { + return ResourceHelper.GetRuleId(source.Module, With, ResourceIdKind.Unknown); + } } } diff --git a/src/PSRule/Definitions/Resource.cs b/src/PSRule/Definitions/Resource.cs index a5ad105704..9382bdaa15 100644 --- a/src/PSRule/Definitions/Resource.cs +++ b/src/PSRule/Definitions/Resource.cs @@ -496,19 +496,24 @@ internal static void ParseIdString(string id, out string scope, out string name) /// An array of RuleIds. internal static ResourceId[] GetRuleId(string defaultScope, string[] name, ResourceIdKind kind) { - if (name == null) + if (name == null || name.Length == 0) return null; var result = new ResourceId[name.Length]; for (var i = 0; i < name.Length; i++) - result[i] = name[i].IndexOf(SCOPE_SEPARATOR) > 0 ? ResourceId.Parse(name[i], kind) : new ResourceId(defaultScope, name[i], kind); + result[i] = GetRuleId(defaultScope, name[i], kind); return (result.Length == 0) ? null : result; } + internal static ResourceId GetRuleId(string defaultScope, string name, ResourceIdKind kind) + { + return name.IndexOf(SCOPE_SEPARATOR) > 0 ? ResourceId.Parse(name, kind) : new ResourceId(defaultScope, name, kind); + } + internal static ResourceId? GetIdNullable(string scope, string name, ResourceIdKind kind) { - return string.IsNullOrEmpty(name) ? null : (ResourceId?)new ResourceId(scope, name, kind); + return string.IsNullOrEmpty(name) ? null : new ResourceId(scope, name, kind); } internal static bool IsObsolete(ResourceMetadata metadata) diff --git a/src/PSRule/Host/HostHelper.cs b/src/PSRule/Host/HostHelper.cs index aee48ad67e..c163edfa07 100644 --- a/src/PSRule/Host/HostHelper.cs +++ b/src/PSRule/Host/HostHelper.cs @@ -358,6 +358,7 @@ public static void InvokeRuleBlock(RunspaceContext context, RuleBlock ruleBlock, try { + context.EnterSourceScope(ruleBlock.Source); var invokeResult = condition.If(); if (invokeResult == null) { @@ -390,6 +391,11 @@ public static void InvokeRuleBlock(RunspaceContext context, RuleBlock ruleBlock, { context.Error(ex); } + // TODO: Exit scope + //finally + //{ + // context.ExitSourceScope(); + //} } private static RuleException ThrowDuplicateRuleId(IDependencyTarget block) diff --git a/src/PSRule/Runtime/RunspaceContext.cs b/src/PSRule/Runtime/RunspaceContext.cs index 6155d1f4fa..a7ee9eac7c 100644 --- a/src/PSRule/Runtime/RunspaceContext.cs +++ b/src/PSRule/Runtime/RunspaceContext.cs @@ -575,6 +575,8 @@ private string GetLogPrefix() internal SourceScope EnterSourceScope(SourceFile source) { + // TODO: Look at scope caching, and a scope stack. + if (!source.Exists()) throw new FileNotFoundException(PSRuleResources.ScriptNotFound, source.Path); @@ -591,6 +593,8 @@ internal SourceScope EnterSourceScope(SourceFile source) internal void ExitSourceScope() { + // Look at scope poping and validation. + Source = null; } @@ -617,8 +621,12 @@ public void ExitTargetObject() public bool TrySelector(string name) { - name = ResourceHelper.GetIdString(Source.File.Module, name); - if (TargetObject == null || Pipeline == null || !Pipeline.Selector.TryGetValue(name, out var selector)) + return TrySelector(ResourceHelper.GetRuleId(Source.File.Module, name, ResourceIdKind.Unknown)); + } + + public bool TrySelector(ResourceId id) + { + if (TargetObject == null || Pipeline == null || !Pipeline.Selector.TryGetValue(id.Value, out var selector)) return false; var annotation = TargetObject.GetAnnotation(); From de58334a37d839d4d8ac79395be70323d97164cb Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sat, 13 Aug 2022 15:36:43 +1000 Subject: [PATCH 025/156] Fixes repository information not in output #1219 (#1221) --- docs/CHANGELOG-v2.md | 3 +++ schemas/PSRule-options.schema.json | 4 ++-- src/PSRule/Common/GitHelper.cs | 4 ++-- src/PSRule/Pipeline/Formatters/AssertFormatter.cs | 3 ++- src/PSRule/Pipeline/Output/SarifOutputWriter.cs | 2 +- src/PSRule/Pipeline/PipelineBuilder.cs | 10 ++++++++++ tests/PSRule.Tests/PSRule.Options.Tests.ps1 | 5 +++++ 7 files changed, 25 insertions(+), 6 deletions(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 11e0ccb289..96065841a4 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -20,6 +20,9 @@ What's changed since v2.3.2: [#1209](https://github.com/microsoft/PSRule/issues/1209) - Bump Microsoft.NET.Test.Sdk to v17.3.0. [#1213](https://github.com/microsoft/PSRule/pull/1208) +- Bug fixes: + - Fixed repository information not in output by @BernieWhite. + [#1219](https://github.com/microsoft/PSRule/issues/1219) ## v2.3.2 diff --git a/schemas/PSRule-options.schema.json b/schemas/PSRule-options.schema.json index f01ddd6c8f..828a347ce7 100644 --- a/schemas/PSRule-options.schema.json +++ b/schemas/PSRule-options.schema.json @@ -776,8 +776,8 @@ "url": { "type": "string", "title": "Repository URL", - "description": "The URL to the repository.", - "markdownDescription": "The URL to the repository." + "description": "Sets the repository URL reported in output. By default, the repository URL is detected from environment variables set by the build system.", + "markdownDescription": "Sets the repository URL reported in output. By default, the repository URL is detected from environment variables set by the build system. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#repositoryurl)" } }, "additionalProperties": false diff --git a/src/PSRule/Common/GitHelper.cs b/src/PSRule/Common/GitHelper.cs index bf90100720..844d8e4d1d 100644 --- a/src/PSRule/Common/GitHelper.cs +++ b/src/PSRule/Common/GitHelper.cs @@ -10,8 +10,8 @@ namespace PSRule /// Helper for working with Git and CI tools. /// /// - /// Azure Pipelines: https://docs.microsoft.com/azure/devops/pipelines/build/variables - /// GitHub Actions: https://docs.github.com/actions/learn-github-actions/environment-variables#default-environment-variables + /// Docs for Azure Pipelines and + /// GitHub Actions. /// internal static class GitHelper { diff --git a/src/PSRule/Pipeline/Formatters/AssertFormatter.cs b/src/PSRule/Pipeline/Formatters/AssertFormatter.cs index 127a4b0dc5..27728ef4b5 100644 --- a/src/PSRule/Pipeline/Formatters/AssertFormatter.cs +++ b/src/PSRule/Pipeline/Formatters/AssertFormatter.cs @@ -410,7 +410,8 @@ private void RepositoryInfo() if (!Option.Output.Banner.GetValueOrDefault(BannerFormat.Default).HasFlag(BannerFormat.RepositoryInfo)) return; - if (!GitHelper.TryRepository(out var repository)) + var repository = Option.Repository.Url; + if (string.IsNullOrEmpty(repository)) return; WriteLineFormat(FormatterStrings.Repository_Url, repository); diff --git a/src/PSRule/Pipeline/Output/SarifOutputWriter.cs b/src/PSRule/Pipeline/Output/SarifOutputWriter.cs index af1e953c5c..4007f491ba 100644 --- a/src/PSRule/Pipeline/Output/SarifOutputWriter.cs +++ b/src/PSRule/Pipeline/Output/SarifOutputWriter.cs @@ -53,7 +53,7 @@ public SarifBuilder(Source[] source, PSRuleOption option) /// private static IList GetVersionControl(RepositoryOption option) { - var repository = option.Url ?? (GitHelper.TryRepository(out var url) ? url : null); + var repository = option.Url; return new List() { new VersionControlDetails diff --git a/src/PSRule/Pipeline/PipelineBuilder.cs b/src/PSRule/Pipeline/PipelineBuilder.cs index 30ad181816..5e45f4fc50 100644 --- a/src/PSRule/Pipeline/PipelineBuilder.cs +++ b/src/PSRule/Pipeline/PipelineBuilder.cs @@ -193,6 +193,7 @@ public virtual IPipelineBuilder Configure(PSRuleOption option) Option.Output = new OutputOption(option.Output); Option.Output.Outcome = Option.Output.Outcome ?? OutputOption.Default.Outcome; Option.Output.Banner = Option.Output.Banner ?? OutputOption.Default.Banner; + Option.Repository = GetRepository(Option.Repository); return this; } @@ -355,6 +356,15 @@ protected static string[] GetCulture(string[] culture) return result.Count == 0 ? null : result.ToArray(); } + protected static RepositoryOption GetRepository(RepositoryOption repository) + { + var result = new RepositoryOption(repository); + if (string.IsNullOrEmpty(result.Url) && GitHelper.TryRepository(out var url)) + result.Url = url; + + return result; + } + protected PathFilter GetInputObjectSourceFilter() { return Option.Input.IgnoreObjectSource.GetValueOrDefault(InputOption.Default.IgnoreObjectSource.Value) ? GetInputFilter() : null; diff --git a/tests/PSRule.Tests/PSRule.Options.Tests.ps1 b/tests/PSRule.Tests/PSRule.Options.Tests.ps1 index e00aa4c898..07ccddd95e 100644 --- a/tests/PSRule.Tests/PSRule.Options.Tests.ps1 +++ b/tests/PSRule.Tests/PSRule.Options.Tests.ps1 @@ -1667,6 +1667,11 @@ Describe 'New-PSRuleOption' -Tag 'Option','New-PSRuleOption' { } Context 'Read Repository.Url' { + It 'from default' { + $option = New-PSRuleOption -Default; + $option.Repository.Url | Should -BeNullOrEmpty; + } + It 'from Hashtable' { $option = New-PSRuleOption -Option @{ 'Repository.Url' = 'https://github.com/microsoft/PSRule.UnitTest' }; $option.Repository.Url | Should -Be 'https://github.com/microsoft/PSRule.UnitTest'; From 8e8a42f29049c058828ff4b69faf2356cf7257b8 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sat, 13 Aug 2022 21:37:19 +1000 Subject: [PATCH 026/156] Pre-release v2.4.0-B0009 (#1222) --- docs/CHANGELOG-v2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 96065841a4..ec26b6f188 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -13,6 +13,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.4.0-B0009 (pre-release) + What's changed since v2.3.2: - Engineering: From d6a02571d7105c3b358e5b9bebe3dd0cd15477ba Mon Sep 17 00:00:00 2001 From: Bernie White Date: Tue, 16 Aug 2022 02:32:29 +1000 Subject: [PATCH 027/156] Updates to PSRule engine API #1152 (#1225) * Updates to PSRule engine API #1152 * Updates for testing * Flip paths * Separate options from path environment --- PSRule.sln | 6 + docs/CHANGELOG-v2.md | 7 ++ src/PSRule.SDK/PSRule.SDK.csproj | 1 + src/PSRule.Tool/AnalyzerOptions.cs | 2 + src/PSRule.Tool/ClientBuilder.cs | 33 ++++- src/PSRule.Tool/ClientHelper.cs | 84 +++++++++++-- src/PSRule.Tool/ClientHost.cs | 27 +++++ src/PSRule.Tool/PSRule.Tool.csproj | 2 +- .../Resources/CmdStrings.Designer.cs | 101 ++++++++-------- src/PSRule.Tool/Resources/CmdStrings.resx | 8 +- src/PSRule.Tool/RestoreOptions.cs | 16 +++ src/PSRule.Types/Data/ModuleConstraint.cs | 18 +++ .../Data}/SemanticVersion.cs | 38 ++++-- src/PSRule.Types/PSRule.Types.csproj | 21 ++++ src/PSRule.Types/Properties/AssemblyInfo.cs | 6 + src/PSRule.Types/README.md | 3 + .../Commands/NewRuleDefinitionCommand.cs | 1 - src/PSRule/Common/EnvironmentHelper.cs | 17 ++- src/PSRule/Configuration/BaselineOption.cs | 26 +++- src/PSRule/Configuration/RequiresOption.cs | 19 ++- .../Expressions/LanguageExpressions.cs | 1 + src/PSRule/PSRule.csproj | 3 +- src/PSRule/Pipeline/CommandLineBuilder.cs | 2 +- src/PSRule/Pipeline/ModulePathComparer.cs | 26 ++++ src/PSRule/Pipeline/PipelineBuilder.cs | 46 ++++++- src/PSRule/Pipeline/SourcePipeline.cs | 100 ++++++++++++++-- .../Resources/PSRuleResources.Designer.cs | 2 +- src/PSRule/Resources/PSRuleResources.resx | 2 +- src/PSRule/Runtime/Assert.cs | 2 +- tests/PSRule.Tests/ModulePathComparerTests.cs | 39 ++++++ tests/PSRule.Tests/SemanticVersionTests.cs | 113 +++++++++++------- 31 files changed, 630 insertions(+), 142 deletions(-) create mode 100644 src/PSRule.Tool/RestoreOptions.cs create mode 100644 src/PSRule.Types/Data/ModuleConstraint.cs rename src/{PSRule/Runtime => PSRule.Types/Data}/SemanticVersion.cs (96%) create mode 100644 src/PSRule.Types/PSRule.Types.csproj create mode 100644 src/PSRule.Types/Properties/AssemblyInfo.cs create mode 100644 src/PSRule.Types/README.md create mode 100644 src/PSRule/Pipeline/ModulePathComparer.cs create mode 100644 tests/PSRule.Tests/ModulePathComparerTests.cs diff --git a/PSRule.sln b/PSRule.sln index d362b8f8a7..283bdf4383 100644 --- a/PSRule.sln +++ b/PSRule.sln @@ -22,6 +22,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution src\PSRule.Common.props = src\PSRule.Common.props EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSRule.Types", "src\PSRule.Types\PSRule.Types.csproj", "{6E48E1BA-9A94-4412-BB3F-F5F569E05A4E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -56,6 +58,10 @@ Global {F6CFCA60-72D5-474E-8B8B-1AB973434569}.Debug|Any CPU.Build.0 = Debug|Any CPU {F6CFCA60-72D5-474E-8B8B-1AB973434569}.Release|Any CPU.ActiveCfg = Release|Any CPU {F6CFCA60-72D5-474E-8B8B-1AB973434569}.Release|Any CPU.Build.0 = Release|Any CPU + {6E48E1BA-9A94-4412-BB3F-F5F569E05A4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6E48E1BA-9A94-4412-BB3F-F5F569E05A4E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6E48E1BA-9A94-4412-BB3F-F5F569E05A4E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6E48E1BA-9A94-4412-BB3F-F5F569E05A4E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index ec26b6f188..9930d95d5c 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -13,6 +13,13 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +- Engineering: + - Updates to PSRule engine API by @BernieWhite. + [#1152](https://github.com/microsoft/PSRule/issues/1152) + - Added tool support for baselines parameter. + - Added module path discovery. + - Added output for verbose and debug messages. + ## v2.4.0-B0009 (pre-release) What's changed since v2.3.2: diff --git a/src/PSRule.SDK/PSRule.SDK.csproj b/src/PSRule.SDK/PSRule.SDK.csproj index 309d82e97a..1be7cf1c8b 100644 --- a/src/PSRule.SDK/PSRule.SDK.csproj +++ b/src/PSRule.SDK/PSRule.SDK.csproj @@ -5,6 +5,7 @@ Microsoft.PSRule.SDK README.md false + {07a84e67-1ca3-4766-b9ea-1fdd9df6516f} diff --git a/src/PSRule.Tool/AnalyzerOptions.cs b/src/PSRule.Tool/AnalyzerOptions.cs index de0d194d0e..48dba28f6f 100644 --- a/src/PSRule.Tool/AnalyzerOptions.cs +++ b/src/PSRule.Tool/AnalyzerOptions.cs @@ -11,6 +11,8 @@ internal sealed class AnalyzerOptions public string Option { get; set; } + public string Baseline { get; set; } + public string[] InputPath { get; set; } public bool Verbose { get; set; } diff --git a/src/PSRule.Tool/ClientBuilder.cs b/src/PSRule.Tool/ClientBuilder.cs index a990765b4f..13fe2c639b 100644 --- a/src/PSRule.Tool/ClientBuilder.cs +++ b/src/PSRule.Tool/ClientBuilder.cs @@ -3,7 +3,7 @@ using System.CommandLine; using System.IO; -using PSRule.Tool.Resource; +using PSRule.Tool.Resources; namespace PSRule.Tool { @@ -17,6 +17,7 @@ internal sealed class ClientBuilder private readonly Option _OutputFormat; private readonly Option _InputPath; private readonly Option _Module; + private readonly Option _Baseline; private ClientBuilder(RootCommand cmd) { @@ -48,6 +49,9 @@ private ClientBuilder(RootCommand cmd) _Module = new Option( new string[] { "-m", "--module" } ); + _Baseline = new Option( + new string[] { "--baseline" } + ); cmd.AddGlobalOption(_Option); cmd.AddGlobalOption(_Verbose); @@ -58,9 +62,13 @@ private ClientBuilder(RootCommand cmd) public static Command New() { - var cmd = new RootCommand(); + var cmd = new RootCommand(CmdStrings.Cmd_Description) + { + Name = "ps-rule" + }; var builder = new ClientBuilder(cmd); builder.AddAnalyze(); + builder.AddRestore(); return builder.Command; } @@ -72,6 +80,7 @@ private void AddAnalyze() cmd.AddOption(_OutputFormat); cmd.AddOption(_InputPath); cmd.AddOption(_Module); + cmd.AddOption(_Baseline); cmd.SetHandler((invocation) => { var option = new AnalyzerOptions @@ -80,6 +89,7 @@ private void AddAnalyze() InputPath = invocation.ParseResult.GetValueForOption(_InputPath), Module = invocation.ParseResult.GetValueForOption(_Module), Option = invocation.ParseResult.GetValueForOption(_Option), + Baseline = invocation.ParseResult.GetValueForOption(_Baseline), Verbose = invocation.ParseResult.GetValueForOption(_Verbose), Debug = invocation.ParseResult.GetValueForOption(_Debug), }; @@ -88,5 +98,24 @@ private void AddAnalyze() }); Command.AddCommand(cmd); } + + private void AddRestore() + { + var cmd = new Command("restore", CmdStrings.Restore_Description); + cmd.AddOption(_Path); + cmd.SetHandler((invocation) => + { + var option = new RestoreOptions + { + Path = invocation.ParseResult.GetValueForOption(_Path), + Option = invocation.ParseResult.GetValueForOption(_Option), + Verbose = invocation.ParseResult.GetValueForOption(_Verbose), + Debug = invocation.ParseResult.GetValueForOption(_Debug), + }; + var client = new ClientContext(); + ClientHelper.RunRestore(option, client, invocation); + }); + Command.AddCommand(cmd); + } } } diff --git a/src/PSRule.Tool/ClientHelper.cs b/src/PSRule.Tool/ClientHelper.cs index bf0a41fb26..4bcfd438ea 100644 --- a/src/PSRule.Tool/ClientHelper.cs +++ b/src/PSRule.Tool/ClientHelper.cs @@ -1,28 +1,96 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.CommandLine; using System.CommandLine.Invocation; +using System.Management.Automation; using PSRule.Configuration; +using PSRule.Data; using PSRule.Pipeline; +using SemanticVersion = PSRule.Data.SemanticVersion; namespace PSRule.Tool { internal sealed class ClientHelper { - public static void RunAnalyze(AnalyzerOptions analyzerOptions, ClientContext clientContext, InvocationContext invocation) + public static void RunAnalyze(AnalyzerOptions operationOptions, ClientContext clientContext, InvocationContext invocation) { var option = GetOption(); - var host = new ClientHost(invocation, analyzerOptions.Verbose, analyzerOptions.Debug); + var host = new ClientHost(invocation, operationOptions.Verbose, operationOptions.Debug); + var inputPath = operationOptions.InputPath == null || operationOptions.InputPath.Length == 0 ? + new string[] { PSRuleOption.GetWorkingPath() } : operationOptions.InputPath; - var inputPath = analyzerOptions.InputPath == null || analyzerOptions.InputPath.Length == 0 ? - new string[] { PSRuleOption.GetWorkingPath() } : analyzerOptions.InputPath; - var builder = CommandLineBuilder.Assert(new string[] { "PSRule.Rules.Azure" }, option, host); + // Build command + var builder = CommandLineBuilder.Assert(operationOptions.Module, option, host); + builder.Baseline(BaselineOption.FromString(operationOptions.Baseline)); builder.InputPath(inputPath); using var pipeline = builder.Build(); - pipeline.Begin(); - pipeline.Process(null); - pipeline.End(); + if (pipeline != null) + { + pipeline.Begin(); + pipeline.Process(null); + pipeline.End(); + } + } + + public static void RunRestore(RestoreOptions operationOptions, ClientContext clientContext, InvocationContext invocation) + { + var option = GetOption(); + var host = new ClientHost(invocation, operationOptions.Verbose, operationOptions.Debug); + var requires = option.Requires.ToArray(); + + using var pwsh = PowerShell.Create(); + for (var i = 0; i < requires.Length; i++) + { + invocation.Console.WriteLine($"Getting {requires[i].Module}."); + var version = GetVersion(pwsh, requires[i]); + + invocation.Console.WriteLine($"Installing {requires[i].Module} v{version}."); + InstallVersion(pwsh, requires[i].Module, version); + + if (pwsh.HadErrors) + { + invocation.Console.Error.Write($"Failed to install {requires[i].Module}."); + foreach (var error in pwsh.Streams.Error) + { + invocation.Console.Error.Write(error.Exception.Message); + } + } + } + } + + private static string GetVersion(PowerShell pwsh, ModuleConstraint constraint) + { + pwsh.Commands.Clear(); + pwsh.Streams.ClearStreams(); + pwsh.AddCommand("Find-Module") + .AddParameter("Name", constraint.Module) + .AddParameter("AllVersions"); + + var versions = pwsh.Invoke(); + SemanticVersion.Version result = null; + foreach (var version in versions) + { + if (version.Properties["Version"].Value is string versionString && + SemanticVersion.TryParseVersion(versionString, out var v) && + constraint.Constraint.Equals(v) && + v.CompareTo(result) > 0) + result = v; + } + return result?.ToString(); + } + + private static void InstallVersion(PowerShell pwsh, string name, string version) + { + pwsh.Commands.Clear(); + pwsh.Streams.ClearStreams(); + pwsh.AddCommand("Install-Module") + .AddParameter("Name", name) + .AddParameter("Scope", "CurrentUser") + .AddParameter("Force"); + + pwsh.Invoke(); } private static PSRuleOption GetOption() diff --git a/src/PSRule.Tool/ClientHost.cs b/src/PSRule.Tool/ClientHost.cs index 5852ca2bb9..1258b37a90 100644 --- a/src/PSRule.Tool/ClientHost.cs +++ b/src/PSRule.Tool/ClientHost.cs @@ -22,6 +22,17 @@ public ClientHost(InvocationContext invocation, bool verbose, bool debug) _Debug = debug; } + public override ActionPreference GetPreferenceVariable(string variableName) + { + if (variableName == "VerbosePreference") + return _Verbose ? ActionPreference.Continue : ActionPreference.SilentlyContinue; + + if (variableName == "DebugPreference") + return _Debug ? ActionPreference.Continue : ActionPreference.SilentlyContinue; + + return base.GetPreferenceVariable(variableName); + } + public override void Error(ErrorRecord errorRecord) { _Invocation.Console.Error.WriteLine(errorRecord.Exception.Message); @@ -42,5 +53,21 @@ public override void Information(InformationRecord informationRecord) if (informationRecord?.MessageData is HostInformationMessage info) _Invocation.Console.WriteLine(info.Message); } + + public override void Verbose(string text) + { + if (!_Verbose) + return; + + _Invocation.Console.WriteLine(text); + } + + public override void Debug(string text) + { + if (!_Debug) + return; + + _Invocation.Console.WriteLine(text); + } } } diff --git a/src/PSRule.Tool/PSRule.Tool.csproj b/src/PSRule.Tool/PSRule.Tool.csproj index df3eec61b7..0b487cee8b 100644 --- a/src/PSRule.Tool/PSRule.Tool.csproj +++ b/src/PSRule.Tool/PSRule.Tool.csproj @@ -16,7 +16,7 @@ - + all diff --git a/src/PSRule.Tool/Resources/CmdStrings.Designer.cs b/src/PSRule.Tool/Resources/CmdStrings.Designer.cs index 80e317ad6c..1374640dae 100644 --- a/src/PSRule.Tool/Resources/CmdStrings.Designer.cs +++ b/src/PSRule.Tool/Resources/CmdStrings.Designer.cs @@ -8,11 +8,10 @@ // //------------------------------------------------------------------------------ -namespace PSRule.Tool.Resource - { +namespace PSRule.Tool.Resources { using System; - - + + /// /// A strongly-typed resource class, for looking up localized strings, etc. /// @@ -23,105 +22,105 @@ namespace PSRule.Tool.Resource [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class CmdStrings - { - + internal class CmdStrings { + private static global::System.Resources.ResourceManager resourceMan; - + private static global::System.Globalization.CultureInfo resourceCulture; - + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal CmdStrings() - { + internal CmdStrings() { } - + /// /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager - { - get - { - if (object.ReferenceEquals(resourceMan, null)) - { + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PSRule.Tool.Resources.CmdStrings", typeof(CmdStrings).Assembly); resourceMan = temp; } return resourceMan; } } - + /// /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture - { - get - { + internal static global::System.Globalization.CultureInfo Culture { + get { return resourceCulture; } - set - { + set { resourceCulture = value; } } - + /// - /// Looks up a localized string similar to Run rule analysis. + /// Looks up a localized string similar to Run rule analysis.. /// - internal static string Analyze_Description - { - get - { + internal static string Analyze_Description { + get { return ResourceManager.GetString("Analyze_Description", resourceCulture); } } - + + /// + /// Looks up a localized string similar to PSRule CLI. + /// + internal static string Cmd_Description { + get { + return ResourceManager.GetString("Cmd_Description", resourceCulture); + } + } + /// /// Looks up a localized string similar to Return debug output. /// - internal static string Options_Debug_Description - { - get - { + internal static string Options_Debug_Description { + get { return ResourceManager.GetString("Options_Debug_Description", resourceCulture); } } - + /// /// Looks up a localized string similar to An options file. /// - internal static string Options_Option_Description - { - get - { + internal static string Options_Option_Description { + get { return ResourceManager.GetString("Options_Option_Description", resourceCulture); } } - + /// /// Looks up a localized string similar to . /// - internal static string Options_Path_Description - { - get - { + internal static string Options_Path_Description { + get { return ResourceManager.GetString("Options_Path_Description", resourceCulture); } } - + /// /// Looks up a localized string similar to Return verbose output. /// - internal static string Options_Verbose_Description - { - get - { + internal static string Options_Verbose_Description { + get { return ResourceManager.GetString("Options_Verbose_Description", resourceCulture); } } + + /// + /// Looks up a localized string similar to Restore PSRule modules.. + /// + internal static string Restore_Description { + get { + return ResourceManager.GetString("Restore_Description", resourceCulture); + } + } } } diff --git a/src/PSRule.Tool/Resources/CmdStrings.resx b/src/PSRule.Tool/Resources/CmdStrings.resx index de627f9c50..0968fa2c47 100644 --- a/src/PSRule.Tool/Resources/CmdStrings.resx +++ b/src/PSRule.Tool/Resources/CmdStrings.resx @@ -118,7 +118,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Run rule analysis + Run rule analysis. + + + PSRule CLI Return debug output @@ -132,4 +135,7 @@ Return verbose output + + Restore PSRule modules. + \ No newline at end of file diff --git a/src/PSRule.Tool/RestoreOptions.cs b/src/PSRule.Tool/RestoreOptions.cs new file mode 100644 index 0000000000..9add185eac --- /dev/null +++ b/src/PSRule.Tool/RestoreOptions.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Tool +{ + internal sealed class RestoreOptions + { + public string[] Path { get; set; } + + public string Option { get; set; } + + public bool Verbose { get; set; } + + public bool Debug { get; set; } + } +} diff --git a/src/PSRule.Types/Data/ModuleConstraint.cs b/src/PSRule.Types/Data/ModuleConstraint.cs new file mode 100644 index 0000000000..1e4dc4bab5 --- /dev/null +++ b/src/PSRule.Types/Data/ModuleConstraint.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Data +{ + public sealed class ModuleConstraint + { + public ModuleConstraint(string module, IVersionConstraint constraint) + { + Module = module; + Constraint = constraint; + } + + public string Module { get; } + + public IVersionConstraint Constraint { get; } + } +} diff --git a/src/PSRule/Runtime/SemanticVersion.cs b/src/PSRule.Types/Data/SemanticVersion.cs similarity index 96% rename from src/PSRule/Runtime/SemanticVersion.cs rename to src/PSRule.Types/Data/SemanticVersion.cs index d941bbbc45..78a790e326 100644 --- a/src/PSRule/Runtime/SemanticVersion.cs +++ b/src/PSRule.Types/Data/SemanticVersion.cs @@ -6,12 +6,17 @@ using System.Diagnostics; using System.Threading; -namespace PSRule.Runtime +namespace PSRule.Data { + public interface IVersionConstraint + { + bool Equals(SemanticVersion.Version version); + } + /// /// A helper for comparing semantic version strings. /// - internal static class SemanticVersion + public static class SemanticVersion { private const char MINOR = '^'; private const char PATCH = '~'; @@ -71,12 +76,7 @@ internal enum ConstraintModifier Prerelease = 1 } - internal interface IConstraint - { - bool Equals(Version version); - } - - internal sealed class VersionConstraint : IConstraint + public sealed class VersionConstraint : IVersionConstraint { private List _Constraints; @@ -135,7 +135,7 @@ internal void Join(int major, int minor, int patch, PR prid, ComparisonOperator } [DebuggerDisplay("{_Major}.{_Minor}.{_Patch}")] - internal sealed class ConstraintExpression : IConstraint + internal sealed class ConstraintExpression : IVersionConstraint { private readonly ComparisonOperator _Flag; private readonly int _Major; @@ -159,7 +159,7 @@ internal ConstraintExpression(int major, int minor, int patch, PR prid, Comparis public JoinOperator Join { get; } - public static bool TryParse(string value, out IConstraint constraint) + public static bool TryParse(string value, out IVersionConstraint constraint) { return TryParseConstraint(value, out constraint); } @@ -311,7 +311,7 @@ private static bool IsStable(PR prid) } } - internal sealed class Version : IComparable, IEquatable + public sealed class Version : IComparable, IEquatable { public readonly int Major; public readonly int Minor; @@ -333,16 +333,19 @@ public static bool TryParse(string value, out Version version) return TryParseVersion(value, out version); } + /// public override string ToString() { return string.Concat(Major, '.', Minor, '.', Patch); } + /// public override bool Equals(object obj) { return obj is Version version && Equals(version); } + /// public override int GetHashCode() { unchecked // Overflow is fine @@ -389,7 +392,7 @@ public int CompareTo(Version other) } [DebuggerDisplay("{Value}")] - internal sealed class PR + public sealed class PR { internal static readonly PR Empty = new PR(); private static readonly char[] SEPARATORS = new char[] { SEPARATOR }; @@ -454,16 +457,19 @@ public int CompareTo(PR pr) return left.Length > right.Length ? 1 : -1; } + /// public override bool Equals(object obj) { return obj is PR prerelease && Value.Equals(prerelease.Value); } + /// public override int GetHashCode() { return Value.GetHashCode(); } + /// public override string ToString() { return Value.ToString(); @@ -722,7 +728,10 @@ private static bool IsLetter(char c) } } - public static bool TryParseConstraint(string value, out IConstraint constraint, bool includePrerelease = false) + /// + /// Try to parse a version constraint from the provided string. + /// + public static bool TryParseConstraint(string value, out IVersionConstraint constraint, bool includePrerelease = false) { var c = new VersionConstraint(); constraint = c; @@ -748,6 +757,9 @@ public static bool TryParseConstraint(string value, out IConstraint constraint, return true; } + /// + /// Try to parse a version from the provided string. + /// public static bool TryParseVersion(string value, out Version version) { version = null; diff --git a/src/PSRule.Types/PSRule.Types.csproj b/src/PSRule.Types/PSRule.Types.csproj new file mode 100644 index 0000000000..2ee366f9e5 --- /dev/null +++ b/src/PSRule.Types/PSRule.Types.csproj @@ -0,0 +1,21 @@ + + + + Microsoft.PSRule.Types + PSRule + Microsoft.PSRule.Types + Library + {5fe4db0b-63d1-4ddb-9762-9c0d29168bc9} + README.md + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/src/PSRule.Types/Properties/AssemblyInfo.cs b/src/PSRule.Types/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..d20fb5e7fc --- /dev/null +++ b/src/PSRule.Types/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("PSRule.Tests")] diff --git a/src/PSRule.Types/README.md b/src/PSRule.Types/README.md new file mode 100644 index 0000000000..5c3402922a --- /dev/null +++ b/src/PSRule.Types/README.md @@ -0,0 +1,3 @@ +# PSRule Types library for .NET + +Microsoft.PSRule.Types provides underlying support PSRule types. diff --git a/src/PSRule/Commands/NewRuleDefinitionCommand.cs b/src/PSRule/Commands/NewRuleDefinitionCommand.cs index b8543c742e..b58e1dd257 100644 --- a/src/PSRule/Commands/NewRuleDefinitionCommand.cs +++ b/src/PSRule/Commands/NewRuleDefinitionCommand.cs @@ -3,7 +3,6 @@ using System; using System.Collections; -using System.Collections.Generic; using System.Management.Automation; using System.Threading; using PSRule.Definitions; diff --git a/src/PSRule/Common/EnvironmentHelper.cs b/src/PSRule/Common/EnvironmentHelper.cs index 1a92f67c6f..71d40f2cae 100644 --- a/src/PSRule/Common/EnvironmentHelper.cs +++ b/src/PSRule/Common/EnvironmentHelper.cs @@ -40,6 +40,10 @@ public static string GetRunId(this EnvironmentHelper helper) internal sealed class EnvironmentHelper { private readonly static char[] STRINGARRAY_SEPARATOR = new char[] { ';' }; + private readonly static char[] WINDOWS_PATH_ENV_SEPARATOR = new char[] { ';' }; + private readonly static char[] LINUX_PATH_ENV_SEPARATOR = new char[] { ':' }; + + private const string DEFAULT_CREDENTIAL_USERNAME = "na"; public static readonly EnvironmentHelper Default = new EnvironmentHelper(); @@ -54,7 +58,7 @@ internal bool TrySecureString(string key, out SecureString value) if (!TryString(key, out var variable)) return false; - value = new NetworkCredential("na", variable).SecurePassword; + value = new NetworkCredential(DEFAULT_CREDENTIAL_USERNAME, variable).SecurePassword; return true; } @@ -86,6 +90,17 @@ internal bool TryStringArray(string key, out string[] value) return value != null; } + internal bool TryPathEnvironmentVariable(string key, out string[] value) + { + value = default; + if (!TryVariable(key, out var variable)) + return false; + + var separator = Environment.OSVersion.Platform == PlatformID.Win32NT ? WINDOWS_PATH_ENV_SEPARATOR : LINUX_PATH_ENV_SEPARATOR; + value = variable.Split(separator, options: StringSplitOptions.RemoveEmptyEntries); + return value != null; + } + private static bool TryVariable(string key, out string variable) { variable = Environment.GetEnvironmentVariable(key); diff --git a/src/PSRule/Configuration/BaselineOption.cs b/src/PSRule/Configuration/BaselineOption.cs index 7222758343..bc3c20633b 100644 --- a/src/PSRule/Configuration/BaselineOption.cs +++ b/src/PSRule/Configuration/BaselineOption.cs @@ -7,6 +7,10 @@ namespace PSRule.Configuration { + /// + /// A subset of options that can be defined within a baseline. + /// These options can be passes as a baseline for use within a pipeline. + /// public class BaselineOption { internal sealed class BaselineRef : BaselineOption @@ -37,16 +41,31 @@ public BaselineInline() public RuleOption Rule { get; set; } } + /// + /// Creates a baseline option from a hashtable of key/ values. + /// + /// A hashtable of key/ values. + /// A baseline option composed of provided key/ values. public static implicit operator BaselineOption(Hashtable hashtable) { return FromHashtable(hashtable); } + /// + /// Creates a reference to a baseline by name which is resolved at runtime. + /// + /// The name of the baseline. + /// A reference to a baseline option. public static implicit operator BaselineOption(string value) { return FromString(value); } + /// + /// Creates a baseline option from a hashtable of key/ values. + /// + /// A hashtable of key/ values. + /// A baseline option composed of provided key/ values. public static BaselineOption FromHashtable(Hashtable hashtable) { var option = new BaselineInline(); @@ -59,9 +78,14 @@ public static BaselineOption FromHashtable(Hashtable hashtable) return option; } + /// + /// Creates a reference to a baseline by name which is resolved at runtime. + /// + /// The name of the baseline. + /// A reference to a baseline option. public static BaselineOption FromString(string value) { - return new BaselineRef(value); + return string.IsNullOrEmpty(value) ? null : new BaselineRef(value); } internal static void Load(IBaselineSpec option, EnvironmentHelper env) diff --git a/src/PSRule/Configuration/RequiresOption.cs b/src/PSRule/Configuration/RequiresOption.cs index c409c459eb..b26c46b7e3 100644 --- a/src/PSRule/Configuration/RequiresOption.cs +++ b/src/PSRule/Configuration/RequiresOption.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Collections.Generic; +using PSRule.Data; namespace PSRule.Configuration { @@ -30,12 +31,26 @@ public RequiresOption() internal RequiresOption(RequiresOption option) : base(option) { } + /// + /// Returns an array of Key/Values. + /// + public ModuleConstraint[] ToArray() + { + var result = new List(); + foreach (var kv in this) + { + if (SemanticVersion.TryParseConstraint(kv.Value, out var constraint)) + result.Add(new ModuleConstraint(kv.Key, constraint)); + } + return result.ToArray(); + } + /// /// Load Requires option from environment variables. /// internal void Load(EnvironmentHelper env) { - base.Load(ENVIRONMENT_PREFIX, env, ConvertUnderscore); + Load(ENVIRONMENT_PREFIX, env, ConvertUnderscore); } /// @@ -43,7 +58,7 @@ internal void Load(EnvironmentHelper env) /// internal void Load(IDictionary dictionary) { - base.Load(DICTIONARY_PREFIX, dictionary); + Load(DICTIONARY_PREFIX, dictionary); } /// diff --git a/src/PSRule/Definitions/Expressions/LanguageExpressions.cs b/src/PSRule/Definitions/Expressions/LanguageExpressions.cs index 7579e90565..72f1c78667 100644 --- a/src/PSRule/Definitions/Expressions/LanguageExpressions.cs +++ b/src/PSRule/Definitions/Expressions/LanguageExpressions.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading; using PSRule.Configuration; +using PSRule.Data; using PSRule.Pipeline; using PSRule.Resources; using PSRule.Runtime; diff --git a/src/PSRule/PSRule.csproj b/src/PSRule/PSRule.csproj index c64591b8e2..94b376f746 100644 --- a/src/PSRule/PSRule.csproj +++ b/src/PSRule/PSRule.csproj @@ -18,12 +18,11 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + diff --git a/src/PSRule/Pipeline/CommandLineBuilder.cs b/src/PSRule/Pipeline/CommandLineBuilder.cs index a8656d694a..d3366c9ff2 100644 --- a/src/PSRule/Pipeline/CommandLineBuilder.cs +++ b/src/PSRule/Pipeline/CommandLineBuilder.cs @@ -45,7 +45,7 @@ public static IInvokePipelineBuilder Invoke(string[] module, PSRuleOption option public static IInvokePipelineBuilder Assert(string[] module, PSRuleOption option, IHostContext hostContext) { var sourcePipeline = new SourcePipelineBuilder(hostContext, option); - for (var i = 0; i < module.Length; i++) + for (var i = 0; module != null && i < module.Length; i++) sourcePipeline.ModuleByName(module[i]); var source = sourcePipeline.Build(); diff --git a/src/PSRule/Pipeline/ModulePathComparer.cs b/src/PSRule/Pipeline/ModulePathComparer.cs new file mode 100644 index 0000000000..409f6ba3c1 --- /dev/null +++ b/src/PSRule/Pipeline/ModulePathComparer.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.IO; +using PSRule.Data; + +namespace PSRule.Pipeline +{ + /// + /// Sort paths by versions in decending order. Use orginal order when versions are compariable. + /// + internal sealed class ModulePathComparer : IComparer + { + /// + public int Compare(string x, string y) + { + var x_name = Path.GetFileName(x); + var y_name = Path.GetFileName(y); + if (!SemanticVersion.TryParseVersion(x_name, out var x_version)) + return SemanticVersion.TryParseVersion(y_name, out _) ? 1 : 0; + + return !SemanticVersion.TryParseVersion(y_name, out var y_version) ? -1 : y_version.CompareTo(x_version); + } + } +} diff --git a/src/PSRule/Pipeline/PipelineBuilder.cs b/src/PSRule/Pipeline/PipelineBuilder.cs index 5e45f4fc50..dca8f3cf66 100644 --- a/src/PSRule/Pipeline/PipelineBuilder.cs +++ b/src/PSRule/Pipeline/PipelineBuilder.cs @@ -10,11 +10,11 @@ using System.Management.Automation; using System.Reflection; using PSRule.Configuration; +using PSRule.Data; using PSRule.Definitions; using PSRule.Definitions.Baselines; using PSRule.Pipeline.Output; using PSRule.Resources; -using PSRule.Runtime; namespace PSRule.Pipeline { @@ -23,6 +23,16 @@ namespace PSRule.Pipeline /// public static class PipelineBuilder { + /// + /// Create a builder for an Assert pipeline. + /// + /// + /// Assert pipelines process objects with rules and produce text-based output suitable for output to a CI pipeline. + /// + /// An array of sources. + /// Options that configure PSRule. + /// An implementation of a host context that will recieve output and results. + /// A builder object to configure the pipeline. public static IInvokePipelineBuilder Assert(Source[] source, PSRuleOption option, IHostContext hostContext) { var pipeline = new AssertPipelineBuilder(source, hostContext); @@ -30,6 +40,16 @@ public static IInvokePipelineBuilder Assert(Source[] source, PSRuleOption option return pipeline; } + /// + /// Create a builder for an Invoke pipeline. + /// + /// + /// Invoke piplines process objects and produce records indicating the outcome of each rule. + /// + /// An array of sources. + /// Options that configure PSRule. + /// An implementation of a host context that will recieve output and results. + /// A builder object to configure the pipeline. public static IInvokePipelineBuilder Invoke(Source[] source, PSRuleOption option, IHostContext hostContext) { var pipeline = new InvokeRulePipelineBuilder(source, hostContext); @@ -86,6 +106,9 @@ public static IGetTargetPipelineBuilder GetTarget(PSRuleOption option, IHostCont } } + /// + /// A helper to build a PSRule pipeline. + /// public interface IPipelineBuilder { /// @@ -93,6 +116,12 @@ public interface IPipelineBuilder /// IPipelineBuilder Configure(PSRuleOption option); + /// + /// Configure the pipeline to use a specific baseline. + /// + /// A baseline option or the name of a baseline. + void Baseline(BaselineOption baseline); + /// /// Build the pipeline. /// @@ -100,6 +129,9 @@ public interface IPipelineBuilder IPipeline Build(IPipelineWriter writer = null); } + /// + /// An instance of a PSRule pipeline. + /// public interface IPipeline : IDisposable { /// @@ -155,6 +187,7 @@ protected PipelineBuilderBase(Source[] source, IHostContext hostContext) VisitTargetObject = PipelineReceiverActions.PassThru; } + /// public void Name(string[] name) { if (name == null || name.Length == 0) @@ -163,6 +196,7 @@ public void Name(string[] name) _Include = name; } + /// public void Tag(Hashtable tag) { if (tag == null || tag.Count == 0) @@ -171,6 +205,7 @@ public void Tag(Hashtable tag) _Tag = tag; } + /// public void Convention(string[] convention) { if (convention == null || convention.Length == 0) @@ -179,6 +214,7 @@ public void Convention(string[] convention) _Convention = convention; } + /// public virtual IPipelineBuilder Configure(PSRuleOption option) { if (option == null) @@ -197,12 +233,20 @@ public virtual IPipelineBuilder Configure(PSRuleOption option) return this; } + /// public abstract IPipeline Build(IPipelineWriter writer = null); /// /// Use a baseline, either by name or by path. /// + [Obsolete()] public void UseBaseline(BaselineOption baseline) + { + Baseline(baseline); + } + + /// + public void Baseline(BaselineOption baseline) { if (baseline == null) return; diff --git a/src/PSRule/Pipeline/SourcePipeline.cs b/src/PSRule/Pipeline/SourcePipeline.cs index f1a47d3700..44c1deccbd 100644 --- a/src/PSRule/Pipeline/SourcePipeline.cs +++ b/src/PSRule/Pipeline/SourcePipeline.cs @@ -80,30 +80,41 @@ internal SourcePipelineBuilder(IHostContext hostContext, PSRuleOption option) public bool ShouldLoadModule => _HostContext.GetAutoLoadingPreference() == PSModuleAutoLoadingPreference.All; + #region Logging + public void VerboseScanSource(string path) { - if (!_Writer.ShouldWriteVerbose()) - return; - - _Writer.WriteVerbose(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.ScanSource, path)); + Log(PSRuleResources.ScanSource, path); } public void VerboseFoundModules(int count) + { + Log(PSRuleResources.FoundModules, count); + } + + public void VerboseScanModule(string moduleName) + { + Log(PSRuleResources.ScanModule, moduleName); + } + + private void Log(string message, params object[] args) { if (!_Writer.ShouldWriteVerbose()) return; - _Writer.WriteVerbose(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.FoundModules, count)); + _Writer.WriteVerbose(string.Format(Thread.CurrentThread.CurrentCulture, message, args)); } - public void VerboseScanModule(string moduleName) + private void Debug(string message, params object[] args) { - if (!_Writer.ShouldWriteVerbose()) + if (!_Writer.ShouldWriteDebug()) return; - _Writer.WriteVerbose(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.ScanModule, moduleName)); + _Writer.WriteDebug(string.Format(Thread.CurrentThread.CurrentCulture, message, args)); } + #endregion Logging + /// /// Add loose files as a source. /// @@ -151,6 +162,9 @@ public void Module(PSModuleInfo[] module) public void ModuleByName(string name) { var basePath = FindModule(name); + if (basePath == null) + throw new PipelineBuilderException(PSRuleResources.ModuleNotFound); + var info = LoadManifest(basePath); if (info == null) throw new PipelineBuilderException(PSRuleResources.ModuleNotFound); @@ -169,13 +183,66 @@ public void ModuleByName(string name) private string FindModule(string name) { - return PSRuleOption.GetRootedBasePath(Path.Combine(_LocalPath, "Modules", name)); + return TryPackagedModule(name, out var path) || + TryInstalledModule(name, out path) ? path : null; + } + + /// + /// Try to find a packaged module found relative to the tool. + /// + private bool TryPackagedModule(string name, out string path) + { + Log($"Looking for modules in: {_LocalPath}"); + path = PSRuleOption.GetRootedBasePath(Path.Combine(_LocalPath, "Modules", name)); + return System.IO.Directory.Exists(path); + } + + /// + /// Try to find a module installed into PowerShell. + /// + private bool TryInstalledModule(string name, out string path) + { + path = null; + if (!EnvironmentHelper.Default.TryPathEnvironmentVariable("PSModulePath", out var searchPaths)) + return false; + + var unsorted = new List(); + Log("Looking for modules installed to PowerShell."); + for (var i = 0; i < searchPaths.Length; i++) + { + Debug($"Looking for modules search paths: {searchPaths[i]}"); + var searchPath = PSRuleOption.GetRootedBasePath(Path.Combine(searchPaths[i], name)); + if (System.IO.Directory.Exists(searchPath)) + { + foreach (var versionPath in System.IO.Directory.EnumerateDirectories(searchPath)) + { + var manifestPath = Path.Combine(versionPath, GetManifestName(name)); + if (File.Exists(manifestPath)) + unsorted.Add(versionPath); + } + } + } + if (unsorted.Count == 0) + return false; + + var sorted = SortModulePath(unsorted); + if (sorted.Length > 0) + path = sorted[0]; + + return sorted.Length > 0; + } + + private static string[] SortModulePath(IEnumerable values) + { + var results = values.ToArray(); + Array.Sort(results, new ModulePathComparer()); + return results; } private static Source.ModuleInfo LoadManifest(string basePath) { var name = Path.GetFileName(Path.GetDirectoryName(basePath)); - var path = Path.Combine(basePath, string.Concat(name, ".psd1")); + var path = Path.Combine(basePath, GetManifestName(name)); if (!File.Exists(path)) return null; @@ -196,12 +263,19 @@ private static Source.ModuleInfo LoadManifest(string basePath) var prerelease = psData["Prerelease"] as string; var requiredAssemblies = manifest["RequiredAssemblies"] as Array; - foreach (var a in requiredAssemblies.OfType()) - Assembly.LoadFile(Path.Combine(basePath, a)); - + if (requiredAssemblies != null) + { + foreach (var a in requiredAssemblies.OfType()) + Assembly.LoadFile(Path.Combine(basePath, a)); + } return new Source.ModuleInfo(basePath, name, version, projectUri, guid, companyName, prerelease); } + private static string GetManifestName(string name) + { + return string.Concat(name, ".psd1"); + } + /// /// Add a module source /// diff --git a/src/PSRule/Resources/PSRuleResources.Designer.cs b/src/PSRule/Resources/PSRuleResources.Designer.cs index 7028b08425..12e9d59e5f 100644 --- a/src/PSRule/Resources/PSRuleResources.Designer.cs +++ b/src/PSRule/Resources/PSRuleResources.Designer.cs @@ -331,7 +331,7 @@ internal static string ModuleManifestBaseline { } /// - /// Looks up a localized string similar to Not valid module can be found with that name.. + /// Looks up a localized string similar to No valid module can be found with that name.. /// internal static string ModuleNotFound { get { diff --git a/src/PSRule/Resources/PSRuleResources.resx b/src/PSRule/Resources/PSRuleResources.resx index 6ac20172fb..a816838778 100644 --- a/src/PSRule/Resources/PSRuleResources.resx +++ b/src/PSRule/Resources/PSRuleResources.resx @@ -340,7 +340,7 @@ Output written to the following file: '{0}' - Not valid module can be found with that name. + No valid module can be found with that name. [PSRule][R][Trace] -- {0} diff --git a/src/PSRule/Runtime/Assert.cs b/src/PSRule/Runtime/Assert.cs index 69bce3ea9b..5712f44862 100644 --- a/src/PSRule/Runtime/Assert.cs +++ b/src/PSRule/Runtime/Assert.cs @@ -1353,7 +1353,7 @@ private bool GuardSemanticVersion(IOperand operand, object fieldValue, out Seman { result = null; value = null; - if (ExpressionHelpers.TryString(fieldValue, out var sversion) && Runtime.SemanticVersion.TryParseVersion(sversion, out value)) + if (ExpressionHelpers.TryString(fieldValue, out var sversion) && SemanticVersion.TryParseVersion(sversion, out value)) return false; result = Fail(operand, ReasonStrings.Version, fieldValue); diff --git a/tests/PSRule.Tests/ModulePathComparerTests.cs b/tests/PSRule.Tests/ModulePathComparerTests.cs new file mode 100644 index 0000000000..5b5d5731a7 --- /dev/null +++ b/tests/PSRule.Tests/ModulePathComparerTests.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using PSRule.Pipeline; +using Xunit; + +namespace PSRule +{ + public sealed class ModulePathComparerTests + { + [Fact] + public void Sort() + { + var paths = new string[] + { + "C:/modules/PSRule/1.0.0", + "C:/modules/PSRule/1.2.0", + "C:/other/PSRule/1.0.0", + "C:/modules/PSRule/10.0.0", + "C:/other/PSRule/10.0.0", + "C:/other/PSRule/1.2.0", + "C:/other/PSRule/version", + "C:/other/PSRule/0.1.0", + }; + var comparer = new ModulePathComparer(); + Array.Sort(paths, comparer); + + Assert.Equal("C:/modules/PSRule/10.0.0", paths[0]); + Assert.Equal("C:/other/PSRule/10.0.0", paths[1]); + Assert.Equal("C:/modules/PSRule/1.2.0", paths[2]); + Assert.Equal("C:/other/PSRule/1.2.0", paths[3]); + Assert.Equal("C:/modules/PSRule/1.0.0", paths[4]); + Assert.Equal("C:/other/PSRule/1.0.0", paths[5]); + Assert.Equal("C:/other/PSRule/0.1.0", paths[6]); + Assert.Equal("C:/other/PSRule/version", paths[7]); + } + } +} diff --git a/tests/PSRule.Tests/SemanticVersionTests.cs b/tests/PSRule.Tests/SemanticVersionTests.cs index b290c00b49..13f864300a 100644 --- a/tests/PSRule.Tests/SemanticVersionTests.cs +++ b/tests/PSRule.Tests/SemanticVersionTests.cs @@ -1,70 +1,98 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using PSRule.Data; using Xunit; namespace PSRule { public sealed class SemanticVersionTests { + /// + /// Test parsing of versions. + /// [Fact] public void Version() { - Assert.True(Runtime.SemanticVersion.TryParseVersion("1.2.3-alpha.3+7223b39", out var actual1)); + Assert.True(SemanticVersion.TryParseVersion("1.2.3-alpha.3+7223b39", out var actual1)); Assert.Equal(1, actual1.Major); Assert.Equal(2, actual1.Minor); Assert.Equal(3, actual1.Patch); Assert.Equal("alpha.3", actual1.Prerelease.Value); Assert.Equal("7223b39", actual1.Build); - Assert.True(Runtime.SemanticVersion.TryParseVersion("v1.2.3-alpha.3", out var actual2)); + Assert.True(SemanticVersion.TryParseVersion("v1.2.3-alpha.3", out var actual2)); Assert.Equal(1, actual2.Major); Assert.Equal(2, actual2.Minor); Assert.Equal(3, actual2.Patch); Assert.Equal("alpha.3", actual2.Prerelease.Value); - Assert.True(Runtime.SemanticVersion.TryParseVersion("v1.2.3+7223b39", out var actual3)); + Assert.True(SemanticVersion.TryParseVersion("v1.2.3+7223b39", out var actual3)); Assert.Equal(1, actual3.Major); Assert.Equal(2, actual3.Minor); Assert.Equal(3, actual3.Patch); Assert.Equal("7223b39", actual3.Build); } + /// + /// Test ordering of versions by comparison. + /// + [Fact] + public void VersionOrder() + { + SemanticVersion.TryParseVersion("1.0.0", out var actual1); + SemanticVersion.TryParseVersion("1.2.0", out var actual2); + SemanticVersion.TryParseVersion("10.0.0", out var actual3); + SemanticVersion.TryParseVersion("1.0.2", out var actual4); + + Assert.True(actual1.CompareTo(actual1) == 0); + Assert.True(actual1.CompareTo(actual2) < 0); + Assert.True(actual1.CompareTo(actual3) < 0); + Assert.True(actual1.CompareTo(actual4) < 0); + Assert.True(actual2.CompareTo(actual2) == 0); + Assert.True(actual2.CompareTo(actual1) > 0); + Assert.True(actual2.CompareTo(actual3) < 0); + Assert.True(actual2.CompareTo(actual4) > 0); + } + + /// + /// Test parsing of constraints. + /// [Fact] public void Constraint() { // Versions - Assert.True(Runtime.SemanticVersion.TryParseVersion("1.2.3", out var version1)); - Assert.True(Runtime.SemanticVersion.TryParseVersion("1.2.3-alpha.3+7223b39", out var version2)); - Assert.True(Runtime.SemanticVersion.TryParseVersion("3.4.5-alpha.9", out var version3)); - Assert.True(Runtime.SemanticVersion.TryParseVersion("3.4.5", out var version4)); - Assert.False(Runtime.SemanticVersion.TryParseVersion("1.2.3-", out var _)); - Assert.True(Runtime.SemanticVersion.TryParseVersion("1.2.3-0", out var _)); - Assert.False(Runtime.SemanticVersion.TryParseVersion("1.2.3-0123", out var _)); - Assert.True(Runtime.SemanticVersion.TryParseVersion("1.2.3-0A", out var _)); + Assert.True(SemanticVersion.TryParseVersion("1.2.3", out var version1)); + Assert.True(SemanticVersion.TryParseVersion("1.2.3-alpha.3+7223b39", out var version2)); + Assert.True(SemanticVersion.TryParseVersion("3.4.5-alpha.9", out var version3)); + Assert.True(SemanticVersion.TryParseVersion("3.4.5", out var version4)); + Assert.False(SemanticVersion.TryParseVersion("1.2.3-", out var _)); + Assert.True(SemanticVersion.TryParseVersion("1.2.3-0", out var _)); + Assert.False(SemanticVersion.TryParseVersion("1.2.3-0123", out var _)); + Assert.True(SemanticVersion.TryParseVersion("1.2.3-0A", out var _)); // Constraints - Assert.True(Runtime.SemanticVersion.TryParseConstraint("1.2.3", out var actual1)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint("1.2.3-alpha.3", out var actual2)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint(">1.2.3-alpha.3", out var actual3)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint(">1.2.3-alpha.1", out var actual4)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint("<1.2.3-beta", out var actual5)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint("^1.2.3-alpha", out var actual6)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint("<3.4.6", out var actual7)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint("=v1.2.3", out var actual8)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint(">=v1.2.3", out var actual9)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint(">=v1.2.3-0", out var actual10)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint("<3.4.5", out var actual11)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint("<3.4.5-9999999999", out var actual12)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint("^1.0.0", out var actual13)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint("<1.2.3-0", out var actual14)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint("1.2.3|| >=3.4.5-0 3.4.5", out var actual15)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint("1.2.3 ||>=3.4.5-0 || 3.4.5", out var actual16)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint("1.2.3||3.4.5", out var actual17)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint(">=1.2.3", out var actual18, includePrerelease: true)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint("<=3.4.5-0", out var actual19, includePrerelease: true)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint("@pre >=1.2.3", out var actual20)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint("@prerelease <=3.4.5-0", out var actual21)); + Assert.True(SemanticVersion.TryParseConstraint("1.2.3", out var actual1)); + Assert.True(SemanticVersion.TryParseConstraint("1.2.3-alpha.3", out var actual2)); + Assert.True(SemanticVersion.TryParseConstraint(">1.2.3-alpha.3", out var actual3)); + Assert.True(SemanticVersion.TryParseConstraint(">1.2.3-alpha.1", out var actual4)); + Assert.True(SemanticVersion.TryParseConstraint("<1.2.3-beta", out var actual5)); + Assert.True(SemanticVersion.TryParseConstraint("^1.2.3-alpha", out var actual6)); + Assert.True(SemanticVersion.TryParseConstraint("<3.4.6", out var actual7)); + Assert.True(SemanticVersion.TryParseConstraint("=v1.2.3", out var actual8)); + Assert.True(SemanticVersion.TryParseConstraint(">=v1.2.3", out var actual9)); + Assert.True(SemanticVersion.TryParseConstraint(">=v1.2.3-0", out var actual10)); + Assert.True(SemanticVersion.TryParseConstraint("<3.4.5", out var actual11)); + Assert.True(SemanticVersion.TryParseConstraint("<3.4.5-9999999999", out var actual12)); + Assert.True(SemanticVersion.TryParseConstraint("^1.0.0", out var actual13)); + Assert.True(SemanticVersion.TryParseConstraint("<1.2.3-0", out var actual14)); + Assert.True(SemanticVersion.TryParseConstraint("1.2.3|| >=3.4.5-0 3.4.5", out var actual15)); + Assert.True(SemanticVersion.TryParseConstraint("1.2.3 ||>=3.4.5-0 || 3.4.5", out var actual16)); + Assert.True(SemanticVersion.TryParseConstraint("1.2.3||3.4.5", out var actual17)); + Assert.True(SemanticVersion.TryParseConstraint(">=1.2.3", out var actual18, includePrerelease: true)); + Assert.True(SemanticVersion.TryParseConstraint("<=3.4.5-0", out var actual19, includePrerelease: true)); + Assert.True(SemanticVersion.TryParseConstraint("@pre >=1.2.3", out var actual20)); + Assert.True(SemanticVersion.TryParseConstraint("@prerelease <=3.4.5-0", out var actual21)); // Version1 - 1.2.3 Assert.True(actual1.Equals(version1)); @@ -159,17 +187,20 @@ public void Constraint() Assert.False(actual21.Equals(version4)); } + /// + /// Test parsing and order of pre-releases. + /// [Fact] public void Prerelease() { - var actual1 = new Runtime.SemanticVersion.PR(null); - var actual2 = new Runtime.SemanticVersion.PR("alpha"); - var actual3 = new Runtime.SemanticVersion.PR("alpha.1"); - var actual4 = new Runtime.SemanticVersion.PR("alpha.beta"); - var actual5 = new Runtime.SemanticVersion.PR("beta"); - var actual6 = new Runtime.SemanticVersion.PR("beta.2"); - var actual7 = new Runtime.SemanticVersion.PR("beta.11"); - var actual8 = new Runtime.SemanticVersion.PR("rc.1"); + var actual1 = new SemanticVersion.PR(null); + var actual2 = new SemanticVersion.PR("alpha"); + var actual3 = new SemanticVersion.PR("alpha.1"); + var actual4 = new SemanticVersion.PR("alpha.beta"); + var actual5 = new SemanticVersion.PR("beta"); + var actual6 = new SemanticVersion.PR("beta.2"); + var actual7 = new SemanticVersion.PR("beta.11"); + var actual8 = new SemanticVersion.PR("rc.1"); Assert.True(actual1.CompareTo(actual1) == 0); Assert.True(actual1.CompareTo(actual2) > 0); From 85b7614c173b41a7bab364511909ace659b00f36 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Aug 2022 03:04:37 +1000 Subject: [PATCH 028/156] Bump mkdocs-material from 8.3.9 to 8.4.0 (#1224) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.3.9 to 8.4.0. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.3.9...8.4.0) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index d7749a9dcf..cab30f018d 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.3.1 -mkdocs-material==8.3.9 +mkdocs-material==8.4.0 pymdown-extensions==9.5 mike==1.1.2 mkdocs-simple-hooks==0.1.5 From c1dc6e79b1b506f0e855f83cbf91cf428a7706db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Aug 2022 03:19:29 +1000 Subject: [PATCH 029/156] Bump mkdocs-redirects from 1.0.5 to 1.0.6 (#1223) Bumps [mkdocs-redirects](https://github.com/datarobot/mkdocs-redirects) from 1.0.5 to 1.0.6. - [Release notes](https://github.com/datarobot/mkdocs-redirects/releases) - [Commits](https://github.com/datarobot/mkdocs-redirects/compare/v1.0.5...v1.0.6) --- updated-dependencies: - dependency-name: mkdocs-redirects dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index cab30f018d..c348b64f4e 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -5,4 +5,4 @@ mike==1.1.2 mkdocs-simple-hooks==0.1.5 mkdocs-git-revision-date-plugin==0.3.2 mdx-truly-sane-lists==1.3 -mkdocs-redirects==1.0.5 +mkdocs-redirects==1.0.6 From 13357d76bfaf24381d31d9dff07ae1a13ec01958 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Tue, 16 Aug 2022 03:45:36 +1000 Subject: [PATCH 030/156] Pre-release v2.4.0-B0022 (#1226) --- docs/CHANGELOG-v2.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 9930d95d5c..3188dac922 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -13,6 +13,10 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.4.0-B0022 (pre-release) + +What's changed since pre-release v2.4.0-B0009: + - Engineering: - Updates to PSRule engine API by @BernieWhite. [#1152](https://github.com/microsoft/PSRule/issues/1152) @@ -28,7 +32,7 @@ What's changed since v2.3.2: - Bump support projects to .NET 6 by @BernieWhite. [#1209](https://github.com/microsoft/PSRule/issues/1209) - Bump Microsoft.NET.Test.Sdk to v17.3.0. - [#1213](https://github.com/microsoft/PSRule/pull/1208) + [#1213](https://github.com/microsoft/PSRule/pull/1213) - Bug fixes: - Fixed repository information not in output by @BernieWhite. [#1219](https://github.com/microsoft/PSRule/issues/1219) From b37a09832cb8edde897a7bbd8522b7f5074d068c Mon Sep 17 00:00:00 2001 From: Bernie White Date: Wed, 17 Aug 2022 23:47:39 +1000 Subject: [PATCH 031/156] Added initial function support #1227 (#1228) --- .vscode/settings.json | 13 + docs/CHANGELOG-v2.md | 13 + docs/expressions/functions.md | 127 +++ docs/specs/function-spec.md | 34 + mkdocs.yml | 2 + schemas/PSRule-language.schema.json | 409 +++++++++- src/PSRule/Common/DictionaryExtensions.cs | 37 +- src/PSRule/Common/ExpressionHelpers.cs | 31 +- src/PSRule/Common/JsonConverters.cs | 157 +++- src/PSRule/Common/JsonReaderExtensions.cs | 36 + src/PSRule/Common/YamlConverters.cs | 126 ++- .../Definitions/Expressions/Exceptions.cs | 169 ++++ .../Expressions/ExpressionContext.cs | 7 +- .../Expressions/FunctionBuilder.cs | 121 +++ .../Definitions/Expressions/Functions.cs | 227 ++++++ .../Expressions/LanguageExpressions.cs | 158 ++-- .../Definitions/Expressions/Primitives.cs | 14 +- src/PSRule/Definitions/Rules/RuleVisitor.cs | 2 +- .../Definitions/Selectors/SelectorVisitor.cs | 2 +- .../SuppressionGroupVisitor.cs | 2 +- src/PSRule/Host/HostHelper.cs | 2 +- .../Resources/PSRuleResources.Designer.cs | 753 ++++++++++++------ src/PSRule/Resources/PSRuleResources.resx | 31 + .../Resources/ReasonStrings.Designer.cs | 636 ++++++--------- src/PSRule/Resources/ReasonStrings.resx | 9 + src/PSRule/Runtime/Configuration.cs | 20 +- src/PSRule/Runtime/Operand.cs | 12 +- src/PSRule/Runtime/PSRule.cs | 2 +- src/PSRule/Runtime/RunspaceContext.cs | 27 +- tests/PSRule.Tests/FunctionBuilderTests.cs | 65 ++ tests/PSRule.Tests/FunctionTests.cs | 282 +++++++ tests/PSRule.Tests/Functions.Rule.jsonc | 133 ++++ tests/PSRule.Tests/Functions.Rule.yaml | 88 ++ tests/PSRule.Tests/PSRule.Tests.csproj | 6 + tests/PSRule.Tests/SelectorTests.cs | 36 +- 35 files changed, 3008 insertions(+), 781 deletions(-) create mode 100644 docs/expressions/functions.md create mode 100644 docs/specs/function-spec.md create mode 100644 src/PSRule/Definitions/Expressions/Exceptions.cs create mode 100644 src/PSRule/Definitions/Expressions/FunctionBuilder.cs create mode 100644 src/PSRule/Definitions/Expressions/Functions.cs create mode 100644 tests/PSRule.Tests/FunctionBuilderTests.cs create mode 100644 tests/PSRule.Tests/FunctionTests.cs create mode 100644 tests/PSRule.Tests/Functions.Rule.jsonc create mode 100644 tests/PSRule.Tests/Functions.Rule.yaml diff --git a/.vscode/settings.json b/.vscode/settings.json index e4c4f5964b..75e83e463a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -58,6 +58,18 @@ ], "files.insertFinalNewline": true }, + "[jsonc]": { + "editor.tabSize": 4, + "editor.tabCompletion": "on", + "editor.quickSuggestions": { + "strings": true + }, + "editor.suggest.insertMode": "replace", + "gitlens.codeLens.scopes": [ + "document" + ], + "files.insertFinalNewline": true + }, "files.associations": { "**/.azure-pipelines/*.yaml": "azure-pipelines", "**/.azure-pipelines/jobs/*.yaml": "azure-pipelines", @@ -67,6 +79,7 @@ "APPSERVICEMININSTANCECOUNT", "cmdlet", "cmdlets", + "concat", "datetime", "deserialize", "deserialized", diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 3188dac922..7c42ed3360 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -11,8 +11,21 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers [2]: https://microsoft.github.io/PSRule/latest/deprecations/#deprecations-for-v3 +**Experimental features**: + +- Functions within YAML expressions can be used to perform manipulation prior to testing a condition. + ## Unreleased +What's changed since pre-release v2.4.0-B0022: + +- New features: + - **Experimental**: Added support for functions within YAML and JSON expressions. + [#1227](https://github.com/microsoft/PSRule/issues/1227) + - Added conversion functions `boolean`, `string`, and `integer`. + - Added lookup functions `configuration`, and `path`. + - Added string functions `concat`, `substring`. + ## v2.4.0-B0022 (pre-release) What's changed since pre-release v2.4.0-B0009: diff --git a/docs/expressions/functions.md b/docs/expressions/functions.md new file mode 100644 index 0000000000..124ed9f1d1 --- /dev/null +++ b/docs/expressions/functions.md @@ -0,0 +1,127 @@ +# Expression functions + +!!! Abstract + Functions are an advanced lanaguage feature specific to YAML and JSON resources. + That extend the language to allow for more complex use cases with expressions. + +!!! Experimental + Functions are a work in progress and subject to change. + We hope to add more functions, broader support, and more detailed documentation in the future. + [Join or start a disucssion][1] to let us know how we can improve this feature going forward. + + [1]: https://github.com/microsoft/PSRule/discussions + +## Using functions + +It may be necessary to perform minor transformation before evaluating a condition. + +- `boolean` - Convert a value to a boolean. +- `string` - Convert a value to a string. +- `integer` - Convert a value to an integer. +- `concat` - Concatenate multiple values. +- `substring` - Extract a substring from a string. +- `configuration` - Get a configuration value. +- `path` - Get a value from an object path. + +## Supported conditions + +Currently functions are only supported on a subset of conditions. +The conditions that are supported are: + +- `equals` +- `notEquals` +- `count` +- `less` +- `lessOrEquals` +- `greater` +- `greaterOrEquals` + +## Examples + +```yaml +--- +# Synopsis: An expression function example. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: Yaml.Fn.Example1 +spec: + if: + value: + $: + substring: + path: name + length: 7 + equals: TestObj + +--- +# Synopsis: An expression function example. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: Yaml.Fn.Example2 +spec: + if: + value: + $: + configuration: 'ConfigArray' + count: 5 + +--- +# Synopsis: An expression function example. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: Yaml.Fn.Example3 +spec: + if: + value: + $: + boolean: true + equals: true + +--- +# Synopsis: An expression function example. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: Yaml.Fn.Example4 +spec: + if: + value: + $: + concat: + - path: name + - string: '-' + - path: name + equals: TestObject1-TestObject1 + +--- +# Synopsis: An expression function example. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: Yaml.Fn.Example5 +spec: + if: + value: + $: + integer: 6 + greater: 5 + +--- +# Synopsis: An expression function example. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: Yaml.Fn.Example6 +spec: + if: + value: TestObject1-TestObject1 + equals: + $: + concat: + - path: name + - string: '-' + - path: name +``` diff --git a/docs/specs/function-spec.md b/docs/specs/function-spec.md new file mode 100644 index 0000000000..8378dcdb40 --- /dev/null +++ b/docs/specs/function-spec.md @@ -0,0 +1,34 @@ +# PSRule function expressions spec (draft) + +This is a spec for implementing function expressions in PSRule v2. + +## Synopsis + +Functions are available to handle complex conditions within YAML and JSON expressions. + +## Schema driven + +While functions allow handing for complex use cases, they should still remain schema driven. +A schema driven design allows auto-completion and validation during authoring in a broad set of tools. + +## Syntax + +Functions can be used within YAML and JSON expressions by using the `$` object property. +For example: + +```yaml +--- +# Synopsis: An expression function example. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: Yaml.Fn.Example1 +spec: + if: + value: + $: + substring: + path: name + length: 3 + equals: abc +``` diff --git a/mkdocs.yml b/mkdocs.yml index b7bbc8a882..240ab34e5f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -55,6 +55,8 @@ nav: - Azure resource tagging example: scenarios/azure-tags/azure-tags.md - Kubernetes resource validation example: scenarios/kubernetes-resources/kubernetes-resources.md - Concepts: + - Expressions: + - Functions: expressions/functions.md - Using within continuous integration: scenarios/validation-pipeline/validation-pipeline.md # - Troubleshooting: troubleshooting.md - License and contributing: license-contributing.md diff --git a/schemas/PSRule-language.schema.json b/schemas/PSRule-language.schema.json index 69db8259d2..e4346d5dac 100644 --- a/schemas/PSRule-language.schema.json +++ b/schemas/PSRule-language.schema.json @@ -1,23 +1,26 @@ { "$schema": "https://json-schema.org/draft-07/schema#", - "oneOf": [ - { - "$ref": "#/definitions/rule-v1" - }, - { - "$ref": "#/definitions/baseline-v1" - }, - { - "$ref": "#/definitions/moduleConfig-v1" - }, - { - "$ref": "#/definitions/selector-v1" - }, - { - "$ref": "#/definitions/suppressionGroup-v1" - } - ], + "$ref": "#/definitions/resource-v1", "definitions": { + "resource-v1": { + "oneOf": [ + { + "$ref": "#/definitions/rule-v1" + }, + { + "$ref": "#/definitions/baseline-v1" + }, + { + "$ref": "#/definitions/moduleConfig-v1" + }, + { + "$ref": "#/definitions/selector-v1" + }, + { + "$ref": "#/definitions/suppressionGroup-v1" + } + ] + }, "resource-metadata": { "type": "object", "title": "Metadata", @@ -808,6 +811,9 @@ "oneOf": [ { "$ref": "#/definitions/selectorPropertyField" + }, + { + "$ref": "#/definitions/selectorPropertyValue" } ] }, @@ -816,6 +822,9 @@ { "$ref": "#/definitions/selectorPropertyField" }, + { + "$ref": "#/definitions/selectorPropertyValue" + }, { "$ref": "#/definitions/selectorPropertyType" }, @@ -841,6 +850,16 @@ "field" ] }, + "selectorPropertyValue": { + "properties": { + "value": { + "$ref": "#/definitions/selectorExpressionValue" + } + }, + "required": [ + "value" + ] + }, "selectorPropertyType": { "properties": { "type": { @@ -1270,10 +1289,18 @@ "type": "object", "properties": { "greater": { - "type": "integer", "title": "Greater", "description": "Must be greater then the specified value.", - "markdownDescription": "Must be greater then the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#greater)" + "markdownDescription": "Must be greater then the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#greater)", + "oneOf": [ + { + "type": "integer" + }, + { + "type": "object", + "$ref": "#/definitions/fn" + } + ] }, "convert": { "type": "boolean", @@ -1936,15 +1963,351 @@ "selectorExpressionValue": { "oneOf": [ { - "type": "string" + "type": "string", + "title": "Value from string", + "description": "A value to compare." + }, + { + "type": "boolean", + "title": "Value from boolean", + "description": "A value to compare." + }, + { + "type": "integer", + "title": "Value from integer", + "description": "A value to compare." }, { - "type": "boolean" + "type": "object", + "title": "Value for object", + "description": "A value to compare.", + "not": { + "propertyNames": { + "enum": [ + "$" + ] + } + } }, { - "type": "integer" + "$ref": "#/definitions/fn" } ] + }, + "fn": { + "title": "Value from function", + "description": "A function expression that once evaluated specifies the value.", + "markdownDescription": "A function expression that once evaluated specifies the value.", + "properties": { + "$": { + "type": "object", + "title": "Value from function", + "description": "A function expression that once evaluated specifies the value.", + "markdownDescription": "A function expression that once evaluated specifies the value.", + "$ref": "#/definitions/fn/definitions/function" + } + }, + "required": [ + "$" + ], + "definitions": { + "function": { + "oneOf": [ + { + "$ref": "#/definitions/fn/definitions/function/definitions/substring" + }, + { + "$ref": "#/definitions/fn/definitions/function/definitions/string" + }, + { + "$ref": "#/definitions/fn/definitions/function/definitions/boolean" + }, + { + "$ref": "#/definitions/fn/definitions/function/definitions/integer" + }, + { + "$ref": "#/definitions/fn/definitions/function/definitions/concat" + }, + { + "$ref": "#/definitions/fn/definitions/function/definitions/path" + }, + { + "$ref": "#/definitions/fn/definitions/function/definitions/configuration" + } + ], + "definitions": { + "equal": { + "type": "object", + "properties": { + "equal": { + "type": "array", + "title": "Equal", + "description": "The equal operator checks for equity between two operands.", + "markdownDescription": "The `equal` operator checks for equity two operands.", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/fn/definitions/function" + }, + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "boolean" + } + ] + }, + "additionalItems": false, + "minItems": 2, + "maxItems": 2 + } + }, + "additionalProperties": false, + "required": [ + "equal" + ] + }, + "substring": { + "type": "object", + "properties": { + "substring": { + "title": "Substring", + "description": "The substring function, copies a number of characters from input starting from a zero based position.", + "markdownDescription": "The `substring` function, copies a number of characters from input starting from a zero based position.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "$ref": "#/definitions/fn/definitions/function", + "defaultSnippets": [ + { + "label": "From string", + "description": "Set value to a specific value.", + "body": "" + }, + { + "label": "From configuration", + "description": "Configure length from configuration.", + "body": { + "configuration": "${1}" + } + } + ] + } + ] + }, + "start": { + "title": "Start", + "description": "The zero based position to start copying characters from.", + "default": 0, + "oneOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "object", + "$ref": "#/definitions/fn/definitions/function" + } + ], + "defaultSnippets": [ + { + "label": "From integer", + "description": "Set start to a specific value.", + "body": 0 + }, + { + "label": "From configuration", + "description": "Configure start from configuration.", + "body": { + "configuration": "${1}" + } + } + ] + }, + "length": { + "title": "Length", + "description": "The number of character to copy from the source string.", + "oneOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "object", + "$ref": "#/definitions/fn/definitions/function" + } + ], + "defaultSnippets": [ + { + "label": "From integer", + "description": "Set length to a specific value.", + "body": 0 + }, + { + "label": "From configuration", + "description": "Configure length from configuration.", + "body": { + "configuration": "${1}" + } + } + ] + } + }, + "additionalProperties": false, + "required": [ + "substring" + ], + "defaultSnippets": [ + { + "label": "Substring from string", + "description": "Set value to a specific value.", + "body": { + "substring": "${1}", + "length": "${2}" + } + }, + { + "label": "Substring from configuration", + "description": "Configure length from configuration.", + "body": { + "substring": { + "configuration": "${1}" + }, + "length": "${2}" + } + } + ] + }, + "string": { + "type": "object", + "properties": { + "string": { + "title": "String", + "oneOf": [ + { + "type": "string", + "description": "A literal string value." + }, + { + "type": "object", + "description": "Converts the operand in to a string value.", + "$ref": "#/definitions/fn/definitions/function" + } + ] + } + }, + "additionalProperties": false, + "required": [ + "string" + ] + }, + "integer": { + "type": "object", + "properties": { + "integer": { + "title": "Integer", + "oneOf": [ + { + "type": "integer", + "description": "A literal integer value." + }, + { + "type": "object", + "description": "Converts the operand in to an integer.", + "$ref": "#/definitions/fn/definitions/function" + } + ] + } + }, + "additionalProperties": false, + "required": [ + "integer" + ] + }, + "boolean": { + "type": "object", + "properties": { + "boolean": { + "title": "Boolean", + "oneOf": [ + { + "type": "boolean", + "description": "A literal boolean value.", + "markdownDescription": "A literal boolean value." + }, + { + "type": "object", + "description": "Converts the operand to a boolean value.", + "markdownDescription": "Converts the operand to a boolean value.", + "$ref": "#/definitions/fn/definitions/function" + } + ] + } + }, + "additionalProperties": false, + "required": [ + "boolean" + ] + }, + "concat": { + "type": "object", + "properties": { + "concat": { + "type": "array", + "description": "The concat function combines two or more operands.", + "markdownDescription": "The `concat` function combines two or more operands.", + "items": { + "$ref": "#/definitions/fn/definitions/function" + }, + "additionalItems": false, + "minItems": 2 + } + }, + "additionalProperties": false, + "required": [ + "concat" + ] + }, + "path": { + "type": "object", + "properties": { + "path": { + "type": "string", + "title": "Path", + "description": "The path function returns a value from an object path.", + "markdownDescription": "The `path` function returns a value from an object path." + } + }, + "additionalProperties": false, + "required": [ + "path" + ] + }, + "configuration": { + "type": "object", + "properties": { + "configuration": { + "type": "string", + "title": "Configuration", + "description": "The configuration function returns a value retrieved from configuration by name.", + "markdownDescription": "The `configuration` function returns a value retrieved from configuration by name.", + "minLength": 1 + } + }, + "additionalProperties": false, + "required": [ + "configuration" + ] + } + } + } + } } } -} \ No newline at end of file +} diff --git a/src/PSRule/Common/DictionaryExtensions.cs b/src/PSRule/Common/DictionaryExtensions.cs index 42a8276d50..fa65fdffca 100644 --- a/src/PSRule/Common/DictionaryExtensions.cs +++ b/src/PSRule/Common/DictionaryExtensions.cs @@ -1,7 +1,8 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -83,9 +84,24 @@ public static bool TryGetLong(this IDictionary dictionary, strin if (!dictionary.TryGetValue(key, out var o)) return false; - if (o is long lvalue || (o is string svalue && long.TryParse(svalue, out lvalue))) + if (ExpressionHelpers.TryLong(o, true, out var i_value)) { - value = lvalue; + value = i_value; + return true; + } + return false; + } + + [DebuggerStepThrough] + public static bool TryGetInt(this IDictionary dictionary, string key, out int? value) + { + value = null; + if (!dictionary.TryGetValue(key, out var o)) + return false; + + if (ExpressionHelpers.TryInt(o, true, out var i_value)) + { + value = i_value; return true; } return false; @@ -106,6 +122,21 @@ public static bool TryGetString(this IDictionary dictionary, str return false; } + [DebuggerStepThrough] + public static bool TryGetEnumerable(this IDictionary dictionary, string key, out IEnumerable value) + { + value = null; + if (!dictionary.TryGetValue(key, out var o)) + return false; + + if (o is IEnumerable evalue) + { + value = evalue; + return true; + } + return false; + } + [DebuggerStepThrough] public static bool TryGetStringArray(this IDictionary dictionary, string key, out string[] value) { diff --git a/src/PSRule/Common/ExpressionHelpers.cs b/src/PSRule/Common/ExpressionHelpers.cs index 8d5e9aabfa..8249200744 100644 --- a/src/PSRule/Common/ExpressionHelpers.cs +++ b/src/PSRule/Common/ExpressionHelpers.cs @@ -65,6 +65,16 @@ internal static bool Equal(object expectedValue, object actualValue, bool caseSe return expectedBase.Equals(actualBase) || expectedValue.Equals(actualValue); } + internal static int Compare(object left, object right) + { + if (TryString(left, out var stringLeft) && TryString(right, out var stringRight)) + return StringComparer.Ordinal.Compare(stringLeft, stringRight); + else if (CompareNumeric(left, right, convert: false, out var compare, out _)) + return compare; + + return Comparer.Default.Compare(left, right); + } + internal static bool CompareNumeric(object actual, object expected, bool convert, out int compare, out object value) { if (TryInt(actual, convert, out var actualInt) && TryInt(expected, convert: true, value: out var expectedInt)) @@ -131,9 +141,19 @@ internal static bool TryString(object o, bool convert, out string value) value = evalue.ToString(); return true; } - else if (convert && TryInt(o, false, out var ivalue)) + else if (convert && TryLong(o, false, out var l_value)) + { + value = l_value.ToString(Thread.CurrentThread.CurrentCulture); + return true; + } + else if (convert && TryBool(o, false, out var b_value)) { - value = ivalue.ToString(Thread.CurrentThread.CurrentCulture); + value = b_value.ToString(Thread.CurrentThread.CurrentCulture); + return true; + } + else if (convert && TryInt(o, false, out var i_value)) + { + value = i_value.ToString(Thread.CurrentThread.CurrentCulture); return true; } return false; @@ -215,6 +235,11 @@ internal static bool TryBool(object o, bool convert, out bool value) value = bvalue; return true; } + else if (convert && TryLong(o, convert: false, out var lvalue)) + { + value = lvalue > 0; + return true; + } value = default; return false; } @@ -525,7 +550,7 @@ private static void SetPipelineCache(string prefix, string key, T value) /// internal static object GetBaseObject(object o) { - return o is PSObject pso && pso.BaseObject != null && !(pso.BaseObject is PSCustomObject) ? pso.BaseObject : o; + return o is PSObject pso && pso.BaseObject != null && pso.BaseObject is not PSCustomObject ? pso.BaseObject : o; } private static PSRuleTargetInfo GetTargetInfo(object o) diff --git a/src/PSRule/Common/JsonConverters.cs b/src/PSRule/Common/JsonConverters.cs index cc3c8b5411..e4481f8c04 100644 --- a/src/PSRule/Common/JsonConverters.cs +++ b/src/PSRule/Common/JsonConverters.cs @@ -21,7 +21,7 @@ namespace PSRule { /// - /// A base PSObject converter. + /// A base converter. /// internal abstract class PSObjectBaseConverter : JsonConverter { @@ -345,7 +345,7 @@ protected override IList CreateProperties(Type type, MemberSeriali } /// - /// A customer deserializer to convert JSON into ResourceObject + /// A custom deserializer to convert JSON into ResourceObject /// internal sealed class ResourceObjectJsonConverter : JsonConverter { @@ -385,6 +385,7 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s private IResource MapResource(JsonReader reader, JsonSerializer serializer) { reader.GetSourceExtent(RunspaceContext.CurrentThread.Source.File.Path, out var extent); + reader.SkipComments(); if (reader.TokenType != JsonToken.StartObject || !reader.Read()) throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed); @@ -619,23 +620,24 @@ private static bool SkipComments(JsonReader reader) } /// - /// A JSON converter for deserializing Language Expressions + /// A custom converter for deserializing JSON into a language expression. /// internal sealed class LanguageExpressionJsonConverter : JsonConverter { private const string OPERATOR_IF = "if"; - public override bool CanRead => true; - - public override bool CanWrite => false; - private readonly LanguageExpressionFactory _Factory; + private readonly FunctionBuilder _FunctionBuilder; public LanguageExpressionJsonConverter() { _Factory = new LanguageExpressionFactory(); + _FunctionBuilder = new FunctionBuilder(); } + public override bool CanRead => true; + public override bool CanWrite => false; + public override bool CanConvert(Type objectType) { return typeof(LanguageExpression).IsAssignableFrom(objectType); @@ -653,32 +655,24 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s } /// - /// Skip JSON comments. + /// Map an operator. /// - private static bool SkipComments(JsonReader reader) - { - var hasComments = false; - while (reader.TokenType == JsonToken.Comment && reader.Read()) - hasComments = true; - - return hasComments; - } - private LanguageExpression MapOperator(string type, JsonReader reader) { if (TryExpression(type, out LanguageOperator result)) { + // If and Not if (reader.TokenType == JsonToken.StartObject) { result.Add(MapExpression(reader)); reader.Read(); } - + // AllOf and AnyOf else if (reader.TokenType == JsonToken.StartArray && reader.Read()) { while (reader.TokenType != JsonToken.EndArray) { - if (SkipComments(reader)) + if (reader.SkipComments()) continue; result.Add(MapExpression(reader)); @@ -687,7 +681,6 @@ private LanguageExpression MapOperator(string type, JsonReader reader) reader.Read(); } } - return result; } @@ -707,47 +700,96 @@ private LanguageExpression MapCondition(string type, LanguageExpression.Property private LanguageExpression MapExpression(JsonReader reader) { LanguageExpression result = null; - var properties = new LanguageExpression.PropertyBag(); - MapProperty(properties, reader, out var key); - if (key != null && TryCondition(key)) { result = MapCondition(key, properties, reader); } - - else if (TryOperator(key) && - (reader.TokenType == JsonToken.StartObject || - reader.TokenType == JsonToken.StartArray)) + else if ((reader.TokenType == JsonToken.StartObject || reader.TokenType == JsonToken.StartArray) && + TryOperator(key)) { result = MapOperator(key, reader); } + return result; + } + private ExpressionFnOuter MapFunction(string type, JsonReader reader) + { + _FunctionBuilder.Push(); + while (reader.TokenType != JsonToken.EndObject) + { + var name = reader.Value as string; + if (name != null) + { + reader.Consume(JsonToken.PropertyName); + if (reader.TryConsume(JsonToken.StartObject)) + { + var child = MapFunction(name, reader); + _FunctionBuilder.Add(name, child); + reader.Consume(JsonToken.EndObject); + } + else if (reader.TryConsume(JsonToken.StartArray)) + { + var sequence = MapSequence(name, reader); + _FunctionBuilder.Add(name, sequence); + reader.Consume(JsonToken.EndArray); + } + else + { + _FunctionBuilder.Add(name, reader.Value); + reader.Read(); + } + } + } + var result = _FunctionBuilder.Pop(); return result; } + private object MapSequence(string name, JsonReader reader) + { + var result = new List(); + while (reader.TokenType != JsonToken.EndArray) + { + if (reader.TryConsume(JsonToken.StartObject)) + { + var child = MapFunction(name, reader); + result.Add(child); + reader.Consume(JsonToken.EndObject); + } + else + { + result.Add(reader.Value); + reader.Read(); + } + } + return result.ToArray(); + } + private void MapProperty(LanguageExpression.PropertyBag properties, JsonReader reader, out string name) { if (reader.TokenType != JsonToken.StartObject || !reader.Read()) - { throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed); - } name = null; - while (reader.TokenType != JsonToken.EndObject) { var key = reader.Value.ToString(); - if (TryCondition(key) || TryOperator(key)) - { name = key; - } if (reader.Read()) { - if (reader.TokenType == JsonToken.StartObject) + if (TryValue(key, reader, out var value)) + { + properties[key] = value; + } + else if (TryCondition(key) && reader.TryConsume(JsonToken.StartObject)) + { + if (TryFunction(reader, key, out var fn)) + properties.Add(key, fn); + } + else if (reader.TokenType == JsonToken.StartObject) break; else if (reader.TokenType == JsonToken.StartArray) @@ -758,12 +800,12 @@ private void MapProperty(LanguageExpression.PropertyBag properties, JsonReader r var objects = new List(); while (reader.TokenType != JsonToken.EndArray) { - if (SkipComments(reader)) + if (reader.SkipComments()) continue; - var value = reader.ReadAsString(); - if (!string.IsNullOrEmpty(value)) - objects.Add(value); + var item = reader.ReadAsString(); + if (!string.IsNullOrEmpty(item)) + objects.Add(item); } properties.Add(key, objects.ToArray()); } @@ -773,7 +815,6 @@ private void MapProperty(LanguageExpression.PropertyBag properties, JsonReader r properties.Add(key, reader.Value); } } - reader.Read(); } } @@ -788,6 +829,44 @@ private bool TryCondition(string key) return _Factory.IsCondition(key); } + private bool TryValue(string key, JsonReader reader, out object value) + { + value = null; + if (key != "value") + return false; + + if (reader.TryConsume(JsonToken.StartObject) && + TryFunction(reader, reader.Value as string, out var fn)) + { + value = fn; + return true; + } + return false; + } + + private bool TryFunction(JsonReader reader, string key, out ExpressionFnOuter fn) + { + fn = null; + if (!IsFunction(reader)) + return false; + + reader.Consume(JsonToken.PropertyName); + reader.Consume(JsonToken.StartObject); + fn = MapFunction("$", reader); + if (fn == null) + throw new Exception(); + + reader.Consume(JsonToken.EndObject); + return true; + } + + private static bool IsFunction(JsonReader reader) + { + return reader.TokenType == JsonToken.PropertyName && + reader.Value is string s && + s == "$"; + } + private bool TryExpression(string type, out T expression) where T : LanguageExpression { expression = null; diff --git a/src/PSRule/Common/JsonReaderExtensions.cs b/src/PSRule/Common/JsonReaderExtensions.cs index 9bb5c0c123..6285368c9d 100644 --- a/src/PSRule/Common/JsonReaderExtensions.cs +++ b/src/PSRule/Common/JsonReaderExtensions.cs @@ -1,8 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; +using System.Diagnostics; using Newtonsoft.Json; using PSRule.Definitions; +using PSRule.Pipeline; +using PSRule.Resources; namespace PSRule { @@ -29,5 +33,37 @@ public static bool GetSourceExtent(this JsonReader reader, string file, out ISou extent = new SourceExtent(file, lineNumber, linePosition); return true; } + + [DebuggerStepThrough] + public static bool TryConsume(this JsonReader reader, JsonToken token) + { + if (reader.TokenType != token) + return false; + + reader.Read(); + return true; + } + + [DebuggerStepThrough] + public static void Consume(this JsonReader reader, JsonToken token) + { + if (reader.TokenType != token) + throw new PipelineSerializationException(PSRuleResources.ReadJsonFailedExpectedToken, Enum.GetName(typeof(JsonToken), reader.TokenType)); + + reader.Read(); + } + + /// + /// Skip JSON comments. + /// + [DebuggerStepThrough] + public static bool SkipComments(this JsonReader reader) + { + var hasComments = false; + while (reader.TokenType == JsonToken.Comment && reader.Read()) + hasComments = true; + + return hasComments; + } } } diff --git a/src/PSRule/Common/YamlConverters.cs b/src/PSRule/Common/YamlConverters.cs index 6b5344fa2b..1027fc7d9b 100644 --- a/src/PSRule/Common/YamlConverters.cs +++ b/src/PSRule/Common/YamlConverters.cs @@ -115,7 +115,7 @@ public void WriteYaml(IEmitter emitter, object value, Type type) emitter.Emit(new MappingStart()); emitter.Emit(new MappingEnd()); } - if (!(value is FieldMap map)) + if (value is not FieldMap map) return; emitter.Emit(new MappingStart()); @@ -179,7 +179,7 @@ private PSNoteProperty ReadNoteProperty(IParser parser, string name) if (parser.TryConsume(out _)) { var values = new List(); - while (!(parser.Current is SequenceEnd)) + while (parser.Current is not SequenceEnd) { if (parser.Current is MappingStart) { @@ -308,16 +308,16 @@ public bool Resolve(NodeEvent nodeEvent, ref Type currentType) /// internal sealed class OrderedPropertiesTypeInspector : TypeInspectorSkeleton { - private readonly ITypeInspector _innerTypeDescriptor; + private readonly ITypeInspector _InnerTypeDescriptor; public OrderedPropertiesTypeInspector(ITypeInspector innerTypeDescriptor) { - _innerTypeDescriptor = innerTypeDescriptor; + _InnerTypeDescriptor = innerTypeDescriptor; } public override IEnumerable GetProperties(Type type, object container) { - return _innerTypeDescriptor + return _InnerTypeDescriptor .GetProperties(type, container) .OrderBy(prop => prop.Name); } @@ -598,17 +598,22 @@ private bool TryResource(IParser reader, string apiVersion, string kind, Func + /// A custom deserializer to convert YAML into a language expression. + /// internal sealed class LanguageExpressionDeserializer : INodeDeserializer { private const string OPERATOR_IF = "if"; private readonly INodeDeserializer _Next; private readonly LanguageExpressionFactory _Factory; + private readonly FunctionBuilder _FunctionBuilder; public LanguageExpressionDeserializer(INodeDeserializer next) { _Next = next; _Factory = new LanguageExpressionFactory(); + _FunctionBuilder = new FunctionBuilder(); } bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func nestedObjectDeserializer, out object value) @@ -625,6 +630,9 @@ bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func + /// Map an operator. + /// private LanguageExpression MapOperator(string type, IParser reader, Func nestedObjectDeserializer) { if (TryExpression(reader, type, nestedObjectDeserializer, out LanguageOperator result)) @@ -674,17 +682,78 @@ private LanguageExpression MapExpression(IParser reader, Func(out _)) { result = MapOperator(key, reader, nestedObjectDeserializer); } - else if (TryOperator(key) && reader.Accept(out SequenceStart sequence)) + else if (TryOperator(key) && reader.Accept(out _)) { result = MapOperator(key, reader, nestedObjectDeserializer); } return result; } + private ExpressionFnOuter MapFunction(string type, IParser reader, Func nestedObjectDeserializer) + { + _FunctionBuilder.Push(); + string name = null; + while (!(reader.Accept(out _) || reader.Accept(out _))) + { + if (reader.TryConsume(out var s)) + { + if (name != null) + { + _FunctionBuilder.Add(name, s.Value); + name = null; + } + else + { + name = s.Value; + } + } + else if (reader.TryConsume(out _)) + { + var child = MapFunction(name, reader, nestedObjectDeserializer); + if (name != null) + { + _FunctionBuilder.Add(name, child); + name = null; + } + reader.Consume(); + } + else if (reader.TryConsume(out _)) + { + var sequence = MapSequence(name, reader, nestedObjectDeserializer); + if (name != null) + { + _FunctionBuilder.Add(name, sequence); + name = null; + } + reader.Consume(); + } + } + var result = _FunctionBuilder.Pop(); + return result; + } + + private object MapSequence(string name, IParser reader, Func nestedObjectDeserializer) + { + var result = new List(); + while (!reader.Accept(out _)) + { + if (reader.TryConsume(out var s)) + result.Add(s.Value); + + else if (reader.TryConsume(out _)) + { + var child = MapFunction(name, reader, nestedObjectDeserializer); + result.Add(child); + reader.Consume(); + } + } + return result.ToArray(); + } + private void MapProperty(LanguageExpression.PropertyBag properties, IParser reader, Func nestedObjectDeserializer, out string name) { name = null; @@ -698,6 +767,15 @@ private void MapProperty(LanguageExpression.PropertyBag properties, IParser read { properties[key] = scalar.Value; } + else if (TryValue(key, reader, nestedObjectDeserializer, out var value)) + { + properties[key] = value; + } + else if (TryCondition(key) && reader.TryConsume(out _)) + { + if (TryFunction(reader, nestedObjectDeserializer, out var fn)) + properties[key] = fn; + } else if (TryCondition(key) && reader.TryConsume(out _)) { var objects = new List(); @@ -723,6 +801,40 @@ private bool TryCondition(string key) return _Factory.IsCondition(key); } + private bool TryValue(string key, IParser reader, Func nestedObjectDeserializer, out object value) + { + value = null; + if (key != "value") + return false; + + if (reader.TryConsume(out _) && TryFunction(reader, nestedObjectDeserializer, out var fn)) + { + value = fn; + return true; + } + reader.SkipThisAndNestedEvents(); + return false; + } + + private bool TryFunction(IParser reader, Func nestedObjectDeserializer, out ExpressionFnOuter fn) + { + fn = null; + if (!IsFunction(reader)) + return false; + + reader.Consume(); + reader.Consume(); + fn = MapFunction("$", reader, nestedObjectDeserializer); + reader.Consume(); + reader.Consume(); + return true; + } + + private static bool IsFunction(IParser reader) + { + return reader.Accept(out var scalar) || scalar.Value == "$"; + } + private bool TryExpression(IParser reader, string type, Func nestedObjectDeserializer, out T expression) where T : LanguageExpression { expression = null; diff --git a/src/PSRule/Definitions/Expressions/Exceptions.cs b/src/PSRule/Definitions/Expressions/Exceptions.cs new file mode 100644 index 0000000000..eda4c4e5c1 --- /dev/null +++ b/src/PSRule/Definitions/Expressions/Exceptions.cs @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Runtime.Serialization; +using System.Security.Permissions; +using PSRule.Pipeline; + +namespace PSRule.Definitions.Expressions +{ + /// + /// A base class for runtime exceptions. + /// + public abstract class SelectorException : PipelineException + { + protected SelectorException() + : base() { } + + protected SelectorException(string message) + : base(message) { } + + protected SelectorException(string message, Exception innerException) + : base(message, innerException) { } + + protected SelectorException(SerializationInfo info, StreamingContext context) + : base(info, context) { } + } + + [Serializable] + public sealed class ExpressionParseException : SelectorException + { + public ExpressionParseException() + { + } + + public ExpressionParseException(string message) + : base(message) { } + + public ExpressionParseException(string message, Exception innerException) + : base(message, innerException) { } + + internal ExpressionParseException(string expression, string message) + : base(message) + { + Expression = expression; + } + + internal ExpressionParseException(string expression, string message, Exception innerException) + : base(message, innerException) + { + Expression = expression; + } + + private ExpressionParseException(SerializationInfo info, StreamingContext context) + : base(info, context) { } + + public string Expression { get; } + + [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); + } + } + + public abstract class ExpressionException : SelectorException + { + protected ExpressionException() + { + } + + protected ExpressionException(string message) + : base(message) { } + + protected ExpressionException(string message, Exception innerException) + : base(message, innerException) { } + + protected ExpressionException(string expression, string message) + : base(message) + { + Expression = expression; + } + + protected ExpressionException(string expression, string message, Exception innerException) + : base(message, innerException) + { + Expression = expression; + } + + protected ExpressionException(SerializationInfo info, StreamingContext context) + : base(info, context) { } + + public string Expression { get; } + + [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); + } + } + + [Serializable] + public sealed class ExpressionReferenceException : SelectorException + { + public ExpressionReferenceException() + { + } + + public ExpressionReferenceException(string message) + : base(message) { } + + public ExpressionReferenceException(string message, Exception innerException) + : base(message, innerException) { } + + internal ExpressionReferenceException(string expression, string message) + : base(message) + { + Expression = expression; + } + + internal ExpressionReferenceException(string expression, string message, Exception innerException) + : base(message, innerException) + { + Expression = expression; + } + + private ExpressionReferenceException(SerializationInfo info, StreamingContext context) + : base(info, context) { } + + public string Expression { get; } + + [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); + } + } + + + [Serializable] + public sealed class ExpressionArgumentException : ExpressionException + { + public ExpressionArgumentException() + { + } + + public ExpressionArgumentException(string message) + : base(message) { } + + public ExpressionArgumentException(string message, Exception innerException) + : base(message, innerException) { } + + internal ExpressionArgumentException(string expression, string message) + : base(expression, message) { } + + private ExpressionArgumentException(SerializationInfo info, StreamingContext context) + : base(info, context) { } + + [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); + } + } +} diff --git a/src/PSRule/Definitions/Expressions/ExpressionContext.cs b/src/PSRule/Definitions/Expressions/ExpressionContext.cs index e4e24687dd..1cc576be6f 100644 --- a/src/PSRule/Definitions/Expressions/ExpressionContext.cs +++ b/src/PSRule/Definitions/Expressions/ExpressionContext.cs @@ -16,6 +16,8 @@ internal interface IExpressionContext : IBindingContext void Reason(IOperand operand, string text, params object[] args); + object Current { get; } + RunspaceContext GetContext(); } @@ -25,12 +27,13 @@ internal sealed class ExpressionContext : IExpressionContext, IBindingContext private List _Reason; - internal ExpressionContext(SourceFile source, ResourceKind kind) + internal ExpressionContext(SourceFile source, ResourceKind kind, object current) { Source = source; LanguageScope = source.Module; Kind = kind; _NameTokenCache = new Dictionary(); + Current = current; } public SourceFile Source { get; } @@ -39,6 +42,8 @@ internal ExpressionContext(SourceFile source, ResourceKind kind) public ResourceKind Kind { get; } + public object Current { get; } + [DebuggerStepThrough] void IBindingContext.CachePathExpression(string path, PathExpression expression) { diff --git a/src/PSRule/Definitions/Expressions/FunctionBuilder.cs b/src/PSRule/Definitions/Expressions/FunctionBuilder.cs new file mode 100644 index 0000000000..2f1e2aac11 --- /dev/null +++ b/src/PSRule/Definitions/Expressions/FunctionBuilder.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace PSRule.Definitions.Expressions +{ + internal delegate object ExpressionFnOuter(IExpressionContext context); + internal delegate object ExpressionFn(IExpressionContext context, object[] args); + + internal delegate ExpressionFnOuter ExpressionBuilderFn(IExpressionContext context, LanguageExpression.PropertyBag properties); + + internal abstract class FunctionReader + { + public abstract bool TryProperty(out string propertyName); + } + + internal sealed class FunctionBuilder + { + private readonly Stack _Stack; + private readonly FunctionFactory _Functions; + + private LanguageExpression.PropertyBag _Current; + + internal FunctionBuilder() : this(new FunctionFactory()) { } + + internal FunctionBuilder(FunctionFactory expressionFactory) + { + _Functions = expressionFactory; + _Stack = new Stack(); + } + + public void Push() + { + _Current = new LanguageExpression.PropertyBag(); + _Stack.Push(_Current); + } + + internal void Add(string name, object value) + { + _Current.Add(name, value); + } + + public ExpressionFnOuter Pop() + { + var properties = _Stack.Pop(); + _Current = _Stack.Count > 0 ? _Stack.Peek() : null; + return TryFunction(properties, out var descriptor) ? descriptor.Fn(null, properties) : null; + } + + private bool TryFunction(LanguageExpression.PropertyBag properties, out IFunctionDescriptor descriptor) + { + descriptor = null; + foreach (var property in properties) + { + if (_Functions.TryDescriptor(property.Key, out descriptor)) + return true; + } + return false; + } + } + + internal sealed class FunctionFactory + { + private readonly Dictionary _Descriptors; + + public FunctionFactory() + { + _Descriptors = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var d in Functions.Builtin) + With(d); + } + + public bool TryDescriptor(string name, out IFunctionDescriptor descriptor) + { + return _Descriptors.TryGetValue(name, out descriptor); + } + + public void With(IFunctionDescriptor descriptor) + { + _Descriptors.Add(descriptor.Name, descriptor); + } + } + + /// + /// A structure describing a specific function. + /// + [DebuggerDisplay("Function: {Name}")] + internal sealed class FunctionDescriptor : IFunctionDescriptor + { + public FunctionDescriptor(string name, ExpressionBuilderFn fn) + { + Name = name; + Fn = fn; + } + + /// + public string Name { get; } + + /// + public ExpressionBuilderFn Fn { get; } + } + + /// + /// A structure describing a specific function. + /// + internal interface IFunctionDescriptor + { + /// + /// The name of the function. + /// + string Name { get; } + + /// + /// The function delegate. + /// + ExpressionBuilderFn Fn { get; } + } +} diff --git a/src/PSRule/Definitions/Expressions/Functions.cs b/src/PSRule/Definitions/Expressions/Functions.cs new file mode 100644 index 0000000000..b369310a77 --- /dev/null +++ b/src/PSRule/Definitions/Expressions/Functions.cs @@ -0,0 +1,227 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text; +using System.Threading; +using PSRule.Resources; +using PSRule.Runtime; +using static PSRule.Definitions.Expressions.LanguageExpression; + +namespace PSRule.Definitions.Expressions +{ + /// + /// Implementation of Azure Resource Manager template functions as ExpressionFn. + /// + internal static class Functions + { + private const string BOOLEAN = "boolean"; + private const string STRING = "string"; + private const string INTEGER = "integer"; + private const string CONCAT = "concat"; + private const string SUBSTRING = "substring"; + private const string CONFIGURATION = "configuration"; + private const string PATH = "path"; + private const string LENGTH = "length"; + + /// + /// The available built-in functions. + /// + internal readonly static IFunctionDescriptor[] Builtin = new IFunctionDescriptor[] + { + new FunctionDescriptor(CONFIGURATION, Configuration), + new FunctionDescriptor(PATH, Path), + new FunctionDescriptor(BOOLEAN, Boolean), + new FunctionDescriptor(STRING, String), + new FunctionDescriptor(INTEGER, Integer), + new FunctionDescriptor(CONCAT, Concat), + new FunctionDescriptor(SUBSTRING, Substring), + }; + + private static ExpressionFnOuter Boolean(IExpressionContext context, PropertyBag properties) + { + if (properties == null || + properties.Count == 0 || + !TryProperty(properties, BOOLEAN, out ExpressionFnOuter next)) + return null; + + return (context) => + { + var value = next(context); + ExpressionHelpers.TryBool(value, true, out var b); + return b; + }; + } + + private static ExpressionFnOuter String(IExpressionContext context, PropertyBag properties) + { + if (properties == null || + properties.Count == 0 || + !TryProperty(properties, STRING, out ExpressionFnOuter next)) + return null; + + return (context) => + { + var value = next(context); + ExpressionHelpers.TryString(value, true, out var s); + return s; + }; + } + + private static ExpressionFnOuter Integer(IExpressionContext context, PropertyBag properties) + { + if (properties == null || + properties.Count == 0 || + !TryProperty(properties, INTEGER, out ExpressionFnOuter next)) + return null; + + return (context) => + { + var value = next(context); + ExpressionHelpers.TryInt(value, true, out var i); + return i; + }; + } + + private static ExpressionFnOuter Configuration(IExpressionContext context, PropertyBag properties) + { + if (properties == null || properties.Count == 0 || + !properties.TryGetString(CONFIGURATION, out var name)) + return null; + + // Lookup a configuration value. + return (context) => + { + return context.GetContext().TryGetConfigurationValue(name, out var value) ? value : null; + }; + } + + private static ExpressionFnOuter Path(IExpressionContext context, PropertyBag properties) + { + if (properties == null || properties.Count == 0 || + !properties.TryGetString(PATH, out var path)) + return null; + + return (context) => + { + return ObjectHelper.GetPath(context, context.Current, path, false, out object value) ? value : null; + }; + } + + private static ExpressionFnOuter Concat(IExpressionContext context, PropertyBag properties) + { + if (properties == null || + properties.Count == 0 || + !properties.TryGetEnumerable(CONCAT, out var values)) + return null; + + return (context) => + { + var sb = new StringBuilder(); + foreach (var value in values) + sb.Append(Value(context, value)); + + return sb.ToString(); + }; + } + + private static ExpressionFnOuter Substring(IExpressionContext context, PropertyBag properties) + { + if (properties == null || + properties.Count == 0 || + !TryProperty(properties, LENGTH, out int? length) || + !TryProperty(properties, SUBSTRING, out ExpressionFnOuter next)) + return null; + + return (context) => + { + var value = next(context); + if (value is string s) + { + length = s.Length < length ? s.Length : length; + return s.Substring(0, length.Value); + } + return null; + }; + } + + #region Helper functions + + private static bool TryProperty(PropertyBag properties, string name, out int? value) + { + return properties.TryGetInt(name, out value); + } + + private static bool TryProperty(PropertyBag properties, string name, out ExpressionFnOuter value) + { + value = null; + if (properties.TryGetValue(name, out var v) && v is ExpressionFnOuter fn) + value = fn; + + else if (properties.TryGetBool(name, out var b_value)) + value = (context) => b_value; + + else if (properties.TryGetLong(name, out var l_value)) + value = (context) => l_value; + + else if (properties.TryGetInt(name, out var i_value)) + value = (context) => i_value; + + else if (properties.TryGetValue(name, out var o_value)) + value = (context) => o_value; + + return value != null; + } + + private static object Value(IExpressionContext context, object value) + { + return value is ExpressionFnOuter fn ? fn(context) : value; + } + + #endregion Helper functions + + #region Exceptions + + private static ExpressionArgumentException ArgumentsOutOfRange(string expression, object[] args) + { + var length = args == null ? 0 : args.Length; + return new ExpressionArgumentException( + expression, + string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.ArgumentsOutOfRange, expression, length) + ); + } + + private static ExpressionArgumentException ArgumentFormatInvalid(string expression) + { + return new ExpressionArgumentException( + expression, + string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.ArgumentFormatInvalid, expression) + ); + } + + private static ExpressionArgumentException ArgumentInvalidInteger(string expression, string operand) + { + return new ExpressionArgumentException( + expression, + string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.ArgumentInvalidInteger, operand, expression) + ); + } + + private static ExpressionArgumentException ArgumentInvalidBoolean(string expression, string operand) + { + return new ExpressionArgumentException( + expression, + string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.ArgumentInvalidBoolean, operand, expression) + ); + } + + private static ExpressionArgumentException ArgumentInvalidString(string expression, string operand) + { + return new ExpressionArgumentException( + expression, + string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.ArgumentInvalidString, operand, expression) + ); + } + + #endregion Exceptions + } +} diff --git a/src/PSRule/Definitions/Expressions/LanguageExpressions.cs b/src/PSRule/Definitions/Expressions/LanguageExpressions.cs index 72f1c78667..9a7dd18e13 100644 --- a/src/PSRule/Definitions/Expressions/LanguageExpressions.cs +++ b/src/PSRule/Definitions/Expressions/LanguageExpressions.cs @@ -21,7 +21,9 @@ internal enum LanguageExpressionType { Operator = 1, - Condition = 2 + Condition = 2, + + Function = 3 } internal sealed class ExpressionInfo @@ -47,7 +49,9 @@ public LanguageExpressionFactory() public bool TryDescriptor(string name, out ILanguageExpresssionDescriptor descriptor) { - return _Descriptors.TryGetValue(name, out descriptor); + descriptor = null; + return !string.IsNullOrEmpty(name) && + _Descriptors.TryGetValue(name, out descriptor); } public bool IsOperator(string name) @@ -60,6 +64,12 @@ public bool IsCondition(string name) return TryDescriptor(name, out var d) && d != null && d.Type == LanguageExpressionType.Condition; } + public bool IsFunction(string name) + { + return TryDescriptor(name, out var d) && + d != null && d.Type == LanguageExpressionType.Function; + } + private void With(ILanguageExpresssionDescriptor descriptor) { _Descriptors.Add(descriptor.Name, descriptor); @@ -350,6 +360,7 @@ internal sealed class LanguageExpressions private const string INCLUDEPRERELEASE = "includePrerelease"; private const string PROPERTY_SCHEMA = "$schema"; private const string SOURCE = "source"; + private const string VALUE = "value"; // Comparisons private const string LESS_THAN = "<"; @@ -476,7 +487,7 @@ internal static bool Equals(ExpressionContext context, ExpressionInfo info, obje return Condition( context, operand, - ExpressionHelpers.Equal(propertyValue, operand.Value, caseSensitive, convertExpected: true, convertActual: convert), + ExpressionHelpers.Equal(Value(context, propertyValue), Value(context, operand), caseSensitive, convertExpected: true, convertActual: convert), ReasonStrings.Assert_IsSetTo, operand.Value ); @@ -500,7 +511,7 @@ internal static bool NotEquals(ExpressionContext context, ExpressionInfo info, o return Condition( context, operand, - !ExpressionHelpers.Equal(propertyValue, operand.Value, caseSensitive, convertExpected: true, convertActual: convert), + !ExpressionHelpers.Equal(Value(context, propertyValue), Value(context, operand), caseSensitive, convertExpected: true, convertActual: convert), ReasonStrings.Assert_IsSetTo, operand.Value ); @@ -684,59 +695,50 @@ internal static bool Subset(ExpressionContext context, ExpressionInfo info, obje internal static bool Count(ExpressionContext context, ExpressionInfo info, object[] args, object o) { var properties = GetProperties(args); - if (TryPropertyLong(properties, COUNT, out var expectedValue) && TryField(properties, out var field)) - { - context.ExpressionTrace(COUNT, field, expectedValue); - if (!ObjectHelper.GetPath(context, o, field, caseSensitive: false, out object value)) - return NotHasField(context, field); + if (!TryPropertyLong(context, properties, COUNT, out var expectedValue) || + !TryOperand(context, COUNT, o, properties, out var operand)) + return Invalid(context, COUNT); - if (value == null) - return Fail(context, field, ReasonStrings.Null, field); + var operandValue = Value(context, operand); + if (operandValue == null) + return Fail(context, operand, ReasonStrings.Assert_IsNull); - if (ExpressionHelpers.TryEnumerableLength(value, value: out var actualValue)) - return Condition( - context, - field, - actualValue == expectedValue, - ReasonStrings.Count, - field, - actualValue, - expectedValue - ); - } - return Invalid(context, COUNT); + // int, string, bool + return Condition( + context, + operand, + ExpressionHelpers.TryEnumerableLength(operandValue, value: out var actualValue) && actualValue == expectedValue, + ReasonStrings.Assert_Count, + actualValue, + expectedValue + ); } internal static bool NotCount(ExpressionContext context, ExpressionInfo info, object[] args, object o) { var properties = GetProperties(args); - if (TryPropertyLong(properties, NOTCOUNT, out var expectedValue) && TryField(properties, out var field)) - { - context.ExpressionTrace(NOTCOUNT, field, expectedValue); - if (!ObjectHelper.GetPath(context, o, field, caseSensitive: false, out object value)) - return NotHasField(context, field); + if (!TryPropertyLong(context, properties, NOTCOUNT, out var expectedValue) || + !TryOperand(context, NOTCOUNT, o, properties, out var operand)) + return Invalid(context, NOTCOUNT); - if (value == null) - return Fail(context, field, ReasonStrings.Null, field); + var operandValue = Value(context, operand); + if (operandValue == null) + return Fail(context, operand, ReasonStrings.Assert_IsNull); - if (ExpressionHelpers.TryEnumerableLength(value, value: out var actualValue)) - return Condition( - context, - field, - actualValue != expectedValue, - ReasonStrings.NotCount, - field, - actualValue, - expectedValue - ); - } - return Invalid(context, NOTCOUNT); + // int, string, bool + return Condition( + context, + operand, + ExpressionHelpers.TryEnumerableLength(operandValue, value: out var actualValue) && actualValue != expectedValue, + ReasonStrings.Assert_NotCount, + actualValue + ); } internal static bool Less(ExpressionContext context, ExpressionInfo info, object[] args, object o) { var properties = GetProperties(args); - if (!TryPropertyLong(properties, LESS, out var propertyValue) || + if (!TryPropertyLong(context, properties, LESS, out var propertyValue) || !TryOperand(context, LESS, o, properties, out var operand) || !GetConvert(properties, out var convert)) return Invalid(context, LESS); @@ -749,8 +751,9 @@ internal static bool Less(ExpressionContext context, ExpressionInfo info, object ReasonStrings.Assert_IsNullOrEmpty ); + var operandValue = Value(context, operand); if (!ExpressionHelpers.CompareNumeric( - operand.Value, + operandValue, propertyValue, convert, compare: out var compare, @@ -763,7 +766,7 @@ internal static bool Less(ExpressionContext context, ExpressionInfo info, object operand, compare < 0, ReasonStrings.Assert_NotComparedTo, - operand.Value, + operandValue, LESS_THAN, propertyValue ); @@ -772,7 +775,7 @@ internal static bool Less(ExpressionContext context, ExpressionInfo info, object internal static bool LessOrEquals(ExpressionContext context, ExpressionInfo info, object[] args, object o) { var properties = GetProperties(args); - if (!TryPropertyLong(properties, LESSOREQUALS, out var propertyValue) || + if (!TryPropertyLong(context, properties, LESSOREQUALS, out var propertyValue) || !TryOperand(context, LESSOREQUALS, o, properties, out var operand) || !GetConvert(properties, out var convert)) return Invalid(context, LESSOREQUALS); @@ -785,8 +788,9 @@ internal static bool LessOrEquals(ExpressionContext context, ExpressionInfo info ReasonStrings.Assert_IsNullOrEmpty ); + var operandValue = Value(context, operand); if (!ExpressionHelpers.CompareNumeric( - operand.Value, + operandValue, propertyValue, convert, compare: out var compare, @@ -799,7 +803,7 @@ internal static bool LessOrEquals(ExpressionContext context, ExpressionInfo info operand, compare <= 0, ReasonStrings.Assert_NotComparedTo, - operand.Value, + operandValue, LESS_THAN_EQUALS, propertyValue ); @@ -808,7 +812,7 @@ internal static bool LessOrEquals(ExpressionContext context, ExpressionInfo info internal static bool Greater(ExpressionContext context, ExpressionInfo info, object[] args, object o) { var properties = GetProperties(args); - if (!TryPropertyLong(properties, GREATER, out var propertyValue) || + if (!TryPropertyLong(context, properties, GREATER, out var propertyValue) || !TryOperand(context, GREATER, o, properties, out var operand) || !GetConvert(properties, out var convert)) return Invalid(context, GREATER); @@ -821,8 +825,9 @@ internal static bool Greater(ExpressionContext context, ExpressionInfo info, obj ReasonStrings.Assert_IsNullOrEmpty ); + var operandValue = Value(context, operand); if (!ExpressionHelpers.CompareNumeric( - operand.Value, + operandValue, propertyValue, convert, compare: out var compare, @@ -835,7 +840,7 @@ internal static bool Greater(ExpressionContext context, ExpressionInfo info, obj operand, compare > 0, ReasonStrings.Assert_NotComparedTo, - operand.Value, + operandValue, GREATER_THAN, propertyValue ); @@ -844,7 +849,7 @@ internal static bool Greater(ExpressionContext context, ExpressionInfo info, obj internal static bool GreaterOrEquals(ExpressionContext context, ExpressionInfo info, object[] args, object o) { var properties = GetProperties(args); - if (!TryPropertyLong(properties, GREATEROREQUALS, out var propertyValue) || + if (!TryPropertyLong(context, properties, GREATEROREQUALS, out var propertyValue) || !TryOperand(context, GREATEROREQUALS, o, properties, out var operand) || !GetConvert(properties, out var convert)) return Invalid(context, GREATEROREQUALS); @@ -857,8 +862,9 @@ internal static bool GreaterOrEquals(ExpressionContext context, ExpressionInfo i ReasonStrings.Assert_IsNullOrEmpty ); + var operandValue = Value(context, operand); if (!ExpressionHelpers.CompareNumeric( - operand.Value, + operandValue, propertyValue, convert, compare: out var compare, @@ -871,7 +877,7 @@ internal static bool GreaterOrEquals(ExpressionContext context, ExpressionInfo i operand, compare >= 0, ReasonStrings.Assert_NotComparedTo, - operand.Value, + operandValue, GREATER_THAN_EQUALS, propertyValue ); @@ -1484,9 +1490,17 @@ private static bool TryPropertyBoolOrDefault(LanguageExpression.PropertyBag prop return true; } - private static bool TryPropertyLong(LanguageExpression.PropertyBag properties, string propertyName, out long? propertyValue) + private static bool TryPropertyLong(ExpressionContext context, LanguageExpression.PropertyBag properties, string propertyName, out long? propertyValue) { - return properties.TryGetLong(propertyName, out propertyValue); + propertyValue = null; + if (!properties.TryGetValue(propertyName, out var value)) + return false; + + if (!ExpressionHelpers.TryLong(Value(context, value), true, out var l_value)) + return false; + + propertyValue = l_value; + return true; } private static bool TryField(LanguageExpression.PropertyBag properties, out string field) @@ -1537,7 +1551,6 @@ private static bool TryType(IExpressionContext context, LanguageExpression.Prope if (string.IsNullOrEmpty(type)) return Invalid(context, svalue); - operand = Operand.FromType(type, binding.TargetTypePath); } return operand != null; @@ -1557,6 +1570,36 @@ private static bool TrySource(IExpressionContext context, LanguageExpression.Pro return operand != null; } + private static bool TryValue(IExpressionContext context, LanguageExpression.PropertyBag properties, out IOperand operand) + { + operand = null; + if (properties.TryGetValue(VALUE, out var value)) + { + // TODO: Propogate path + operand = Operand.FromValue(value); + } + return operand != null; + } + + /// + /// Unwrap a function delegate or a literal value. + /// + private static object Value(IExpressionContext context, IOperand operand) + { + if (operand == null) + return null; + + return operand.Value is ExpressionFnOuter fn ? fn(context) : operand.Value; + } + + /// + /// Unwrap a function delegate or a literal value. + /// + private static object Value(IExpressionContext context, object value) + { + return value is ExpressionFnOuter fn ? fn(context) : value; + } + private static bool GetCaseSensitive(LanguageExpression.PropertyBag properties, out bool caseSensitive, bool defaultValue = false) { return TryPropertyBoolOrDefault(properties, CASESENSITIVE, out caseSensitive, defaultValue); @@ -1587,6 +1630,7 @@ private static bool TryOperand(ExpressionContext context, string name, object o, TryType(context, properties, out operand) || TryName(context, properties, out operand) || TrySource(context, properties, out operand) || + TryValue(context, properties, out operand) || Invalid(context, name); } diff --git a/src/PSRule/Definitions/Expressions/Primitives.cs b/src/PSRule/Definitions/Expressions/Primitives.cs index dccd59300a..0f71649944 100644 --- a/src/PSRule/Definitions/Expressions/Primitives.cs +++ b/src/PSRule/Definitions/Expressions/Primitives.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Collections.Generic; @@ -39,7 +39,7 @@ public LanguageExpression CreateInstance(SourceFile source, LanguageExpression.P if (Type == LanguageExpressionType.Condition) return new LanguageCondition(this, properties); - return null; + return Type == LanguageExpressionType.Function ? new LanguageFunction(this) : null; } } @@ -104,4 +104,14 @@ internal void Add(PropertyBag properties) Property.AddUnique(properties); } } + + [DebuggerDisplay("Selector {Descriptor.Name}")] + internal sealed class LanguageFunction : LanguageExpression + { + internal LanguageFunction(LanguageExpresssionDescriptor descriptor) + : base(descriptor) + { + + } + } } diff --git a/src/PSRule/Definitions/Rules/RuleVisitor.cs b/src/PSRule/Definitions/Rules/RuleVisitor.cs index 0a886245f5..608d5ae9f9 100644 --- a/src/PSRule/Definitions/Rules/RuleVisitor.cs +++ b/src/PSRule/Definitions/Rules/RuleVisitor.cs @@ -50,7 +50,7 @@ public void Dispose() public IConditionResult If() { - var context = new ExpressionContext(Source, ResourceKind.Rule); + var context = new ExpressionContext(Source, ResourceKind.Rule, RunspaceContext.CurrentThread.TargetObject.Value); context.Debug(PSRuleResources.RuleMatchTrace, Id); context.PushScope(RunspaceScope.Rule); try diff --git a/src/PSRule/Definitions/Selectors/SelectorVisitor.cs b/src/PSRule/Definitions/Selectors/SelectorVisitor.cs index 81bfcd7f9a..d7ab303dbf 100644 --- a/src/PSRule/Definitions/Selectors/SelectorVisitor.cs +++ b/src/PSRule/Definitions/Selectors/SelectorVisitor.cs @@ -42,7 +42,7 @@ public SelectorVisitor(ResourceId id, SourceFile source, LanguageIf expression) public bool Match(object o) { - var context = new ExpressionContext(Source, ResourceKind.Selector); + var context = new ExpressionContext(Source, ResourceKind.Selector, o); context.Debug(PSRuleResources.SelectorMatchTrace, Id); return _Fn(context, o).GetValueOrDefault(false); } diff --git a/src/PSRule/Definitions/SuppressionGroups/SuppressionGroupVisitor.cs b/src/PSRule/Definitions/SuppressionGroups/SuppressionGroupVisitor.cs index f8b1d2a914..9d8ace4efb 100644 --- a/src/PSRule/Definitions/SuppressionGroups/SuppressionGroupVisitor.cs +++ b/src/PSRule/Definitions/SuppressionGroups/SuppressionGroupVisitor.cs @@ -77,7 +77,7 @@ internal void Hit() public bool TryMatch(object o, out ISuppressionInfo suppression) { suppression = null; - var context = new ExpressionContext(Source, ResourceKind.SuppressionGroup); + var context = new ExpressionContext(Source, ResourceKind.SuppressionGroup, o); context.Debug(PSRuleResources.SelectorMatchTrace, Id); if (_Fn(context, o).GetValueOrDefault(false)) { diff --git a/src/PSRule/Host/HostHelper.cs b/src/PSRule/Host/HostHelper.cs index c163edfa07..0b6fb4c4fc 100644 --- a/src/PSRule/Host/HostHelper.cs +++ b/src/PSRule/Host/HostHelper.cs @@ -260,7 +260,7 @@ private static ILanguageBlock[] GetYamlLanguageBlocks(Source[] sources, Runspace context.EnterSourceScope(source: file); using var reader = new StreamReader(file.Path); - var parser = new YamlDotNet.Core.Parser(reader); + var parser = new Parser(reader); parser.TryConsume(out _); while (parser.Current is DocumentStart) { diff --git a/src/PSRule/Resources/PSRuleResources.Designer.cs b/src/PSRule/Resources/PSRuleResources.Designer.cs index 12e9d59e5f..881e558509 100644 --- a/src/PSRule/Resources/PSRuleResources.Designer.cs +++ b/src/PSRule/Resources/PSRuleResources.Designer.cs @@ -8,10 +8,11 @@ // //------------------------------------------------------------------------------ -namespace PSRule.Resources { +namespace PSRule.Resources +{ using System; - - + + /// /// A strongly-typed resource class, for looking up localized strings, etc. /// @@ -22,706 +23,980 @@ namespace PSRule.Resources { [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class PSRuleResources { - + internal class PSRuleResources + { + private static global::System.Resources.ResourceManager resourceMan; - + private static global::System.Globalization.CultureInfo resourceCulture; - + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal PSRuleResources() { + internal PSRuleResources() + { } - + /// /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if (object.ReferenceEquals(resourceMan, null)) + { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PSRule.Resources.PSRuleResources", typeof(PSRuleResources).Assembly); resourceMan = temp; } return resourceMan; } } - + /// /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { + internal static global::System.Globalization.CultureInfo Culture + { + get + { return resourceCulture; } - set { + set + { resourceCulture = value; } } - + + /// + /// Looks up a localized string similar to The arguments for '{0}' are not in the expected format or type.. + /// + internal static string ArgumentFormatInvalid + { + get + { + return ResourceManager.GetString("ArgumentFormatInvalid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The argument '{0}' for '{1}' is not a valid boolean.. + /// + internal static string ArgumentInvalidBoolean + { + get + { + return ResourceManager.GetString("ArgumentInvalidBoolean", resourceCulture); + } + } + /// - /// Looks up a localized string similar to The {0} resource '{1}' is currently referencing '{2}' using the alias '{3}'. Consider updating the reference to use name or id directly.. + /// Looks up a localized string similar to The argument '{0}' for '{1}' is not a valid integer.. /// - internal static string AliasReference { - get { + internal static string ArgumentInvalidInteger + { + get + { + return ResourceManager.GetString("ArgumentInvalidInteger", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The argument '{0}' for '{1}' is not a valid string.. + /// + internal static string ArgumentInvalidString + { + get + { + return ResourceManager.GetString("ArgumentInvalidString", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The number of arguments '{1}' is not within the allowed range for '{0}'.. + /// + internal static string ArgumentsOutOfRange + { + get + { + return ResourceManager.GetString("ArgumentsOutOfRange", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The baseline '{0}' is obsolete. Consider switching to an alternative baseline.. + /// + internal static string AliasReference + { + get + { return ResourceManager.GetString("AliasReference", resourceCulture); } } - + /// /// Looks up a localized string similar to Suppression for the rule '{0}' was configured using the alias '{1}'. Consider updating the suppression to use the name or id directly.. /// - internal static string AliasSuppression { - get { + internal static string AliasSuppression + { + get + { return ResourceManager.GetString("AliasSuppression", resourceCulture); } } - + /// /// Looks up a localized string similar to Binding functions are not supported in this language mode.. /// - internal static string ConstrainedTargetBinding { - get { + internal static string ConstrainedTargetBinding + { + get + { return ResourceManager.GetString("ConstrainedTargetBinding", resourceCulture); } } - + /// /// Looks up a localized string similar to {0}: The property '${1}.{2}' is obsolete and will be removed in the next major version.. /// - internal static string DebugPropertyObsolete { - get { + internal static string DebugPropertyObsolete + { + get + { return ResourceManager.GetString("DebugPropertyObsolete", resourceCulture); } } - + /// /// Looks up a localized string similar to Target failed If precondition. /// - internal static string DebugTargetIfMismatch { - get { + internal static string DebugTargetIfMismatch + { + get + { return ResourceManager.GetString("DebugTargetIfMismatch", resourceCulture); } } - + /// /// Looks up a localized string similar to Target failed Rule precondition. /// - internal static string DebugTargetRuleMismatch { - get { + internal static string DebugTargetRuleMismatch + { + get + { return ResourceManager.GetString("DebugTargetRuleMismatch", resourceCulture); } } - + /// /// Looks up a localized string similar to Target failed Type precondition. /// - internal static string DebugTargetTypeMismatch { - get { + internal static string DebugTargetTypeMismatch + { + get + { return ResourceManager.GetString("DebugTargetTypeMismatch", resourceCulture); } } - + /// /// Looks up a localized string similar to A circular rule dependency was detected. The rule '{0}' depends on '{1}' which also depend on '{0}'.. /// - internal static string DependencyCircularReference { - get { + internal static string DependencyCircularReference + { + get + { return ResourceManager.GetString("DependencyCircularReference", resourceCulture); } } - + /// /// Looks up a localized string similar to The dependency '{0}' for '{1}' could not be found. Check that the rule is defined in a .Rule.ps1 file within the search path.. /// - internal static string DependencyNotFound { - get { + internal static string DependencyNotFound + { + get + { return ResourceManager.GetString("DependencyNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to A rule with the same id '{0}' already exists.. /// - internal static string DuplicateRuleId { - get { + internal static string DuplicateRuleId + { + get + { return ResourceManager.GetString("DuplicateRuleId", resourceCulture); } } - + /// /// Looks up a localized string similar to A rule with the same name '{0}' already exists.. /// - internal static string DuplicateRuleName { - get { + internal static string DuplicateRuleName + { + get + { return ResourceManager.GetString("DuplicateRuleName", resourceCulture); } } - + /// /// Looks up a localized string similar to {0} : Reported '{1}'. At {2}:{3} char:{4}. /// - internal static string ErrorDetailMessage { - get { + internal static string ErrorDetailMessage + { + get + { return ResourceManager.GetString("ErrorDetailMessage", resourceCulture); } } - + /// /// Looks up a localized string similar to One or more errors occured.. /// - internal static string ErrorPipelineException { - get { + internal static string ErrorPipelineException + { + get + { return ResourceManager.GetString("ErrorPipelineException", resourceCulture); } } - + /// /// Looks up a localized string similar to Exists: {0}. /// - internal static string ExistsTrue { - get { + internal static string ExistsTrue + { + get + { return ResourceManager.GetString("ExistsTrue", resourceCulture); } } - + /// /// Looks up a localized string similar to File. /// - internal static string FileSourceType { - get { + internal static string FileSourceType + { + get + { return ResourceManager.GetString("FileSourceType", resourceCulture); } } - + + /// + /// Looks up a localized string similar to Failed to parse expression. The expression may not be valid. Expression: "{0}". + /// + internal static string ExpressionInvalid + { + get + { + return ResourceManager.GetString("ExpressionInvalid", resourceCulture); + } + } + /// /// Looks up a localized string similar to [PSRule][D] -- Found {0} PSRule module(s). /// - internal static string FoundModules { - get { + internal static string FoundModules + { + get + { return ResourceManager.GetString("FoundModules", resourceCulture); } } - + + /// + /// Looks up a localized string similar to The function "{0}" was not found.. + /// + internal static string FunctionNotFound + { + get + { + return ResourceManager.GetString("FunctionNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The language expression index '{0}' is not valid for the object.. + /// + internal static string IndexInvalid + { + get + { + return ResourceManager.GetString("IndexInvalid", resourceCulture); + } + } + /// /// Looks up a localized string similar to Output written to the following file: '{0}'. /// - internal static string InfoOutputPath { - get { + internal static string InfoOutputPath + { + get + { return ResourceManager.GetString("InfoOutputPath", resourceCulture); } } - + /// /// Looks up a localized string similar to An invalid ErrorAction ({0}) was specified for rule at {1}. Ignore | Stop are supported.. /// - internal static string InvalidErrorAction { - get { + internal static string InvalidErrorAction + { + get + { return ResourceManager.GetString("InvalidErrorAction", resourceCulture); } } - + /// /// Looks up a localized string similar to The resource name '{0}' is not valid at {1}. Each resource name must be between 3-128 characters in length, must start and end with a letter or number, and only contain letters, numbers, hyphens, dots, or underscores. See https://aka.ms/ps-rule/naming for more information.. /// - internal static string InvalidResourceName { - get { + internal static string InvalidResourceName + { + get + { return ResourceManager.GetString("InvalidResourceName", resourceCulture); } } - + /// /// Looks up a localized string similar to Rule nesting was detected for rule at {0}. Rules must not be nested.. /// - internal static string InvalidRuleNesting { - get { + internal static string InvalidRuleNesting + { + get + { return ResourceManager.GetString("InvalidRuleNesting", resourceCulture); } } - + /// /// Looks up a localized string similar to An invalid rule result was returned for {0}. Conditions must return boolean $True or $False.. /// - internal static string InvalidRuleResult { - get { + internal static string InvalidRuleResult + { + get + { return ResourceManager.GetString("InvalidRuleResult", resourceCulture); } } - + /// /// Looks up a localized string similar to The keyword '{0}' can only be used within a Rule block.. /// - internal static string KeywordConditionScope { - get { + internal static string KeywordConditionScope + { + get + { return ResourceManager.GetString("KeywordConditionScope", resourceCulture); } } - + /// /// Looks up a localized string similar to The keyword '{0}' can only be used within a Rule block or script precondition.. /// - internal static string KeywordRuleScope { - get { + internal static string KeywordRuleScope + { + get + { return ResourceManager.GetString("KeywordRuleScope", resourceCulture); } } - + /// /// Looks up a localized string similar to The keyword '{0}' can not be nested in a Rule block.. /// - internal static string KeywordSourceScope { - get { + internal static string KeywordSourceScope + { + get + { return ResourceManager.GetString("KeywordSourceScope", resourceCulture); } } - + /// /// Looks up a localized string similar to [PSRule][{0}][Trace] -- {1}: {2}. /// - internal static string LanguageExpressionTraceP2 { - get { + internal static string LanguageExpressionTraceP2 + { + get + { return ResourceManager.GetString("LanguageExpressionTraceP2", resourceCulture); } } - + /// /// Looks up a localized string similar to [PSRule][{0}][Trace] -- {1}: {2} {1} {3}. /// - internal static string LanguageExpressionTraceP3 { - get { + internal static string LanguageExpressionTraceP3 + { + get + { return ResourceManager.GetString("LanguageExpressionTraceP3", resourceCulture); } } - + + internal static string LanguageExpressionTrace + { + get + { + return ResourceManager.GetString("LanguageExpressionTrace", resourceCulture); + } + } + /// /// Looks up a localized string similar to Please open your browser to the following location: {0}. /// - internal static string LaunchBrowser { - get { + internal static string LaunchBrowser + { + get + { return ResourceManager.GetString("LaunchBrowser", resourceCulture); } } - + /// /// Looks up a localized string similar to Wildcard match requires exactly one name.. /// - internal static string MatchSingleName { - get { + internal static string MatchSingleName + { + get + { return ResourceManager.GetString("MatchSingleName", resourceCulture); } } - + /// /// Looks up a localized string similar to Matches: {0}. /// - internal static string MatchTrue { - get { + internal static string MatchTrue + { + get + { return ResourceManager.GetString("MatchTrue", resourceCulture); } } - + /// /// Looks up a localized string similar to Update module '{0}' to set the default baseline using a module configuration resource instead. Configuring the default baseline via manifest will be removed in the next major version. See https://aka.ms/ps-rule/module-config.. /// - internal static string ModuleManifestBaseline { - get { + internal static string ModuleManifestBaseline + { + get + { return ResourceManager.GetString("ModuleManifestBaseline", resourceCulture); } } - + /// /// Looks up a localized string similar to No valid module can be found with that name.. /// - internal static string ModuleNotFound { - get { + internal static string ModuleNotFound + { + get + { return ResourceManager.GetString("ModuleNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to Target object '{0}' has not been processed because no matching rules were found.. /// - internal static string ObjectNotProcessed { - get { + internal static string ObjectNotProcessed + { + get + { return ResourceManager.GetString("ObjectNotProcessed", resourceCulture); } } - + /// /// Looks up a localized string similar to Object path not found.. /// - internal static string ObjectPathNotFound { - get { + internal static string ObjectPathNotFound + { + get + { return ResourceManager.GetString("ObjectPathNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to Options file does not exist.. /// - internal static string OptionsNotFound { - get { + internal static string OptionsNotFound + { + get + { return ResourceManager.GetString("OptionsNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to # Source: {0}. /// - internal static string OptionsSourceComment { - get { + internal static string OptionsSourceComment + { + get + { return ResourceManager.GetString("OptionsSourceComment", resourceCulture); } } - + /// /// Looks up a localized string similar to Error. /// - internal static string OutcomeError { - get { + internal static string OutcomeError + { + get + { return ResourceManager.GetString("OutcomeError", resourceCulture); } } - + /// /// Looks up a localized string similar to Fail. /// - internal static string OutcomeFail { - get { + internal static string OutcomeFail + { + get + { return ResourceManager.GetString("OutcomeFail", resourceCulture); } } - + /// /// Looks up a localized string similar to Pass. /// - internal static string OutcomePass { - get { + internal static string OutcomePass + { + get + { return ResourceManager.GetString("OutcomePass", resourceCulture); } } - + /// /// Looks up a localized string similar to [FAIL] -- {0}:: Reported for '{1}'. /// - internal static string OutcomeRuleFail { - get { + internal static string OutcomeRuleFail + { + get + { return ResourceManager.GetString("OutcomeRuleFail", resourceCulture); } } - + /// /// Looks up a localized string similar to [PASS] -- {0}:: Reported for '{1}'. /// - internal static string OutcomeRulePass { - get { + internal static string OutcomeRulePass + { + get + { return ResourceManager.GetString("OutcomeRulePass", resourceCulture); } } - + /// /// Looks up a localized string similar to Unknown. /// - internal static string OutcomeUnknown { - get { + internal static string OutcomeUnknown + { + get + { return ResourceManager.GetString("OutcomeUnknown", resourceCulture); } } - + + /// + /// Looks up a localized string similar to The language expression property '{0}' doesn't exist.. + /// + internal static string PropertyNotFound + { + get + { + return ResourceManager.GetString("PropertyNotFound", resourceCulture); + } + } + /// /// Looks up a localized string similar to The property '${0}.{1}' is obsolete and will be removed in the next major version.. /// - internal static string PropertyObsolete { - get { + internal static string PropertyObsolete + { + get + { return ResourceManager.GetString("PropertyObsolete", resourceCulture); } } - + /// /// Looks up a localized string similar to Failed to deserialize the file '{0}': {1}. /// - internal static string ReadFileFailed { - get { + internal static string ReadFileFailed + { + get + { return ResourceManager.GetString("ReadFileFailed", resourceCulture); } } - + /// /// Looks up a localized string similar to Read JSON failed.. /// - internal static string ReadJsonFailed { - get { + internal static string ReadJsonFailed + { + get + { return ResourceManager.GetString("ReadJsonFailed", resourceCulture); } } - + /// /// Looks up a localized string similar to Read JSON failed because the token ({0}) was not expected.. /// - internal static string ReadJsonFailedExpectedToken { - get { + internal static string ReadJsonFailedExpectedToken + { + get + { return ResourceManager.GetString("ReadJsonFailedExpectedToken", resourceCulture); } } - + /// /// Looks up a localized string similar to The module version '{1}' for '{0}' does not match the required version '{2}'. To continue, first update the module to match the version requirement.. /// - internal static string RequiredVersionMismatch { - get { + internal static string RequiredVersionMismatch + { + get + { return ResourceManager.GetString("RequiredVersionMismatch", resourceCulture); } } - + /// /// Looks up a localized string similar to The {0} '{1}' is obsolete. Consider switching to an alternative {0}.. /// - internal static string ResourceObsolete { - get { + internal static string ResourceObsolete + { + get + { return ResourceManager.GetString("ResourceObsolete", resourceCulture); } } - + /// /// Looks up a localized string similar to {0} rule/s were suppressed for '{1}'.. /// - internal static string RuleCountSuppressed { - get { + internal static string RuleCountSuppressed + { + get + { return ResourceManager.GetString("RuleCountSuppressed", resourceCulture); } } - + /// /// Looks up a localized string similar to One or more rules reported errors.. /// - internal static string RuleErrorPipelineException { - get { + internal static string RuleErrorPipelineException + { + get + { return ResourceManager.GetString("RuleErrorPipelineException", resourceCulture); } } - + /// /// Looks up a localized string similar to One or more rules reported failure.. /// - internal static string RuleFailPipelineException { - get { + internal static string RuleFailPipelineException + { + get + { return ResourceManager.GetString("RuleFailPipelineException", resourceCulture); } } - + /// /// Looks up a localized string similar to Inconclusive result reported for '{1}' @{0}.. /// - internal static string RuleInconclusive { - get { + internal static string RuleInconclusive + { + get + { return ResourceManager.GetString("RuleInconclusive", resourceCulture); } } - + /// /// Looks up a localized string similar to [PSRule][R][Trace] -- {0}. /// - internal static string RuleMatchTrace { - get { + internal static string RuleMatchTrace + { + get + { return ResourceManager.GetString("RuleMatchTrace", resourceCulture); } } - + /// /// Looks up a localized string similar to Could not find a matching rule. Please check that Path, Name and Tag parameters are correct.. /// - internal static string RuleNotFound { - get { + internal static string RuleNotFound + { + get + { return ResourceManager.GetString("RuleNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to Could not find required rule definition parameter '{0}' on rule at {1}.. /// - internal static string RuleParameterNotFound { - get { + internal static string RuleParameterNotFound + { + get + { return ResourceManager.GetString("RuleParameterNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to No matching .Rule.ps1 files were found. Rule definitions should be saved into script files with the .Rule.ps1 extension.. /// - internal static string RulePathNotFound { - get { + internal static string RulePathNotFound + { + get + { return ResourceManager.GetString("RulePathNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to at Rule '{0}', {1}: line {2}. /// - internal static string RuleStackTrace { - get { + internal static string RuleStackTrace + { + get + { return ResourceManager.GetString("RuleStackTrace", resourceCulture); } } - + /// /// Looks up a localized string similar to Rule '{0}' was suppressed for '{1}'.. /// - internal static string RuleSuppressed { - get { + internal static string RuleSuppressed + { + get + { return ResourceManager.GetString("RuleSuppressed", resourceCulture); } } - + /// /// Looks up a localized string similar to Rule '{0}' was suppressed by suppression group '{1}' for '{2}'.. /// - internal static string RuleSuppressionGroup { - get { + internal static string RuleSuppressionGroup + { + get + { return ResourceManager.GetString("RuleSuppressionGroup", resourceCulture); } } - + /// /// Looks up a localized string similar to {0} rule/s were suppressed by suppression group '{1}' for '{2}'.. /// - internal static string RuleSuppressionGroupCount { - get { + internal static string RuleSuppressionGroupCount + { + get + { return ResourceManager.GetString("RuleSuppressionGroupCount", resourceCulture); } } - + /// /// Looks up a localized string similar to Rule '{0}' was suppressed by suppression group '{1}' for '{2}'. {3}. /// - internal static string RuleSuppressionGroupExtended { - get { + internal static string RuleSuppressionGroupExtended + { + get + { return ResourceManager.GetString("RuleSuppressionGroupExtended", resourceCulture); } } - + /// /// Looks up a localized string similar to {0} rule/s were suppressed by suppression group '{1}' for '{2}'. {3}. /// - internal static string RuleSuppressionGroupExtendedCount { - get { + internal static string RuleSuppressionGroupExtendedCount + { + get + { return ResourceManager.GetString("RuleSuppressionGroupExtendedCount", resourceCulture); } } - + /// /// Looks up a localized string similar to [PSRule][D] -- Scanning for source files in module: {0}. /// - internal static string ScanModule { - get { + internal static string ScanModule + { + get + { return ResourceManager.GetString("ScanModule", resourceCulture); } } - + /// /// Looks up a localized string similar to [PSRule][D] -- Scanning for source files: {0}. /// - internal static string ScanSource { - get { + internal static string ScanSource + { + get + { return ResourceManager.GetString("ScanSource", resourceCulture); } } - + /// /// Looks up a localized string similar to The script was not found.. /// - internal static string ScriptNotFound { - get { + internal static string ScriptNotFound + { + get + { return ResourceManager.GetString("ScriptNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to [PSRule][S][Trace] -- {0}. /// - internal static string SelectorMatchTrace { - get { + internal static string SelectorMatchTrace + { + get + { return ResourceManager.GetString("SelectorMatchTrace", resourceCulture); } } - + + /// + /// Looks up a localized string similar to [PSRule][S][Trace] -- {0}: {1}. + /// + internal static string SelectorTrace + { + get + { + return ResourceManager.GetString("SelectorTrace", resourceCulture); + } + } + /// /// Looks up a localized string similar to Can not serialize a null PSObject.. /// - internal static string SerializeNullPSObject { - get { + internal static string SerializeNullPSObject + { + get + { return ResourceManager.GetString("SerializeNullPSObject", resourceCulture); } } - + /// /// Looks up a localized string similar to Create path. /// - internal static string ShouldCreatePath { - get { + internal static string ShouldCreatePath + { + get + { return ResourceManager.GetString("ShouldCreatePath", resourceCulture); } } - + /// /// Looks up a localized string similar to Write file. /// - internal static string ShouldWriteFile { - get { + internal static string ShouldWriteFile + { + get + { return ResourceManager.GetString("ShouldWriteFile", resourceCulture); } } - + /// /// Looks up a localized string similar to The source was not found.. /// - internal static string SourceNotFound { - get { + internal static string SourceNotFound + { + get + { return ResourceManager.GetString("SourceNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to Using invariant culture may cause rule infomation to be displayed incorrectly. Consider using -Culture or set the Output.Culture option.. /// - internal static string UsingInvariantCulture { - get { + internal static string UsingInvariantCulture + { + get + { return ResourceManager.GetString("UsingInvariantCulture", resourceCulture); } } - + /// /// Looks up a localized string similar to The variable '${0}' can only be used within a Rule block.. /// - internal static string VariableConditionScope { - get { + internal static string VariableConditionScope + { + get + { return ResourceManager.GetString("VariableConditionScope", resourceCulture); } } - + /// /// Looks up a localized string similar to The version constraint '{0}' is not valid.. /// - internal static string VersionConstraintInvalid { - get { + internal static string VersionConstraintInvalid + { + get + { return ResourceManager.GetString("VersionConstraintInvalid", resourceCulture); } } - + /// /// Looks up a localized string similar to The Within parameter Value must be a string when the Like parameter is used.. /// - internal static string WithinLikeNotString { - get { + internal static string WithinLikeNotString + { + get + { return ResourceManager.GetString("WithinLikeNotString", resourceCulture); } } - + /// /// Looks up a localized string similar to Within: {0}. /// - internal static string WithinTrue { - get { + internal static string WithinTrue + { + get + { return ResourceManager.GetString("WithinTrue", resourceCulture); } } diff --git a/src/PSRule/Resources/PSRuleResources.resx b/src/PSRule/Resources/PSRuleResources.resx index a816838778..378f86ad61 100644 --- a/src/PSRule/Resources/PSRuleResources.resx +++ b/src/PSRule/Resources/PSRuleResources.resx @@ -121,6 +121,25 @@ The {0} '{1}' is obsolete. Consider switching to an alternative {0}. Occurs when a resource is used that has been flagged as obsolete. + + The arguments for '{0}' are not in the expected format or type. + + + The argument '{0}' for '{1}' is not a valid boolean. + + + The argument '{0}' for '{1}' is not a valid integer. + + + The argument '{0}' for '{1}' is not a valid string. + + + The number of arguments '{1}' is not within the allowed range for '{0}'. + + + The baseline '{0}' is obsolete. Consider switching to an alternative baseline. + Occurs when a baseline is used that has been flagged as obsolete. + Binding functions are not supported in this language mode. @@ -164,9 +183,18 @@ File + + Failed to parse expression. The expression may not be valid. Expression: "{0}" + [PSRule][D] -- Found {0} PSRule module(s) + + The function "{0}" was not found. + + + The language expression index '{0}' is not valid for the object. + An invalid ErrorAction ({0}) was specified for rule at {1}. Ignore | Stop are supported. @@ -229,6 +257,9 @@ Unknown + + The language expression property '{0}' doesn't exist. + The property '${0}.{1}' is obsolete and will be removed in the next major version. diff --git a/src/PSRule/Resources/ReasonStrings.Designer.cs b/src/PSRule/Resources/ReasonStrings.Designer.cs index f19da8323d..b9dacde3f6 100644 --- a/src/PSRule/Resources/ReasonStrings.Designer.cs +++ b/src/PSRule/Resources/ReasonStrings.Designer.cs @@ -8,11 +8,10 @@ // //------------------------------------------------------------------------------ -namespace PSRule.Resources -{ +namespace PSRule.Resources { using System; - - + + /// /// A strongly-typed resource class, for looking up localized strings, etc. /// @@ -23,85 +22,80 @@ namespace PSRule.Resources [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class ReasonStrings - { - + internal class ReasonStrings { + private static global::System.Resources.ResourceManager resourceMan; - + private static global::System.Globalization.CultureInfo resourceCulture; - + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal ReasonStrings() - { + internal ReasonStrings() { } - + /// /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager - { - get - { - if (object.ReferenceEquals(resourceMan, null)) - { + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PSRule.Resources.ReasonStrings", typeof(ReasonStrings).Assembly); resourceMan = temp; } return resourceMan; } } - + /// /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture - { - get - { + internal static global::System.Globalization.CultureInfo Culture { + get { return resourceCulture; } - set - { + set { resourceCulture = value; } } - + /// /// Looks up a localized string similar to The value '{0}' contains '{1}'.. /// - internal static string Assert_Contains - { - get - { + internal static string Assert_Contains { + get { return ResourceManager.GetString("Assert_Contains", resourceCulture); } } - + + /// + /// Looks up a localized string similar to The number of items is '{0}' instead of '{1}'.. + /// + internal static string Assert_Count { + get { + return ResourceManager.GetString("Assert_Count", resourceCulture); + } + } + /// /// Looks up a localized string similar to The value '{0}' does not match the expression '{1}'.. /// - internal static string Assert_DoesNotMatch - { - get - { + internal static string Assert_DoesNotMatch { + get { return ResourceManager.GetString("Assert_DoesNotMatch", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' ends with '{1}'.. /// - internal static string Assert_EndsWith - { - get - { + internal static string Assert_EndsWith { + get { return ResourceManager.GetString("Assert_EndsWith", resourceCulture); } } - + /// /// Looks up a localized string similar to Does not exist.. /// @@ -114,135 +108,129 @@ internal static string Assert_Exists { /// /// Looks up a localized string similar to The value '{0}' does not contain only lowercase characters.. /// - internal static string Assert_IsLower - { - get - { + internal static string Assert_IsLower { + get { return ResourceManager.GetString("Assert_IsLower", resourceCulture); } } - + + /// + /// Looks up a localized string similar to The value is null.. + /// + internal static string Assert_IsNull { + get { + return ResourceManager.GetString("Assert_IsNull", resourceCulture); + } + } + /// /// Looks up a localized string similar to Is null or empty.. /// - internal static string Assert_IsNullOrEmpty - { - get - { + internal static string Assert_IsNullOrEmpty { + get { return ResourceManager.GetString("Assert_IsNullOrEmpty", resourceCulture); } } - + /// /// Looks up a localized string similar to Is set to '{0}'.. /// - internal static string Assert_IsSetTo - { - get - { + internal static string Assert_IsSetTo { + get { return ResourceManager.GetString("Assert_IsSetTo", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' does not contain only uppercase characters.. /// - internal static string Assert_IsUpper - { - get - { + internal static string Assert_IsUpper { + get { return ResourceManager.GetString("Assert_IsUpper", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' is like '{1}'.. /// - internal static string Assert_Like - { - get - { + internal static string Assert_Like { + get { return ResourceManager.GetString("Assert_Like", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' matches the expression '{1}'.. /// - internal static string Assert_Matches - { - get - { + internal static string Assert_Matches { + get { return ResourceManager.GetString("Assert_Matches", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' is not an array.. /// - internal static string Assert_NotArray - { - get - { + internal static string Assert_NotArray { + get { return ResourceManager.GetString("Assert_NotArray", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' is not a boolean.. /// - internal static string Assert_NotBoolean - { - get - { + internal static string Assert_NotBoolean { + get { return ResourceManager.GetString("Assert_NotBoolean", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' is not {1} {2}.. /// - internal static string Assert_NotComparedTo - { - get - { + internal static string Assert_NotComparedTo { + get { return ResourceManager.GetString("Assert_NotComparedTo", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' does not contain any of {1}.. /// - internal static string Assert_NotContains - { - get - { + internal static string Assert_NotContains { + get { return ResourceManager.GetString("Assert_NotContains", resourceCulture); } } - + + /// + /// Looks up a localized string similar to The number of items is '{0}'.. + /// + internal static string Assert_NotCount { + get { + return ResourceManager.GetString("Assert_NotCount", resourceCulture); + } + } + /// /// Looks up a localized string similar to The value '{0}' is not a date.. /// - internal static string Assert_NotDateTime - { - get - { + internal static string Assert_NotDateTime { + get { return ResourceManager.GetString("Assert_NotDateTime", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' does not end with any of {1}.. /// - internal static string Assert_NotEndsWith - { - get - { + internal static string Assert_NotEndsWith { + get { return ResourceManager.GetString("Assert_NotEndsWith", resourceCulture); } } - + /// /// Looks up a localized string similar to Exists.. /// @@ -255,615 +243,503 @@ internal static string Assert_NotExists { /// /// Looks up a localized string similar to The value '{0}' was not in {1}.. /// - internal static string Assert_NotInSet - { - get - { + internal static string Assert_NotInSet { + get { return ResourceManager.GetString("Assert_NotInSet", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' is not an integer.. /// - internal static string Assert_NotInteger - { - get - { + internal static string Assert_NotInteger { + get { return ResourceManager.GetString("Assert_NotInteger", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' is not like '{1}'.. /// - internal static string Assert_NotLike - { - get - { + internal static string Assert_NotLike { + get { return ResourceManager.GetString("Assert_NotLike", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' is not numeric.. /// - internal static string Assert_NotNumeric - { - get - { + internal static string Assert_NotNumeric { + get { return ResourceManager.GetString("Assert_NotNumeric", resourceCulture); } } - + /// /// Looks up a localized string similar to None of the specified schemas match '{0}'.. /// - internal static string Assert_NotSpecifiedSchema - { - get - { + internal static string Assert_NotSpecifiedSchema { + get { return ResourceManager.GetString("Assert_NotSpecifiedSchema", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' does not start with any of {1}.. /// - internal static string Assert_NotStartsWith - { - get - { + internal static string Assert_NotStartsWith { + get { return ResourceManager.GetString("Assert_NotStartsWith", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' is not a string.. /// - internal static string Assert_NotString - { - get - { + internal static string Assert_NotString { + get { return ResourceManager.GetString("Assert_NotString", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' starts with '{1}'.. /// - internal static string Assert_StartsWith - { - get - { + internal static string Assert_StartsWith { + get { return ResourceManager.GetString("Assert_StartsWith", resourceCulture); } } - + /// /// Looks up a localized string similar to The field value '{0}' can not be compared with '{1}'.. /// - internal static string Compare - { - get - { + internal static string Compare { + get { return ResourceManager.GetString("Compare", resourceCulture); } } - + /// /// Looks up a localized string similar to The field '{0}' does not contain '{1}'.. /// - internal static string Contains - { - get - { + internal static string Contains { + get { return ResourceManager.GetString("Contains", resourceCulture); } } - + /// /// Looks up a localized string similar to The field '{0}' has '{1}' items instead of '{2}'.. /// - internal static string Count - { - get - { + internal static string Count { + get { return ResourceManager.GetString("Count", resourceCulture); } } - + /// /// Looks up a localized string similar to The field '{0}' does not end with '{1}'.. /// - internal static string EndsWith - { - get - { + internal static string EndsWith { + get { return ResourceManager.GetString("EndsWith", resourceCulture); } } - + /// /// Looks up a localized string similar to None of the field(s) existed: {0}. /// - internal static string Exists - { - get - { + internal static string Exists { + get { return ResourceManager.GetString("Exists", resourceCulture); } } - + /// /// Looks up a localized string similar to The field(s) existed: {0}. /// - internal static string ExistsNot - { - get - { + internal static string ExistsNot { + get { return ResourceManager.GetString("ExistsNot", resourceCulture); } } - + /// /// Looks up a localized string similar to The header was not set.. /// - internal static string FileHeader - { - get - { + internal static string FileHeader { + get { return ResourceManager.GetString("FileHeader", resourceCulture); } } - + /// /// Looks up a localized string similar to The file '{0}' does not exist.. /// - internal static string FilePath - { - get - { + internal static string FilePath { + get { return ResourceManager.GetString("FilePath", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' was not > '{1}'.. /// - internal static string Greater - { - get - { + internal static string Greater { + get { return ResourceManager.GetString("Greater", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' was not >= '{1}'.. /// - internal static string GreaterOrEqual - { - get - { + internal static string GreaterOrEqual { + get { return ResourceManager.GetString("GreaterOrEqual", resourceCulture); } } - + /// /// Looks up a localized string similar to The field '{0}' is set to '{1}'.. /// - internal static string HasExpectedFieldValue - { - get - { + internal static string HasExpectedFieldValue { + get { return ResourceManager.GetString("HasExpectedFieldValue", resourceCulture); } } - + /// /// Looks up a localized string similar to The field '{0}' exists.. /// - internal static string HasField - { - get - { + internal static string HasField { + get { return ResourceManager.GetString("HasField", resourceCulture); } } - + /// /// Looks up a localized string similar to None of the specified schemas match '{0}'.. /// - internal static string HasJsonSchema - { - get - { + internal static string HasJsonSchema { + get { return ResourceManager.GetString("HasJsonSchema", resourceCulture); } } - + /// /// Looks up a localized string similar to The field value '{0}' was not included in the set.. /// - internal static string In - { - get - { + internal static string In { + get { return ResourceManager.GetString("In", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' does not contain only letters.. /// - internal static string IsLetter - { - get - { + internal static string IsLetter { + get { return ResourceManager.GetString("IsLetter", resourceCulture); } } - + /// /// Looks up a localized string similar to Failed schema validation on {0}. {1}. /// - internal static string JsonSchemaInvalid - { - get - { + internal static string JsonSchemaInvalid { + get { return ResourceManager.GetString("JsonSchemaInvalid", resourceCulture); } } - + /// /// Looks up a localized string similar to The JSON schema '{0}' could not be found.. /// - internal static string JsonSchemaNotFound - { - get - { + internal static string JsonSchemaNotFound { + get { return ResourceManager.GetString("JsonSchemaNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' was not < '{1}'.. /// - internal static string Less - { - get - { + internal static string Less { + get { return ResourceManager.GetString("Less", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' was not <= '{1}'.. /// - internal static string LessOrEqual - { - get - { + internal static string LessOrEqual { + get { return ResourceManager.GetString("LessOrEqual", resourceCulture); } } - + /// /// Looks up a localized string similar to None of the regex(s) matched: {0}. /// - internal static string Match - { - get - { + internal static string Match { + get { return ResourceManager.GetString("Match", resourceCulture); } } - + /// /// Looks up a localized string similar to The regex '{0}' matched.. /// - internal static string MatchNot - { - get - { + internal static string MatchNot { + get { return ResourceManager.GetString("MatchNot", resourceCulture); } } - + /// /// Looks up a localized string similar to The field value '{0}' does not match the pattern '{1}'.. /// - internal static string MatchPattern - { - get - { + internal static string MatchPattern { + get { return ResourceManager.GetString("MatchPattern", resourceCulture); } } - + /// /// Looks up a localized string similar to The field '{0}' has '{1}' items instead of not '{2}'.. /// - internal static string NotCount - { - get - { + internal static string NotCount { + get { return ResourceManager.GetString("NotCount", resourceCulture); } } - + /// /// Looks up a localized string similar to The field '{0}' is not enumerable.. /// - internal static string NotEnumerable - { - get - { + internal static string NotEnumerable { + get { return ResourceManager.GetString("NotEnumerable", resourceCulture); } } - + /// /// Looks up a localized string similar to The field '{0}' does not exist.. /// - internal static string NotHasField - { - get - { + internal static string NotHasField { + get { return ResourceManager.GetString("NotHasField", resourceCulture); } } - + /// /// Looks up a localized string similar to The value of '{0}' is null or empty.. /// - internal static string NotHasFieldValue - { - get - { + internal static string NotHasFieldValue { + get { return ResourceManager.GetString("NotHasFieldValue", resourceCulture); } } - + /// /// Looks up a localized string similar to The field value '{0}' was in the set.. /// - internal static string NotIn - { - get - { + internal static string NotIn { + get { return ResourceManager.GetString("NotIn", resourceCulture); } } - + /// /// Looks up a localized string similar to The field value '{0}' matches the pattern '{1}'.. /// - internal static string NotMatchPattern - { - get - { + internal static string NotMatchPattern { + get { return ResourceManager.GetString("NotMatchPattern", resourceCulture); } } - + /// /// Looks up a localized string similar to The field value '{0}' is not null.. /// - internal static string NotNull - { - get - { + internal static string NotNull { + get { return ResourceManager.GetString("NotNull", resourceCulture); } } - + /// /// Looks up a localized string similar to The file '{0}' is within the path '{1}'.. /// - internal static string NotWithinPath - { - get - { + internal static string NotWithinPath { + get { return ResourceManager.GetString("NotWithinPath", resourceCulture); } } - + /// /// Looks up a localized string similar to The field value '{0}' is null.. /// - internal static string Null - { - get - { + internal static string Null { + get { return ResourceManager.GetString("Null", resourceCulture); } } - + /// /// Looks up a localized string similar to The field '{0}' is not empty.. /// - internal static string NullOrEmpty - { - get - { + internal static string NullOrEmpty { + get { return ResourceManager.GetString("NullOrEmpty", resourceCulture); } } - + /// /// Looks up a localized string similar to The parameter '{0}' is null or empty.. /// - internal static string NullOrEmptyParameter - { - get - { + internal static string NullOrEmptyParameter { + get { return ResourceManager.GetString("NullOrEmptyParameter", resourceCulture); } } - + /// /// Looks up a localized string similar to The parameter '{0}' is null.. /// - internal static string NullParameter - { - get - { + internal static string NullParameter { + get { return ResourceManager.GetString("NullParameter", resourceCulture); } } - + /// /// Looks up a localized string similar to No results were provided.. /// - internal static string ResultsNotProvided - { - get - { + internal static string ResultsNotProvided { + get { return ResourceManager.GetString("ResultsNotProvided", resourceCulture); } } - + /// /// Looks up a localized string similar to The field '{0}' does not start with '{1}'.. /// - internal static string StartsWith - { - get - { + internal static string StartsWith { + get { return ResourceManager.GetString("StartsWith", resourceCulture); } } - + /// /// Looks up a localized string similar to The field value '{0}' is not a string.. /// - internal static string String - { - get - { + internal static string String { + get { return ResourceManager.GetString("String", resourceCulture); } } - + /// /// Looks up a localized string similar to The field '{0}' did not contain '{1}'.. /// - internal static string Subset - { - get - { + internal static string Subset { + get { return ResourceManager.GetString("Subset", resourceCulture); } } - + /// /// Looks up a localized string similar to The field '{0}' included multiple instances of '{1}'.. /// - internal static string SubsetDuplicate - { - get - { + internal static string SubsetDuplicate { + get { return ResourceManager.GetString("SubsetDuplicate", resourceCulture); } } - + /// /// Looks up a localized string similar to The field value '{2}' of type {1} is not {0}.. /// - internal static string Type - { - get - { + internal static string Type { + get { return ResourceManager.GetString("Type", resourceCulture); } } - + /// /// Looks up a localized string similar to The field value '{1}' of type {0} is not an integer.. /// - internal static string TypeInteger - { - get - { + internal static string TypeInteger { + get { return ResourceManager.GetString("TypeInteger", resourceCulture); } } - + /// /// Looks up a localized string similar to The field value '{1}' of type {0} is not numeric.. /// - internal static string TypeNumeric - { - get - { + internal static string TypeNumeric { + get { return ResourceManager.GetString("TypeNumeric", resourceCulture); } } - + /// /// Looks up a localized string similar to None of the type name(s) match: {0}. /// - internal static string TypeOf - { - get - { + internal static string TypeOf { + get { return ResourceManager.GetString("TypeOf", resourceCulture); } } - + /// /// Looks up a localized string similar to The field value '{0}' is not a version string.. /// - internal static string Version - { - get - { + internal static string Version { + get { return ResourceManager.GetString("Version", resourceCulture); } } - + /// /// Looks up a localized string similar to The version '{0}' does not match the constraint '{1}'.. /// - internal static string VersionContraint - { - get - { + internal static string VersionContraint { + get { return ResourceManager.GetString("VersionContraint", resourceCulture); } } - + /// /// Looks up a localized string similar to The field value didn't match the set.. /// - internal static string Within - { - get - { + internal static string Within { + get { return ResourceManager.GetString("Within", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' was within the set.. /// - internal static string WithinNot - { - get - { + internal static string WithinNot { + get { return ResourceManager.GetString("WithinNot", resourceCulture); } } - + /// /// Looks up a localized string similar to The file '{0}' is not within the path '{1}'.. /// - internal static string WithinPath - { - get - { + internal static string WithinPath { + get { return ResourceManager.GetString("WithinPath", resourceCulture); } } diff --git a/src/PSRule/Resources/ReasonStrings.resx b/src/PSRule/Resources/ReasonStrings.resx index 80c0bf534e..2b51ac2acb 100644 --- a/src/PSRule/Resources/ReasonStrings.resx +++ b/src/PSRule/Resources/ReasonStrings.resx @@ -345,4 +345,13 @@ Exists. + + The number of items is '{0}' instead of '{1}'. + + + The value is null. + + + The number of items is '{0}'. + \ No newline at end of file diff --git a/src/PSRule/Runtime/Configuration.cs b/src/PSRule/Runtime/Configuration.cs index e9f09d51ac..d64874d768 100644 --- a/src/PSRule/Runtime/Configuration.cs +++ b/src/PSRule/Runtime/Configuration.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Collections; @@ -69,23 +69,7 @@ public int GetIntegerOrDefault(string configurationKey, int defaultValue) private bool TryGetValue(string name, out object value) { value = null; - if (_Context == null) - return false; - - // Get from baseline configuration - if (_Context.LanguageScope.TryConfigurationValue(name, out var result)) - { - value = result; - return true; - } - - // Check if value exists in Rule definition defaults - if (_Context.RuleBlock == null || _Context.RuleBlock.Configuration == null || !_Context.RuleBlock.Configuration.ContainsKey(name)) - return false; - - // Get from rule default - value = _Context.RuleBlock.Configuration[name]; - return true; + return _Context != null && _Context.TryGetConfigurationValue(name, out value); } private static bool TryBool(object o, out bool value) diff --git a/src/PSRule/Runtime/Operand.cs b/src/PSRule/Runtime/Operand.cs index 550c8597a7..224e7fe3ec 100644 --- a/src/PSRule/Runtime/Operand.cs +++ b/src/PSRule/Runtime/Operand.cs @@ -38,7 +38,12 @@ public enum OperandKind /// /// The target object itself. /// - Target = 5 + Target = 5, + + /// + /// A literal value or function. + /// + Value = 6 } /// @@ -117,6 +122,11 @@ internal static IOperand FromTarget() return new Operand(OperandKind.Target, null, null); } + internal static IOperand FromValue(object value) + { + return new Operand(OperandKind.Value, null, value); + } + internal static string JoinPath(string p1, string p2) { if (IsEmptyPath(p1)) diff --git a/src/PSRule/Runtime/PSRule.cs b/src/PSRule/Runtime/PSRule.cs index 0285b8a592..83d0a53c39 100644 --- a/src/PSRule/Runtime/PSRule.cs +++ b/src/PSRule/Runtime/PSRule.cs @@ -136,7 +136,7 @@ public IEnumerable Output /// /// The current target object. /// - public PSObject TargetObject => GetContext().RuleRecord.TargetObject; + public PSObject TargetObject => GetContext().TargetObject.Value; /// /// The bound name of the target object. diff --git a/src/PSRule/Runtime/RunspaceContext.cs b/src/PSRule/Runtime/RunspaceContext.cs index a7ee9eac7c..31b5c2046e 100644 --- a/src/PSRule/Runtime/RunspaceContext.cs +++ b/src/PSRule/Runtime/RunspaceContext.cs @@ -771,7 +771,6 @@ public void Init(Source[] source) { InitLanguageScopes(source); Host.HostHelper.ImportResource(source, this); - foreach (var languageScope in _LanguageScopes.Get()) Pipeline.Baseline.BuildScope(languageScope); } @@ -841,6 +840,32 @@ internal bool ShouldWarnOnce(params string[] key) return true; } + #region Configuration + + internal bool TryGetConfigurationValue(string name, out object value) + { + value = null; + if (string.IsNullOrEmpty(name)) + return false; + + // Get from baseline configuration + if (LanguageScope.TryConfigurationValue(name, out var result)) + { + value = result; + return true; + } + + // Check if value exists in Rule definition defaults + if (RuleBlock == null || RuleBlock.Configuration == null || !RuleBlock.Configuration.ContainsKey(name)) + return false; + + // Get from rule default + value = RuleBlock.Configuration[name]; + return true; + } + + #endregion Configuration + #region IDisposable public void Dispose() diff --git a/tests/PSRule.Tests/FunctionBuilderTests.cs b/tests/PSRule.Tests/FunctionBuilderTests.cs new file mode 100644 index 0000000000..963551a2c1 --- /dev/null +++ b/tests/PSRule.Tests/FunctionBuilderTests.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Linq; +using PSRule.Configuration; +using PSRule.Definitions.Selectors; +using PSRule.Host; +using PSRule.Pipeline; +using PSRule.Runtime; +using Xunit; +using Assert = Xunit.Assert; + +namespace PSRule +{ + public sealed class FunctionBuilderTests + { + private const string FunctionYamlFileName = "Functions.Rule.yaml"; + private const string FunctionJsonFileName = "Functions.Rule.jsonc"; + + [Theory] + [InlineData("Yaml", FunctionYamlFileName)] + [InlineData("Json", FunctionJsonFileName)] + public void Build(string type, string path) + { + Assert.NotNull(GetSelectorVisitor($"{type}.Fn.Example1", GetSource(path), out _)); + Assert.NotNull(GetSelectorVisitor($"{type}.Fn.Example2", GetSource(path), out _)); + Assert.NotNull(GetSelectorVisitor($"{type}.Fn.Example3", GetSource(path), out _)); + Assert.NotNull(GetSelectorVisitor($"{type}.Fn.Example4", GetSource(path), out _)); + Assert.NotNull(GetSelectorVisitor($"{type}.Fn.Example5", GetSource(path), out _)); + Assert.NotNull(GetSelectorVisitor($"{type}.Fn.Example6", GetSource(path), out _)); + } + + #region Helper methods + + private static PSRuleOption GetOption() + { + return new PSRuleOption(); + } + + private static Source[] GetSource(string path) + { + var builder = new SourcePipelineBuilder(null, null); + builder.Directory(GetSourcePath(path)); + return builder.Build(); + } + + private static SelectorVisitor GetSelectorVisitor(string name, Source[] source, out RunspaceContext context) + { + context = new RunspaceContext(PipelineContext.New(GetOption(), null, null, PipelineHookActions.BindTargetName, PipelineHookActions.BindTargetType, PipelineHookActions.BindField, new OptionContext(), null), null); + context.Init(source); + context.Begin(); + var selector = HostHelper.GetSelector(source, context).ToArray().FirstOrDefault(s => s.Name == name); + return new SelectorVisitor(selector.Id, selector.Source, selector.Spec.If); + } + + private static string GetSourcePath(string fileName) + { + return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName); + } + + #endregion Helper methods + } +} diff --git a/tests/PSRule.Tests/FunctionTests.cs b/tests/PSRule.Tests/FunctionTests.cs new file mode 100644 index 0000000000..8dedd43b18 --- /dev/null +++ b/tests/PSRule.Tests/FunctionTests.cs @@ -0,0 +1,282 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Linq; +using System.Management.Automation; +using PSRule.Configuration; +using PSRule.Definitions.Expressions; +using PSRule.Pipeline; +using Xunit; + +namespace PSRule +{ + /// + /// Define tests for expression functions are working correctly. + /// + public sealed class FunctionTests + { + [Fact] + public void Concat() + { + var context = GetContext(); + var fn = GetFunction("concat"); + + var properties = new LanguageExpression.PropertyBag + { + { "concat", new object[] { "1", "2", "3" } } + }; + Assert.Equal("123", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "concat", new object[] { 1, 2, 3 } } + }; + Assert.Equal("123", fn(context, properties)(context)); + } + + [Fact] + public void Configuration() + { + var context = GetContext(); + var fn = GetFunction("configuration"); + + var properties = new LanguageExpression.PropertyBag + { + { "configuration", "config1" } + }; + Assert.Equal("123", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "configuration", "notconfig" } + }; + Assert.Null(fn(context, properties)(context)); + } + + [Fact] + public void Boolean() + { + var context = GetContext(); + var fn = GetFunction("boolean"); + + var properties = new LanguageExpression.PropertyBag + { + { "boolean", true } + }; + Assert.Equal(true, fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "boolean", false } + }; + Assert.Equal(false, fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "boolean", "true" } + }; + Assert.Equal(true, fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "boolean", "false" } + }; + Assert.Equal(false, fn(context, properties)(context)); + } + + [Fact] + public void Integer() + { + var context = GetContext(); + var fn = GetFunction("integer"); + + var properties = new LanguageExpression.PropertyBag + { + { "integer", 1 } + }; + Assert.Equal(1, fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "integer", -1 } + }; + Assert.Equal(-1, fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "integer", 0 } + }; + Assert.Equal(0, fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "integer", "1" } + }; + Assert.Equal(1, fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "integer", "-1" } + }; + Assert.Equal(-1, fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "integer", "0" } + }; + Assert.Equal(0, fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "integer", "not" } + }; + Assert.Equal(0, fn(context, properties)(context)); + } + + [Fact] + public void String() + { + var context = GetContext(); + var fn = GetFunction("string"); + + var properties = new LanguageExpression.PropertyBag + { + { "string", 1 } + }; + Assert.Equal("1", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "string", -1 } + }; + Assert.Equal("-1", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "string", "1" } + }; + Assert.Equal("1", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "string", "-1" } + }; + Assert.Equal("-1", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "string", "0" } + }; + Assert.Equal("0", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "string", "abc" } + }; + Assert.Equal("abc", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "string", true } + }; + Assert.Equal("True", fn(context, properties)(context)); + } + + [Fact] + public void Substring() + { + var context = GetContext(); + var fn = GetFunction("substring"); + + var properties = new LanguageExpression.PropertyBag + { + { "substring", "TestObject" }, + { "length", 7 } + }; + Assert.Equal("TestObj", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "substring", "TestObject" }, + { "length", "7" } + }; + Assert.Equal("TestObj", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "substring", 10000 }, + { "length", 2 } + }; + Assert.Null(fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "substring", "Test" }, + { "length", 100 } + }; + Assert.Equal("Test", fn(context, properties)(context)); + } + + [Fact] + public void Path() + { + var context = GetContext(); + var fn = GetFunction("path"); + + var properties = new LanguageExpression.PropertyBag + { + { "path", "name" } + }; + Assert.Equal("TestObject1", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "path", "notProperty" } + }; + Assert.Null(fn(context, properties)(context)); + } + + #region Helper methods + + private static ExpressionBuilderFn GetFunction(string name) + { + return Functions.Builtin.Single(f => f.Name == name).Fn; + } + + private static PSRuleOption GetOption() + { + var option = new PSRuleOption(); + option.Configuration["config1"] = "123"; + return option; + } + + private static Source[] GetSource() + { + var builder = new SourcePipelineBuilder(null, null); + builder.Directory(GetSourcePath("Selectors.Rule.yaml")); + return builder.Build(); + } + + private static ExpressionContext GetContext() + { + var targetObject = new PSObject(); + targetObject.Properties.Add(new PSNoteProperty("name", "TestObject1")); + var context = new Runtime.RunspaceContext(PipelineContext.New(GetOption(), null, null, PipelineHookActions.BindTargetName, PipelineHookActions.BindTargetType, PipelineHookActions.BindField, new OptionContextBuilder(GetOption(), null, null, null).Build(), null), null); + var s = GetSource(); + var result = new ExpressionContext(s[0].File[0], Definitions.ResourceKind.Rule, targetObject); + context.Init(s); + context.Begin(); + context.PushScope(Runtime.RunspaceScope.Precondition); + context.EnterTargetObject(new TargetObject(targetObject)); + return result; + } + + private static string GetSourcePath(string fileName) + { + return System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName); + } + + #endregion Helper methods + } +} diff --git a/tests/PSRule.Tests/Functions.Rule.jsonc b/tests/PSRule.Tests/Functions.Rule.jsonc new file mode 100644 index 0000000000..4caba42731 --- /dev/null +++ b/tests/PSRule.Tests/Functions.Rule.jsonc @@ -0,0 +1,133 @@ +[ + { + // Synopsis: An expression function example. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.Example1" + }, + "spec": { + "if": { + "value": { + "$": { + "substring": { + "path": "name" + }, + "length": 7 + } + }, + "equals": "TestObj" + } + } + }, + { + // Synopsis: An expression function example. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.Example2" + }, + "spec": { + "if": { + "value": { + "$": { + "configuration": "ConfigArray" + } + }, + "count": 5 + } + } + }, + { + // Synopsis: An expression function example. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.Example3" + }, + "spec": { + "if": { + "value": { + "$": { + "boolean": true + } + }, + "equals": true + } + } + }, + { + // Synopsis: An expression function example. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.Example4" + }, + "spec": { + "if": { + "value": { + "$": { + "concat": [ + { + "path": "name" + }, + { + "string": "-" + }, + { + "path": "name" + } + ] + } + }, + "equals": "TestObject1-TestObject1" + } + } + }, + { + // Synopsis: An expression function example. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.Example5" + }, + "spec": { + "if": { + "value": { + "$": { + "integer": 6 + } + }, + "greater": 5 + } + } + }, + { + // Synopsis: An expression function example. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.Example6" + }, + "spec": { + "if": { + "value": "TestObject1-TestObject1", + "equals": { + "$": { + "concat": [ + { + "path": "name" + }, + { + "string": "-" + }, + { + "path": "name" + } + ] + } + } + } + } + } +] diff --git a/tests/PSRule.Tests/Functions.Rule.yaml b/tests/PSRule.Tests/Functions.Rule.yaml new file mode 100644 index 0000000000..ba3f42f0eb --- /dev/null +++ b/tests/PSRule.Tests/Functions.Rule.yaml @@ -0,0 +1,88 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +--- +# Synopsis: An expression function example. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: Yaml.Fn.Example1 +spec: + if: + value: + $: + substring: + path: name + length: 7 + equals: TestObj + +--- +# Synopsis: An expression function example. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: Yaml.Fn.Example2 +spec: + if: + value: + $: + configuration: 'ConfigArray' + count: 5 + +--- +# Synopsis: An expression function example. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: Yaml.Fn.Example3 +spec: + if: + value: + $: + boolean: true + equals: true + +--- +# Synopsis: An expression function example. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: Yaml.Fn.Example4 +spec: + if: + value: + $: + concat: + - path: name + - string: '-' + - path: name + equals: TestObject1-TestObject1 + +--- +# Synopsis: An expression function example. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: Yaml.Fn.Example5 +spec: + if: + value: + $: + integer: 6 + greater: 5 + +--- +# Synopsis: An expression function example. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: Yaml.Fn.Example6 +spec: + if: + value: TestObject1-TestObject1 + equals: + $: + concat: + - path: name + - string: '-' + - path: name diff --git a/tests/PSRule.Tests/PSRule.Tests.csproj b/tests/PSRule.Tests/PSRule.Tests.csproj index ccae856278..3feade5467 100644 --- a/tests/PSRule.Tests/PSRule.Tests.csproj +++ b/tests/PSRule.Tests/PSRule.Tests.csproj @@ -70,6 +70,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest diff --git a/tests/PSRule.Tests/SelectorTests.cs b/tests/PSRule.Tests/SelectorTests.cs index 65595f1059..1f1c087178 100644 --- a/tests/PSRule.Tests/SelectorTests.cs +++ b/tests/PSRule.Tests/SelectorTests.cs @@ -27,6 +27,8 @@ public sealed class SelectorTests { private const string SelectorYamlFileName = "Selectors.Rule.yaml"; private const string SelectorJsonFileName = "Selectors.Rule.jsonc"; + private const string FunctionsYamlFileName = "Functions.Rule.yaml"; + private const string FunctionsJsonFileName = "Functions.Rule.jsonc"; [Theory] [InlineData("Yaml", SelectorYamlFileName)] @@ -1651,11 +1653,40 @@ public void Name(string type, string path) #endregion Properties + #region Functions + + [Theory] + [InlineData("Yaml", FunctionsYamlFileName)] + [InlineData("Json", FunctionsJsonFileName)] + public void WithFunction(string type, string path) + { + var example1 = GetSelectorVisitor($"{type}.Fn.Example1", GetSource(path), out _); + var example2 = GetSelectorVisitor($"{type}.Fn.Example2", GetSource(path), out _); + var example3 = GetSelectorVisitor($"{type}.Fn.Example3", GetSource(path), out _); + var example4 = GetSelectorVisitor($"{type}.Fn.Example4", GetSource(path), out _); + var example5 = GetSelectorVisitor($"{type}.Fn.Example5", GetSource(path), out _); + var example6 = GetSelectorVisitor($"{type}.Fn.Example6", GetSource(path), out _); + var actual1 = GetObject( + (name: "Name", value: "TestObject1") + ); + + Assert.True(example1.Match(actual1)); + Assert.True(example2.Match(actual1)); + Assert.True(example3.Match(actual1)); + Assert.True(example4.Match(actual1)); + Assert.True(example5.Match(actual1)); + Assert.True(example6.Match(actual1)); + } + + #endregion Functions + #region Helper methods private static PSRuleOption GetOption() { - return new PSRuleOption(); + var option = new PSRuleOption(); + option.Configuration["ConfigArray"] = new string[] { "1", "2", "3", "4", "5" }; + return option; } private static Source[] GetSource(string path) @@ -1676,7 +1707,8 @@ private static PSObject GetObject(params (string name, object value)[] propertie private static SelectorVisitor GetSelectorVisitor(string name, Source[] source, out RunspaceContext context) { - context = new RunspaceContext(PipelineContext.New(GetOption(), null, null, PipelineHookActions.BindTargetName, PipelineHookActions.BindTargetType, PipelineHookActions.BindField, new OptionContext(), null), null); + var builder = new OptionContextBuilder(GetOption(), null, null, null); + context = new RunspaceContext(PipelineContext.New(GetOption(), null, null, PipelineHookActions.BindTargetName, PipelineHookActions.BindTargetType, PipelineHookActions.BindField, builder.Build(), null), null); context.Init(source); context.Begin(); var selector = HostHelper.GetSelector(source, context).ToArray().FirstOrDefault(s => s.Name == name); From 4bab75f1ff19babecf0224ae7c88a434e5f8cf6b Mon Sep 17 00:00:00 2001 From: Bernie White Date: Thu, 18 Aug 2022 17:26:25 +1000 Subject: [PATCH 032/156] Fixes reporting of duplicate identifiers #1229 (#1231) * Fixes reporting of duplicate identifiers #1229 * Ignore warning for invarient culture. --- README.md | 95 +++++------ docs/CHANGELOG-v2.md | 5 + .../PSRule/en-US/about_PSRule_Options.md | 58 +++++++ schemas/PSRule-options.schema.json | 29 +++- src/PSRule/Common/HashSetExtensions.cs | 97 ++++++++++++ ...=> RunspaceContextDiagnosticExtensions.cs} | 30 +++- .../ExecutionActionPreference.cs | 33 ++++ src/PSRule/Configuration/ExecutionOption.cs | 28 +++- src/PSRule/Configuration/PSRuleOption.cs | 3 + src/PSRule/Host/HostHelper.cs | 148 ++++++++++-------- src/PSRule/PSRule.psm1 | 49 ++++-- src/PSRule/Pipeline/GetRuleHelpPipeline.cs | 3 +- src/PSRule/Pipeline/GetRulePipelineBuiler.cs | 5 +- src/PSRule/Pipeline/InvokePipelineBuilder.cs | 5 - src/PSRule/Pipeline/PipelineBuilder.cs | 27 +++- .../Resources/PSRuleResources.Designer.cs | 6 +- src/PSRule/Resources/PSRuleResources.resx | 6 +- tests/PSRule.Tests/PSRule.Common.Tests.ps1 | 6 +- tests/PSRule.Tests/PSRule.Options.Tests.ps1 | 47 ++++++ tests/PSRule.Tests/PSRule.Tests.yml | 1 + 20 files changed, 514 insertions(+), 167 deletions(-) create mode 100644 src/PSRule/Common/HashSetExtensions.cs rename src/PSRule/Common/{RunspaceContextExtensions.cs => RunspaceContextDiagnosticExtensions.cs} (58%) create mode 100644 src/PSRule/Configuration/ExecutionActionPreference.cs diff --git a/README.md b/README.md index d6fe505a0c..8e1577b4af 100644 --- a/README.md +++ b/README.md @@ -240,53 +240,54 @@ The following conceptual topics exist in the `PSRule` module: - [Type](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#type) - [WithinPath](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#withinpath) - [Version](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#version) -- [Options](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/) - - [Binding.Field](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingfield) - - [Binding.IgnoreCase](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingignorecase) - - [Binding.NameSeparator](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingnameseparator) - - [Binding.PreferTargetInfo](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingprefertargetinfo) - - [Binding.TargetName](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingtargetname) - - [Binding.TargetType](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingtargettype) - - [Binding.UseQualifiedName](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingusequalifiedname) - - [Configuration](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#configuration) - - [Convention.Include](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#conventioninclude) - - [Execution.AliasReferenceWarning](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executionaliasreferencewarning) - - [Execution.LanguageMode](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executionlanguagemode) - - [Execution.InconclusiveWarning](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executioninconclusivewarning) - - [Execution.NotProcessedWarning](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executionnotprocessedwarning) - - [Execution.SuppressedRuleWarning](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executionsuppressedrulewarning) - - [Execution.InvariantCultureWarning](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executioninvariantculturewarning) - - [Include.Module](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#includemodule) - - [Include.Path](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#includepath) - - [Input.Format](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#inputformat) - - [Input.IgnoreGitPath](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#inputignoregitpath) - - [Input.IgnoreObjectSource](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#inputignoreobjectsource) - - [Input.IgnoreRepositoryCommon](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#inputignorerepositorycommon) - - [Input.ObjectPath](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#inputobjectpath) - - [Input.PathIgnore](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#inputpathignore) - - [Input.TargetType](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#inputtargettype) - - [Logging.LimitDebug](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#logginglimitdebug) - - [Logging.LimitVerbose](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#logginglimitverbose) - - [Logging.RuleFail](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#loggingrulefail) - - [Logging.RulePass](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#loggingrulepass) - - [Output.As](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputas) - - [Output.Banner](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputbanner) - - [Output.Culture](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputculture) - - [Output.Encoding](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputencoding) - - [Output.Footer](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputfooter) - - [Output.Format](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputformat) - - [Output.JsonIndent](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputjsonindent) - - [Output.Outcome](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputoutcome) - - [Output.Path](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputpath) - - [Output.SarifProblemsOnly](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputsarifproblemsonly) - - [Output.Style](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputstyle) - - [Requires](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#requires) - - [Rule.Baseline](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#rulebaseline) - - [Rule.Include](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#ruleinclude) - - [Rule.IncludeLocal](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#ruleincludelocal) - - [Rule.Exclude](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#ruleexclude) - - [Rule.Tag](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#ruletag) - - [Suppression](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#suppression) +- [Options](https://aka.ms/ps-rule/options) + - [Binding.Field](https://aka.ms/ps-rule/options#bindingfield) + - [Binding.IgnoreCase](https://aka.ms/ps-rule/options#bindingignorecase) + - [Binding.NameSeparator](https://aka.ms/ps-rule/options#bindingnameseparator) + - [Binding.PreferTargetInfo](https://aka.ms/ps-rule/options#bindingprefertargetinfo) + - [Binding.TargetName](https://aka.ms/ps-rule/options#bindingtargetname) + - [Binding.TargetType](https://aka.ms/ps-rule/options#bindingtargettype) + - [Binding.UseQualifiedName](https://aka.ms/ps-rule/options#bindingusequalifiedname) + - [Configuration](https://aka.ms/ps-rule/options#configuration) + - [Convention.Include](https://aka.ms/ps-rule/options#conventioninclude) + - [Execution.AliasReferenceWarning](https://aka.ms/ps-rule/options#executionaliasreferencewarning) + - [Execution.DuplicateResourceId](https://aka.ms/ps-rule/options#executionduplicateresourceid) + - [Execution.LanguageMode](https://aka.ms/ps-rule/options#executionlanguagemode) + - [Execution.InconclusiveWarning](https://aka.ms/ps-rule/options#executioninconclusivewarning) + - [Execution.NotProcessedWarning](https://aka.ms/ps-rule/options#executionnotprocessedwarning) + - [Execution.SuppressedRuleWarning](https://aka.ms/ps-rule/options#executionsuppressedrulewarning) + - [Execution.InvariantCultureWarning](https://aka.ms/ps-rule/options#executioninvariantculturewarning) + - [Include.Module](https://aka.ms/ps-rule/options#includemodule) + - [Include.Path](https://aka.ms/ps-rule/options#includepath) + - [Input.Format](https://aka.ms/ps-rule/options#inputformat) + - [Input.IgnoreGitPath](https://aka.ms/ps-rule/options#inputignoregitpath) + - [Input.IgnoreObjectSource](https://aka.ms/ps-rule/options#inputignoreobjectsource) + - [Input.IgnoreRepositoryCommon](https://aka.ms/ps-rule/options#inputignorerepositorycommon) + - [Input.ObjectPath](https://aka.ms/ps-rule/options#inputobjectpath) + - [Input.PathIgnore](https://aka.ms/ps-rule/options#inputpathignore) + - [Input.TargetType](https://aka.ms/ps-rule/options#inputtargettype) + - [Logging.LimitDebug](https://aka.ms/ps-rule/options#logginglimitdebug) + - [Logging.LimitVerbose](https://aka.ms/ps-rule/options#logginglimitverbose) + - [Logging.RuleFail](https://aka.ms/ps-rule/options#loggingrulefail) + - [Logging.RulePass](https://aka.ms/ps-rule/options#loggingrulepass) + - [Output.As](https://aka.ms/ps-rule/options#outputas) + - [Output.Banner](https://aka.ms/ps-rule/options#outputbanner) + - [Output.Culture](https://aka.ms/ps-rule/options#outputculture) + - [Output.Encoding](https://aka.ms/ps-rule/options#outputencoding) + - [Output.Footer](https://aka.ms/ps-rule/options#outputfooter) + - [Output.Format](https://aka.ms/ps-rule/options#outputformat) + - [Output.JsonIndent](https://aka.ms/ps-rule/options#outputjsonindent) + - [Output.Outcome](https://aka.ms/ps-rule/options#outputoutcome) + - [Output.Path](https://aka.ms/ps-rule/options#outputpath) + - [Output.SarifProblemsOnly](https://aka.ms/ps-rule/options#outputsarifproblemsonly) + - [Output.Style](https://aka.ms/ps-rule/options#outputstyle) + - [Requires](https://aka.ms/ps-rule/options#requires) + - [Rule.Baseline](https://aka.ms/ps-rule/options#rulebaseline) + - [Rule.Include](https://aka.ms/ps-rule/options#ruleinclude) + - [Rule.IncludeLocal](https://aka.ms/ps-rule/options#ruleincludelocal) + - [Rule.Exclude](https://aka.ms/ps-rule/options#ruleexclude) + - [Rule.Tag](https://aka.ms/ps-rule/options#ruletag) + - [Suppression](https://aka.ms/ps-rule/options#suppression) - [Rules](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Rules/) - [Selectors](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Selectors/) - [Suppression Groups](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_SuppressionGroups/) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 7c42ed3360..402dfca95a 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -25,6 +25,11 @@ What's changed since pre-release v2.4.0-B0022: - Added conversion functions `boolean`, `string`, and `integer`. - Added lookup functions `configuration`, and `path`. - Added string functions `concat`, `substring`. +- Bug fixes: + - Fixed reporting of duplicate identifiers which were not generating an error for all cases by @BernieWhite. + [#1229](https://github.com/microsoft/PSRule/issues/1229) + - Added `Execution.DuplicateResourceId` option to configure PSRule behaviour. + - By default, duplicate resource identifiers return an error. ## v2.4.0-B0022 (pre-release) diff --git a/docs/concepts/PSRule/en-US/about_PSRule_Options.md b/docs/concepts/PSRule/en-US/about_PSRule_Options.md index 65bacf0b20..5e07cb7f48 100644 --- a/docs/concepts/PSRule/en-US/about_PSRule_Options.md +++ b/docs/concepts/PSRule/en-US/about_PSRule_Options.md @@ -15,6 +15,7 @@ The following workspace options are available for use: - [Convention.Include](#conventioninclude) - [Execution.AliasReferenceWarning](#executionaliasreferencewarning) +- [Execution.DuplicateResourceId](#executionduplicateresourceid) - [Execution.LanguageMode](#executionlanguagemode) - [Execution.InconclusiveWarning](#executioninconclusivewarning) - [Execution.NotProcessedWarning](#executionnotprocessedwarning) @@ -719,6 +720,63 @@ variables: value: false ``` +### Execution.DuplicateResourceId + +Determines how to handle duplicate resources identifiers during execution. +A duplicate resource identifier may exist if two resources are defined with the same name, ref, or alias. +By defaut, an error is thrown, however this behaviour can be modified by this option. + +If this option is configured to `Warn` or `Ignore` only the first resource will be used, +however PSRule will continue to execute. + +The following preferences are available: + +- `None` (0) - No preference. + Inherits the default of `Error`. +- `Ignore` (1) - Continue to execute silently. +- `Warn` (2) - Continue to execute but log a warning. +- `Error` (3) - Abort and throw an error. + This is the default. + +```powershell +# PowerShell: Using the DuplicateResourceId parameter +$option = New-PSRuleOption -DuplicateResourceId 'Warn'; +``` + +```powershell +# PowerShell: Using the Execution.DuplicateResourceId hashtable key +$option = New-PSRuleOption -Option @{ 'Execution.DuplicateResourceId' = 'Warn' }; +``` + +```powershell +# PowerShell: Using the DuplicateResourceId parameter to set YAML +Set-PSRuleOption -DuplicateResourceId 'Warn'; +``` + +```yaml +# YAML: Using the execution/duplicateResourceId property +execution: + duplicateResourceId: Warn +``` + +```bash +# Bash: Using environment variable +export PSRULE_EXECUTION_DUPLICATERESOURCEID=Warn +``` + +```yaml +# GitHub Actions: Using environment variable +env: + PSRULE_EXECUTION_DUPLICATERESOURCEID: Warn +``` + +```yaml +# Azure Pipelines: Using environment variable +variables: +- name: PSRULE_EXECUTION_DUPLICATERESOURCEID + value: Warn +``` + ### Execution.LanguageMode Unless PowerShell has been constrained, full language features of PowerShell are available to use within rule definitions. diff --git a/schemas/PSRule-options.schema.json b/schemas/PSRule-options.schema.json index 828a347ce7..e1f435ef85 100644 --- a/schemas/PSRule-options.schema.json +++ b/schemas/PSRule-options.schema.json @@ -257,8 +257,28 @@ "execution-option": { "type": "object", "title": "Execution options", - "description": "Options that affect rule execution.", + "description": "Options that affect execution.", + "markdownDescription": "Options that affect execution. [See help](https://aka.ms/ps-rule/options)", "properties": { + "aliasReferenceWarning": { + "type": "boolean", + "title": "Warn on resource aliases", + "description": "Enable or disable warnings when an alias to a resource is used. The default is true.", + "markdownDescription": "Enable or disable warnings when an alias to a resource is used. The default is `true`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executionaliasreferencewarning)", + "default": true + }, + "duplicateResourceId": { + "type": "string", + "title": "Duplicate resource identifiers", + "description": "Determines how to handle duplicate resources identifiers during execution. Regardless of the value, only the first resource will be used. By defaut, an error is thrown. When set to Warn, a warning is generated. When set to Ignore, no output will be displayed.", + "markdownDescription": "Determines how to handle duplicate resources identifiers during execution.\n\nRegardless of the value, only the first resource will be used. By defaut, an error is thrown. When set to `Warn`, a warning is generated. When set to `Ignore`, no output will be displayed. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executionduplicateresourceid)", + "enum": [ + "Ignore", + "Warn", + "Error" + ], + "default": "Error" + }, "languageMode": { "type": "string", "title": "Language mode", @@ -291,13 +311,6 @@ "markdownDescription": "Enable or disable warnings for suppressed rules. The default is `true`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executionsuppressedrulewarning)", "default": true }, - "aliasReferenceWarning": { - "type": "boolean", - "title": "Warn on resource aliases", - "description": "Enable or disable warnings when an alias to a resource is used. The default is true.", - "markdownDescription": "Enable or disable warnings when an alias to a resource is used. The default is `true`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executionaliasreferencewarning)", - "default": true - }, "invariantCultureWarning": { "type": "boolean", "title": "Warn on invariant culture", diff --git a/src/PSRule/Common/HashSetExtensions.cs b/src/PSRule/Common/HashSetExtensions.cs new file mode 100644 index 0000000000..fb065f898e --- /dev/null +++ b/src/PSRule/Common/HashSetExtensions.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using PSRule.Definitions; + +namespace PSRule +{ + internal static class HashSetExtensions + { + internal static bool ContainsIds(this HashSet hashset, ResourceId id, ResourceId? @ref, ResourceId[] aliases, out ResourceId? duplicate) + { + duplicate = null; + if (hashset == null || hashset.Count == 0) + return false; + + if (hashset.Contains(id)) + { + duplicate = id; + return true; + } + if (@ref.HasValue && hashset.Contains(@ref.Value)) + { + duplicate = @ref.Value; + return true; + } + for (var i = 0; aliases != null && i < aliases.Length; i++) + { + if (hashset.Contains(aliases[i])) + { + duplicate = aliases[i]; + return true; + } + } + return false; + } + + internal static bool ContainsNames(this HashSet hashset, ResourceId id, ResourceId? @ref, ResourceId[] aliases, out string duplicate) + { + duplicate = null; + if (hashset == null || hashset.Count == 0) + return false; + + if (hashset.Contains(id.Name)) + { + duplicate = id.Name; + return true; + } + if (@ref.HasValue && hashset.Contains(@ref.Value.Name)) + { + duplicate = @ref.Value.Name; + return true; + } + for (var i = 0; aliases != null && i < aliases.Length; i++) + { + if (hashset.Contains(aliases[i].Name)) + { + duplicate = aliases[i].Name; + return true; + } + } + return false; + } + + internal static void AddIds(this HashSet hashset, ResourceId id, ResourceId? @ref, ResourceId[] aliases) + { + if (hashset == null) + return; + + if (!hashset.Contains(id)) + hashset.Add(id); + + if (@ref.HasValue && !hashset.Contains(@ref.Value)) + hashset.Add(@ref.Value); + + for (var i = 0; aliases != null && i < aliases.Length; i++) + if (!hashset.Contains(aliases[i])) + hashset.Add(aliases[i]); + } + + internal static void AddNames(this HashSet hashset, ResourceId id, ResourceId? @ref, ResourceId[] aliases) + { + if (hashset == null) + return; + + if (!hashset.Contains(id.Name)) + hashset.Add(id.Name); + + if (@ref.HasValue && !hashset.Contains(@ref.Value.Name)) + hashset.Add(@ref.Value.Name); + + for (var i = 0; aliases != null && i < aliases.Length; i++) + if (!hashset.Contains(aliases[i].Name)) + hashset.Add(aliases[i].Name); + } + } +} diff --git a/src/PSRule/Common/RunspaceContextExtensions.cs b/src/PSRule/Common/RunspaceContextDiagnosticExtensions.cs similarity index 58% rename from src/PSRule/Common/RunspaceContextExtensions.cs rename to src/PSRule/Common/RunspaceContextDiagnosticExtensions.cs index 1b6e88c7bb..d3d0082783 100644 --- a/src/PSRule/Common/RunspaceContextExtensions.cs +++ b/src/PSRule/Common/RunspaceContextDiagnosticExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Threading; using PSRule.Configuration; using PSRule.Definitions; using PSRule.Pipeline; @@ -10,11 +11,11 @@ namespace PSRule { - internal static class RunspaceContextExtensions + internal static class RunspaceContextDiagnosticExtensions { private const string WARN_KEY_PROPERTY = "Property"; - public static void WarnResourceObsolete(this RunspaceContext context, ResourceKind kind, string id) + internal static void WarnResourceObsolete(this RunspaceContext context, ResourceKind kind, string id) { if (context.Writer == null || !context.Writer.ShouldWriteWarning()) return; @@ -22,7 +23,7 @@ public static void WarnResourceObsolete(this RunspaceContext context, ResourceKi context.Writer.WriteWarning(PSRuleResources.ResourceObsolete, Enum.GetName(typeof(ResourceKind), kind), id); } - public static void WarnPropertyObsolete(this RunspaceContext context, string variableName, string propertyName) + internal static void WarnPropertyObsolete(this RunspaceContext context, string variableName, string propertyName) { context.DebugPropertyObsolete(variableName, propertyName); if (context.Writer == null || !context.Writer.ShouldWriteWarning() || !context.ShouldWarnOnce(WARN_KEY_PROPERTY, variableName, propertyName)) @@ -31,7 +32,7 @@ public static void WarnPropertyObsolete(this RunspaceContext context, string var context.Writer.WriteWarning(PSRuleResources.PropertyObsolete, variableName, propertyName); } - public static void WarnRuleNotFound(this RunspaceContext context) + internal static void WarnRuleNotFound(this RunspaceContext context) { if (context.Writer == null || !context.Writer.ShouldWriteWarning()) return; @@ -39,7 +40,7 @@ public static void WarnRuleNotFound(this RunspaceContext context) context.Writer.WriteWarning(PSRuleResources.RuleNotFound); } - public static void WarnDuplicateRuleName(this RunspaceContext context, string ruleName) + internal static void WarnDuplicateRuleName(this RunspaceContext context, string ruleName) { if (context.Writer == null || !context.Writer.ShouldWriteWarning()) return; @@ -47,7 +48,20 @@ public static void WarnDuplicateRuleName(this RunspaceContext context, string ru context.Writer.WriteWarning(PSRuleResources.DuplicateRuleName, ruleName); } - public static void DebugPropertyObsolete(this RunspaceContext context, string variableName, string propertyName) + internal static void DuplicateResourceId(this RunspaceContext context, ResourceId id, ResourceId duplicateId) + { + if (context == null) + return; + + var action = context.Pipeline.Option.Execution.DuplicateResourceId.GetValueOrDefault(ExecutionOption.Default.DuplicateResourceId.Value); + if (action == ExecutionActionPreference.Error) + throw new RuleException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.DuplicateResourceId, id.Value, duplicateId.Value)); + + else if (action == ExecutionActionPreference.Warn && context.Writer != null && context.Writer.ShouldWriteWarning()) + context.Writer.WriteWarning(PSRuleResources.DuplicateResourceId, id.Value, duplicateId.Value); + } + + internal static void DebugPropertyObsolete(this RunspaceContext context, string variableName, string propertyName) { if (context.Writer == null || !context.Writer.ShouldWriteDebug()) return; @@ -55,7 +69,7 @@ public static void DebugPropertyObsolete(this RunspaceContext context, string va context.Writer.WriteDebug(PSRuleResources.DebugPropertyObsolete, context.RuleBlock.Name, variableName, propertyName); } - public static void WarnAliasReference(this RunspaceContext context, ResourceKind kind, string resourceId, string targetId, string alias) + internal static void WarnAliasReference(this RunspaceContext context, ResourceKind kind, string resourceId, string targetId, string alias) { if (context.Writer == null || !context.Writer.ShouldWriteWarning() || !context.Pipeline.Option.Execution.AliasReferenceWarning.GetValueOrDefault(ExecutionOption.Default.AliasReferenceWarning.Value)) return; @@ -63,7 +77,7 @@ public static void WarnAliasReference(this RunspaceContext context, ResourceKind context.Writer.WriteWarning(PSRuleResources.AliasReference, kind.ToString(), resourceId, targetId, alias); } - public static void WarnAliasSuppression(this RunspaceContext context, string targetId, string alias) + internal static void WarnAliasSuppression(this RunspaceContext context, string targetId, string alias) { if (context.Writer == null || !context.Writer.ShouldWriteWarning() || !context.Pipeline.Option.Execution.AliasReferenceWarning.GetValueOrDefault(ExecutionOption.Default.AliasReferenceWarning.Value)) return; diff --git a/src/PSRule/Configuration/ExecutionActionPreference.cs b/src/PSRule/Configuration/ExecutionActionPreference.cs new file mode 100644 index 0000000000..10a6cac518 --- /dev/null +++ b/src/PSRule/Configuration/ExecutionActionPreference.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Configuration +{ + /// + /// Determines the action to take for execution behaviors. + /// See for the specific behaviors that are configurable. + /// + public enum ExecutionActionPreference + { + /// + /// No preference. + /// This will inherit from the default. + /// + None = 0, + + /// + /// Continue to execute silently. + /// + Ignore = 1, + + /// + /// Continue to execute but log a warning. + /// + Warn = 2, + + /// + /// Generate an error. + /// + Error = 3 + } +} diff --git a/src/PSRule/Configuration/ExecutionOption.cs b/src/PSRule/Configuration/ExecutionOption.cs index a71b86f2ee..6d688b6cea 100644 --- a/src/PSRule/Configuration/ExecutionOption.cs +++ b/src/PSRule/Configuration/ExecutionOption.cs @@ -10,6 +10,9 @@ namespace PSRule.Configuration /// /// Options that configure the execution sandbox. /// + /// + /// See . + /// public sealed class ExecutionOption : IEquatable { private const LanguageMode DEFAULT_LANGUAGEMODE = Configuration.LanguageMode.FullLanguage; @@ -18,15 +21,17 @@ public sealed class ExecutionOption : IEquatable private const bool DEFAULT_SUPPRESSEDRULEWARNING = true; private const bool DEFAULT_ALIASREFERENCEWARNING = true; private const bool DEFAULT_INVARIANTCULTUREWARNING = true; + private const ExecutionActionPreference DEFAULT_DUPLICATERESOURCEID = ExecutionActionPreference.Error; internal static readonly ExecutionOption Default = new() { AliasReferenceWarning = DEFAULT_ALIASREFERENCEWARNING, + DuplicateResourceId = DEFAULT_DUPLICATERESOURCEID, LanguageMode = DEFAULT_LANGUAGEMODE, InconclusiveWarning = DEFAULT_INCONCLUSIVEWARNING, NotProcessedWarning = DEFAULT_NOTPROCESSEDWARNING, SuppressedRuleWarning = DEFAULT_SUPPRESSEDRULEWARNING, - InvariantCultureWarning = DEFAULT_INVARIANTCULTUREWARNING + InvariantCultureWarning = DEFAULT_INVARIANTCULTUREWARNING, }; /// @@ -35,6 +40,7 @@ public sealed class ExecutionOption : IEquatable public ExecutionOption() { AliasReferenceWarning = null; + DuplicateResourceId = null; LanguageMode = null; InconclusiveWarning = null; NotProcessedWarning = null; @@ -52,6 +58,7 @@ public ExecutionOption(ExecutionOption option) return; AliasReferenceWarning = option.AliasReferenceWarning; + DuplicateResourceId = option.DuplicateResourceId; LanguageMode = option.LanguageMode; InconclusiveWarning = option.InconclusiveWarning; NotProcessedWarning = option.NotProcessedWarning; @@ -70,6 +77,7 @@ public bool Equals(ExecutionOption other) { return other != null && AliasReferenceWarning == other.AliasReferenceWarning && + DuplicateResourceId == other.DuplicateResourceId && LanguageMode == other.LanguageMode && InconclusiveWarning == other.InconclusiveWarning && NotProcessedWarning == other.NotProcessedWarning && @@ -84,6 +92,7 @@ public override int GetHashCode() { var hash = 17; hash = hash * 23 + (AliasReferenceWarning.HasValue ? AliasReferenceWarning.Value.GetHashCode() : 0); + hash = hash * 23 + (DuplicateResourceId.HasValue ? DuplicateResourceId.Value.GetHashCode() : 0); hash = hash * 23 + (LanguageMode.HasValue ? LanguageMode.Value.GetHashCode() : 0); hash = hash * 23 + (InconclusiveWarning.HasValue ? InconclusiveWarning.Value.GetHashCode() : 0); hash = hash * 23 + (NotProcessedWarning.HasValue ? NotProcessedWarning.Value.GetHashCode() : 0); @@ -102,6 +111,7 @@ internal static ExecutionOption Combine(ExecutionOption o1, ExecutionOption o2) var result = new ExecutionOption(o1) { AliasReferenceWarning = o1.AliasReferenceWarning ?? o2.AliasReferenceWarning, + DuplicateResourceId = o1.DuplicateResourceId ?? o2.DuplicateResourceId, LanguageMode = o1.LanguageMode ?? o2.LanguageMode, InconclusiveWarning = o1.InconclusiveWarning ?? o2.InconclusiveWarning, NotProcessedWarning = o1.NotProcessedWarning ?? o2.NotProcessedWarning, @@ -117,6 +127,16 @@ internal static ExecutionOption Combine(ExecutionOption o1, ExecutionOption o2) [DefaultValue(null)] public bool? AliasReferenceWarning { get; set; } + /// + /// Determines how to handle duplicate resources identifiers during execution. + /// Regardless of the value, only the first resource will be used. + /// By defaut, an error is thrown. + /// When set to Warn, a warning is generated. + /// When set to Ignore, no output will be displayed. + /// + [DefaultValue(null)] + public ExecutionActionPreference? DuplicateResourceId { get; set; } + /// /// The langauge mode to execute PowerShell code with. /// @@ -152,6 +172,9 @@ internal void Load(EnvironmentHelper env) if (env.TryBool("PSRULE_EXECUTION_ALIASREFERENCEWARNING", out var bvalue)) AliasReferenceWarning = bvalue; + if (env.TryEnum("PSRULE_EXECUTION_DUPLICATERESOURCEID", out ExecutionActionPreference duplicateResourceId)) + DuplicateResourceId = duplicateResourceId; + if (env.TryEnum("PSRULE_EXECUTION_LANGUAGEMODE", out LanguageMode languageMode)) LanguageMode = languageMode; @@ -173,6 +196,9 @@ internal void Load(Dictionary index) if (index.TryPopBool("Execution.AliasReferenceWarning", out var bvalue)) AliasReferenceWarning = bvalue; + if (index.TryPopEnum("Execution.DuplicateResourceId", out ExecutionActionPreference duplicateResourceId)) + DuplicateResourceId = duplicateResourceId; + if (index.TryPopEnum("Execution.LanguageMode", out LanguageMode languageMode)) LanguageMode = languageMode; diff --git a/src/PSRule/Configuration/PSRuleOption.cs b/src/PSRule/Configuration/PSRuleOption.cs index 9bcc1049f7..7b782a3151 100644 --- a/src/PSRule/Configuration/PSRuleOption.cs +++ b/src/PSRule/Configuration/PSRuleOption.cs @@ -25,6 +25,9 @@ namespace PSRule.Configuration /// /// A structure that stores PSRule configuration options. /// + /// + /// See . + /// public sealed class PSRuleOption : IEquatable, IBaselineSpec { private const string DEFAULT_FILENAME = "ps-rule.yaml"; diff --git a/src/PSRule/Host/HostHelper.cs b/src/PSRule/Host/HostHelper.cs index 0b6fb4c4fc..e1fda6342e 100644 --- a/src/PSRule/Host/HostHelper.cs +++ b/src/PSRule/Host/HostHelper.cs @@ -7,7 +7,6 @@ using System.IO; using System.Linq; using System.Management.Automation; -using System.Threading; using Newtonsoft.Json; using PSRule.Annotations; using PSRule.Definitions; @@ -19,7 +18,6 @@ using PSRule.Definitions.SuppressionGroups; using PSRule.Help; using PSRule.Pipeline; -using PSRule.Resources; using PSRule.Rules; using PSRule.Runtime; using YamlDotNet.Core; @@ -45,13 +43,13 @@ internal static IRuleV1[] GetRule(Source[] source, RunspaceContext context, bool internal static RuleHelpInfo[] GetRuleHelp(Source[] source, RunspaceContext context) { - return ToRuleHelp(ToRuleBlockV1(GetLanguageBlock(context, source), context).GetAll(), context); + return ToRuleHelp(ToRuleBlockV1(GetLanguageBlock(context, source), context, skipDuplicateName: true).GetAll(), context); } internal static DependencyGraph GetRuleBlockGraph(Source[] source, RunspaceContext context) { var blocks = GetLanguageBlock(context, source); - var rules = ToRuleBlockV1(blocks, context); + var rules = ToRuleBlockV1(blocks, context, skipDuplicateName: false); Import(GetConventions(blocks, context), context); var builder = new DependencyGraphBuilder(context, includeDependencies: true, includeDisabled: false); builder.Include(rules, filter: (b) => Match(context, b)); @@ -60,7 +58,7 @@ internal static DependencyGraph GetRuleBlockGraph(Source[] source, Ru internal static IEnumerable GetRuleYamlBlocks(Source[] source, RunspaceContext context) { - return ToRuleBlockV1(GetYamlLanguageBlocks(source, context), context).GetAll(); + return ToRuleBlockV1(GetYamlLanguageBlocks(source, context), context, skipDuplicateName: true).GetAll(); } private static IEnumerable GetYamlJsonLanguageBlocks(Source[] source, RunspaceContext context) @@ -398,11 +396,6 @@ public static void InvokeRuleBlock(RunspaceContext context, RuleBlock ruleBlock, //} } - private static RuleException ThrowDuplicateRuleId(IDependencyTarget block) - { - return new RuleException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.DuplicateRuleId, block.Id.Value)); - } - /// /// Convert matching langauge blocks to rules. /// @@ -410,10 +403,24 @@ private static DependencyTargetCollection ToRuleV1(ILanguageBlock[] blo { // 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); + try { 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); + results.TryAdd(new Rule { Id = block.Id, @@ -427,11 +434,21 @@ private static DependencyTargetCollection ToRuleV1(ILanguageBlock[] blo Flags = block.Flags, Extent = block.Extent, }); - //throw ThrowDuplicateRuleId(block); + 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)) + { + context.DuplicateResourceId(block.Id, duplicateId.Value); + continue; + } + + if (knownRuleNames.ContainsNames(block.Id, block.Ref, block.Alias, out var duplicateName)) + context.WarnDuplicateRuleName(duplicateName); + context.EnterSourceScope(block.Source); var info = GetRuleHelpInfo(context, block.Name, block.Synopsis); results.TryAdd(new Rule @@ -447,7 +464,8 @@ private static DependencyTargetCollection ToRuleV1(ILanguageBlock[] blo Flags = block.Flags, Extent = block.Extent, }); - //throw ThrowDuplicateRuleId(block); + knownRuleNames.AddNames(block.Id, block.Ref, block.Alias); + knownRuleIds.AddIds(block.Id, block.Ref, block.Alias); } } finally @@ -457,76 +475,76 @@ private static DependencyTargetCollection ToRuleV1(ILanguageBlock[] blo return results; } - private static DependencyTargetCollection ToRuleBlockV1(ILanguageBlock[] blocks, RunspaceContext context) + 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 seenRuleNames = new HashSet(); - var seenRuleIds = new HashSet(); + var knownRuleNames = new HashSet(StringComparer.OrdinalIgnoreCase); + var knownRuleIds = new HashSet(ResourceIdEqualityComparer.Default); try { foreach (var block in blocks.OfType()) { - var ruleName = block.Name; - var ruleId = block.Id; - - if (seenRuleIds.Contains(ruleId)) - throw ThrowDuplicateRuleId(block); - - else if (seenRuleNames.Contains(ruleName)) - context.WarnDuplicateRuleName(ruleName); - - else + if (knownRuleIds.ContainsIds(block.Id, block.Ref, block.Alias, out var duplicateId)) { - results.TryAdd(block); - seenRuleNames.Add(ruleName); - seenRuleIds.Add(ruleId); + 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); } - foreach (var yaml in blocks.OfType()) + foreach (var block in blocks.OfType()) { - var ruleName = yaml.Name; - var ruleId = yaml.Id; - - if (seenRuleIds.Contains(ruleId)) - throw ThrowDuplicateRuleId(yaml); - - else if (seenRuleNames.Contains(ruleName)) - context.WarnDuplicateRuleName(ruleName); - - else + var ruleName = block.Name; + if (knownRuleIds.ContainsIds(block.Id, block.Ref, block.Alias, out var duplicateId)) { - context.EnterSourceScope(yaml.Source); - var info = GetRuleHelpInfo(context, ruleName, yaml.Synopsis) ?? new RuleHelpInfo( - ruleName, - ruleName, - yaml.Source.Module, - synopsis: new InfoString(yaml.Synopsis) - ); - - var block = new RuleBlock - ( - source: yaml.Source, - id: yaml.Id, - @ref: yaml.Ref, - level: yaml.Level, - info: info, - condition: new RuleVisitor(yaml.Id, yaml.Source, yaml.Spec), - alias: yaml.Alias, - tag: yaml.Metadata.Tags, - dependsOn: null, // TODO: No support for DependsOn yet - configuration: null, // TODO: No support for rule configuration use module or workspace config - extent: null, - flags: yaml.Flags - ); - results.TryAdd(block); - seenRuleNames.Add(ruleName); - seenRuleIds.Add(ruleId); + 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.EnterSourceScope(block.Source); + var info = GetRuleHelpInfo(context, ruleName, block.Synopsis) ?? new RuleHelpInfo( + ruleName, + ruleName, + block.Source.Module, + synopsis: new InfoString(block.Synopsis) + ); + + results.TryAdd(new RuleBlock + ( + source: block.Source, + id: block.Id, + @ref: block.Ref, + level: block.Level, + info: info, + condition: new RuleVisitor(block.Id, block.Source, block.Spec), + alias: block.Alias, + tag: block.Metadata.Tags, + dependsOn: null, // TODO: No support for DependsOn yet + configuration: null, // TODO: No support for rule configuration use module or workspace config + extent: null, + flags: block.Flags + )); + knownRuleNames.AddNames(block.Id, block.Ref, block.Alias); + knownRuleIds.AddIds(block.Id, block.Ref, block.Alias); } } finally diff --git a/src/PSRule/PSRule.psm1 b/src/PSRule/PSRule.psm1 index c1172fcdd5..6f5577e196 100644 --- a/src/PSRule/PSRule.psm1 +++ b/src/PSRule/PSRule.psm1 @@ -1164,6 +1164,16 @@ function New-PSRuleOption { [Alias('ConventionInclude')] [String[]]$Convention, + # Sets the Execution.AliasReferenceWarning option + [Parameter(Mandatory = $False)] + [Alias('ExecutionAliasReferenceWarning')] + [System.Boolean]$AliasReferenceWarning = $True, + + # Sets the Execution.DuplicateResourceId option + [Parameter(Mandatory = $False)] + [Alias('ExecutionDuplicateResourceId')] + [PSRule.Configuration.ExecutionActionPreference]$DuplicateResourceId = [PSRule.Configuration.ExecutionActionPreference]::Error, + # Sets the Execution.InconclusiveWarning option [Parameter(Mandatory = $False)] [Alias('ExecutionInconclusiveWarning')] @@ -1179,10 +1189,6 @@ function New-PSRuleOption { [Alias('ExecutionSuppressedRuleWarning')] [System.Boolean]$SuppressedRuleWarning = $True, - # Sets the Execution.AliasReferenceWarning option - [Parameter(Mandatory = $False)] - [Alias('ExecutionAliasReferenceWarning')] - [System.Boolean]$AliasReferenceWarning = $True, # Sets the Execution.InvariantCultureWarning option [Parameter(Mandatory = $False)] @@ -1438,6 +1444,16 @@ function Set-PSRuleOption { [Alias('ConventionInclude')] [String[]]$Convention, + # Sets the Execution.AliasReferenceWarning option + [Parameter(Mandatory = $False)] + [Alias('ExecutionAliasReferenceWarning')] + [System.Boolean]$AliasReferenceWarning = $True, + + # Sets the Execution.DuplicateResourceId option + [Parameter(Mandatory = $False)] + [Alias('ExecutionDuplicateResourceId')] + [PSRule.Configuration.ExecutionActionPreference]$DuplicateResourceId = [PSRule.Configuration.ExecutionActionPreference]::Error, + # Sets the Execution.InconclusiveWarning option [Parameter(Mandatory = $False)] [Alias('ExecutionInconclusiveWarning')] @@ -1453,11 +1469,6 @@ function Set-PSRuleOption { [Alias('ExecutionSuppressedRuleWarning')] [System.Boolean]$SuppressedRuleWarning = $True, - # Sets the Execution.AliasReferenceWarning option - [Parameter(Mandatory = $False)] - [Alias('ExecutionAliasReferenceWarning')] - [System.Boolean]$AliasReferenceWarning = $True, - # Sets the Execution.InvariantCultureWarning option [Parameter(Mandatory = $False)] [Alias('ExecutionInvariantCultureWarning')] @@ -2155,6 +2166,16 @@ function SetOptions { [Alias('ConventionInclude')] [String[]]$Convention, + # Sets the Execution.AliasReferenceWarning option + [Parameter(Mandatory = $False)] + [Alias('ExecutionAliasReferenceWarning')] + [System.Boolean]$AliasReferenceWarning = $True, + + # Sets the Execution.DuplicateResourceId option + [Parameter(Mandatory = $False)] + [Alias('ExecutionDuplicateResourceId')] + [PSRule.Configuration.ExecutionActionPreference]$DuplicateResourceId = [PSRule.Configuration.ExecutionActionPreference]::Error, + # Sets the Execution.InconclusiveWarning option [Parameter(Mandatory = $False)] [Alias('ExecutionInconclusiveWarning')] @@ -2170,11 +2191,6 @@ function SetOptions { [Alias('ExecutionSuppressedRuleWarning')] [System.Boolean]$SuppressedRuleWarning = $True, - # Sets the Execution.AliasReferenceWarning option - [Parameter(Mandatory = $False)] - [Alias('ExecutionAliasReferenceWarning')] - [System.Boolean]$AliasReferenceWarning = $True, - # Sets the Execution.InvariantCultureWarning option [Parameter(Mandatory = $False)] [Alias('ExecutionInvariantCultureWarning')] @@ -2360,6 +2376,11 @@ function SetOptions { $Option.Execution.AliasReferenceWarning = $AliasReferenceWarning; } + # Sets option Execution.DuplicateResourceId + if ($PSBoundParameters.ContainsKey('DuplicateResourceId')) { + $Option.Execution.DuplicateResourceId = $DuplicateResourceId; + } + # Sets option Execution.InvariantCultureWarning if ($PSBoundParameters.ContainsKey('InvariantCultureWarning')) { $Option.Execution.InvariantCultureWarning = $InvariantCultureWarning; diff --git a/src/PSRule/Pipeline/GetRuleHelpPipeline.cs b/src/PSRule/Pipeline/GetRuleHelpPipeline.cs index 9fb7f1e181..0bb3be50ee 100644 --- a/src/PSRule/Pipeline/GetRuleHelpPipeline.cs +++ b/src/PSRule/Pipeline/GetRuleHelpPipeline.cs @@ -32,8 +32,7 @@ public override IPipelineBuilder Configure(PSRuleOption option) if (option == null) return this; - Option.Execution.LanguageMode = option.Execution.LanguageMode ?? ExecutionOption.Default.LanguageMode; - Option.Execution.InvariantCultureWarning = option.Execution.InvariantCultureWarning ?? ExecutionOption.Default.InvariantCultureWarning; + Option.Execution = GetExecutionOption(option.Execution); Option.Output.Culture = GetCulture(option.Output.Culture); if (option.Rule != null) diff --git a/src/PSRule/Pipeline/GetRulePipelineBuiler.cs b/src/PSRule/Pipeline/GetRulePipelineBuiler.cs index 2ff19e4d4b..609bd9a11b 100644 --- a/src/PSRule/Pipeline/GetRulePipelineBuiler.cs +++ b/src/PSRule/Pipeline/GetRulePipelineBuiler.cs @@ -25,8 +25,7 @@ public override IPipelineBuilder Configure(PSRuleOption option) if (option == null) return this; - Option.Execution.LanguageMode = option.Execution.LanguageMode ?? ExecutionOption.Default.LanguageMode; - Option.Execution.InvariantCultureWarning = option.Execution.InvariantCultureWarning ?? ExecutionOption.Default.InvariantCultureWarning; + Option.Execution = GetExecutionOption(option.Execution); Option.Output.Culture = GetCulture(option.Output.Culture); Option.Output.Format = SuppressFormat(option.Output.Format); Option.Requires = new RequiresOption(option.Requires); @@ -67,4 +66,4 @@ private static OutputFormat SuppressFormat(OutputFormat? format) format == OutputFormat.Yaml) ? OutputFormat.None : format.Value; } } -} \ No newline at end of file +} diff --git a/src/PSRule/Pipeline/InvokePipelineBuilder.cs b/src/PSRule/Pipeline/InvokePipelineBuilder.cs index 80fa3ee8ba..438106d877 100644 --- a/src/PSRule/Pipeline/InvokePipelineBuilder.cs +++ b/src/PSRule/Pipeline/InvokePipelineBuilder.cs @@ -54,11 +54,6 @@ public override IPipelineBuilder Configure(PSRuleOption option) base.Configure(option); - Option.Execution.InconclusiveWarning = option.Execution.InconclusiveWarning ?? ExecutionOption.Default.InconclusiveWarning; - Option.Execution.NotProcessedWarning = option.Execution.NotProcessedWarning ?? ExecutionOption.Default.NotProcessedWarning; - Option.Execution.SuppressedRuleWarning = option.Execution.SuppressedRuleWarning ?? ExecutionOption.Default.SuppressedRuleWarning; - Option.Execution.InvariantCultureWarning = option.Execution.InvariantCultureWarning ?? ExecutionOption.Default.InvariantCultureWarning; - Option.Logging.RuleFail = option.Logging.RuleFail ?? LoggingOption.Default.RuleFail; Option.Logging.RulePass = option.Logging.RulePass ?? LoggingOption.Default.RulePass; Option.Logging.LimitVerbose = option.Logging.LimitVerbose; diff --git a/src/PSRule/Pipeline/PipelineBuilder.cs b/src/PSRule/Pipeline/PipelineBuilder.cs index dca8f3cf66..1856753757 100644 --- a/src/PSRule/Pipeline/PipelineBuilder.cs +++ b/src/PSRule/Pipeline/PipelineBuilder.cs @@ -222,13 +222,12 @@ public virtual IPipelineBuilder Configure(PSRuleOption option) Option.Binding = new BindingOption(option.Binding); Option.Convention = new ConventionOption(option.Convention); - Option.Execution = new ExecutionOption(option.Execution); - Option.Execution.LanguageMode = option.Execution.LanguageMode ?? ExecutionOption.Default.LanguageMode; + Option.Execution = GetExecutionOption(option.Execution); Option.Input = new InputOption(option.Input); - Option.Input.Format = Option.Input.Format ?? InputOption.Default.Format; + Option.Input.Format ??= InputOption.Default.Format; Option.Output = new OutputOption(option.Output); - Option.Output.Outcome = Option.Output.Outcome ?? OutputOption.Default.Outcome; - Option.Output.Banner = Option.Output.Banner ?? OutputOption.Default.Banner; + Option.Output.Outcome ??= OutputOption.Default.Outcome; + Option.Output.Banner ??= OutputOption.Default.Banner; Option.Repository = GetRepository(Option.Repository); return this; } @@ -400,15 +399,29 @@ protected static string[] GetCulture(string[] culture) return result.Count == 0 ? null : result.ToArray(); } - protected static RepositoryOption GetRepository(RepositoryOption repository) + protected static RepositoryOption GetRepository(RepositoryOption option) { - var result = new RepositoryOption(repository); + var result = new RepositoryOption(option); if (string.IsNullOrEmpty(result.Url) && GitHelper.TryRepository(out var url)) result.Url = url; return result; } + /// + /// Coalesce execution options with defaults. + /// + protected static ExecutionOption GetExecutionOption(ExecutionOption option) + { + var result = ExecutionOption.Combine(option, ExecutionOption.Default); + //result.InconclusiveWarning ??= ExecutionOption.Default.InconclusiveWarning; + //result.NotProcessedWarning ??= ExecutionOption.Default.NotProcessedWarning; + //result.SuppressedRuleWarning ??= ExecutionOption.Default.SuppressedRuleWarning; + //result.InvariantCultureWarning ??= ExecutionOption.Default.InvariantCultureWarning; + result.DuplicateResourceId = result.DuplicateResourceId == ExecutionActionPreference.None ? ExecutionOption.Default.DuplicateResourceId.Value : result.DuplicateResourceId; + return result; + } + protected PathFilter GetInputObjectSourceFilter() { return Option.Input.IgnoreObjectSource.GetValueOrDefault(InputOption.Default.IgnoreObjectSource.Value) ? GetInputFilter() : null; diff --git a/src/PSRule/Resources/PSRuleResources.Designer.cs b/src/PSRule/Resources/PSRuleResources.Designer.cs index 881e558509..f6d2da4b2e 100644 --- a/src/PSRule/Resources/PSRuleResources.Designer.cs +++ b/src/PSRule/Resources/PSRuleResources.Designer.cs @@ -224,13 +224,13 @@ internal static string DependencyNotFound } /// - /// Looks up a localized string similar to A rule with the same id '{0}' already exists.. + /// Looks up a localized string similar to The resource '{0}' is using a duplicate resource identifier. A resource with the identifier '{1}' already exists. Each resource must have a unique name, ref, and aliases. See https://aka.ms/ps-rule/naming for guidance on naming within PSRule.. /// - internal static string DuplicateRuleId + internal static string DuplicateResourceId { get { - return ResourceManager.GetString("DuplicateRuleId", resourceCulture); + return ResourceManager.GetString("DuplicateResourceId", resourceCulture); } } diff --git a/src/PSRule/Resources/PSRuleResources.resx b/src/PSRule/Resources/PSRuleResources.resx index 378f86ad61..71168f0047 100644 --- a/src/PSRule/Resources/PSRuleResources.resx +++ b/src/PSRule/Resources/PSRuleResources.resx @@ -163,9 +163,9 @@ The dependency '{0}' for '{1}' could not be found. Check that the rule is defined in a .Rule.ps1 file within the search path. Occurs when a rule dependency is not discovered. - - A rule with the same id '{0}' already exists. - Occurs when the same rule id is used. + + The resource '{0}' is using a duplicate resource identifier. A resource with the identifier '{1}' already exists. Each resource must have a unique name, ref, and aliases. See https://aka.ms/ps-rule/naming for guidance on naming within PSRule. + Occurs when the same resource id is used. A rule with the same name '{0}' already exists. diff --git a/tests/PSRule.Tests/PSRule.Common.Tests.ps1 b/tests/PSRule.Tests/PSRule.Common.Tests.ps1 index 51d15f4a15..81602d889a 100644 --- a/tests/PSRule.Tests/PSRule.Common.Tests.ps1 +++ b/tests/PSRule.Tests/PSRule.Common.Tests.ps1 @@ -2425,7 +2425,11 @@ Describe 'Get-PSRuleHelp' -Tag 'Get-PSRuleHelp', 'Common' { try { Push-Location $searchPath; - { Get-PSRuleHelp -Path $PWD } | Should -Throw "A rule with the same id '.\M1.Rule2' already exists."; + { Get-PSRuleHelp -Path $PWD } | Should -Throw "The resource '.\M1.Rule2' is using a duplicate resource identifier. A resource with the identifier '.\M1.Rule2' already exists. Each resource must have a unique name, ref, and aliases. See https://aka.ms/ps-rule/naming for guidance on naming within PSRule."; + Get-PSRuleHelp -Path $PWD -Option @{ 'Execution.DuplicateResourceId' = 'Warn'; 'Execution.InvariantCultureWarning' = $False } -WarningVariable outWarn -WarningAction SilentlyContinue; + $warnings = @($outWarn); + $warnings.Count | Should -Be 1; + $warnings | Should -Be "The resource '.\M1.Rule2' is using a duplicate resource identifier. A resource with the identifier '.\M1.Rule2' already exists. Each resource must have a unique name, ref, and aliases. See https://aka.ms/ps-rule/naming for guidance on naming within PSRule."; } finally { Pop-Location; diff --git a/tests/PSRule.Tests/PSRule.Options.Tests.ps1 b/tests/PSRule.Tests/PSRule.Options.Tests.ps1 index 07ccddd95e..d58cc4b0c7 100644 --- a/tests/PSRule.Tests/PSRule.Options.Tests.ps1 +++ b/tests/PSRule.Tests/PSRule.Options.Tests.ps1 @@ -778,6 +778,53 @@ Describe 'New-PSRuleOption' -Tag 'Option','New-PSRuleOption' { } } + Context 'Read Execution.DuplicateResourceId' { + It 'from default' { + $option = New-PSRuleOption -Default; + $option.Execution.DuplicateResourceId | Should -Be 'Error' + } + + It 'from Hashtable' { + $option = New-PSRuleOption -Option @{ 'Execution.DuplicateResourceId' = 'warn' }; + $option.Execution.DuplicateResourceId | Should -Be 'Warn'; + + $option = New-PSRuleOption -Option @{ 'Execution.DuplicateResourceId' = 'Warn' }; + $option.Execution.DuplicateResourceId | Should -Be 'Warn'; + } + + It 'from YAML' { + $option = New-PSRuleOption -Option (Join-Path -Path $here -ChildPath 'PSRule.Tests.yml'); + $option.Execution.DuplicateResourceId | Should -Be 'Warn'; + } + + It 'from Environment' { + try { + # With enum + $Env:PSRULE_EXECUTION_DUPLICATERESOURCEID = 'warn'; + $option = New-PSRuleOption; + $option.Execution.DuplicateResourceId | Should -Be 'Warn'; + + # With enum + $Env:PSRULE_EXECUTION_DUPLICATERESOURCEID = 'Warn'; + $option = New-PSRuleOption; + $option.Execution.DuplicateResourceId | Should -Be 'Warn'; + + # With int + $Env:PSRULE_EXECUTION_DUPLICATERESOURCEID = '2'; + $option = New-PSRuleOption; + $option.Execution.DuplicateResourceId | Should -Be 'Warn'; + } + finally { + Remove-Item 'Env:PSRULE_EXECUTION_DUPLICATERESOURCEID' -Force; + } + } + + It 'from parameter' { + $option = New-PSRuleOption -DuplicateResourceId 'Warn' -Path $emptyOptionsFilePath; + $option.Execution.DuplicateResourceId | Should -Be 'Warn'; + } + } + Context 'Read Execution.InvariantCultureWarning' { It 'from default' { $option = New-PSRuleOption -Default; diff --git a/tests/PSRule.Tests/PSRule.Tests.yml b/tests/PSRule.Tests/PSRule.Tests.yml index dfcdc84c06..d151c1d77a 100644 --- a/tests/PSRule.Tests/PSRule.Tests.yml +++ b/tests/PSRule.Tests/PSRule.Tests.yml @@ -49,6 +49,7 @@ binding: # Configure execution options execution: aliasReferenceWarning: false + duplicateResourceId: Warn languageMode: ConstrainedLanguage inconclusiveWarning: false notProcessedWarning: false From 867739c71ea361970bef6ac2d94fb690f70fc681 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Thu, 18 Aug 2022 18:00:25 +1000 Subject: [PATCH 033/156] Fixes exception on resource without synopsis #1230 (#1232) --- docs/CHANGELOG-v2.md | 2 ++ src/PSRule/Common/JsonConverters.cs | 8 ++++---- src/PSRule/Common/JsonReaderExtensions.cs | 15 ++++++++++----- src/PSRule/Definitions/SpecFactory.cs | 2 +- src/PSRule/Host/HostHelper.cs | 15 +++++---------- tests/PSRule.Tests/Baseline.Rule.jsonc | 4 ++-- tests/PSRule.Tests/Baseline.Rule.yaml | 2 +- tests/PSRule.Tests/BaselineTests.cs | 12 ++++++++++++ 8 files changed, 37 insertions(+), 23 deletions(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 402dfca95a..bb5925c650 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,8 @@ What's changed since pre-release v2.4.0-B0022: [#1229](https://github.com/microsoft/PSRule/issues/1229) - Added `Execution.DuplicateResourceId` option to configure PSRule behaviour. - By default, duplicate resource identifiers return an error. + - Fixed exception on JSON baseline without a synopsis by @BernieWhite. + [#1230](https://github.com/microsoft/PSRule/issues/1230) ## v2.4.0-B0022 (pre-release) diff --git a/src/PSRule/Common/JsonConverters.cs b/src/PSRule/Common/JsonConverters.cs index e4481f8c04..6878446bd2 100644 --- a/src/PSRule/Common/JsonConverters.cs +++ b/src/PSRule/Common/JsonConverters.cs @@ -345,7 +345,7 @@ protected override IList CreateProperties(Type type, MemberSeriali } /// - /// A custom deserializer to convert JSON into ResourceObject + /// A custom deserializer to convert JSON into a . /// internal sealed class ResourceObjectJsonConverter : JsonConverter { @@ -385,7 +385,7 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s private IResource MapResource(JsonReader reader, JsonSerializer serializer) { reader.GetSourceExtent(RunspaceContext.CurrentThread.Source.File.Path, out var extent); - reader.SkipComments(); + reader.SkipComments(out _); if (reader.TokenType != JsonToken.StartObject || !reader.Read()) throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed); @@ -672,7 +672,7 @@ private LanguageExpression MapOperator(string type, JsonReader reader) { while (reader.TokenType != JsonToken.EndArray) { - if (reader.SkipComments()) + if (reader.SkipComments(out var hasComments) && hasComments) continue; result.Add(MapExpression(reader)); @@ -800,7 +800,7 @@ private void MapProperty(LanguageExpression.PropertyBag properties, JsonReader r var objects = new List(); while (reader.TokenType != JsonToken.EndArray) { - if (reader.SkipComments()) + if (reader.SkipComments(out var hasComments) && hasComments) continue; var item = reader.ReadAsString(); diff --git a/src/PSRule/Common/JsonReaderExtensions.cs b/src/PSRule/Common/JsonReaderExtensions.cs index 6285368c9d..da140b3c9c 100644 --- a/src/PSRule/Common/JsonReaderExtensions.cs +++ b/src/PSRule/Common/JsonReaderExtensions.cs @@ -57,13 +57,18 @@ public static void Consume(this JsonReader reader, JsonToken token) /// Skip JSON comments. /// [DebuggerStepThrough] - public static bool SkipComments(this JsonReader reader) + public static bool SkipComments(this JsonReader reader, out bool hasComments) { - var hasComments = false; - while (reader.TokenType == JsonToken.Comment && reader.Read()) - hasComments = true; + hasComments = false; + while (reader.TokenType == JsonToken.Comment || reader.TokenType == JsonToken.None) + { + if (reader.TokenType == JsonToken.Comment) + hasComments = true; - return hasComments; + if (!reader.Read()) + return false; + } + return true; } } } diff --git a/src/PSRule/Definitions/SpecFactory.cs b/src/PSRule/Definitions/SpecFactory.cs index f361563e31..9b3148e273 100644 --- a/src/PSRule/Definitions/SpecFactory.cs +++ b/src/PSRule/Definitions/SpecFactory.cs @@ -75,7 +75,7 @@ public SpecDescriptor(string apiVersion, string name) public IResource CreateInstance(SourceFile source, ResourceMetadata metadata, CommentMetadata comment, ISourceExtent extent, object spec) { - var info = new ResourceHelpInfo(metadata.Name, metadata.Name, new InfoString(comment.Synopsis), new InfoString()); + var info = new ResourceHelpInfo(metadata.Name, metadata.Name, new InfoString(comment?.Synopsis), new InfoString()); return (IResource)Activator.CreateInstance(typeof(T), ApiVersion, source, metadata, info, extent, spec); } } diff --git a/src/PSRule/Host/HostHelper.cs b/src/PSRule/Host/HostHelper.cs index e1fda6342e..eea83a859a 100644 --- a/src/PSRule/Host/HostHelper.cs +++ b/src/PSRule/Host/HostHelper.cs @@ -313,14 +313,10 @@ private static ILanguageBlock[] GetJsonLanguageBlocks(Source[] sources, Runspace using var reader = new JsonTextReader(new StreamReader(file.Path)); // Consume lines until start of array - while (reader.TokenType == JsonToken.None || reader.TokenType == JsonToken.Comment) - { - if (!reader.Read()) - break; - } - - if (reader.TokenType == JsonToken.StartArray && reader.Read()) + reader.SkipComments(out _); + if (reader.TryConsume(JsonToken.StartArray)) { + reader.SkipComments(out _); while (reader.TokenType != JsonToken.EndArray) { var value = deserializer.Deserialize(reader); @@ -331,8 +327,7 @@ private static ILanguageBlock[] GetJsonLanguageBlocks(Source[] sources, Runspace } // Consume all end objects at the end of each resource - while (reader.TokenType == JsonToken.EndObject) - reader.Read(); + while (reader.TryConsume(JsonToken.EndObject)) { } } } } @@ -798,7 +793,7 @@ internal static RuleHelpInfo GetRuleHelpInfo(RunspaceContext context, string nam internal static void UpdateHelpInfo(RunspaceContext context, IResource resource) { - if (resource == null || !TryHelpPath(context, resource.Name, out var path) || !TryHelpInfo(path, out var info)) + if (context == null || resource == null || !TryHelpPath(context, resource.Name, out var path) || !TryHelpInfo(path, out var info)) return; resource.Info.Update(info); diff --git a/tests/PSRule.Tests/Baseline.Rule.jsonc b/tests/PSRule.Tests/Baseline.Rule.jsonc index 853592536b..a291d1ba82 100644 --- a/tests/PSRule.Tests/Baseline.Rule.jsonc +++ b/tests/PSRule.Tests/Baseline.Rule.jsonc @@ -86,8 +86,8 @@ } } }, + // Baseline without synopsis. { - // Synopsis: This is an example baseline "apiVersion": "github.com/microsoft/PSRule/v1", "kind": "Baseline", "metadata": { @@ -116,4 +116,4 @@ }, "spec": {} } -] \ No newline at end of file +] diff --git a/tests/PSRule.Tests/Baseline.Rule.yaml b/tests/PSRule.Tests/Baseline.Rule.yaml index b8a57b6223..d70503fd28 100644 --- a/tests/PSRule.Tests/Baseline.Rule.yaml +++ b/tests/PSRule.Tests/Baseline.Rule.yaml @@ -57,8 +57,8 @@ spec: tag: category: group2 +# Baseline without synopsis. --- -# Synopsis: This is an example baseline apiVersion: github.com/microsoft/PSRule/v1 kind: Baseline metadata: diff --git a/tests/PSRule.Tests/BaselineTests.cs b/tests/PSRule.Tests/BaselineTests.cs index 9437a5e67a..25bd24d8f7 100644 --- a/tests/PSRule.Tests/BaselineTests.cs +++ b/tests/PSRule.Tests/BaselineTests.cs @@ -36,6 +36,7 @@ public void ReadBaselineYaml() Assert.Equal("github.com/microsoft/PSRule/v1", baseline[0].ApiVersion); Assert.Equal("value", baseline[0].Metadata.Annotations["key"]); Assert.False(baseline[0].Obsolete); + Assert.Equal("This is an example baseline", baseline[0].Info.Synopsis.Text); var config = baseline[0].Spec.Configuration["key2"] as Array; Assert.NotNull(config); @@ -46,10 +47,15 @@ public void ReadBaselineYaml() pso = config.GetValue(1) as PSObject; Assert.Equal("def", pso.PropertyValue("value2")); + // TestBaseline4 + Assert.Equal("TestBaseline4", baseline[3].Name); + Assert.Null(baseline[3].Info.Synopsis.Text); + // TestBaseline5 Assert.Equal("TestBaseline5", baseline[4].Name); Assert.Equal("github.com/microsoft/PSRule/v1", baseline[4].ApiVersion); Assert.True(baseline[4].Obsolete); + Assert.Equal("This is an example obsolete baseline", baseline[4].Info.Synopsis.Text); } [Fact] @@ -64,6 +70,7 @@ public void ReadBaselineJson() Assert.Equal("github.com/microsoft/PSRule/v1", baseline[0].ApiVersion); Assert.Equal("value", baseline[0].Metadata.Annotations["key"]); Assert.False(baseline[0].Obsolete); + Assert.Equal("This is an example baseline", baseline[0].Info.Synopsis.Text); var config = (JArray)baseline[0].Spec.Configuration["key2"]; Assert.NotNull(config); @@ -73,10 +80,15 @@ public void ReadBaselineJson() Assert.True(config[1].Type == JTokenType.Object); Assert.Equal("def", config[1]["value2"]); + // TestBaseline4 + Assert.Equal("TestBaseline4", baseline[3].Name); + Assert.Null(baseline[3].Info.Synopsis.Text); + // TestBaseline5 Assert.Equal("TestBaseline5", baseline[4].Name); Assert.Equal("github.com/microsoft/PSRule/v1", baseline[4].ApiVersion); Assert.True(baseline[4].Obsolete); + Assert.Equal("This is an example obsolete baseline", baseline[4].Info.Synopsis.Text); } [Theory] From 7ba8cf36abc481a4c4aab609939602f19d5ecd72 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Thu, 18 Aug 2022 18:19:07 +1000 Subject: [PATCH 034/156] Pre-release v2.4.0-B0039 (#1233) --- docs/CHANGELOG-v2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index bb5925c650..7fe06d4eba 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -17,6 +17,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.4.0-B0039 (pre-release) + What's changed since pre-release v2.4.0-B0022: - New features: From fc289d7211d31813989f47c5b8277e35bbd5f701 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Thu, 18 Aug 2022 19:34:58 +1000 Subject: [PATCH 035/156] Minor documentation updates (#1234) --- docs/CHANGELOG-v2.md | 2 +- docs/analysis-output.md | 4 ++-- docs/creating-your-pipeline.md | 2 +- docs/install-instructions.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 7fe06d4eba..0c4296bd67 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -22,7 +22,7 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers What's changed since pre-release v2.4.0-B0022: - New features: - - **Experimental**: Added support for functions within YAML and JSON expressions. + - **Experimental**: Added support for functions within YAML and JSON expressions by @BernieWhite. [#1227](https://github.com/microsoft/PSRule/issues/1227) - Added conversion functions `boolean`, `string`, and `integer`. - Added lookup functions `configuration`, and `path`. diff --git a/docs/analysis-output.md b/docs/analysis-output.md index 99264903ff..7857fa7ba0 100644 --- a/docs/analysis-output.md +++ b/docs/analysis-output.md @@ -29,7 +29,7 @@ The output format can be configuring by setting the `Output.Format` option to on ```yaml hl_lines="5-6" # Analyze and save results - name: Analyze repository - uses: microsoft/ps-rule@v2.1.0 + uses: microsoft/ps-rule@v2.3.2 with: outputFormat: Sarif outputPath: reports/ps-rule-results.sarif @@ -237,7 +237,7 @@ To configure GitHub Actions, perform the following steps: uses: actions/checkout@v3 - name: Run PSRule analysis - uses: microsoft/ps-rule@v2.1.0 + uses: microsoft/ps-rule@v2.3.2 with: outputFormat: Sarif outputPath: reports/ps-rule-results.sarif diff --git a/docs/creating-your-pipeline.md b/docs/creating-your-pipeline.md index 70b90034ff..b18a646352 100644 --- a/docs/creating-your-pipeline.md +++ b/docs/creating-your-pipeline.md @@ -28,7 +28,7 @@ Within the root directory of your IaC repository: # Analyze Azure resources using PSRule for Azure - name: Analyze Azure template files - uses: microsoft/ps-rule@v2.1.0 + uses: microsoft/ps-rule@v2.3.2 with: modules: 'PSRule.Rules.Azure' ``` diff --git a/docs/install-instructions.md b/docs/install-instructions.md index 2f59c6ac17..934a737868 100644 --- a/docs/install-instructions.md +++ b/docs/install-instructions.md @@ -20,7 +20,7 @@ Install and use PSRule with GitHub Actions by referencing the `Microsoft/ps-rule ```yaml - name: Analyze Azure template files - uses: microsoft/ps-rule@v2.1.0 + uses: microsoft/ps-rule@v2.3.2 ``` This will automatically install compatible versions of all dependencies. From 5bf21f582d48acd32babb338885134cb93f4b81a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Aug 2022 21:32:14 +1000 Subject: [PATCH 036/156] Bump mkdocs-material from 8.4.0 to 8.4.1 (#1236) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.4.0 to 8.4.1. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.4.0...8.4.1) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index c348b64f4e..c2f675d3e3 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.3.1 -mkdocs-material==8.4.0 +mkdocs-material==8.4.1 pymdown-extensions==9.5 mike==1.1.2 mkdocs-simple-hooks==0.1.5 From 6d59f18be82252c8d9f8f6bd0c334096a3cf9703 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Aug 2022 10:24:29 +1000 Subject: [PATCH 037/156] Bump mkdocs-redirects from 1.0.6 to 1.1.0 (#1237) Bumps [mkdocs-redirects](https://github.com/datarobot/mkdocs-redirects) from 1.0.6 to 1.1.0. - [Release notes](https://github.com/datarobot/mkdocs-redirects/releases) - [Commits](https://github.com/datarobot/mkdocs-redirects/compare/v1.0.6...v1.1.0) --- updated-dependencies: - dependency-name: mkdocs-redirects dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bernie White --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index c2f675d3e3..89cffd2bed 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -5,4 +5,4 @@ mike==1.1.2 mkdocs-simple-hooks==0.1.5 mkdocs-git-revision-date-plugin==0.3.2 mdx-truly-sane-lists==1.3 -mkdocs-redirects==1.0.6 +mkdocs-redirects==1.1.0 From 85fa989a41c24ea1e95539149f52d3df722520eb Mon Sep 17 00:00:00 2001 From: Bernie White Date: Fri, 26 Aug 2022 15:13:44 +1000 Subject: [PATCH 038/156] Added sub-selectors for YAML and JSON #1024 #1045 (#1238) --- .markdownlint.json | 2 +- .vscode/settings.json | 4 +- docs/CHANGELOG-v2.md | 19 +- docs/expressions/SubSelectors.Rule.jsonc | 74 + docs/expressions/SubSelectors.Rule.yaml | 57 + docs/expressions/functions.md | 12 +- docs/expressions/sub-selectors.md | 272 ++ docs/versioning.md | 37 + mkdocs.yml | 7 +- schemas/PSRule-language.schema.json | 3228 ++++++++++------- .../Common/ConditionResultExtensions.cs | 11 +- src/PSRule/Common/JsonConverters.cs | 70 +- src/PSRule/Common/PSObjectExtensions.cs | 5 - src/PSRule/Common/YamlConverters.cs | 43 +- src/PSRule/Configuration/PipelineHook.cs | 7 +- .../Expressions/ExpressionContext.cs | 16 +- .../Definitions/Expressions/Functions.cs | 2 +- .../Expressions/LanguageExpressions.cs | 110 +- .../Definitions/Expressions/Primitives.cs | 14 +- src/PSRule/Definitions/ILanguageBlock.cs | 17 + src/PSRule/Definitions/Resource.cs | 68 + src/PSRule/Definitions/Rules/Rule.cs | 75 +- src/PSRule/Definitions/Rules/RuleVisitor.cs | 14 +- .../Definitions/Selectors/SelectorVisitor.cs | 7 +- .../SuppressionGroupVisitor.cs | 7 +- src/PSRule/Host/HostHelper.cs | 11 +- src/PSRule/Pipeline/PipelineBuilder.cs | 4 +- src/PSRule/Pipeline/PipelineContext.cs | 5 +- src/PSRule/Pipeline/PipelineHookActions.cs | 61 +- src/PSRule/Pipeline/TargetBinder.cs | 142 +- .../Resources/PSRuleResources.Designer.cs | 764 ++-- src/PSRule/Resources/PSRuleResources.resx | 3 + src/PSRule/Runtime/LanguageScope.cs | 68 +- .../Runtime/ObjectPath/PathExpression.cs | 13 +- .../ObjectPath/PathExpressionBuilder.cs | 56 +- src/PSRule/Runtime/RunspaceContext.cs | 12 +- .../FromFileSubSelector.Rule.jsonc | 66 + .../FromFileSubSelector.Rule.yaml | 52 + tests/PSRule.Tests/FunctionBuilderTests.cs | 2 +- tests/PSRule.Tests/FunctionTests.cs | 2 +- tests/PSRule.Tests/PSRule.Tests.csproj | 6 + tests/PSRule.Tests/RulesTests.cs | 126 +- tests/PSRule.Tests/SelectorTests.cs | 2 +- tests/PSRule.Tests/TargetBinderTests.cs | 19 +- 44 files changed, 3669 insertions(+), 1923 deletions(-) create mode 100644 docs/expressions/SubSelectors.Rule.jsonc create mode 100644 docs/expressions/SubSelectors.Rule.yaml create mode 100644 docs/expressions/sub-selectors.md create mode 100644 docs/versioning.md create mode 100644 tests/PSRule.Tests/FromFileSubSelector.Rule.jsonc create mode 100644 tests/PSRule.Tests/FromFileSubSelector.Rule.yaml diff --git a/.markdownlint.json b/.markdownlint.json index 26eef824f2..e38419f104 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -20,7 +20,7 @@ "no-reversed-links": true, "no-multiple-blanks": true, "line-length": { - "line_length": 100, + "line_length": 120, "code_blocks": false, "tables": false, "headers": true diff --git a/.vscode/settings.json b/.vscode/settings.json index 75e83e463a..199be65f0d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,7 +22,8 @@ "./schemas/PSRule-language.schema.json": [ "/tests/PSRule.Tests/**.Rule.yaml", "/tests/PSRule.Tests/**/**.Rule.yaml", - "/docs/scenarios/*/*.Rule.yaml" + "/docs/scenarios/*/*.Rule.yaml", + "/docs/expressions/**/*.Rule.yaml" ] }, "json.schemas": [ @@ -30,6 +31,7 @@ "fileMatch": [ "/tests/PSRule.Tests/**.Rule.jsonc", "/tests/PSRule.Tests/**/**.Rule.jsonc", + "/docs/expressions/**/*.Rule.jsonc" ], "url": "./schemas/PSRule-resources.schema.json" } diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 0c4296bd67..45bc126391 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -13,10 +13,26 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers **Experimental features**: -- Functions within YAML expressions can be used to perform manipulation prior to testing a condition. +- Functions within YAML and JSON expressions can be used to perform manipulation prior to testing a condition. + See [functions][3] for more information. +- Sub-selectors within YAML and JSON expressions can be used to filter rules and list properties. + See [sub-selectors][4] for more information. + + [3]: expressions/functions.md + [4]: expressions/sub-selectors.md ## Unreleased +What's changed since pre-release v2.4.0-B0039: + +- New features: + - **Experimental**: Added support for sub-selectors YAML and JSON expressions by @BernieWhite. + [#1024](https://github.com/microsoft/PSRule/issues/1024) + [#1045](https://github.com/microsoft/PSRule/issues/1045) + - Sub-selector pre-conditions add an additional expression to determine if a rule is executed. + - Sub-selector object filters provide an way to filter items from list properties. + - See [sub-selectors][4] for more information. + ## v2.4.0-B0039 (pre-release) What's changed since pre-release v2.4.0-B0022: @@ -27,6 +43,7 @@ What's changed since pre-release v2.4.0-B0022: - Added conversion functions `boolean`, `string`, and `integer`. - Added lookup functions `configuration`, and `path`. - Added string functions `concat`, `substring`. + - See [functions][3] for more information. - Bug fixes: - Fixed reporting of duplicate identifiers which were not generating an error for all cases by @BernieWhite. [#1229](https://github.com/microsoft/PSRule/issues/1229) diff --git a/docs/expressions/SubSelectors.Rule.jsonc b/docs/expressions/SubSelectors.Rule.jsonc new file mode 100644 index 0000000000..699cfca155 --- /dev/null +++ b/docs/expressions/SubSelectors.Rule.jsonc @@ -0,0 +1,74 @@ +[ + { + // Synopsis: A rule with a sub-selector precondition. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "Json.Subselector.Precondition" + }, + "spec": { + "where": { + "field": "kind", + "equals": "api" + }, + "condition": { + "field": "resources", + "count": 10 + } + } + }, + { + // Synopsis: A rule with a sub-selector filter. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "Json.Subselector.Filter" + }, + "spec": { + "condition": { + "field": "resources", + "where": { + "type": ".", + "equals": "Microsoft.Web/sites/config" + }, + "allOf": [ + { + "field": "properties.detailedErrorLoggingEnabled", + "equals": true + } + ] + } + } + }, + { + // Synopsis: A rule with a sub-selector filter. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "Json.Subselector.FilterOr" + }, + "spec": { + "condition": { + "anyOf": [ + { + "field": "resources", + "where": { + "type": ".", + "equals": "Microsoft.Web/sites/config" + }, + "allOf": [ + { + "field": "properties.detailedErrorLoggingEnabled", + "equals": true + } + ] + }, + { + "field": "resources", + "exists": false + } + ] + } + } + } +] diff --git a/docs/expressions/SubSelectors.Rule.yaml b/docs/expressions/SubSelectors.Rule.yaml new file mode 100644 index 0000000000..6d373392b3 --- /dev/null +++ b/docs/expressions/SubSelectors.Rule.yaml @@ -0,0 +1,57 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# YAML-based rules for documentation +# + +--- +# Synopsis: A rule with a sub-selector precondition. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Rule +metadata: + name: Yaml.Subselector.Precondition +spec: + where: + field: 'kind' + equals: 'api' + condition: + field: resources + count: 10 + +--- +# Synopsis: A rule with a sub-selector filter. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Rule +metadata: + name: Yaml.Subselector.Filter +spec: + condition: + field: resources + where: + type: '.' + equals: 'Microsoft.Web/sites/config' + allOf: + - field: properties.detailedErrorLoggingEnabled + equals: true + +--- +# Synopsis: A rule with a sub-selector filter. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Rule +metadata: + name: Yaml.Subselector.FilterOr +spec: + condition: + anyOf: + + - field: resources + where: + type: '.' + equals: 'Microsoft.Web/sites/config' + allOf: + - field: properties.detailedErrorLoggingEnabled + equals: true + + - field: resources + exists: false diff --git a/docs/expressions/functions.md b/docs/expressions/functions.md index 124ed9f1d1..deab0deda1 100644 --- a/docs/expressions/functions.md +++ b/docs/expressions/functions.md @@ -1,16 +1,22 @@ -# Expression functions +# Functions !!! Abstract - Functions are an advanced lanaguage feature specific to YAML and JSON resources. + _Functions_ are an advanced lanaguage feature specific to YAML and JSON expressions. That extend the language to allow for more complex use cases with expressions. + Functions don't apply to script expressions because PowerShell already has rich support for complex manipulation. !!! Experimental - Functions are a work in progress and subject to change. + _Functions_ are a work in progress and subject to change. We hope to add more functions, broader support, and more detailed documentation in the future. [Join or start a disucssion][1] to let us know how we can improve this feature going forward. [1]: https://github.com/microsoft/PSRule/discussions +Functions cover two (2) main scenarios: + +- **Transformation** — you need to perform minor transformation before a condition. +- **Configuration** — you want to configure an input into a condition. + ## Using functions It may be necessary to perform minor transformation before evaluating a condition. diff --git a/docs/expressions/sub-selectors.md b/docs/expressions/sub-selectors.md new file mode 100644 index 0000000000..c502f90f20 --- /dev/null +++ b/docs/expressions/sub-selectors.md @@ -0,0 +1,272 @@ +# Sub-selectors + +!!! Abstract + This topic covers _sub-selectors_ which are a PSRule language feature specific to YAML and JSON expressions. + They are useful for filtering out objects that you do not want to evaluate. + Sub-selectors don't apply to script expressions because PowerShell already has rich support for filtering. + +!!! Experimental + _Sub-selectors_ are a work in progress and subject to change. + We hope to add broader support, and more detailed documentation in the future. + [Join or start a disucssion][1] to let us know how we can improve this feature going forward. + + [1]: https://github.com/microsoft/PSRule/discussions + +Sub-selectors cover two (2) main scenarios: + +- **Pre-conditions** — you want to filtering out objects before a rule is run. +- **Object filtering** — you want to limit a condition to specific elements in a list of items. + +## Pre-conditions + +PSRule can process many different types of objects. +Rules however, are normally written to test a specific property or type of object. +So it is important that rules only run on objects that you want to evaluate. +Pre-condition sub-selectors are one way you can determine if a rule should be run. + +To use a sub-selector as a pre-condition, use the `where` property, directly under the `spec`. +The expressions in the sub-selector follow the same form that you can use in rules. + +For example: + +=== "YAML" + + ```yaml hl_lines="8-10" + --- + # Synopsis: A rule with a sub-selector precondition. + apiVersion: github.com/microsoft/PSRule/v1 + kind: Rule + metadata: + name: Yaml.Subselector.Precondition + spec: + where: + field: 'kind' + equals: 'api' + condition: + field: resources + count: 10 + ``` + +=== "JSON" + + ```json hl_lines="9-12" + { + // Synopsis: A rule with a sub-selector precondition. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "Json.Subselector.Precondition" + }, + "spec": { + "where": { + "field": "kind", + "equals": "api" + }, + "condition": { + "field": "resources", + "count": 10 + } + } + } + ``` + +In the example: + +1. The `where` property is the start of a sub-selector. +2. The sub-selector checks if the `kind` property equals `api`. + +The rule does not run if the: + +- The object does not have a `kind` property. **OR** +- The value of the `kind` property is not `api`. + +!!! Tip + Other types of pre-conditions also exist that allow you to filter based on type or by a shared selector. + +## Object filter + +When you are evaluating an object, you can use sub-selectors to limit the condition. +This is helpful when dealing with properties that are a list of items. +Properties that contain a list of items may contain a sub-set of items that you want to evaluate. + +For example, the object may look like this: + +=== "YAML" + + ```yaml + name: app1 + type: Microsoft.Web/sites + resources: + - name: web + type: Microsoft.Web/sites/config + properties: + detailedErrorLoggingEnabled: true + ``` + +=== "JSON" + + ```json + { + "name": "app1", + "type": "Microsoft.Web/sites", + "resources": [ + { + "name": "web", + "type": "Microsoft.Web/sites/config", + "properties": { + "detailedErrorLoggingEnabled": true + } + } + ] + } + ``` + +A rule to test if any sub-resources with the `detailedErrorLoggingEnabled` set to `true` exist might look like this: + +=== "YAML" + + ```yaml hl_lines="10-12" + --- + # Synopsis: A rule with a sub-selector filter. + apiVersion: github.com/microsoft/PSRule/v1 + kind: Rule + metadata: + name: Yaml.Subselector.Filter + spec: + condition: + field: resources + where: + type: '.' + equals: 'Microsoft.Web/sites/config' + allOf: + - field: properties.detailedErrorLoggingEnabled + equals: true + ``` + +=== "JSON" + + ```json + { + // Synopsis: A rule with a sub-selector filter. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "Json.Subselector.Filter" + }, + "spec": { + "condition": { + "field": "resources", + "where": { + "type": ".", + "equals": "Microsoft.Web/sites/config" + }, + "allOf": [ + { + "field": "properties.detailedErrorLoggingEnabled", + "equals": true + } + ] + } + } + } + ``` + +In the example: + +- If the array property `resources` exists, any items with a type of `Microsoft.Web/sites/config` are evaluated. + - Each item must have the `properties.detailedErrorLoggingEnabled` property set to `true` to pass. + - Items without the `properties.detailedErrorLoggingEnabled` property fail. + - Items with the `properties.detailedErrorLoggingEnabled` property set to a value other then `true` fail. +- If the `resources` property does not exist, the rule fails. +- If the `resources` property exists but has 0 items of type `Microsoft.Web/sites/config`, the rule fails. +- If the `resources` property exists and has any items of type `Microsoft.Web/sites/config` but any fail, the rule fails. +- If the `resources` property exists and has any items of type `Microsoft.Web/sites/config` and all pass, the rule passes. + +### When there are no results + +Given the example, is important to understand what happens if: + +- The `resources` property doesn't exist. +- The `resources` property doesn't contain any items that match the sub-selector condition. + +In either of these two cases, the sub-selector will return `false` and fail the rule. +The rule fails because there is no secondary conditions that could be used instead. + +If this was not the desired behavior, you could: + +- Use a pre-condition to avoid running the rule. +- Group the sub-selector into a `anyOf`, and provide a secondary condition. + +For example: + +=== "YAML" + + ```yaml hl_lines="9 11-14 19-20" + --- + # Synopsis: A rule with a sub-selector filter. + apiVersion: github.com/microsoft/PSRule/v1 + kind: Rule + metadata: + name: Yaml.Subselector.FilterOr + spec: + condition: + anyOf: + + - field: resources + where: + type: '.' + equals: 'Microsoft.Web/sites/config' + allOf: + - field: properties.detailedErrorLoggingEnabled + equals: true + + - field: resources + exists: false + ``` + +=== "JSON" + + ```json hl_lines="10 12-16 25-26" + { + // Synopsis: A rule with a sub-selector filter. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "Json.Subselector.FilterOr" + }, + "spec": { + "condition": { + "anyOf": [ + { + "field": "resources", + "where": { + "type": ".", + "equals": "Microsoft.Web/sites/config" + }, + "allOf": [ + { + "field": "properties.detailedErrorLoggingEnabled", + "equals": true + } + ] + }, + { + "field": "resources", + "exists": false + } + ] + } + } + } + ``` + +In the example: + +- If the array property `resources` exists, any items with a type of `Microsoft.Web/sites/config` are evaluated. + - Each item must have the `properties.detailedErrorLoggingEnabled` property set to `true` to pass. + - Items without the `properties.detailedErrorLoggingEnabled` property fail. + - Items with the `properties.detailedErrorLoggingEnabled` property set to a value other then `true` fail. +- If the `resources` property does not exist, the rule passes. +- If the `resources` property exists but has 0 items of type `Microsoft.Web/sites/config`, the rule fails. +- If the `resources` property exists and has any items of type `Microsoft.Web/sites/config` but any fail, the rule fails. +- If the `resources` property exists and has any items of type `Microsoft.Web/sites/config` and all pass, the rule passes. diff --git a/docs/versioning.md b/docs/versioning.md new file mode 100644 index 0000000000..00bfa70464 --- /dev/null +++ b/docs/versioning.md @@ -0,0 +1,37 @@ +# Changes and versioning + +PSRule uses [semantic versioning][1] to declare breaking changes. +The latest module version can be installed from the PowerShell Gallery. +For a list of module changes please see the [change log][2]. + + [1]: https://semver.org/ + [2]: https://aka.ms/ps-rule/changelog + +## Pre-releases + +Pre-release module versions are created on major commits and can be installed from the PowerShell Gallery. +Module versions and change log details for pre-releases will be removed as stable releases are made available. + +!!! Important + Pre-release versions should be considered work in progress. + These releases should not be used in production. + We may introduce breaking changes between a pre-release as we work towards a stable version release. + +## Experimental features + +From time to time we may ship experimential features. +These features are generally marked experimential in the change log as these features ship. +Experimental features may ship in stable releases, however to use them you may need to: + +- Enabled or explictly reference them. + +!!! Important + Experimental features should be considered work in progress. + These features may be incomplete and should not be used in production. + We may introduce breaking changes for experimental features as we work towards a general release for the feature. + +## Reporting bugs + +If you experience an issue with an pre-release or experimental feature please let us know by logging an issue as a [bug][3]. + + [3]: https://github.com/microsoft/PSRule/issues diff --git a/mkdocs.yml b/mkdocs.yml index 240ab34e5f..2da2e1241e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -29,6 +29,7 @@ theme: level: 1 - navigation.tabs - content.code.annotate + - content.tabs.link nav: - Home: index.md @@ -55,8 +56,9 @@ nav: - Azure resource tagging example: scenarios/azure-tags/azure-tags.md - Kubernetes resource validation example: scenarios/kubernetes-resources/kubernetes-resources.md - Concepts: - - Expressions: - - Functions: expressions/functions.md + - Functions: expressions/functions.md + - Sub-selectors: expressions/sub-selectors.md + - Scenarios: - Using within continuous integration: scenarios/validation-pipeline/validation-pipeline.md # - Troubleshooting: troubleshooting.md - License and contributing: license-contributing.md @@ -68,6 +70,7 @@ nav: - v0: 'CHANGELOG-v0.md' - Upgrade notes: upgrade-notes.md - Deprecations: deprecations.md + - Changes and versioning: versioning.md - Support: support.md # - Setup: # - Configuring options: setup/configuring-options.md diff --git a/schemas/PSRule-language.schema.json b/schemas/PSRule-language.schema.json index e4346d5dac..e7bc131ad2 100644 --- a/schemas/PSRule-language.schema.json +++ b/schemas/PSRule-language.schema.json @@ -458,7 +458,7 @@ "title": "If", "description": "A condition is made up of one or more expressions that will determine if an object is selected by the selector.", "markdownDescription": "A condition is made up of one or more [expressions](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/) that will determine if an object is selected by the selector. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Selectors/)", - "$ref": "#/definitions/selectorExpression" + "$ref": "#/definitions/expressions" } }, "required": [ @@ -520,7 +520,7 @@ }, "if": { "type": "object", - "$ref": "#/definitions/selectorExpression" + "$ref": "#/definitions/expressions" } }, "required": [ @@ -577,8 +577,12 @@ "type": "object", "title": "Condition", "description": "A condition is made up of one or more expressions that will determine if the rule passes or fails.", - "markdownDescription": "A condition is made up of one or more [expressions](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/) that will determine if the rule passes or fails. [See help](https://microsoft.github.io/PSRule/v2/authoring/writing-rules/)", - "$ref": "#/definitions/selectorExpression" + "markdownDescription": "A condition is made up of one or more [expressions](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/) that will determine if the rule passes or fails.\n\n[See help](https://microsoft.github.io/PSRule/v2/authoring/writing-rules/)", + "oneOf": [ + { + "$ref": "#/definitions/expressions" + } + ] }, "level": { "type": "string", @@ -597,7 +601,8 @@ "title": "Type pre-condition", "description": "This rule only applies to objects that match the specifies types.", "items": { - "type": "string" + "type": "string", + "default": "" }, "uniqueItems": true }, @@ -606,15 +611,30 @@ "title": "Selector pre-condition", "description": "This rule only applies to objects that match the specified selectors.", "items": { - "type": "string" + "type": "string", + "default": "" }, "uniqueItems": true + }, + "where": { + "type": "object", + "title": "Sub-selector pre-condition", + "description": "The rule only applies to objects that match the sub-selector condition.", + "markdownDescription": "The rule only applies to objects that match the sub-selector condition.\n\n[See help](https://microsoft.github.io/PSRule/v2/expressions/sub-selectors/)", + "oneOf": [ + { + "$ref": "#/definitions/expressions" + } + ] } }, "required": [ "condition" ], - "additionalProperties": false + "additionalProperties": false, + "default": { + "condition": {} + } }, "ruleMetadata": { "type": "object", @@ -666,1338 +686,2122 @@ "name" ] }, - "selectorExpression": { - "type": "object", + "selectorExpressionValueMultiString": { "oneOf": [ { - "$ref": "#/definitions/selectorOperator" + "type": "string" }, { - "$ref": "#/definitions/selectorCondition" + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true } ] }, - "selectorOperator": { - "type": "object", + "selectorExpressionValue": { "oneOf": [ { - "$ref": "#/definitions/selectorOperatorAllOf" + "type": "string", + "ztitle": "Value from string", + "zdescription": "A value to compare.", + "default": "" + }, + { + "type": "boolean", + "ztitle": "Value from boolean", + "zdescription": "A value to compare.", + "default": true + }, + { + "type": "integer", + "ztitle": "Value from integer", + "zdescription": "A value to compare.", + "default": 0 }, { - "$ref": "#/definitions/selectorOperatorAnyOf" + "type": "object", + "ztitle": "Value for object", + "zdescription": "A value to compare.", + "not": { + "propertyNames": { + "enum": [ + "$" + ] + } + } }, { - "$ref": "#/definitions/selectorOperatorNot" + "$ref": "#/definitions/fn" } ] }, - "selectorCondition": { + "expressions": { "type": "object", "oneOf": [ { - "$ref": "#/definitions/selectorConditionExists" + "$ref": "#/definitions/expressions/definitions/operators/definitions/allOf" }, { - "$ref": "#/definitions/selectorConditionEquals" + "$ref": "#/definitions/expressions/definitions/operators/definitions/anyOf" }, { - "$ref": "#/definitions/selectorConditionNotEquals" + "$ref": "#/definitions/expressions/definitions/operators/definitions/not" }, { - "$ref": "#/definitions/selectorConditionHasValue" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/exists" }, { - "$ref": "#/definitions/selectorConditionMatch" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/equals" }, { - "$ref": "#/definitions/selectorConditionNotMatch" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notEquals" }, { - "$ref": "#/definitions/selectorConditionIn" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/hasValue" }, { - "$ref": "#/definitions/selectorConditionNotIn" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/match" }, { - "$ref": "#/definitions/selectorConditionSetOf" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notMatch" }, { - "$ref": "#/definitions/selectorConditionSubset" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/in" }, { - "$ref": "#/definitions/selectorConditionCount" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notIn" }, { - "$ref": "#/definitions/selectorConditionNotCount" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/setOf" }, { - "$ref": "#/definitions/selectorConditionLess" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/subset" }, { - "$ref": "#/definitions/selectorConditionLessOrEquals" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/count" }, { - "$ref": "#/definitions/selectorConditionGreater" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notCount" }, { - "$ref": "#/definitions/selectorConditionGreaterOrEquals" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/less" }, { - "$ref": "#/definitions/selectorConditionStartsWith" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/lessOrEquals" }, { - "$ref": "#/definitions/selectorConditionNotStartsWith" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/greater" }, { - "$ref": "#/definitions/selectorConditionEndsWith" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/greaterOrEquals" }, { - "$ref": "#/definitions/selectorConditionNotEndsWith" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/startsWith" }, { - "$ref": "#/definitions/selectorConditionContains" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notStartsWith" }, { - "$ref": "#/definitions/selectorConditionNotContains" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/endsWith" }, { - "$ref": "#/definitions/selectorConditionIsString" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notEndsWith" }, { - "$ref": "#/definitions/selectorConditionIsLower" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/contains" }, { - "$ref": "#/definitions/selectorConditionIsArray" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notContains" }, { - "$ref": "#/definitions/selectorConditionIsBoolean" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isString" }, { - "$ref": "#/definitions/selectorConditionIsDateTime" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isLower" }, { - "$ref": "#/definitions/selectorConditionIsInteger" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isArray" }, { - "$ref": "#/definitions/selectorConditionIsNumeric" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isBoolean" }, { - "$ref": "#/definitions/selectorConditionIsUpper" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isDateTime" }, { - "$ref": "#/definitions/selectorConditionHasSchema" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isInteger" }, { - "$ref": "#/definitions/selectorConditionVersion" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isNumeric" }, { - "$ref": "#/definitions/selectorConditionHasDefault" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isUpper" }, { - "$ref": "#/definitions/selectorConditionWithinPath" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/hasSchema" }, { - "$ref": "#/definitions/selectorConditionNotWithinPath" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/version" }, { - "$ref": "#/definitions/selectorConditionLike" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/hasDefault" }, { - "$ref": "#/definitions/selectorConditionNotLike" - } - ] - }, - "selectorProperties": { - "oneOf": [ + "$ref": "#/definitions/expressions/definitions/conditions/definitions/withinPath" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notWithinPath" + }, { - "$ref": "#/definitions/selectorPropertyField" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/like" }, { - "$ref": "#/definitions/selectorPropertyValue" + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notLike" } - ] - }, - "selectorPropertiesString": { - "oneOf": [ + ], + "defaultSnippets": [ { - "$ref": "#/definitions/selectorPropertyField" + "label": "field", + "description": "The object path of a field to compare.", + "markdownDescription": "The object path of a field to compare.", + "body": { + "field": "${1}" + } }, { - "$ref": "#/definitions/selectorPropertyValue" + "label": "value", + "body": { + "value": "${1}" + } }, { - "$ref": "#/definitions/selectorPropertyType" + "label": "name", + "body": { + "name": "." + } }, { - "$ref": "#/definitions/selectorPropertyName" + "label": "type", + "body": { + "type": "." + } }, { - "$ref": "#/definitions/selectorPropertySource" - } - ] - }, - "selectorPropertyField": { - "properties": { - "field": { - "type": "string", - "title": "Field", - "description": "The object path of a field to compare.", - "markdownDescription": "The object path of a field to compare. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#field)", - "default": "." - } - }, - "required": [ - "field" - ] - }, - "selectorPropertyValue": { - "properties": { - "value": { - "$ref": "#/definitions/selectorExpressionValue" - } - }, - "required": [ - "value" - ] - }, - "selectorPropertyType": { - "properties": { - "type": { - "type": "string", - "title": "Type", - "description": "The target type of the object.", - "markdownDescription": "The target type of the object. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#type)", - "default": "." - } - }, - "required": [ - "type" - ] - }, - "selectorPropertyName": { - "properties": { - "name": { - "type": "string", - "title": "Name", - "description": "The target name of the object.", - "markdownDescription": "The target name of the object. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#name)", - "default": "." - } - }, - "required": [ - "name" - ] - }, - "selectorPropertySource": { - "properties": { - "source": { - "type": "string", - "title": "Source", - "description": "The source of the object currently being processed by the pipeline.", - "markdownDescription": "The source of the object currently being processed by the pipeline. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#source)" - } - }, - "required": [ - "source" - ] - }, - "selectorOperatorAllOf": { - "type": "object", - "properties": { - "allOf": { - "type": "array", - "title": "All Of", - "description": "All of the expressions must be true.", - "markdownDescription": "All of the expressions must be true. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#allof)", - "items": { - "$ref": "#/definitions/selectorExpression" - } - } - }, - "required": [ - "allOf" - ], - "additionalProperties": false - }, - "selectorOperatorAnyOf": { - "type": "object", - "properties": { - "anyOf": { - "type": "array", - "title": "Any Of", - "description": "One of the expressions must be true.", - "markdownDescription": "All of the expressions must be true. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#anyof)", - "items": { - "$ref": "#/definitions/selectorExpression" + "label": "source", + "body": { + "source": "${1}" } - } - }, - "required": [ - "anyOf" - ], - "additionalProperties": false - }, - "selectorOperatorNot": { - "type": "object", - "properties": { - "not": { - "type": "object", - "title": "Not", - "description": "The nested expression must not be true.", - "markdownDescription": "The nested expression must not be true. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#not)", - "$ref": "#/definitions/selectorExpression" - } - }, - "required": [ - "not" - ] - }, - "selectorConditionExists": { - "type": "object", - "properties": { - "exists": { - "type": "boolean", - "title": "Exists", - "description": "Must have the named field.", - "markdownDescription": "Must have the named field. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#exists)" - }, - "field": { - "type": "string", - "title": "Field", - "description": "The path of the field.", - "markdownDescription": "The path of the field. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#field)" - } - }, - "required": [ - "exists", - "field" - ] - }, - "selectorConditionEquals": { - "type": "object", - "properties": { - "equals": { - "title": "Equals", - "description": "Must have the specified value.", - "markdownDescription": "Must have the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#equals)", - "$ref": "#/definitions/selectorExpressionValue" - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type of compared operand.", - "markdownDescription": "Convert type of compared operand. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#equals)", - "default": false }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive. Only applies to string values.", - "markdownDescription": "Determines if comparing values is case-sensitive. Only applies to string values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#equals)", - "default": false - } - }, - "required": [ - "equals" - ], - "oneOf": [ { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionNotEquals": { - "type": "object", - "properties": { - "notEquals": { - "title": "Not Equals", - "description": "Must not have the specified value.", - "markdownDescription": "Must not have the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notequals)", - "$ref": "#/definitions/selectorExpressionValue" - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type of compared operand.", - "markdownDescription": "Convert type of compared operand. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notequals)", - "default": false + "label": "allOf", + "body": { + "allOf": [ + "${1}" + ] + } }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive. Only applies to string values.", - "markdownDescription": "Determines if comparing values is case-sensitive. Only applies to string values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notequals)", - "default": false - } - }, - "required": [ - "notEquals" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionHasValue": { - "type": "object", - "properties": { - "hasValue": { - "type": "boolean", - "title": "Has Value", - "description": "Must have a non-empty value.", - "markdownDescription": "Must have a non-empty value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasvalue)" - } - }, - "required": [ - "hasValue" - ], - "oneOf": [ { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionMatch": { - "type": "object", - "properties": { - "match": { - "type": "string", - "title": "Match", - "description": "Must match the regular expression.", - "markdownDescription": "Must match the regular expression. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#match)" - } - }, - "required": [ - "match" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionNotMatch": { - "type": "object", - "properties": { - "notMatch": { - "type": "string", - "title": "Not Match", - "description": "Must not match the regular expression.", - "markdownDescription": "Must not match the regular expression. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notmatch)" - } - }, - "required": [ - "notMatch" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionIn": { - "type": "object", - "properties": { - "in": { - "type": "array", - "title": "In", - "description": "Must equal one of the specified values.", - "markdownDescription": "Must equal one of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#in)" - } - }, - "required": [ - "in" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionNotIn": { - "type": "object", - "properties": { - "notIn": { - "type": "array", - "title": "Not In", - "description": "Must not equal any of the specified values.", - "markdownDescription": "Must not equal any of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notin)" - } - }, - "required": [ - "notIn" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionSetOf": { - "type": "object", - "properties": { - "setOf": { - "type": "array", - "title": "SetOf", - "description": "Must include all of but only specified values.", - "markdownDescription": "Must include all of but only values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#setof)" + "label": "anyOf", + "body": { + "anyOf": [ + "${1}" + ] + } }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#setof)", - "default": false + { + "label": "not", + "body": { + "not": {} + } } - }, - "required": [ - "setOf" ], - "oneOf": [ - { - "$ref": "#/definitions/selectorProperties" - } - ] - }, - "selectorConditionSubset": { - "type": "object", - "properties": { - "subset": { - "type": "array", - "title": "Subset", - "description": "Must include all of the specified values.", - "markdownDescription": "Must include all of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#subset)" - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#subset)", - "default": false - }, - "unique": { - "type": "boolean", - "title": "Unique", - "description": "Determines if each of the field values must be unique.", - "markdownDescription": "Determines if each of the field values must be unique. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#subset)", - "default": false - } - }, - "required": [ - "subset" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorProperties" - } - ] - }, - "selectorConditionCount": { - "type": "object", - "properties": { - "count": { - "type": "integer", - "title": "Count", - "description": "Must include all of the specified values.", - "markdownDescription": "Must include all of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#count)", - "minimum": 0 - } - }, - "required": [ - "count" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorProperties" - } - ] - }, - "selectorConditionNotCount": { - "type": "object", - "properties": { - "notCount": { - "type": "integer", - "title": "NotCount", - "description": "Determines if operand does not have number of items.", - "markdownDescription": "Determines if operand does not have number of items. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notcount)", - "minimum": 0 - } - }, - "required": [ - "notCount" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorProperties" - } - ] - }, - "selectorConditionLess": { - "type": "object", - "properties": { - "less": { - "type": "integer", - "title": "Less", - "description": "Must be less then the specified value.", - "markdownDescription": "Must be less then the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)" - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type of compared operand.", - "markdownDescription": "Convert type of compared operand. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", - "default": false - } - }, - "required": [ - "less" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionLessOrEquals": { - "type": "object", - "properties": { - "lessOrEquals": { - "type": "integer", - "title": "Less or Equal to", - "description": "Must be less or equal to the specified value.", - "markdownDescription": "Must be less or equal to the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#lessorequals)" - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type of compared operand.", - "markdownDescription": "Convert type of compared operand. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", - "default": false - } - }, - "required": [ - "lessOrEquals" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionGreater": { - "type": "object", - "properties": { - "greater": { - "title": "Greater", - "description": "Must be greater then the specified value.", - "markdownDescription": "Must be greater then the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#greater)", + "definitions": { + "operands": { "oneOf": [ { - "type": "integer" + "$ref": "#/definitions/expressions/definitions/operands/definitions/field" + }, + { + "$ref": "#/definitions/expressions/definitions/operands/definitions/value" + }, + { + "$ref": "#/definitions/expressions/definitions/operands/definitions/type" + }, + { + "$ref": "#/definitions/expressions/definitions/operands/definitions/name" }, { + "$ref": "#/definitions/expressions/definitions/operands/definitions/source" + } + ], + "definitions": { + "string-only": { + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands/definitions/field" + }, + { + "$ref": "#/definitions/expressions/definitions/operands/definitions/value" + }, + { + "$ref": "#/definitions/expressions/definitions/operands/definitions/type" + }, + { + "$ref": "#/definitions/expressions/definitions/operands/definitions/name" + }, + { + "$ref": "#/definitions/expressions/definitions/operands/definitions/source" + } + ] + }, + "field": { + "type": "object", + "properties": { + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "where": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/where" + } + }, + "required": [ + "field" + ] + }, + "value": { + "type": "object", + "properties": { + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "value" + ] + }, + "type": { + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + } + }, + "required": [ + "type" + ] + }, + "name": { + "type": "object", + "properties": { + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + } + }, + "required": [ + "name" + ] + }, + "source": { "type": "object", - "$ref": "#/definitions/fn" + "properties": { + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "source" + ] } - ] - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type of compared operand.", - "markdownDescription": "Convert type of compared operand. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", - "default": false - } - }, - "required": [ - "greater" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionGreaterOrEquals": { - "type": "object", - "properties": { - "greaterOrEquals": { - "type": "integer", - "title": "Greater or Equal to", - "description": "Must be greater or equal to the specified value.", - "markdownDescription": "Must be greater or equal to the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#greaterorequals)" - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type of compared operand.", - "markdownDescription": "Convert type of compared operand. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", - "default": false - } - }, - "required": [ - "greaterOrEquals" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionStartsWith": { - "type": "object", - "properties": { - "startsWith": { - "title": "Starts with", - "description": "Must start with one of the specified values.", - "markdownDescription": "Must start with one of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#startswith)", - "$ref": "#/definitions/selectorExpressionValueMultiString" - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type to string.", - "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#startswith)", - "default": false - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#startswith)", - "default": false - } - }, - "required": [ - "startsWith" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionNotStartsWith": { - "type": "object", - "properties": { - "notStartsWith": { - "title": "Not starts with", - "description": "Must not start with any of the specified values.", - "markdownDescription": "Must not start with any of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notstartswith)", - "$ref": "#/definitions/selectorExpressionValueMultiString" - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type to string.", - "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notstartswith)", - "default": false - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notstartswith)", - "default": false - } - }, - "required": [ - "notStartsWith" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionEndsWith": { - "type": "object", - "properties": { - "endsWith": { - "title": "Ends with", - "description": "Must end with one of the specified values.", - "markdownDescription": "Must end with one of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#endswith)", - "$ref": "#/definitions/selectorExpressionValueMultiString" - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type to string.", - "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#endswith)", - "default": false - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#endswith)", - "default": false - } - }, - "required": [ - "endsWith" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionNotEndsWith": { - "type": "object", - "properties": { - "notEndsWith": { - "title": "Not Ends with", - "description": "Must not end with any of the specified values.", - "markdownDescription": "Must not end with any of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notendswith)", - "$ref": "#/definitions/selectorExpressionValueMultiString" + } }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type to string.", - "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notendswith)", - "default": false + "properties": { + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands/definitions/field" + }, + { + "$ref": "#/definitions/expressions/definitions/operands/definitions/value" + }, + { + "$ref": "#/definitions/expressions/definitions/operands/definitions/name" + }, + { + "$ref": "#/definitions/expressions/definitions/operands/definitions/type" + }, + { + "$ref": "#/definitions/expressions/definitions/operands/definitions/source" + } + ], + "definitions": { + "field": { + "type": "string", + "title": "Field", + "description": "The object path of a field to compare.", + "markdownDescription": "The object path of a field to compare.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#field)", + "minLength": 1 + }, + "where": { + "type": "object", + "title": "Sub-selector filter", + "description": "Limits the condition to matching items.", + "markdownDescription": "Limits the condition to matching items.\n\n[See help](https://microsoft.github.io/PSRule/v2/expressions/sub-selectors/)", + "allOf": [ + { + "$ref": "#/definitions/expressions" + } + ] + }, + "value": { + "allOf": [ + { + "$ref": "#/definitions/selectorExpressionValue" + } + ] + }, + "name": { + "type": "string", + "title": "Name", + "description": "The target name of the object.", + "markdownDescription": "The target name of the object. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#name)", + "default": ".", + "enum": [ + "." + ] + }, + "type": { + "type": "string", + "title": "Type", + "description": "The target type of the object.", + "markdownDescription": "The target type of the object. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#type)", + "default": ".", + "enum": [ + "." + ] + }, + "source": { + "type": "string", + "title": "Source", + "description": "The source of the object currently being processed by the pipeline.", + "markdownDescription": "The source of the object currently being processed by the pipeline. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#source)", + "default": "" + } + } }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notendswith)", - "default": false - } - }, - "required": [ - "notEndsWith" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionContains": { - "type": "object", - "properties": { - "contains": { - "title": "Contains", - "description": "Must contain one of the specified values.", - "markdownDescription": "Must contain one of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#contains)", - "$ref": "#/definitions/selectorExpressionValueMultiString" - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type to string.", - "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#contains)", - "default": false - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#contains)", - "default": false - } - }, - "required": [ - "contains" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionNotContains": { - "type": "object", - "properties": { - "notContains": { - "title": "Not Contains", - "description": "Must not contain any of the specified values.", - "markdownDescription": "Must not contain any of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notcontains)", - "$ref": "#/definitions/selectorExpressionValueMultiString" - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type to string.", - "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notcontains)", - "default": false - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notcontains)", - "default": false - } - }, - "required": [ - "notContains" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionIsString": { - "type": "object", - "properties": { - "isString": { - "type": "boolean", - "title": "Is string", - "description": "Must be a string type.", - "markdownDescription": "Must be a string type. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isstring)" - } - }, - "required": [ - "isString" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionIsArray": { - "type": "object", - "properties": { - "isArray": { - "type": "boolean", - "title": "Is array", - "description": "Must be an array type.", - "markdownDescription": "Must be an array type. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isarray)" - } - }, - "required": [ - "isArray" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionIsBoolean": { - "type": "object", - "properties": { - "isBoolean": { - "type": "boolean", - "title": "Is boolean", - "description": "Must be a boolean type.", - "markdownDescription": "Must be a boolean type. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isboolean)" - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type to boolean.", - "markdownDescription": "Convert type to boolean. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isboolean)", - "default": false - } - }, - "required": [ - "isBoolean" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionIsDateTime": { - "type": "object", - "properties": { - "isDateTime": { - "type": "boolean", - "title": "Is datetime", - "description": "Must be a datetime type.", - "markdownDescription": "Must be a datetime type. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isdatetime)" - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type to datetime.", - "markdownDescription": "Convert type to datetime. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isdatetime)", - "default": false - } - }, - "required": [ - "isDateTime" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionIsInteger": { - "type": "object", - "properties": { - "isInteger": { - "type": "boolean", - "title": "Is integer", - "description": "Must be an integer type.", - "markdownDescription": "Must be an integer type. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isinteger)" - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type to integer.", - "markdownDescription": "Convert type to integer. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isinteger)", - "default": false - } - }, - "required": [ - "isInteger" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionIsNumeric": { - "type": "object", - "properties": { - "isNumeric": { - "type": "boolean", - "title": "Is numeric", - "description": "Must be a numeric type.", - "markdownDescription": "Must be a numeric type. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isnumeric)" - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type to numeric.", - "markdownDescription": "Convert type to numeric. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isnumeric)", - "default": false - } - }, - "required": [ - "isNumeric" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionIsLower": { - "type": "object", - "properties": { - "isLower": { - "type": "boolean", - "title": "Is Lowercase", - "description": "Must be a lowercase string.", - "markdownDescription": "Must be a lowercase string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#islower)" - } - }, - "required": [ - "isLower" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionIsUpper": { - "type": "object", - "properties": { - "isUpper": { - "type": "boolean", - "title": "Is Uppercase", - "description": "Must be an uppercase string.", - "markdownDescription": "Must be an uppercase string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isupper)" - } - }, - "required": [ - "isUpper" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionHasSchema": { - "type": "object", - "properties": { - "hasSchema": { - "type": "array", - "title": "Has schema", - "description": "Must use one of the specified schemas of the value of $schema. If an empty array is specified any non-empty $schema can be specified.", - "markdownDescription": "Must use one of the specified schemas of the value of `$schema`. If an empty array is specified any non-empty `$schema` can be specified [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasschema)", - "default": [], - "items": { - "type": "string", - "title": "Has schema", - "description": "Must use one of the specified schemas of the value of $schema. If an empty array is specified any non-empty $schema can be specified.", - "markdownDescription": "Must use one of the specified schemas of the value of `$schema`. If an empty array is specified any non-empty `$schema` can be specified [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasschema)", - "minLength": 1 - }, - "uniqueItems": true - }, - "ignoreScheme": { - "type": "boolean", - "title": "Ignore scheme", - "description": "Determines comparing values is case-sensitive.", - "markdownDescription": "Determines comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasschema)", - "default": false - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing schemas is case-sensitive.", - "markdownDescription": "Determines if comparing schemas is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasschema)", - "default": false - } - }, - "required": [ - "hasSchema" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorProperties" - } - ] - }, - "selectorConditionVersion": { - "type": "object", - "properties": { - "version": { - "type": "string", - "title": "Version", - "description": "Must be a valid semantic version. A constraint can optionally be provided to require the semantic version to be within a range.", - "markdownDescription": "Must be a valid semantic version. A constraint can optionally be provided to require the semantic version to be within a range. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#version)", - "default": "" - }, - "includePrerelease": { - "type": "boolean", - "title": "Include pre-release", - "description": "Determines if pre-release versions are included. By default, pre-release versions are not included.", - "markdownDescription": "Determines if pre-release versions are included. By default, pre-release versions are not included. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#version)", - "default": false - } - }, - "required": [ - "version" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorProperties" - } - ] - }, - "selectorConditionHasDefault": { - "type": "object", - "properties": { - "hasDefault": { - "title": "Has Default", - "description": "The field must either not exist or be set to the configured value.", - "markdownDescription": "The field must either not exist or be set to the configured value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasdefault)", - "$ref": "#/definitions/selectorExpressionValue" - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasdefault)", - "default": false - } - }, - "required": [ - "hasDefault" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorProperties" - } - ] - }, - "selectorConditionWithinPath": { - "type": "object", - "properties": { - "withinPath": { - "type": "array", - "title": "Within Path", - "description": "The file path must exist within the required paths.", - "markdownDescription": "The file path must exist within the required paths. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#withinpath)", - "default": [], - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#withinpath)", - "default": false - } - }, - "required": [ - "withinPath" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionNotWithinPath": { - "type": "object", - "properties": { - "notWithinPath": { - "type": "array", - "title": "Not Within Path", - "description": "The file path must not exist within the required paths.", - "markdownDescription": "The file path must not exist within the required paths. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notwithinpath)", - "default": [], - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notwithinpath)", - "default": false - } - }, - "required": [ - "notWithinPath" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionLike": { - "type": "object", - "properties": { - "like": { - "title": "Like", - "description": "Must match any of the specified wildcard patterns.", - "markdownDescription": "Must match any of the specified wildcard patterns. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#like)", - "$ref": "#/definitions/selectorExpressionValueMultiString" - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type to string.", - "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#like)", - "default": false - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#like)", - "default": false - } - }, - "required": [ - "like" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorConditionNotLike": { - "type": "object", - "properties": { - "notLike": { - "title": "Not like", - "description": "Must not match any of the specified wildcard patterns.", - "markdownDescription": "Must not match any of the specified wildcard patterns. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notlike)", - "$ref": "#/definitions/selectorExpressionValueMultiString" - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type to string.", - "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notlike)", - "default": false - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notlike)", - "default": false - } - }, - "required": [ - "notLike" - ], - "oneOf": [ - { - "$ref": "#/definitions/selectorPropertiesString" - } - ] - }, - "selectorExpressionValueMultiString": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true - } - ] - }, - "selectorExpressionValue": { - "oneOf": [ - { - "type": "string", - "title": "Value from string", - "description": "A value to compare." - }, - { - "type": "boolean", - "title": "Value from boolean", - "description": "A value to compare." - }, - { - "type": "integer", - "title": "Value from integer", - "description": "A value to compare." + "conditions": { + "type": "object", + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/exists" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/equals" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notEquals" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/hasValue" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/match" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notMatch" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/in" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notIn" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/setOf" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/subset" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/count" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notCount" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/less" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/lessOrEquals" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/greater" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/greaterOrEquals" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/startsWith" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notStartsWith" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/endsWith" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notEndsWith" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/contains" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notContains" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isString" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isLower" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isArray" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isBoolean" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isDateTime" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isInteger" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isNumeric" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isUpper" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/hasSchema" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/version" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/hasDefault" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/withinPath" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notWithinPath" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/like" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notLike" + } + ], + "definitions": { + "equals": { + "type": "object", + "title": "equals", + "description": "Must have the specified value.", + "markdownDescription": "Must have the specified value.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#equals)", + "properties": { + "equals": { + "title": "Equals", + "description": "Must have the specified value.", + "markdownDescription": "Must have the specified value.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#equals)", + "default": "", + "oneOf": [ + { + "$ref": "#/definitions/selectorExpressionValue" + } + ] + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type of compared operand.", + "markdownDescription": "Convert type of compared operand.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#equals)", + "default": false + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive. Only applies to string values.", + "markdownDescription": "Determines if comparing values is case-sensitive. Only applies to string values.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#equals)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "equals" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ] + }, + "count": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "title": "Count", + "description": "Must include the specified number of values.", + "markdownDescription": "Must include the specified number of values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#count)", + "minimum": 0 + } + }, + "required": [ + "count" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ] + }, + "exists": { + "type": "object", + "properties": { + "exists": { + "type": "boolean", + "title": "Exists", + "description": "Must have the named field.", + "markdownDescription": "Must have the named field. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#exists)", + "default": true + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + } + }, + "required": [ + "exists", + "field" + ] + }, + "notEquals": { + "type": "object", + "properties": { + "notEquals": { + "title": "Not Equals", + "description": "Must not have the specified value.", + "markdownDescription": "Must not have the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notequals)", + "default": "", + "oneOf": [ + { + "$ref": "#/definitions/selectorExpressionValue" + } + ] + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type of compared operand.", + "markdownDescription": "Convert type of compared operand. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notequals)", + "default": false + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive. Only applies to string values.", + "markdownDescription": "Determines if comparing values is case-sensitive. Only applies to string values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notequals)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "notEquals" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ] + }, + "hasValue": { + "type": "object", + "properties": { + "hasValue": { + "type": "boolean", + "title": "Has Value", + "description": "Must have a non-empty value.", + "markdownDescription": "Must have a non-empty value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasvalue)", + "default": true + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + } + }, + "required": [ + "hasValue" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ] + }, + "match": { + "type": "object", + "properties": { + "match": { + "type": "string", + "title": "Match", + "description": "Must match the regular expression.", + "markdownDescription": "Must match the regular expression. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#match)", + "default": "" + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "match" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ] + }, + "notMatch": { + "type": "object", + "properties": { + "notMatch": { + "type": "string", + "title": "Not Match", + "description": "Must not match the regular expression.", + "markdownDescription": "Must not match the regular expression. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notmatch)", + "default": "" + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "notMatch" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ] + }, + "in": { + "type": "object", + "properties": { + "in": { + "type": "array", + "title": "In", + "description": "Must equal one of the specified values.", + "markdownDescription": "Must equal one of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#in)", + "default": [ + "" + ], + "uniqueItems": true + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "in" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ] + }, + "notIn": { + "type": "object", + "properties": { + "notIn": { + "type": "array", + "title": "Not In", + "description": "Must not equal any of the specified values.", + "markdownDescription": "Must not equal any of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notin)", + "default": [ + "" + ], + "uniqueItems": true + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "notIn" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ] + }, + "setOf": { + "type": "object", + "properties": { + "setOf": { + "type": "array", + "title": "SetOf", + "description": "Must include all of but only specified values.", + "markdownDescription": "Must include all of but only values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#setof)", + "default": [ + "" + ], + "uniqueItems": true + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive.", + "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#setof)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "setOf" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/properties" + } + ] + }, + "subset": { + "type": "object", + "properties": { + "subset": { + "type": "array", + "title": "Subset", + "description": "Must include all of the specified values.", + "markdownDescription": "Must include all of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#subset)", + "default": [ + "" + ], + "uniqueItems": true + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive.", + "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#subset)", + "default": false + }, + "unique": { + "type": "boolean", + "title": "Unique", + "description": "Determines if each of the field values must be unique.", + "markdownDescription": "Determines if each of the field values must be unique. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#subset)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "subset" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/properties" + } + ] + }, + "notCount": { + "type": "object", + "properties": { + "notCount": { + "type": "integer", + "title": "NotCount", + "description": "Determines if operand does not have number of items.", + "markdownDescription": "Determines if operand does not have number of items. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notcount)", + "minimum": 0, + "default": 0 + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "notCount" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/properties" + } + ], + "additionalProperties": false + }, + "less": { + "type": "object", + "properties": { + "less": { + "title": "Less", + "description": "Must be less then the specified value.", + "markdownDescription": "Must be less then the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", + "default": 0, + "oneOf": [ + { + "type": "integer" + }, + { + "type": "object", + "$ref": "#/definitions/fn" + } + ] + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type of compared operand.", + "markdownDescription": "Convert type of compared operand. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "less" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "lessOrEquals": { + "type": "object", + "properties": { + "lessOrEquals": { + "title": "Less or Equal to", + "description": "Must be less or equal to the specified value.", + "markdownDescription": "Must be less or equal to the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#lessorequals)", + "default": 0, + "oneOf": [ + { + "type": "integer" + }, + { + "type": "object", + "$ref": "#/definitions/fn" + } + ] + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type of compared operand.", + "markdownDescription": "Convert type of compared operand. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "lessOrEquals" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "greater": { + "type": "object", + "properties": { + "greater": { + "title": "Greater", + "description": "Must be greater then the specified value.", + "markdownDescription": "Must be greater then the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#greater)", + "default": 0, + "oneOf": [ + { + "type": "integer" + }, + { + "type": "object", + "$ref": "#/definitions/fn" + } + ] + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type of compared operand.", + "markdownDescription": "Convert type of compared operand. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "greater" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "greaterOrEquals": { + "type": "object", + "properties": { + "greaterOrEquals": { + "title": "Greater or Equal to", + "description": "Must be greater or equal to the specified value.", + "markdownDescription": "Must be greater or equal to the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#greaterorequals)", + "default": 0, + "oneOf": [ + { + "type": "integer" + }, + { + "type": "object", + "$ref": "#/definitions/fn" + } + ] + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type of compared operand.", + "markdownDescription": "Convert type of compared operand. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "greaterOrEquals" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "startsWith": { + "type": "object", + "properties": { + "startsWith": { + "title": "Starts with", + "description": "Must start with one of the specified values.", + "markdownDescription": "Must start with one of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#startswith)", + "$ref": "#/definitions/selectorExpressionValueMultiString", + "default": "" + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type to string.", + "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#startswith)", + "default": false + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive.", + "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#startswith)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "startsWith" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "notStartsWith": { + "type": "object", + "properties": { + "notStartsWith": { + "title": "Not starts with", + "description": "Must not start with any of the specified values.", + "markdownDescription": "Must not start with any of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notstartswith)", + "$ref": "#/definitions/selectorExpressionValueMultiString" + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type to string.", + "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notstartswith)", + "default": false + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive.", + "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notstartswith)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "notStartsWith" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "endsWith": { + "type": "object", + "properties": { + "endsWith": { + "title": "Ends with", + "description": "Must end with one of the specified values.", + "markdownDescription": "Must end with one of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#endswith)", + "$ref": "#/definitions/selectorExpressionValueMultiString", + "default": "" + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type to string.", + "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#endswith)", + "default": false + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive.", + "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#endswith)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "endsWith" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "notEndsWith": { + "type": "object", + "properties": { + "notEndsWith": { + "title": "Not Ends with", + "description": "Must not end with any of the specified values.", + "markdownDescription": "Must not end with any of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notendswith)", + "$ref": "#/definitions/selectorExpressionValueMultiString" + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type to string.", + "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notendswith)", + "default": false + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive.", + "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notendswith)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "notEndsWith" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "contains": { + "type": "object", + "title": "contains", + "description": "Must contain one of the specified values.", + "markdownDescription": "Must contain one of the specified values.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#contains)", + "properties": { + "contains": { + "title": "Contains", + "description": "Must contain one of the specified values.", + "markdownDescription": "Must contain one of the specified values.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#contains)", + "default": "", + "allOf": [ + { + "$ref": "#/definitions/selectorExpressionValueMultiString" + } + ] + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type to string.", + "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#contains)", + "default": false + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive.", + "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#contains)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "contains" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false, + "minProperties": 2 + }, + "notContains": { + "type": "object", + "properties": { + "notContains": { + "title": "Not Contains", + "description": "Must not contain any of the specified values.", + "markdownDescription": "Must not contain any of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notcontains)", + "$ref": "#/definitions/selectorExpressionValueMultiString" + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type to string.", + "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notcontains)", + "default": false + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive.", + "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notcontains)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "notContains" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "isString": { + "type": "object", + "properties": { + "isString": { + "type": "boolean", + "title": "Is string", + "description": "Must be a string type.", + "markdownDescription": "Must be a string type. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isstring)", + "default": true + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "isString" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "isArray": { + "type": "object", + "properties": { + "isArray": { + "type": "boolean", + "title": "Is array", + "description": "Must be an array type.", + "markdownDescription": "Must be an array type. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isarray)", + "default": true + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "isArray" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "isBoolean": { + "type": "object", + "properties": { + "isBoolean": { + "type": "boolean", + "title": "Is boolean", + "description": "Must be a boolean type.", + "markdownDescription": "Must be a boolean type. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isboolean)", + "default": true + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type to boolean.", + "markdownDescription": "Convert type to boolean. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isboolean)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "isBoolean" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "isDateTime": { + "type": "object", + "properties": { + "isDateTime": { + "type": "boolean", + "title": "Is datetime", + "description": "Must be a datetime type.", + "markdownDescription": "Must be a datetime type. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isdatetime)", + "default": true + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type to datetime.", + "markdownDescription": "Convert type to datetime. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isdatetime)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "isDateTime" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "isInteger": { + "type": "object", + "properties": { + "isInteger": { + "type": "boolean", + "title": "Is integer", + "description": "Must be an integer type.", + "markdownDescription": "Must be an integer type. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isinteger)", + "default": true + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type to integer.", + "markdownDescription": "Convert type to integer. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isinteger)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "isInteger" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "isNumeric": { + "type": "object", + "properties": { + "isNumeric": { + "type": "boolean", + "title": "Is numeric", + "description": "Must be a numeric type.", + "markdownDescription": "Must be a numeric type. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isnumeric)", + "default": true + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type to numeric.", + "markdownDescription": "Convert type to numeric. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isnumeric)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "isNumeric" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false, + "minProperties": 2 + }, + "isLower": { + "type": "object", + "properties": { + "isLower": { + "type": "boolean", + "title": "Is Lowercase", + "description": "Must be a lowercase string.", + "markdownDescription": "Must be a lowercase string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#islower)", + "default": true + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "isLower" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false, + "minProperties": 2 + }, + "isUpper": { + "type": "object", + "properties": { + "isUpper": { + "type": "boolean", + "title": "Is Uppercase", + "description": "Must be an uppercase string.", + "markdownDescription": "Must be an uppercase string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isupper)", + "default": true + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "isUpper" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false, + "minProperties": 2 + }, + "hasSchema": { + "type": "object", + "properties": { + "hasSchema": { + "type": "array", + "title": "Has schema", + "description": "Must use one of the specified schemas of the value of $schema. If an empty array is specified any non-empty $schema can be specified.", + "markdownDescription": "Must use one of the specified schemas of the value of `$schema`. If an empty array is specified any non-empty `$schema` can be specified [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasschema)", + "default": [], + "items": { + "type": "string", + "title": "Has schema", + "description": "Must use one of the specified schemas of the value of $schema. If an empty array is specified any non-empty $schema can be specified.", + "markdownDescription": "Must use one of the specified schemas of the value of `$schema`. If an empty array is specified any non-empty `$schema` can be specified [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasschema)", + "minLength": 1 + }, + "uniqueItems": true + }, + "ignoreScheme": { + "type": "boolean", + "title": "Ignore scheme", + "description": "Determines comparing values is case-sensitive.", + "markdownDescription": "Determines comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasschema)", + "default": false + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing schemas is case-sensitive.", + "markdownDescription": "Determines if comparing schemas is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasschema)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "hasSchema" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/properties" + } + ], + "additionalProperties": false, + "minProperties": 2 + }, + "version": { + "type": "object", + "properties": { + "version": { + "type": "string", + "title": "Version", + "description": "Must be a valid semantic version. A constraint can optionally be provided to require the semantic version to be within a range.", + "markdownDescription": "Must be a valid semantic version. A constraint can optionally be provided to require the semantic version to be within a range. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#version)", + "default": "" + }, + "includePrerelease": { + "type": "boolean", + "title": "Include pre-release", + "description": "Determines if pre-release versions are included. By default, pre-release versions are not included.", + "markdownDescription": "Determines if pre-release versions are included. By default, pre-release versions are not included. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#version)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "version" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/properties" + } + ], + "additionalProperties": false, + "minProperties": 2 + }, + "hasDefault": { + "type": "object", + "properties": { + "hasDefault": { + "title": "Has Default", + "description": "The field must either not exist or be set to the configured value.", + "markdownDescription": "The field must either not exist or be set to the configured value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasdefault)", + "default": "", + "oneOf": [ + { + "$ref": "#/definitions/selectorExpressionValue" + } + ] + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive.", + "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasdefault)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "hasDefault" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/properties" + } + ], + "additionalProperties": false, + "minProperties": 2 + }, + "withinPath": { + "type": "object", + "properties": { + "withinPath": { + "type": "array", + "title": "Within Path", + "description": "The file path must exist within the required paths.", + "markdownDescription": "The file path must exist within the required paths. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#withinpath)", + "default": [], + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive.", + "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#withinpath)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "withinPath" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false, + "maxProperties": 2 + }, + "notWithinPath": { + "type": "object", + "properties": { + "notWithinPath": { + "type": "array", + "title": "Not Within Path", + "description": "The file path must not exist within the required paths.", + "markdownDescription": "The file path must not exist within the required paths. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notwithinpath)", + "default": [], + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive.", + "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notwithinpath)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "notWithinPath" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false, + "minProperties": 2 + }, + "like": { + "type": "object", + "properties": { + "like": { + "title": "Like", + "description": "Must match any of the specified wildcard patterns.", + "markdownDescription": "Must match any of the specified wildcard patterns. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#like)", + "$ref": "#/definitions/selectorExpressionValueMultiString" + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type to string.", + "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#like)", + "default": false + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive.", + "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#like)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "like" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false, + "minProperties": 2 + }, + "notLike": { + "type": "object", + "properties": { + "notLike": { + "title": "Not like", + "description": "Must not match any of the specified wildcard patterns.", + "markdownDescription": "Must not match any of the specified wildcard patterns. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notlike)", + "$ref": "#/definitions/selectorExpressionValueMultiString" + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type to string.", + "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notlike)", + "default": false + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive.", + "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notlike)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "notLike" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false, + "minProperties": 2 + } + } }, - { + "operators": { "type": "object", - "title": "Value for object", - "description": "A value to compare.", - "not": { - "propertyNames": { - "enum": [ - "$" - ] + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operators/definitions/allOf" + }, + { + "$ref": "#/definitions/expressions/definitions/operators/definitions/anyOf" + }, + { + "$ref": "#/definitions/expressions/definitions/operators/definitions/not" + } + ], + "definitions": { + "allOf": { + "type": "object", + "title": "allOf", + "description": "All of the expressions must be true.", + "markdownDescription": "All of the expressions must be true.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#allof)", + "properties": { + "allOf": { + "type": "array", + "title": "AllOf", + "description": "All of the expressions must be true.", + "markdownDescription": "All of the expressions must be true.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#allof)", + "items": { + "$ref": "#/definitions/expressions" + } + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "where": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/where" + } + }, + "required": [ + "allOf" + ], + "oneOf": [ + { + "properties": { + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "where": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/where" + } + } + } + ], + "additionalProperties": false + }, + "anyOf": { + "type": "object", + "properties": { + "anyOf": { + "type": "array", + "title": "AnyOf", + "description": "One of the expressions must be true.", + "markdownDescription": "All of the expressions must be true.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#anyof)", + "items": { + "$ref": "#/definitions/expressions" + } + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "where": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/where" + } + }, + "required": [ + "anyOf" + ], + "oneOf": [ + { + "properties": { + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "where": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/where" + } + } + } + ], + "additionalProperties": false + }, + "not": { + "type": "object", + "properties": { + "not": { + "type": "object", + "title": "Not", + "description": "The nested expression must not be true.", + "markdownDescription": "The nested expression must not be true.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#not)", + "$ref": "#/definitions/expressions" + } + }, + "required": [ + "not" + ], + "additionalProperties": false } } - }, - { - "$ref": "#/definitions/fn" } - ] + } }, "fn": { - "title": "Value from function", - "description": "A function expression that once evaluated specifies the value.", - "markdownDescription": "A function expression that once evaluated specifies the value.", + "ztitle": "Value from function", + "zdescription": "A function expression that once evaluated specifies the value.", + "zmarkdownDescription": "A function expression that once evaluated specifies the value.", "properties": { "$": { "type": "object", diff --git a/src/PSRule/Common/ConditionResultExtensions.cs b/src/PSRule/Common/ConditionResultExtensions.cs index 62523f3d24..5980c6dff5 100644 --- a/src/PSRule/Common/ConditionResultExtensions.cs +++ b/src/PSRule/Common/ConditionResultExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using PSRule.Definitions; @@ -9,12 +9,17 @@ internal static class ConditionResultExtensions { public static bool AllOf(this IConditionResult result) { - return result.Count > 0 && result.Pass == result.Count; + return result != null && result.Count > 0 && result.Pass == result.Count; } public static bool AnyOf(this IConditionResult result) { - return result.Pass > 0; + return result != null && result.Pass > 0; + } + + public static bool Skipped(this IConditionResult result) + { + return result == null; } } } diff --git a/src/PSRule/Common/JsonConverters.cs b/src/PSRule/Common/JsonConverters.cs index 6878446bd2..7f4c30afea 100644 --- a/src/PSRule/Common/JsonConverters.cs +++ b/src/PSRule/Common/JsonConverters.cs @@ -645,7 +645,7 @@ public override bool CanConvert(Type objectType) public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { - var expression = MapOperator(OPERATOR_IF, reader); + var expression = MapOperator(OPERATOR_IF, null, null, reader); return new LanguageIf(expression); } @@ -657,40 +657,44 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s /// /// Map an operator. /// - private LanguageExpression MapOperator(string type, JsonReader reader) + private LanguageOperator MapOperator(string type, LanguageExpression.PropertyBag properties, LanguageExpression subselector, JsonReader reader) { - if (TryExpression(type, out LanguageOperator result)) + if (TryExpression(type, properties, out LanguageOperator result)) { // If and Not - if (reader.TokenType == JsonToken.StartObject) + if (reader.TryConsume(JsonToken.StartObject)) { result.Add(MapExpression(reader)); - reader.Read(); + //reader.Consume(JsonToken.EndObject); } // AllOf and AnyOf - else if (reader.TokenType == JsonToken.StartArray && reader.Read()) + else if (reader.TryConsume(JsonToken.StartArray)) { while (reader.TokenType != JsonToken.EndArray) { if (reader.SkipComments(out var hasComments) && hasComments) continue; - result.Add(MapExpression(reader)); - reader.Read(); + if (reader.TryConsume(JsonToken.StartObject)) + { + result.Add(MapExpression(reader)); + reader.Consume(JsonToken.EndObject); + } } - reader.Read(); + reader.Consume(JsonToken.EndArray); } + result.Subselector = subselector; } return result; } private LanguageExpression MapCondition(string type, LanguageExpression.PropertyBag properties, JsonReader reader) { - if (TryExpression(type, out LanguageCondition result)) + if (TryExpression(type, null, out LanguageCondition result)) { while (reader.TokenType != JsonToken.EndObject) { - MapProperty(properties, reader, out _); + MapProperty(properties, reader, out _, out _); } result.Add(properties); } @@ -701,7 +705,7 @@ private LanguageExpression MapExpression(JsonReader reader) { LanguageExpression result = null; var properties = new LanguageExpression.PropertyBag(); - MapProperty(properties, reader, out var key); + MapProperty(properties, reader, out var key, out var subselector); if (key != null && TryCondition(key)) { result = MapCondition(key, properties, reader); @@ -709,7 +713,12 @@ private LanguageExpression MapExpression(JsonReader reader) else if ((reader.TokenType == JsonToken.StartObject || reader.TokenType == JsonToken.StartArray) && TryOperator(key)) { - result = MapOperator(key, reader); + var op = MapOperator(key, properties, subselector, reader); + MapProperty(properties, reader, out _, out subselector); + if (subselector != null) + op.Subselector = subselector; + + result = op; } return result; } @@ -766,13 +775,14 @@ private object MapSequence(string name, JsonReader reader) return result.ToArray(); } - private void MapProperty(LanguageExpression.PropertyBag properties, JsonReader reader, out string name) + private void MapProperty(LanguageExpression.PropertyBag properties, JsonReader reader, out string name, out LanguageExpression subselector) { - if (reader.TokenType != JsonToken.StartObject || !reader.Read()) - throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed); + //if (reader.TokenType != JsonToken.StartObject || !reader.Read()) + // throw new PipelineSerializationException(PSRuleResources.ReadJsonFailedExpectedToken, Enum.GetName(typeof(JsonToken), reader.TokenType)); name = null; - while (reader.TokenType != JsonToken.EndObject) + subselector = null; + while (reader.TokenType == JsonToken.PropertyName) { var key = reader.Value.ToString(); if (TryCondition(key) || TryOperator(key)) @@ -780,18 +790,29 @@ private void MapProperty(LanguageExpression.PropertyBag properties, JsonReader r if (reader.Read()) { + // value: if (TryValue(key, reader, out var value)) { properties[key] = value; + reader.Read(); } else if (TryCondition(key) && reader.TryConsume(JsonToken.StartObject)) { if (TryFunction(reader, key, out var fn)) properties.Add(key, fn); + + reader.Consume(JsonToken.EndObject); + } + // where: + else if (TrySubSelector(key) && reader.TryConsume(JsonToken.StartObject)) + { + subselector = MapExpression(reader); + reader.Consume(JsonToken.EndObject); } else if (reader.TokenType == JsonToken.StartObject) + { break; - + } else if (reader.TokenType == JsonToken.StartArray) { if (!TryCondition(key)) @@ -808,17 +829,22 @@ private void MapProperty(LanguageExpression.PropertyBag properties, JsonReader r objects.Add(item); } properties.Add(key, objects.ToArray()); + reader.Consume(JsonToken.EndArray); } - else { properties.Add(key, reader.Value); + reader.Read(); } } - reader.Read(); } } + private bool TrySubSelector(string key) + { + return _Factory.IsSubselector(key); + } + private bool TryOperator(string key) { return _Factory.IsOperator(key); @@ -867,7 +893,7 @@ reader.Value is string s && s == "$"; } - private bool TryExpression(string type, out T expression) where T : LanguageExpression + private bool TryExpression(string type, LanguageExpression.PropertyBag properties, out T expression) where T : LanguageExpression { expression = null; @@ -875,7 +901,7 @@ private bool TryExpression(string type, out T expression) where T : LanguageE { expression = (T)descriptor.CreateInstance( source: RunspaceContext.CurrentThread.Source.File, - properties: null + properties: properties ); return expression != null; diff --git a/src/PSRule/Common/PSObjectExtensions.cs b/src/PSRule/Common/PSObjectExtensions.cs index 595b67ede4..c310d417fb 100644 --- a/src/PSRule/Common/PSObjectExtensions.cs +++ b/src/PSRule/Common/PSObjectExtensions.cs @@ -34,11 +34,6 @@ public static PSObject PropertyValue(this PSObject o, string propertyName) : PSObject.AsPSObject(o.Properties[propertyName].Value); } - public static string ValueAsString(this PSObject o, string propertyName, bool caseSensitive) - { - return ObjectHelper.GetPath(o, propertyName, caseSensitive, out var value) && value != null ? value.ToString() : null; - } - public static bool HasProperty(this PSObject o, string propertyName) { return o.Properties[propertyName] != null; diff --git a/src/PSRule/Common/YamlConverters.cs b/src/PSRule/Common/YamlConverters.cs index 1027fc7d9b..c41a6d641c 100644 --- a/src/PSRule/Common/YamlConverters.cs +++ b/src/PSRule/Common/YamlConverters.cs @@ -620,7 +620,7 @@ bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func /// Map an operator. /// - private LanguageExpression MapOperator(string type, IParser reader, Func nestedObjectDeserializer) + private LanguageOperator MapOperator(string type, LanguageExpression.PropertyBag properties, LanguageExpression subselector, IParser reader, Func nestedObjectDeserializer) { - if (TryExpression(reader, type, nestedObjectDeserializer, out LanguageOperator result)) + if (TryExpression(reader, type, properties, nestedObjectDeserializer, out LanguageOperator result)) { // If and Not if (reader.TryConsume(out _)) @@ -656,17 +656,18 @@ private LanguageExpression MapOperator(string type, IParser reader, Func(); reader.MoveNext(); } + result.Subselector = subselector; } return result; } private LanguageExpression MapCondition(string type, LanguageExpression.PropertyBag properties, IParser reader, Func nestedObjectDeserializer) { - if (TryExpression(reader, type, nestedObjectDeserializer, out LanguageCondition result)) + if (TryExpression(reader, type, null, nestedObjectDeserializer, out LanguageCondition result)) { while (!reader.Accept(out MappingEnd end)) { - MapProperty(properties, reader, nestedObjectDeserializer, out _); + MapProperty(properties, reader, nestedObjectDeserializer, out _, out _); } result.Add(properties); } @@ -677,18 +678,25 @@ private LanguageExpression MapExpression(IParser reader, Func(out _)) { - result = MapOperator(key, reader, nestedObjectDeserializer); + var op = MapOperator(key, properties, subselector, reader, nestedObjectDeserializer); + MapProperty(properties, reader, nestedObjectDeserializer, out _, out _); + result = op; } else if (TryOperator(key) && reader.Accept(out _)) { - result = MapOperator(key, reader, nestedObjectDeserializer); + var op = MapOperator(key, properties, subselector, reader, nestedObjectDeserializer); + MapProperty(properties, reader, nestedObjectDeserializer, out _, out subselector); + if (subselector != null) + op.Subselector = subselector; + + result = op; } return result; } @@ -754,9 +762,10 @@ private object MapSequence(string name, IParser reader, Func nestedObjectDeserializer, out string name) + private void MapProperty(LanguageExpression.PropertyBag properties, IParser reader, Func nestedObjectDeserializer, out string name, out LanguageExpression subselector) { name = null; + subselector = null; while (reader.TryConsume(out Scalar scalar)) { var key = scalar.Value; @@ -767,6 +776,7 @@ private void MapProperty(LanguageExpression.PropertyBag properties, IParser read { properties[key] = scalar.Value; } + // value: else if (TryValue(key, reader, nestedObjectDeserializer, out var value)) { properties[key] = value; @@ -788,9 +798,20 @@ private void MapProperty(LanguageExpression.PropertyBag properties, IParser read } properties[key] = objects.ToArray(); } + // where: + else if (TrySubSelector(key) && reader.TryConsume(out _)) + { + subselector = MapExpression(reader, nestedObjectDeserializer); + reader.Consume(); + } } } + private bool TrySubSelector(string key) + { + return _Factory.IsSubselector(key); + } + private bool TryOperator(string key) { return _Factory.IsOperator(key); @@ -835,12 +856,12 @@ private static bool IsFunction(IParser reader) return reader.Accept(out var scalar) || scalar.Value == "$"; } - private bool TryExpression(IParser reader, string type, Func nestedObjectDeserializer, out T expression) where T : LanguageExpression + private bool TryExpression(IParser reader, string type, LanguageExpression.PropertyBag properties, Func nestedObjectDeserializer, out T expression) where T : LanguageExpression { expression = null; if (_Factory.TryDescriptor(type, out var descriptor)) { - expression = (T)descriptor.CreateInstance(RunspaceContext.CurrentThread.Source.File, null); + expression = (T)descriptor.CreateInstance(RunspaceContext.CurrentThread.Source.File, properties); return expression != null; } return false; diff --git a/src/PSRule/Configuration/PipelineHook.cs b/src/PSRule/Configuration/PipelineHook.cs index fc04805172..3875a1eb80 100644 --- a/src/PSRule/Configuration/PipelineHook.cs +++ b/src/PSRule/Configuration/PipelineHook.cs @@ -2,17 +2,16 @@ // Licensed under the MIT License. using System.Collections.Generic; -using System.Management.Automation; namespace PSRule.Configuration { /// /// Used by custom binding functions. /// - public delegate string BindTargetName(PSObject targetObject); + public delegate string BindTargetName(object targetObject); - internal delegate string BindTargetMethod(string[] propertyNames, bool caseSensitive, bool preferTargetInfo, PSObject targetObject, out string path); - internal delegate string BindTargetFunc(string[] propertyNames, bool caseSensitive, bool preferTargetInfo, PSObject targetObject, BindTargetMethod next, out string path); + internal delegate string BindTargetMethod(string[] propertyNames, bool caseSensitive, bool preferTargetInfo, object targetObject, out string path); + internal delegate string BindTargetFunc(string[] propertyNames, bool caseSensitive, bool preferTargetInfo, object targetObject, BindTargetMethod next, out string path); /// /// Hooks that provide customize pipeline execution. diff --git a/src/PSRule/Definitions/Expressions/ExpressionContext.cs b/src/PSRule/Definitions/Expressions/ExpressionContext.cs index 1cc576be6f..d0687e6067 100644 --- a/src/PSRule/Definitions/Expressions/ExpressionContext.cs +++ b/src/PSRule/Definitions/Expressions/ExpressionContext.cs @@ -18,7 +18,7 @@ internal interface IExpressionContext : IBindingContext object Current { get; } - RunspaceContext GetContext(); + RunspaceContext Context { get; } } internal sealed class ExpressionContext : IExpressionContext, IBindingContext @@ -27,8 +27,9 @@ internal sealed class ExpressionContext : IExpressionContext, IBindingContext private List _Reason; - internal ExpressionContext(SourceFile source, ResourceKind kind, object current) + internal ExpressionContext(RunspaceContext context, SourceFile source, ResourceKind kind, object current) { + Context = context; Source = source; LanguageScope = source.Module; Kind = kind; @@ -44,6 +45,8 @@ internal ExpressionContext(SourceFile source, ResourceKind kind, object current) public object Current { get; } + public RunspaceContext Context { get; } + [DebuggerStepThrough] void IBindingContext.CachePathExpression(string path, PathExpression expression) { @@ -81,7 +84,7 @@ public void Reason(IOperand operand, string text, params object[] args) return; _Reason ??= new List(); - _Reason.Add(new ResultReason(RunspaceContext.CurrentThread?.TargetObject?.Path, operand, text, args)); + _Reason.Add(new ResultReason(Context.TargetObject?.Path, operand, text, args)); } public void Reason(string text, params object[] args) @@ -90,17 +93,12 @@ public void Reason(string text, params object[] args) return; _Reason ??= new List(); - _Reason.Add(new ResultReason(RunspaceContext.CurrentThread?.TargetObject?.Path, null, text, args)); + _Reason.Add(new ResultReason(Context.TargetObject?.Path, null, text, args)); } internal ResultReason[] GetReasons() { return _Reason == null || _Reason.Count == 0 ? Array.Empty() : _Reason.ToArray(); } - - public RunspaceContext GetContext() - { - return RunspaceContext.CurrentThread; - } } } diff --git a/src/PSRule/Definitions/Expressions/Functions.cs b/src/PSRule/Definitions/Expressions/Functions.cs index b369310a77..6262145d1b 100644 --- a/src/PSRule/Definitions/Expressions/Functions.cs +++ b/src/PSRule/Definitions/Expressions/Functions.cs @@ -91,7 +91,7 @@ private static ExpressionFnOuter Configuration(IExpressionContext context, Prope // Lookup a configuration value. return (context) => { - return context.GetContext().TryGetConfigurationValue(name, out var value) ? value : null; + return context.Context.TryGetConfigurationValue(name, out var value) ? value : null; }; } diff --git a/src/PSRule/Definitions/Expressions/LanguageExpressions.cs b/src/PSRule/Definitions/Expressions/LanguageExpressions.cs index 9a7dd18e13..1b1e0c28e4 100644 --- a/src/PSRule/Definitions/Expressions/LanguageExpressions.cs +++ b/src/PSRule/Definitions/Expressions/LanguageExpressions.cs @@ -54,6 +54,11 @@ public bool TryDescriptor(string name, out ILanguageExpresssionDescriptor descri _Descriptors.TryGetValue(name, out descriptor); } + public bool IsSubselector(string name) + { + return name == "where"; + } + public bool IsOperator(string name) { return TryDescriptor(name, out var d) && d != null && d.Type == LanguageExpressionType.Operator; @@ -80,12 +85,14 @@ internal sealed class LanguageExpressionBuilder { private const char Dot = '.'; private const char OpenBracket = '['; - private const char CloseBracket = '['; + private const char CloseBracket = ']'; + private const string Where = ".where"; private readonly bool _Debugger; private string[] _With; private string[] _Type; + private LanguageExpression _When; private string[] _Rule; public LanguageExpressionBuilder(bool debugger = true) @@ -111,6 +118,15 @@ public LanguageExpressionBuilder WithType(string[] type) return this; } + public LanguageExpressionBuilder WithSubselector(LanguageIf subselector) + { + if (subselector == null || subselector.Expression == null) + return this; + + _When = subselector.Expression; + return this; + } + public LanguageExpressionBuilder WithRule(string[] rule) { if (rule == null || rule.Length == 0) @@ -120,12 +136,12 @@ public LanguageExpressionBuilder WithRule(string[] rule) return this; } - public LanguageExpressionOuterFn Build(LanguageIf selectorIf) + public LanguageExpressionOuterFn Build(LanguageIf condition) { - return Precondition(Expression(string.Empty, selectorIf.Expression), _With, _Type, _Rule); + return Precondition(Expression(string.Empty, condition.Expression), _With, _Type, Expression(string.Empty, _When), _Rule); } - private static LanguageExpressionOuterFn Precondition(LanguageExpressionOuterFn expression, string[] with, string[] type, string[] rule) + private static LanguageExpressionOuterFn Precondition(LanguageExpressionOuterFn expression, string[] with, string[] type, LanguageExpressionOuterFn when, string[] rule) { var fn = expression; if (type != null) @@ -134,6 +150,9 @@ private static LanguageExpressionOuterFn Precondition(LanguageExpressionOuterFn if (with != null) fn = PreconditionSelector(with, fn); + if (when != null) + fn = PreconditionSubselector(when, fn); + if (rule != null) fn = PreconditionRule(rule, fn); @@ -182,8 +201,25 @@ private static LanguageExpressionOuterFn PreconditionType(string[] type, Languag }; } + private static LanguageExpressionOuterFn PreconditionSubselector(LanguageExpressionOuterFn subselector, LanguageExpressionOuterFn fn) + { + return (context, o) => + { + // Evalute sub-selector pre-condition + if (!AcceptsSubselector(context, subselector, o)) + { + context.Debug(PSRuleResources.DebugTargetSubselectorMismatch); + return null; + } + return fn(context, o); + }; + } + private LanguageExpressionOuterFn Expression(string path, LanguageExpression expression) { + if (expression == null) + return null; + path = Path(path, expression); if (expression is LanguageOperator selectorOperator) return Scope(Debugger(Operator(path, selectorOperator), path)); @@ -226,7 +262,38 @@ private LanguageExpressionOuterFn Operator(string path, LanguageOperator express } var innerA = inner.ToArray(); var info = new ExpressionInfo(path); - return (context, o) => expression.Descriptor.Fn(context, info, innerA, o); + + // Check for sub-selectors + if (expression.Property == null || expression.Property.Count == 0) + { + return (context, o) => expression.Descriptor.Fn(context, info, innerA, o); + } + else + { + var subselector = expression.Subselector != null ? Expression(string.Concat(path, Where), expression.Subselector) : null; + return (context, o) => + { + if (!ObjectHelper.GetPath(context, o, Value(context, expression.Property["field"]), caseSensitive: false, out object[] items) || + items == null || items.Length == 0) + return false; + + // If any fail, all fail + for (var i = 0; i < items.Length; i++) + { + if (subselector == null || subselector(context, items[i]).GetValueOrDefault(true)) + { + if (!expression.Descriptor.Fn(context, info, innerA, items[i])) + return false; + } + } + return true; + }; + } + } + + private string Value(ExpressionContext context, object v) + { + return v as string; } private LanguageExpressionOuterFn Debugger(LanguageExpressionOuterFn expression, string path) @@ -273,6 +340,11 @@ private static bool AcceptsWith(string[] with) return false; } + private static bool AcceptsSubselector(ExpressionContext context, LanguageExpressionOuterFn subselector, object o) + { + return subselector == null || subselector.Invoke(context, o).GetValueOrDefault(false); + } + private static bool AcceptsRule(string[] rule) { if (rule == null || rule.Length == 0) @@ -1520,38 +1592,36 @@ private static bool TryField(IExpressionContext context, LanguageExpression.Prop return operand != null || NotHasField(context, field); } - private static bool TryName(IExpressionContext context, LanguageExpression.PropertyBag properties, out IOperand operand) + private static bool TryName(IExpressionContext context, LanguageExpression.PropertyBag properties, object o, out IOperand operand) { operand = null; if (properties.TryGetString(NAME, out var svalue)) { - if (svalue != ".") + if (svalue != "." || context?.Context?.LanguageScope == null) return Invalid(context, svalue); - var binding = context.GetContext()?.TargetBinder?.Using(context.LanguageScope); - var name = binding?.TargetName; - if (string.IsNullOrEmpty(name)) + if (!context.Context.LanguageScope.TryGetName(o, out var name, out var path) || + string.IsNullOrEmpty(name)) return Invalid(context, svalue); - operand = Operand.FromName(name, binding.TargetNamePath); + operand = Operand.FromName(name, path); } return operand != null; } - private static bool TryType(IExpressionContext context, LanguageExpression.PropertyBag properties, out IOperand operand) + private static bool TryType(IExpressionContext context, LanguageExpression.PropertyBag properties, object o, out IOperand operand) { operand = null; if (properties.TryGetString(TYPE, out var svalue)) { - if (svalue != ".") + if (svalue != "." || context?.Context?.LanguageScope == null) return Invalid(context, svalue); - var binding = context.GetContext()?.TargetBinder?.Using(context.LanguageScope); - var type = binding?.TargetType; - if (string.IsNullOrEmpty(type)) + if (!context.Context.LanguageScope.TryGetType(o, out var type, out var path) || + string.IsNullOrEmpty(type)) return Invalid(context, svalue); - operand = Operand.FromType(type, binding.TargetTypePath); + operand = Operand.FromType(type, path); } return operand != null; } @@ -1561,7 +1631,7 @@ private static bool TrySource(IExpressionContext context, LanguageExpression.Pro operand = null; if (properties.TryGetString(SOURCE, out var sourceValue)) { - var source = context?.GetContext()?.TargetObject?.Source[sourceValue]; + var source = context?.Context?.TargetObject?.Source[sourceValue]; if (source == null) return Invalid(context, sourceValue); @@ -1627,8 +1697,8 @@ private static bool TryFieldNotExists(ExpressionContext context, object o, Langu private static bool TryOperand(ExpressionContext context, string name, object o, LanguageExpression.PropertyBag properties, out IOperand operand) { return TryField(context, properties, o, out operand) || - TryType(context, properties, out operand) || - TryName(context, properties, out operand) || + TryType(context, properties, o, out operand) || + TryName(context, properties, o, out operand) || TrySource(context, properties, out operand) || TryValue(context, properties, out operand) || Invalid(context, name); diff --git a/src/PSRule/Definitions/Expressions/Primitives.cs b/src/PSRule/Definitions/Expressions/Primitives.cs index 0f71649944..581c9e0d7b 100644 --- a/src/PSRule/Definitions/Expressions/Primitives.cs +++ b/src/PSRule/Definitions/Expressions/Primitives.cs @@ -34,7 +34,7 @@ public LanguageExpresssionDescriptor(string name, LanguageExpressionType type, L public LanguageExpression CreateInstance(SourceFile source, LanguageExpression.PropertyBag properties) { if (Type == LanguageExpressionType.Operator) - return new LanguageOperator(this); + return new LanguageOperator(this, properties); if (Type == LanguageExpressionType.Condition) return new LanguageCondition(this, properties); @@ -74,12 +74,17 @@ public LanguageIf(LanguageExpression expression) [DebuggerDisplay("Selector {Descriptor.Name}")] internal sealed class LanguageOperator : LanguageExpression { - internal LanguageOperator(LanguageExpresssionDescriptor descriptor) + internal LanguageOperator(LanguageExpresssionDescriptor descriptor, PropertyBag properties) : base(descriptor) { + Property = properties ?? new PropertyBag(); Children = new List(); } + public LanguageExpression Subselector { get; set; } + + public PropertyBag Property { get; } + public List Children { get; } public void Add(LanguageExpression item) @@ -109,9 +114,6 @@ internal void Add(PropertyBag properties) internal sealed class LanguageFunction : LanguageExpression { internal LanguageFunction(LanguageExpresssionDescriptor descriptor) - : base(descriptor) - { - - } + : base(descriptor) { } } } diff --git a/src/PSRule/Definitions/ILanguageBlock.cs b/src/PSRule/Definitions/ILanguageBlock.cs index bceec9ac20..ffa3491bd1 100644 --- a/src/PSRule/Definitions/ILanguageBlock.cs +++ b/src/PSRule/Definitions/ILanguageBlock.cs @@ -6,16 +6,33 @@ namespace PSRule.Definitions { + /// + /// A language block. + /// public interface ILanguageBlock { + /// + /// The unique identifier for the block. + /// ResourceId Id { get; } + /// + /// Obsolete. The source file path. + /// Replaced by . + /// [Obsolete("Use Source property instead.")] string SourcePath { get; } + /// + /// Obsolete. The source module. + /// Replaced by . + /// [Obsolete("Use Source property instead.")] string Module { get; } + /// + /// The source location for the block. + /// SourceFile Source { get; } } } diff --git a/src/PSRule/Definitions/Resource.cs b/src/PSRule/Definitions/Resource.cs index 9382bdaa15..408f455bc2 100644 --- a/src/PSRule/Definitions/Resource.cs +++ b/src/PSRule/Definitions/Resource.cs @@ -18,8 +18,14 @@ namespace PSRule.Definitions { + /// + /// The type of resource. + /// public enum ResourceKind { + /// + /// Unknown or empty. + /// None = 0, /// @@ -53,14 +59,26 @@ public enum ResourceKind SuppressionGroup = 6 } + /// + /// Additional flags that indicate the status of the resource. + /// [Flags] public enum ResourceFlags { + /// + /// No flags are set. + /// None = 0, + /// + /// The resource is obsolete. + /// Obsolete = 1 } + /// + /// A resource langange block. + /// public interface IResource : ILanguageBlock { /// @@ -142,6 +160,9 @@ internal sealed class ValidateResourceAnnotation : ResourceAnnotation } + /// + /// A resource object. + /// public sealed class ResourceObject { internal ResourceObject(IResource block) @@ -196,15 +217,24 @@ internal IEnumerable Build() } } + /// + /// Additional resource annotations. + /// public sealed class ResourceAnnotations : Dictionary { } + /// + /// Additional resource tags. + /// public sealed class ResourceTags : Dictionary { private Hashtable _Hashtable; + /// + /// Create an empty set of resource tags. + /// public ResourceTags() : base(StringComparer.OrdinalIgnoreCase) { } /// @@ -287,6 +317,10 @@ private static bool TryArray(object o, out string[] values) return false; } + /// + /// Convert the resourecs tags to a display string for PowerShell views. + /// + /// public string ToViewString() { var sb = new StringBuilder(); @@ -308,8 +342,14 @@ public string ToViewString() } } + /// + /// Additional resource metadata. + /// public sealed class ResourceMetadata { + /// + /// Create an empty set of metadata. + /// public ResourceMetadata() { Annotations = new ResourceAnnotations(); @@ -321,8 +361,14 @@ public ResourceMetadata() /// public string Name { get; set; } + /// + /// A opaque reference for the resource. + /// public string Ref { get; set; } + /// + /// Additional aliases for the resource. + /// public string[] Alias { get; set; } /// @@ -338,16 +384,32 @@ public ResourceMetadata() public ResourceTags Tags { get; set; } } + /// + /// The source location of the resource. + /// public sealed class ResourceExtent { + /// + /// The file where the resource is located. + /// public string File { get; set; } + /// + /// The name of the module if the resource is contained within a module. + /// public string Module { get; set; } } + /// + /// A base class for resources. + /// + /// The type for the resource specification. [DebuggerDisplay("Kind = {Kind}, Id = {Id}")] public abstract class Resource where TSpec : Spec, new() { + /// + /// Create a resource. + /// internal protected Resource(ResourceKind kind, string apiVersion, SourceFile source, ResourceMetadata metadata, IResourceHelpInfo info, ISourceExtent extent, TSpec spec) { Kind = kind; @@ -361,6 +423,9 @@ internal protected Resource(ResourceKind kind, string apiVersion, SourceFile sou Id = new ResourceId(source.Module, Name, ResourceIdKind.Id); } + /// + /// The resource identifier for the resource. + /// [YamlIgnore()] public ResourceId Id { get; } @@ -376,6 +441,9 @@ internal protected Resource(ResourceKind kind, string apiVersion, SourceFile sou [YamlIgnore()] public SourceFile Source { get; } + /// + /// Information about the resource. + /// [YamlIgnore()] public IResourceHelpInfo Info { get; } diff --git a/src/PSRule/Definitions/Rules/Rule.cs b/src/PSRule/Definitions/Rules/Rule.cs index 16aea06632..36dd2d9ec5 100644 --- a/src/PSRule/Definitions/Rules/Rule.cs +++ b/src/PSRule/Definitions/Rules/Rule.cs @@ -14,17 +14,36 @@ namespace PSRule.Definitions.Rules /// public enum SeverityLevel { + /// + /// Severity is unset. + /// None = 0, + /// + /// A failure generates an error. + /// Error = 1, + /// + /// A fiailure generates a warning. + /// Warning = 2, + /// + /// A failure generate an informational message. + /// Information = 3 } + /// + /// A rule resource V1. + /// public interface IRuleV1 : IResource, IDependencyTarget { + /// + /// Obsolete. The name of the rule. + /// Replaced by . + /// [Obsolete("Use Name instead.")] string RuleName { get; } @@ -33,16 +52,32 @@ public interface IRuleV1 : IResource, IDependencyTarget /// SeverityLevel Level { get; } + /// + /// A short description of the rule. + /// string Synopsis { get; } + /// + /// Obsolete. A short description of the rule. + /// Replaced by . + /// [Obsolete("Use Synopsis instead.")] string Description { get; } + /// + /// Any additional tags assigned to the rule. + /// ResourceTags Tag { get; } } + /// + /// A specification for a rule resource. + /// internal interface IRuleSpec { + /// + /// The of the rule condition that will be evaluated. + /// LanguageIf Condition { get; } /// @@ -50,9 +85,20 @@ internal interface IRuleSpec /// SeverityLevel? Level { get; } + /// + /// An optional type pre-condition before the rule is evaluated. + /// string[] Type { get; } + /// + /// An optional selector pre-condition before the rule is evaluated. + /// string[] With { get; } + + /// + /// An optional sub-selector pre-condition before the rule is evaluated. + /// + LanguageIf Where { get; } } [Spec(Specs.V1, Specs.Rule)] @@ -68,10 +114,12 @@ public RuleV1(string apiVersion, SourceFile source, ResourceMetadata metadata, I Level = ResourceHelper.GetLevel(spec.Level); } + /// [JsonIgnore] [YamlIgnore] public ResourceId? Ref { get; } + /// [JsonIgnore] [YamlIgnore] public ResourceId[] Alias { get; } @@ -90,43 +138,52 @@ public RuleV1(string apiVersion, SourceFile source, ResourceMetadata metadata, I [YamlIgnore] public string Synopsis => Info.Synopsis.Text; + /// ResourceId? IDependencyTarget.Ref => Ref; + /// ResourceId[] IDependencyTarget.Alias => Alias; // Not supported with resource rules. ResourceId[] IDependencyTarget.DependsOn => Array.Empty(); + /// bool IDependencyTarget.Dependency => Source.IsDependency(); + /// ResourceId? IResource.Ref => Ref; + /// ResourceId[] IResource.Alias => Alias; + /// string IRuleV1.RuleName => Name; + /// ResourceTags IRuleV1.Tag => Metadata.Tags; + /// string IRuleV1.Description => Info.Synopsis.Text; } + /// + /// A specification for a V1 rule resource. + /// internal sealed class RuleV1Spec : Spec, IRuleSpec { + /// public LanguageIf Condition { get; set; } - /// - /// If the rule fails, how serious is the result. - /// + /// public SeverityLevel? Level { get; set; } - /// - /// An optional type precondition before the rule is evaluated. - /// + /// public string[] Type { get; set; } - /// - /// An optional selector precondition before the rule is evaluated. - /// + /// public string[] With { get; set; } + + /// + public LanguageIf Where { get; set; } } } diff --git a/src/PSRule/Definitions/Rules/RuleVisitor.cs b/src/PSRule/Definitions/Rules/RuleVisitor.cs index 608d5ae9f9..511689b482 100644 --- a/src/PSRule/Definitions/Rules/RuleVisitor.cs +++ b/src/PSRule/Definitions/Rules/RuleVisitor.cs @@ -11,13 +11,18 @@ namespace PSRule.Definitions.Rules { + /// + /// A rule visitor. + /// [DebuggerDisplay("Id: {Id}")] internal sealed class RuleVisitor : ICondition { private readonly LanguageExpressionOuterFn _Condition; + private readonly RunspaceContext _Context; - public RuleVisitor(ResourceId id, SourceFile source, IRuleSpec spec) + public RuleVisitor(RunspaceContext context, ResourceId id, SourceFile source, IRuleSpec spec) { + _Context = context; ErrorAction = ActionPreference.Stop; Id = id; Source = source; @@ -26,6 +31,7 @@ public RuleVisitor(ResourceId id, SourceFile source, IRuleSpec spec) _Condition = builder .WithSelector(spec.With) .WithType(spec.Type) + .WithSubselector(spec.Where) .Build(spec.Condition); } @@ -50,14 +56,14 @@ public void Dispose() public IConditionResult If() { - var context = new ExpressionContext(Source, ResourceKind.Rule, RunspaceContext.CurrentThread.TargetObject.Value); + var context = new ExpressionContext(_Context, Source, ResourceKind.Rule, _Context.TargetObject.Value); context.Debug(PSRuleResources.RuleMatchTrace, Id); context.PushScope(RunspaceScope.Rule); try { - var result = _Condition(context, RunspaceContext.CurrentThread.TargetObject.Value); + var result = _Condition(context, _Context.TargetObject.Value); if (result.HasValue && !result.Value) - RunspaceContext.CurrentThread.WriteReason(context.GetReasons()); + _Context.WriteReason(context.GetReasons()); return result.HasValue ? new RuleConditionResult(result.Value ? 1 : 0, 1, false) : null; } diff --git a/src/PSRule/Definitions/Selectors/SelectorVisitor.cs b/src/PSRule/Definitions/Selectors/SelectorVisitor.cs index d7ab303dbf..34e766de3e 100644 --- a/src/PSRule/Definitions/Selectors/SelectorVisitor.cs +++ b/src/PSRule/Definitions/Selectors/SelectorVisitor.cs @@ -6,6 +6,7 @@ using PSRule.Definitions.Expressions; using PSRule.Pipeline; using PSRule.Resources; +using PSRule.Runtime; namespace PSRule.Definitions.Selectors { @@ -18,9 +19,11 @@ internal interface ISelector : ILanguageBlock internal sealed class SelectorVisitor : ISelector { private readonly LanguageExpressionOuterFn _Fn; + private readonly RunspaceContext _Context; - public SelectorVisitor(ResourceId id, SourceFile source, LanguageIf expression) + public SelectorVisitor(RunspaceContext context, ResourceId id, SourceFile source, LanguageIf expression) { + _Context = context; Id = id; Source = source; InstanceId = Guid.NewGuid(); @@ -42,7 +45,7 @@ public SelectorVisitor(ResourceId id, SourceFile source, LanguageIf expression) public bool Match(object o) { - var context = new ExpressionContext(Source, ResourceKind.Selector, o); + var context = new ExpressionContext(_Context, Source, ResourceKind.Selector, o); context.Debug(PSRuleResources.SelectorMatchTrace, Id); return _Fn(context, o).GetValueOrDefault(false); } diff --git a/src/PSRule/Definitions/SuppressionGroups/SuppressionGroupVisitor.cs b/src/PSRule/Definitions/SuppressionGroups/SuppressionGroupVisitor.cs index 9d8ace4efb..edda73213d 100644 --- a/src/PSRule/Definitions/SuppressionGroups/SuppressionGroupVisitor.cs +++ b/src/PSRule/Definitions/SuppressionGroups/SuppressionGroupVisitor.cs @@ -5,6 +5,7 @@ using PSRule.Definitions.Expressions; using PSRule.Pipeline; using PSRule.Resources; +using PSRule.Runtime; namespace PSRule.Definitions.SuppressionGroups { @@ -12,9 +13,11 @@ internal sealed class SuppressionGroupVisitor { private readonly LanguageExpressionOuterFn _Fn; private readonly SuppressionInfo _Info; + private readonly RunspaceContext _Context; - public SuppressionGroupVisitor(ResourceId id, SourceFile source, ISuppressionGroupSpec spec, IResourceHelpInfo info) + public SuppressionGroupVisitor(RunspaceContext context, ResourceId id, SourceFile source, ISuppressionGroupSpec spec, IResourceHelpInfo info) { + _Context = context; Id = id; Source = source; InstanceId = Guid.NewGuid(); @@ -77,7 +80,7 @@ internal void Hit() public bool TryMatch(object o, out ISuppressionInfo suppression) { suppression = null; - var context = new ExpressionContext(Source, ResourceKind.SuppressionGroup, o); + var context = new ExpressionContext(_Context, Source, ResourceKind.SuppressionGroup, o); context.Debug(PSRuleResources.SelectorMatchTrace, Id); if (_Fn(context, o).GetValueOrDefault(false)) { diff --git a/src/PSRule/Host/HostHelper.cs b/src/PSRule/Host/HostHelper.cs index eea83a859a..f0036bcafe 100644 --- a/src/PSRule/Host/HostHelper.cs +++ b/src/PSRule/Host/HostHelper.cs @@ -56,11 +56,6 @@ internal static DependencyGraph GetRuleBlockGraph(Source[] source, Ru return builder.Build(); } - internal static IEnumerable GetRuleYamlBlocks(Source[] source, RunspaceContext context) - { - return ToRuleBlockV1(GetYamlLanguageBlocks(source, context), context, skipDuplicateName: true).GetAll(); - } - private static IEnumerable GetYamlJsonLanguageBlocks(Source[] source, RunspaceContext context) { var results = new List(); @@ -530,7 +525,7 @@ private static DependencyTargetCollection ToRuleBlockV1(ILanguageBloc @ref: block.Ref, level: block.Level, info: info, - condition: new RuleVisitor(block.Id, block.Source, block.Spec), + condition: new RuleVisitor(context, block.Id, block.Source, block.Spec), alias: block.Alias, tag: block.Metadata.Tags, dependsOn: null, // TODO: No support for DependsOn yet @@ -706,11 +701,11 @@ private static void Import(ILanguageBlock[] blocks, RunspaceContext context) // Process module configurations first foreach (var resource in resources.Where(r => r.Kind == ResourceKind.ModuleConfig).ToArray()) - context.Pipeline.Import(resource); + context.Pipeline.Import(context, resource); // Process other resources foreach (var resource in resources.Where(r => r.Kind != ResourceKind.ModuleConfig).ToArray()) - context.Pipeline.Import(resource); + context.Pipeline.Import(context, resource); } private static void Import(IConvention[] blocks, RunspaceContext context) diff --git a/src/PSRule/Pipeline/PipelineBuilder.cs b/src/PSRule/Pipeline/PipelineBuilder.cs index 1856753757..4564975132 100644 --- a/src/PSRule/Pipeline/PipelineBuilder.cs +++ b/src/PSRule/Pipeline/PipelineBuilder.cs @@ -476,7 +476,7 @@ private static BindTargetMethod AddBindTargetAction(BindTargetFunc action, BindT { // Nest the previous write action in the new supplied action // Execution chain will be: action -> previous -> previous..n - return (string[] propertyNames, bool caseSensitive, bool preferTargetInfo, PSObject targetObject, out string path) => + return (string[] propertyNames, bool caseSensitive, bool preferTargetInfo, object targetObject, out string path) => { return action(propertyNames, caseSensitive, preferTargetInfo, targetObject, previous, out path); }; @@ -484,7 +484,7 @@ private static BindTargetMethod AddBindTargetAction(BindTargetFunc action, BindT private static BindTargetMethod AddBindTargetAction(BindTargetName action, BindTargetMethod previous) { - return AddBindTargetAction((string[] propertyNames, bool caseSensitive, bool preferTargetInfo, PSObject targetObject, BindTargetMethod next, out string path) => + return AddBindTargetAction((string[] propertyNames, bool caseSensitive, bool preferTargetInfo, object targetObject, BindTargetMethod next, out string path) => { path = null; var targetType = action(targetObject); diff --git a/src/PSRule/Pipeline/PipelineContext.cs b/src/PSRule/Pipeline/PipelineContext.cs index c125a3fb0f..c769f1d713 100644 --- a/src/PSRule/Pipeline/PipelineContext.cs +++ b/src/PSRule/Pipeline/PipelineContext.cs @@ -161,7 +161,7 @@ internal Runspace GetRunspace() return _Runspace; } - internal void Import(IResource resource) + internal void Import(RunspaceContext context, IResource resource) { TrackIssue(resource); if (TryBaseline(resource, out var baseline) && TryBaselineRef(resource.Id, out var baselineRef)) @@ -170,7 +170,7 @@ internal void Import(IResource resource) Baseline.Add(new OptionContext.BaselineScope(baselineRef.Type, baseline.BaselineId, resource.Source.Module, baseline.Spec, baseline.Obsolete)); } else if (resource.Kind == ResourceKind.Selector && resource is SelectorV1 selector) - Selector[selector.Id.Value] = new SelectorVisitor(selector.Id, selector.Source, selector.Spec.If); + Selector[selector.Id.Value] = new SelectorVisitor(context, selector.Id, selector.Source, selector.Spec.If); else if (TryModuleConfig(resource, out var moduleConfig)) { if (!string.IsNullOrEmpty(moduleConfig?.Spec?.Rule?.Baseline)) @@ -184,6 +184,7 @@ internal void Import(IResource resource) else if (resource.Kind == ResourceKind.SuppressionGroup && resource is SuppressionGroupV1 suppressionGroup) { SuppressionGroup.Add(new SuppressionGroupVisitor( + context: context, id: suppressionGroup.Id, source: suppressionGroup.Source, spec: suppressionGroup.Spec, diff --git a/src/PSRule/Pipeline/PipelineHookActions.cs b/src/PSRule/Pipeline/PipelineHookActions.cs index 8f193f8455..0d3fcd82f2 100644 --- a/src/PSRule/Pipeline/PipelineHookActions.cs +++ b/src/PSRule/Pipeline/PipelineHookActions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Globalization; using System.Linq; using System.Management.Automation; @@ -19,9 +20,12 @@ internal static class PipelineHookActions private const string Property_TargetName = "TargetName"; private const string Property_Name = "Name"; - public static string BindTargetName(string[] propertyNames, bool caseSensitive, bool preferTargetInfo, PSObject targetObject, out string path) + public static string BindTargetName(string[] propertyNames, bool caseSensitive, bool preferTargetInfo, object targetObject, out string path) { path = null; + if (targetObject == null) + return null; + if (preferTargetInfo && TryGetInfoTargetName(targetObject, out var targetName)) return targetName; @@ -33,9 +37,12 @@ public static string BindTargetName(string[] propertyNames, bool caseSensitive, return DefaultTargetNameBinding(targetObject); } - public static string BindTargetType(string[] propertyNames, bool caseSensitive, bool preferTargetInfo, PSObject targetObject, out string path) + public static string BindTargetType(string[] propertyNames, bool caseSensitive, bool preferTargetInfo, object targetObject, out string path) { path = null; + if (targetObject == null) + return null; + if (preferTargetInfo && TryGetInfoTargetType(targetObject, out var targetType)) return targetType; @@ -47,9 +54,12 @@ public static string BindTargetType(string[] propertyNames, bool caseSensitive, return DefaultTargetTypeBinding(targetObject); } - public static string BindField(string[] propertyNames, bool caseSensitive, bool preferTargetInfo, PSObject targetObject, out string path) + public static string BindField(string[] propertyNames, bool caseSensitive, bool preferTargetInfo, object targetObject, out string path) { path = null; + if (targetObject == null) + return null; + if (propertyNames != null) return propertyNames.Any(n => n.Contains('.')) ? NestedTargetPropertyBinding(propertyNames, caseSensitive, targetObject, DefaultFieldBinding, out path) @@ -63,7 +73,7 @@ public static string BindField(string[] propertyNames, bool caseSensitive, bool /// /// A PSObject to bind. /// The TargetName of the object. - private static string DefaultTargetNameBinding(PSObject targetObject) + private static string DefaultTargetNameBinding(object targetObject) { return TryGetInfoTargetName(targetObject, out var targetName) || TryGetTargetName(targetObject, propertyName: Property_TargetName, targetName: out targetName) || @@ -76,16 +86,17 @@ private static string DefaultTargetNameBinding(PSObject targetObject) /// Get the TargetName of the object by using any of the specified property names. /// /// One or more property names to use to bind TargetName. + /// Determines if binding properties are case-sensitive. /// A PSObject to bind. /// The next delegate function to check if all of the property names can not be found. /// The TargetName of the object. - private static string CustomTargetPropertyBinding(string[] propertyNames, bool caseSensitive, PSObject targetObject, BindTargetName next, out string path) + private static string CustomTargetPropertyBinding(string[] propertyNames, bool caseSensitive, object targetObject, BindTargetName next, out string path) { path = null; string targetName = null; for (var i = 0; i < propertyNames.Length && targetName == null; i++) { - targetName = targetObject.ValueAsString(propertyName: propertyNames[i], caseSensitive: caseSensitive); + targetName = ValueAsString(targetObject, propertyName: propertyNames[i], caseSensitive: caseSensitive); if (targetName != null) path = propertyNames[i]; } @@ -97,10 +108,11 @@ private static string CustomTargetPropertyBinding(string[] propertyNames, bool c /// Get the TargetName of the object by using any of the specified property names. /// /// One or more property names to use to bind TargetName. + /// Determines if binding properties are case-sensitive. /// A PSObject to bind. /// The next delegate function to check if all of the property names can not be found. /// The TargetName of the object. - private static string NestedTargetPropertyBinding(string[] propertyNames, bool caseSensitive, PSObject targetObject, BindTargetName next, out string path) + private static string NestedTargetPropertyBinding(string[] propertyNames, bool caseSensitive, object targetObject, BindTargetName next, out string path) { path = null; string targetName = null; @@ -128,7 +140,7 @@ private static string NestedTargetPropertyBinding(string[] propertyNames, bool c /// /// A PSObject to hash. /// The TargetName of the object. - private static string GetUnboundObjectTargetName(PSObject targetObject) + private static string GetUnboundObjectTargetName(object targetObject) { var settings = new JsonSerializerSettings { @@ -146,9 +158,9 @@ private static string GetUnboundObjectTargetName(PSObject targetObject) /// /// Try to get TargetName from specified property. /// - private static bool TryGetTargetName(PSObject targetObject, string propertyName, out string targetName) + private static bool TryGetTargetName(object targetObject, string propertyName, out string targetName) { - targetName = targetObject.ValueAsString(propertyName, false); + targetName = ValueAsString(targetObject, propertyName, false); return targetName != null; } @@ -157,34 +169,49 @@ private static bool TryGetTargetName(PSObject targetObject, string propertyName, /// /// A PSObject to bind. /// The TargetObject of the object. - private static string DefaultTargetTypeBinding(PSObject targetObject) + private static string DefaultTargetTypeBinding(object targetObject) + { + return TryGetInfoTargetType(targetObject, out var targetType) ? targetType : GetTypeNames(targetObject); + } + + private static string GetTypeNames(object targetObject) { - return TryGetInfoTargetType(targetObject, out var targetType) ? targetType : targetObject.TypeNames[0]; + if (targetObject == null) + return null; + + return targetObject is PSObject pso ? pso.TypeNames[0] : targetObject.GetType().FullName; } - private static string DefaultFieldBinding(PSObject targetObject) + private static string DefaultFieldBinding(object targetObject) { return null; } - private static bool TryGetInfoTargetName(PSObject targetObject, out string targetName) + private static bool TryGetInfoTargetName(object targetObject, out string targetName) { targetName = null; - if (!(targetObject.BaseObject is ITargetInfo info)) + var baseObject = ExpressionHelpers.GetBaseObject(targetObject); + if (baseObject is not ITargetInfo info) return false; targetName = info.TargetName; return true; } - private static bool TryGetInfoTargetType(PSObject targetObject, out string targetType) + private static bool TryGetInfoTargetType(object targetObject, out string targetType) { targetType = null; - if (!(targetObject.BaseObject is ITargetInfo info)) + var baseObject = ExpressionHelpers.GetBaseObject(targetObject); + if (baseObject is not ITargetInfo info) return false; targetType = info.TargetType; return true; } + + private static string ValueAsString(object o, string propertyName, bool caseSensitive) + { + return ObjectHelper.GetPath(bindingContext: null, targetObject: o, path: propertyName, caseSensitive: caseSensitive, value: out object value) && value != null ? value.ToString() : null; + } } } diff --git a/src/PSRule/Pipeline/TargetBinder.cs b/src/PSRule/Pipeline/TargetBinder.cs index df272a9099..6fc3aecfc0 100644 --- a/src/PSRule/Pipeline/TargetBinder.cs +++ b/src/PSRule/Pipeline/TargetBinder.cs @@ -4,8 +4,8 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Management.Automation; using PSRule.Configuration; +using static PSRule.Pipeline.TargetBinder; namespace PSRule.Pipeline { @@ -17,6 +17,8 @@ internal interface ITargetBinder void Bind(TargetObject targetObject); ITargetBindingContext Using(string languageScope); + + ITargetBindingResult Result(string languageScope); } /// @@ -26,6 +28,11 @@ internal interface ITargetBindingContext { string LanguageScope { get; } + ITargetBindingResult Bind(object o); + } + + internal interface ITargetBindingResult + { /// /// The bound TargetName of the target object. /// @@ -46,8 +53,6 @@ internal interface ITargetBindingContext Hashtable Field { get; } bool ShouldFilter { get; } - - void Bind(TargetObject targetObject, BindTargetMethod bindTargetName, BindTargetMethod bindTargetType, BindTargetMethod bindField, HashSet typeFilter); } /// @@ -56,7 +61,7 @@ internal interface ITargetBindingContext internal sealed class TargetBinderBuilder { private readonly List _BindingContext; - private readonly string[] _TypeFilter; + private readonly HashSet _TypeFilter; private readonly BindTargetMethod _BindTargetName; private readonly BindTargetMethod _BindTargetType; private readonly BindTargetMethod _BindField; @@ -67,7 +72,8 @@ public TargetBinderBuilder(BindTargetMethod bindTargetName, BindTargetMethod bin _BindTargetType = bindTargetType; _BindField = bindField; _BindingContext = new List(); - _TypeFilter = typeFilter; + if (typeFilter != null && typeFilter.Length > 0) + _TypeFilter = new HashSet(typeFilter, StringComparer.OrdinalIgnoreCase); } /// @@ -75,7 +81,7 @@ public TargetBinderBuilder(BindTargetMethod bindTargetName, BindTargetMethod bin /// public ITargetBinder Build() { - return new TargetBinder(_BindingContext.ToArray(), _BindTargetName, _BindTargetType, _BindField, _TypeFilter); + return new TargetBinder(_BindingContext.ToArray()); } /// @@ -85,6 +91,14 @@ public void With(ITargetBindingContext bindingContext) { _BindingContext.Add(bindingContext); } + + /// + /// Add a target binding context. + /// + public void With(string languageScope, IBindingOption bindingOption) + { + _BindingContext.Add(new TargetBindingContext(languageScope, bindingOption, _BindTargetName, _BindTargetType, _BindField, _TypeFilter)); + } } /// @@ -94,23 +108,13 @@ internal sealed class TargetBinder : ITargetBinder { private const string STANDALONE_SCOPE = "."; - private readonly BindTargetMethod _BindTargetName; - - private readonly BindTargetMethod _BindTargetType; - private readonly BindTargetMethod _BindField; - private readonly HashSet _TypeFilter; - private readonly Dictionary _BindingContext; + private readonly Dictionary _BindingResult; - internal TargetBinder(ITargetBindingContext[] bindingContext, BindTargetMethod bindTargetName, BindTargetMethod bindTargetType, BindTargetMethod bindField, string[] typeFilter) + internal TargetBinder(ITargetBindingContext[] bindingContext) { _BindingContext = new Dictionary(); - _BindTargetName = bindTargetName; - _BindTargetType = bindTargetType; - _BindField = bindField; - if (typeFilter != null && typeFilter.Length > 0) - _TypeFilter = new HashSet(typeFilter, StringComparer.OrdinalIgnoreCase); - + _BindingResult = new Dictionary(); for (var i = 0; bindingContext != null && i < bindingContext.Length; i++) _BindingContext.Add(bindingContext[i].LanguageScope ?? STANDALONE_SCOPE, bindingContext[i]); } @@ -166,56 +170,79 @@ internal void Protect() } } - internal sealed class TargetBindingContext : ITargetBindingContext + internal sealed class TargetBindingResult : ITargetBindingResult { - private readonly IBindingOption _BindingOption; - - public TargetBindingContext(string languageScope, IBindingOption bindingOption) + public TargetBindingResult(string targetName, string targetNamePath, string targetType, string targetTypePath, bool shouldFilter, Hashtable field) { - LanguageScope = languageScope; - _BindingOption = bindingOption; + TargetName = targetName; + TargetNamePath = targetNamePath; + TargetType = targetType; + TargetTypePath = targetTypePath; + ShouldFilter = shouldFilter; + Field = field; } - public string LanguageScope { get; } + /// + public string TargetName { get; } - /// - /// The bound TargetName of the target object. - /// - public string TargetName { get; private set; } + /// + public string TargetNamePath { get; } - public string TargetNamePath { get; private set; } + /// + public string TargetType { get; } - /// - /// The bound TargetType of the target object. - /// - public string TargetType { get; private set; } + /// + public string TargetTypePath { get; } - public string TargetTypePath { get; private set; } + /// + public bool ShouldFilter { get; } - /// - /// Additional bound fields of the target object. - /// - public Hashtable Field { get; private set; } + /// + public Hashtable Field { get; } + } - /// - /// Determines if the target object should be filtered. - /// - public bool ShouldFilter { get; private set; } + internal sealed class TargetBindingContext : ITargetBindingContext + { + private readonly IBindingOption _BindingOption; + private readonly BindTargetMethod _BindTargetName; + private readonly BindTargetMethod _BindTargetType; + private readonly BindTargetMethod _BindField; + private readonly HashSet _TypeFilter; - public void Bind(TargetObject targetObject, BindTargetMethod bindTargetName, BindTargetMethod bindTargetType, BindTargetMethod bindField, HashSet typeFilter) + public TargetBindingContext(string languageScope, IBindingOption bindingOption, BindTargetMethod bindTargetName, BindTargetMethod bindTargetType, BindTargetMethod bindField, HashSet typeFilter) { - TargetName = bindTargetName(_BindingOption.TargetName, !_BindingOption.IgnoreCase, _BindingOption.PreferTargetInfo, targetObject.Value, out var targetNamePath); - TargetNamePath = targetNamePath; - TargetType = bindTargetType(_BindingOption.TargetType, !_BindingOption.IgnoreCase, _BindingOption.PreferTargetInfo, targetObject.Value, out var targetTypePath); - TargetTypePath = targetTypePath; - ShouldFilter = !(typeFilter == null || typeFilter.Contains(TargetType)); + LanguageScope = languageScope; + _BindingOption = bindingOption; + _BindTargetName = bindTargetName; + _BindTargetType = bindTargetType; + _BindField = bindField; + _TypeFilter = typeFilter; + } + + public string LanguageScope { get; } + + public ITargetBindingResult Bind(object o) + { + var targetName = _BindTargetName(_BindingOption.TargetName, !_BindingOption.IgnoreCase, _BindingOption.PreferTargetInfo, o, out var targetNamePath); + var targetType = _BindTargetType(_BindingOption.TargetType, !_BindingOption.IgnoreCase, _BindingOption.PreferTargetInfo, o, out var targetTypePath); + var shouldFilter = !(_TypeFilter == null || _TypeFilter.Contains(targetType)); // Use qualified name if (_BindingOption.UseQualifiedName) - TargetName = string.Concat(TargetType, _BindingOption.NameSeparator, TargetName); + targetName = string.Concat(targetType, _BindingOption.NameSeparator, targetName); // Bind custom fields - Field = BindField(bindField, _BindingOption.Field, !_BindingOption.IgnoreCase, targetObject.Value); + var field = BindField(_BindField, _BindingOption.Field, !_BindingOption.IgnoreCase, o); + + return new TargetBindingResult + ( + targetName: targetName, + targetNamePath: targetNamePath, + targetType: targetType, + targetTypePath: targetTypePath, + shouldFilter: shouldFilter, + field: field + ); } } @@ -227,7 +254,7 @@ public void Bind(TargetObject targetObject, BindTargetMethod bindTargetName, Bin public void Bind(TargetObject targetObject) { foreach (var bindingContext in _BindingContext.Values) - bindingContext.Bind(targetObject, _BindTargetName, _BindTargetType, _BindField, _TypeFilter); + _BindingResult[bindingContext.LanguageScope] = bindingContext.Bind(targetObject.Value); } public ITargetBindingContext Using(string languageScope) @@ -235,10 +262,15 @@ public ITargetBindingContext Using(string languageScope) return _BindingContext.TryGetValue(languageScope ?? STANDALONE_SCOPE, out var result) ? result : null; } + public ITargetBindingResult Result(string languageScope) + { + return _BindingResult.TryGetValue(languageScope ?? STANDALONE_SCOPE, out var result) ? result : null; + } + /// /// Bind additional fields. /// - private static Hashtable BindField(BindTargetMethod bindField, FieldMap[] map, bool caseSensitive, PSObject targetObject) + private static Hashtable BindField(BindTargetMethod bindField, FieldMap[] map, bool caseSensitive, object o) { if (map == null || map.Length == 0) return null; @@ -254,7 +286,7 @@ private static Hashtable BindField(BindTargetMethod bindField, FieldMap[] map, b if (hashtable.ContainsKey(field.Key)) continue; - hashtable.Add(field.Key, bindField(field.Value, caseSensitive, false, targetObject, out _)); + hashtable.Add(field.Key, bindField(field.Value, caseSensitive, false, o, out _)); } } hashtable.Protect(); diff --git a/src/PSRule/Resources/PSRuleResources.Designer.cs b/src/PSRule/Resources/PSRuleResources.Designer.cs index f6d2da4b2e..c3b501f2cb 100644 --- a/src/PSRule/Resources/PSRuleResources.Designer.cs +++ b/src/PSRule/Resources/PSRuleResources.Designer.cs @@ -8,11 +8,10 @@ // //------------------------------------------------------------------------------ -namespace PSRule.Resources -{ +namespace PSRule.Resources { using System; - - + + /// /// A strongly-typed resource class, for looking up localized strings, etc. /// @@ -23,980 +22,805 @@ namespace PSRule.Resources [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class PSRuleResources - { - + internal class PSRuleResources { + private static global::System.Resources.ResourceManager resourceMan; - + private static global::System.Globalization.CultureInfo resourceCulture; - + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal PSRuleResources() - { + internal PSRuleResources() { } - + /// /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager - { - get - { - if (object.ReferenceEquals(resourceMan, null)) - { + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PSRule.Resources.PSRuleResources", typeof(PSRuleResources).Assembly); resourceMan = temp; } return resourceMan; } } - + /// /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture - { - get - { + internal static global::System.Globalization.CultureInfo Culture { + get { return resourceCulture; } - set - { + set { resourceCulture = value; } } - + + /// + /// Looks up a localized string similar to The {0} resource '{1}' is currently referencing '{2}' using the alias '{3}'. Consider updating the reference to use name or id directly.. + /// + internal static string AliasReference { + get { + return ResourceManager.GetString("AliasReference", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Suppression for the rule '{0}' was configured using the alias '{1}'. Consider updating the suppression to use the name or id directly.. + /// + internal static string AliasSuppression { + get { + return ResourceManager.GetString("AliasSuppression", resourceCulture); + } + } + /// /// Looks up a localized string similar to The arguments for '{0}' are not in the expected format or type.. /// - internal static string ArgumentFormatInvalid - { - get - { + internal static string ArgumentFormatInvalid { + get { return ResourceManager.GetString("ArgumentFormatInvalid", resourceCulture); } } - + /// /// Looks up a localized string similar to The argument '{0}' for '{1}' is not a valid boolean.. /// - internal static string ArgumentInvalidBoolean - { - get - { + internal static string ArgumentInvalidBoolean { + get { return ResourceManager.GetString("ArgumentInvalidBoolean", resourceCulture); } } - + /// /// Looks up a localized string similar to The argument '{0}' for '{1}' is not a valid integer.. /// - internal static string ArgumentInvalidInteger - { - get - { + internal static string ArgumentInvalidInteger { + get { return ResourceManager.GetString("ArgumentInvalidInteger", resourceCulture); } } - + /// /// Looks up a localized string similar to The argument '{0}' for '{1}' is not a valid string.. /// - internal static string ArgumentInvalidString - { - get - { + internal static string ArgumentInvalidString { + get { return ResourceManager.GetString("ArgumentInvalidString", resourceCulture); } } - + /// /// Looks up a localized string similar to The number of arguments '{1}' is not within the allowed range for '{0}'.. /// - internal static string ArgumentsOutOfRange - { - get - { + internal static string ArgumentsOutOfRange { + get { return ResourceManager.GetString("ArgumentsOutOfRange", resourceCulture); } } - + /// /// Looks up a localized string similar to The baseline '{0}' is obsolete. Consider switching to an alternative baseline.. /// - internal static string AliasReference - { - get - { - return ResourceManager.GetString("AliasReference", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Suppression for the rule '{0}' was configured using the alias '{1}'. Consider updating the suppression to use the name or id directly.. - /// - internal static string AliasSuppression - { - get - { - return ResourceManager.GetString("AliasSuppression", resourceCulture); + internal static string BaselineObsolete { + get { + return ResourceManager.GetString("BaselineObsolete", resourceCulture); } } - + /// /// Looks up a localized string similar to Binding functions are not supported in this language mode.. /// - internal static string ConstrainedTargetBinding - { - get - { + internal static string ConstrainedTargetBinding { + get { return ResourceManager.GetString("ConstrainedTargetBinding", resourceCulture); } } - + /// /// Looks up a localized string similar to {0}: The property '${1}.{2}' is obsolete and will be removed in the next major version.. /// - internal static string DebugPropertyObsolete - { - get - { + internal static string DebugPropertyObsolete { + get { return ResourceManager.GetString("DebugPropertyObsolete", resourceCulture); } } - + /// /// Looks up a localized string similar to Target failed If precondition. /// - internal static string DebugTargetIfMismatch - { - get - { + internal static string DebugTargetIfMismatch { + get { return ResourceManager.GetString("DebugTargetIfMismatch", resourceCulture); } } - + /// /// Looks up a localized string similar to Target failed Rule precondition. /// - internal static string DebugTargetRuleMismatch - { - get - { + internal static string DebugTargetRuleMismatch { + get { return ResourceManager.GetString("DebugTargetRuleMismatch", resourceCulture); } } - + + /// + /// Looks up a localized string similar to Target failed sub-selector pre-condition.. + /// + internal static string DebugTargetSubselectorMismatch { + get { + return ResourceManager.GetString("DebugTargetSubselectorMismatch", resourceCulture); + } + } + /// /// Looks up a localized string similar to Target failed Type precondition. /// - internal static string DebugTargetTypeMismatch - { - get - { + internal static string DebugTargetTypeMismatch { + get { return ResourceManager.GetString("DebugTargetTypeMismatch", resourceCulture); } } - + /// /// Looks up a localized string similar to A circular rule dependency was detected. The rule '{0}' depends on '{1}' which also depend on '{0}'.. /// - internal static string DependencyCircularReference - { - get - { + internal static string DependencyCircularReference { + get { return ResourceManager.GetString("DependencyCircularReference", resourceCulture); } } - + /// /// Looks up a localized string similar to The dependency '{0}' for '{1}' could not be found. Check that the rule is defined in a .Rule.ps1 file within the search path.. /// - internal static string DependencyNotFound - { - get - { + internal static string DependencyNotFound { + get { return ResourceManager.GetString("DependencyNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to The resource '{0}' is using a duplicate resource identifier. A resource with the identifier '{1}' already exists. Each resource must have a unique name, ref, and aliases. See https://aka.ms/ps-rule/naming for guidance on naming within PSRule.. /// - internal static string DuplicateResourceId - { - get - { + internal static string DuplicateResourceId { + get { return ResourceManager.GetString("DuplicateResourceId", resourceCulture); } } - + /// /// Looks up a localized string similar to A rule with the same name '{0}' already exists.. /// - internal static string DuplicateRuleName - { - get - { + internal static string DuplicateRuleName { + get { return ResourceManager.GetString("DuplicateRuleName", resourceCulture); } } - + /// /// Looks up a localized string similar to {0} : Reported '{1}'. At {2}:{3} char:{4}. /// - internal static string ErrorDetailMessage - { - get - { + internal static string ErrorDetailMessage { + get { return ResourceManager.GetString("ErrorDetailMessage", resourceCulture); } } - + /// /// Looks up a localized string similar to One or more errors occured.. /// - internal static string ErrorPipelineException - { - get - { + internal static string ErrorPipelineException { + get { return ResourceManager.GetString("ErrorPipelineException", resourceCulture); } } - + /// /// Looks up a localized string similar to Exists: {0}. /// - internal static string ExistsTrue - { - get - { + internal static string ExistsTrue { + get { return ResourceManager.GetString("ExistsTrue", resourceCulture); } } - + /// - /// Looks up a localized string similar to File. + /// Looks up a localized string similar to Failed to parse expression. The expression may not be valid. Expression: "{0}". /// - internal static string FileSourceType - { - get - { - return ResourceManager.GetString("FileSourceType", resourceCulture); + internal static string ExpressionInvalid { + get { + return ResourceManager.GetString("ExpressionInvalid", resourceCulture); } } - + /// - /// Looks up a localized string similar to Failed to parse expression. The expression may not be valid. Expression: "{0}". + /// Looks up a localized string similar to File. /// - internal static string ExpressionInvalid - { - get - { - return ResourceManager.GetString("ExpressionInvalid", resourceCulture); + internal static string FileSourceType { + get { + return ResourceManager.GetString("FileSourceType", resourceCulture); } } - + /// /// Looks up a localized string similar to [PSRule][D] -- Found {0} PSRule module(s). /// - internal static string FoundModules - { - get - { + internal static string FoundModules { + get { return ResourceManager.GetString("FoundModules", resourceCulture); } } - + /// /// Looks up a localized string similar to The function "{0}" was not found.. /// - internal static string FunctionNotFound - { - get - { + internal static string FunctionNotFound { + get { return ResourceManager.GetString("FunctionNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to The language expression index '{0}' is not valid for the object.. /// - internal static string IndexInvalid - { - get - { + internal static string IndexInvalid { + get { return ResourceManager.GetString("IndexInvalid", resourceCulture); } } - + /// /// Looks up a localized string similar to Output written to the following file: '{0}'. /// - internal static string InfoOutputPath - { - get - { + internal static string InfoOutputPath { + get { return ResourceManager.GetString("InfoOutputPath", resourceCulture); } } - + /// /// Looks up a localized string similar to An invalid ErrorAction ({0}) was specified for rule at {1}. Ignore | Stop are supported.. /// - internal static string InvalidErrorAction - { - get - { + internal static string InvalidErrorAction { + get { return ResourceManager.GetString("InvalidErrorAction", resourceCulture); } } - + /// /// Looks up a localized string similar to The resource name '{0}' is not valid at {1}. Each resource name must be between 3-128 characters in length, must start and end with a letter or number, and only contain letters, numbers, hyphens, dots, or underscores. See https://aka.ms/ps-rule/naming for more information.. /// - internal static string InvalidResourceName - { - get - { + internal static string InvalidResourceName { + get { return ResourceManager.GetString("InvalidResourceName", resourceCulture); } } - + /// /// Looks up a localized string similar to Rule nesting was detected for rule at {0}. Rules must not be nested.. /// - internal static string InvalidRuleNesting - { - get - { + internal static string InvalidRuleNesting { + get { return ResourceManager.GetString("InvalidRuleNesting", resourceCulture); } } - + /// /// Looks up a localized string similar to An invalid rule result was returned for {0}. Conditions must return boolean $True or $False.. /// - internal static string InvalidRuleResult - { - get - { + internal static string InvalidRuleResult { + get { return ResourceManager.GetString("InvalidRuleResult", resourceCulture); } } - + /// /// Looks up a localized string similar to The keyword '{0}' can only be used within a Rule block.. /// - internal static string KeywordConditionScope - { - get - { + internal static string KeywordConditionScope { + get { return ResourceManager.GetString("KeywordConditionScope", resourceCulture); } } - + /// /// Looks up a localized string similar to The keyword '{0}' can only be used within a Rule block or script precondition.. /// - internal static string KeywordRuleScope - { - get - { + internal static string KeywordRuleScope { + get { return ResourceManager.GetString("KeywordRuleScope", resourceCulture); } } - + /// /// Looks up a localized string similar to The keyword '{0}' can not be nested in a Rule block.. /// - internal static string KeywordSourceScope - { - get - { + internal static string KeywordSourceScope { + get { return ResourceManager.GetString("KeywordSourceScope", resourceCulture); } } - + /// /// Looks up a localized string similar to [PSRule][{0}][Trace] -- {1}: {2}. /// - internal static string LanguageExpressionTraceP2 - { - get - { + internal static string LanguageExpressionTraceP2 { + get { return ResourceManager.GetString("LanguageExpressionTraceP2", resourceCulture); } } - + /// /// Looks up a localized string similar to [PSRule][{0}][Trace] -- {1}: {2} {1} {3}. /// - internal static string LanguageExpressionTraceP3 - { - get - { + internal static string LanguageExpressionTraceP3 { + get { return ResourceManager.GetString("LanguageExpressionTraceP3", resourceCulture); } } - - internal static string LanguageExpressionTrace - { - get - { - return ResourceManager.GetString("LanguageExpressionTrace", resourceCulture); - } - } - + /// /// Looks up a localized string similar to Please open your browser to the following location: {0}. /// - internal static string LaunchBrowser - { - get - { + internal static string LaunchBrowser { + get { return ResourceManager.GetString("LaunchBrowser", resourceCulture); } } - + /// /// Looks up a localized string similar to Wildcard match requires exactly one name.. /// - internal static string MatchSingleName - { - get - { + internal static string MatchSingleName { + get { return ResourceManager.GetString("MatchSingleName", resourceCulture); } } - + /// /// Looks up a localized string similar to Matches: {0}. /// - internal static string MatchTrue - { - get - { + internal static string MatchTrue { + get { return ResourceManager.GetString("MatchTrue", resourceCulture); } } - + /// /// Looks up a localized string similar to Update module '{0}' to set the default baseline using a module configuration resource instead. Configuring the default baseline via manifest will be removed in the next major version. See https://aka.ms/ps-rule/module-config.. /// - internal static string ModuleManifestBaseline - { - get - { + internal static string ModuleManifestBaseline { + get { return ResourceManager.GetString("ModuleManifestBaseline", resourceCulture); } } - + /// /// Looks up a localized string similar to No valid module can be found with that name.. /// - internal static string ModuleNotFound - { - get - { + internal static string ModuleNotFound { + get { return ResourceManager.GetString("ModuleNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to Target object '{0}' has not been processed because no matching rules were found.. /// - internal static string ObjectNotProcessed - { - get - { + internal static string ObjectNotProcessed { + get { return ResourceManager.GetString("ObjectNotProcessed", resourceCulture); } } - + /// /// Looks up a localized string similar to Object path not found.. /// - internal static string ObjectPathNotFound - { - get - { + internal static string ObjectPathNotFound { + get { return ResourceManager.GetString("ObjectPathNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to Options file does not exist.. /// - internal static string OptionsNotFound - { - get - { + internal static string OptionsNotFound { + get { return ResourceManager.GetString("OptionsNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to # Source: {0}. /// - internal static string OptionsSourceComment - { - get - { + internal static string OptionsSourceComment { + get { return ResourceManager.GetString("OptionsSourceComment", resourceCulture); } } - + /// /// Looks up a localized string similar to Error. /// - internal static string OutcomeError - { - get - { + internal static string OutcomeError { + get { return ResourceManager.GetString("OutcomeError", resourceCulture); } } - + /// /// Looks up a localized string similar to Fail. /// - internal static string OutcomeFail - { - get - { + internal static string OutcomeFail { + get { return ResourceManager.GetString("OutcomeFail", resourceCulture); } } - + /// /// Looks up a localized string similar to Pass. /// - internal static string OutcomePass - { - get - { + internal static string OutcomePass { + get { return ResourceManager.GetString("OutcomePass", resourceCulture); } } - + /// /// Looks up a localized string similar to [FAIL] -- {0}:: Reported for '{1}'. /// - internal static string OutcomeRuleFail - { - get - { + internal static string OutcomeRuleFail { + get { return ResourceManager.GetString("OutcomeRuleFail", resourceCulture); } } - + /// /// Looks up a localized string similar to [PASS] -- {0}:: Reported for '{1}'. /// - internal static string OutcomeRulePass - { - get - { + internal static string OutcomeRulePass { + get { return ResourceManager.GetString("OutcomeRulePass", resourceCulture); } } - + /// /// Looks up a localized string similar to Unknown. /// - internal static string OutcomeUnknown - { - get - { + internal static string OutcomeUnknown { + get { return ResourceManager.GetString("OutcomeUnknown", resourceCulture); } } - + /// /// Looks up a localized string similar to The language expression property '{0}' doesn't exist.. /// - internal static string PropertyNotFound - { - get - { + internal static string PropertyNotFound { + get { return ResourceManager.GetString("PropertyNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to The property '${0}.{1}' is obsolete and will be removed in the next major version.. /// - internal static string PropertyObsolete - { - get - { + internal static string PropertyObsolete { + get { return ResourceManager.GetString("PropertyObsolete", resourceCulture); } } - + /// /// Looks up a localized string similar to Failed to deserialize the file '{0}': {1}. /// - internal static string ReadFileFailed - { - get - { + internal static string ReadFileFailed { + get { return ResourceManager.GetString("ReadFileFailed", resourceCulture); } } - + /// /// Looks up a localized string similar to Read JSON failed.. /// - internal static string ReadJsonFailed - { - get - { + internal static string ReadJsonFailed { + get { return ResourceManager.GetString("ReadJsonFailed", resourceCulture); } } - + /// /// Looks up a localized string similar to Read JSON failed because the token ({0}) was not expected.. /// - internal static string ReadJsonFailedExpectedToken - { - get - { + internal static string ReadJsonFailedExpectedToken { + get { return ResourceManager.GetString("ReadJsonFailedExpectedToken", resourceCulture); } } - + /// /// Looks up a localized string similar to The module version '{1}' for '{0}' does not match the required version '{2}'. To continue, first update the module to match the version requirement.. /// - internal static string RequiredVersionMismatch - { - get - { + internal static string RequiredVersionMismatch { + get { return ResourceManager.GetString("RequiredVersionMismatch", resourceCulture); } } - + /// /// Looks up a localized string similar to The {0} '{1}' is obsolete. Consider switching to an alternative {0}.. /// - internal static string ResourceObsolete - { - get - { + internal static string ResourceObsolete { + get { return ResourceManager.GetString("ResourceObsolete", resourceCulture); } } - + /// /// Looks up a localized string similar to {0} rule/s were suppressed for '{1}'.. /// - internal static string RuleCountSuppressed - { - get - { + internal static string RuleCountSuppressed { + get { return ResourceManager.GetString("RuleCountSuppressed", resourceCulture); } } - + /// /// Looks up a localized string similar to One or more rules reported errors.. /// - internal static string RuleErrorPipelineException - { - get - { + internal static string RuleErrorPipelineException { + get { return ResourceManager.GetString("RuleErrorPipelineException", resourceCulture); } } - + /// /// Looks up a localized string similar to One or more rules reported failure.. /// - internal static string RuleFailPipelineException - { - get - { + internal static string RuleFailPipelineException { + get { return ResourceManager.GetString("RuleFailPipelineException", resourceCulture); } } - + /// /// Looks up a localized string similar to Inconclusive result reported for '{1}' @{0}.. /// - internal static string RuleInconclusive - { - get - { + internal static string RuleInconclusive { + get { return ResourceManager.GetString("RuleInconclusive", resourceCulture); } } - + /// /// Looks up a localized string similar to [PSRule][R][Trace] -- {0}. /// - internal static string RuleMatchTrace - { - get - { + internal static string RuleMatchTrace { + get { return ResourceManager.GetString("RuleMatchTrace", resourceCulture); } } - + /// /// Looks up a localized string similar to Could not find a matching rule. Please check that Path, Name and Tag parameters are correct.. /// - internal static string RuleNotFound - { - get - { + internal static string RuleNotFound { + get { return ResourceManager.GetString("RuleNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to Could not find required rule definition parameter '{0}' on rule at {1}.. /// - internal static string RuleParameterNotFound - { - get - { + internal static string RuleParameterNotFound { + get { return ResourceManager.GetString("RuleParameterNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to No matching .Rule.ps1 files were found. Rule definitions should be saved into script files with the .Rule.ps1 extension.. /// - internal static string RulePathNotFound - { - get - { + internal static string RulePathNotFound { + get { return ResourceManager.GetString("RulePathNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to at Rule '{0}', {1}: line {2}. /// - internal static string RuleStackTrace - { - get - { + internal static string RuleStackTrace { + get { return ResourceManager.GetString("RuleStackTrace", resourceCulture); } } - + /// /// Looks up a localized string similar to Rule '{0}' was suppressed for '{1}'.. /// - internal static string RuleSuppressed - { - get - { + internal static string RuleSuppressed { + get { return ResourceManager.GetString("RuleSuppressed", resourceCulture); } } - + /// /// Looks up a localized string similar to Rule '{0}' was suppressed by suppression group '{1}' for '{2}'.. /// - internal static string RuleSuppressionGroup - { - get - { + internal static string RuleSuppressionGroup { + get { return ResourceManager.GetString("RuleSuppressionGroup", resourceCulture); } } - + /// /// Looks up a localized string similar to {0} rule/s were suppressed by suppression group '{1}' for '{2}'.. /// - internal static string RuleSuppressionGroupCount - { - get - { + internal static string RuleSuppressionGroupCount { + get { return ResourceManager.GetString("RuleSuppressionGroupCount", resourceCulture); } } - + /// /// Looks up a localized string similar to Rule '{0}' was suppressed by suppression group '{1}' for '{2}'. {3}. /// - internal static string RuleSuppressionGroupExtended - { - get - { + internal static string RuleSuppressionGroupExtended { + get { return ResourceManager.GetString("RuleSuppressionGroupExtended", resourceCulture); } } - + /// /// Looks up a localized string similar to {0} rule/s were suppressed by suppression group '{1}' for '{2}'. {3}. /// - internal static string RuleSuppressionGroupExtendedCount - { - get - { + internal static string RuleSuppressionGroupExtendedCount { + get { return ResourceManager.GetString("RuleSuppressionGroupExtendedCount", resourceCulture); } } - + /// /// Looks up a localized string similar to [PSRule][D] -- Scanning for source files in module: {0}. /// - internal static string ScanModule - { - get - { + internal static string ScanModule { + get { return ResourceManager.GetString("ScanModule", resourceCulture); } } - + /// /// Looks up a localized string similar to [PSRule][D] -- Scanning for source files: {0}. /// - internal static string ScanSource - { - get - { + internal static string ScanSource { + get { return ResourceManager.GetString("ScanSource", resourceCulture); } } - + /// /// Looks up a localized string similar to The script was not found.. /// - internal static string ScriptNotFound - { - get - { + internal static string ScriptNotFound { + get { return ResourceManager.GetString("ScriptNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to [PSRule][S][Trace] -- {0}. /// - internal static string SelectorMatchTrace - { - get - { + internal static string SelectorMatchTrace { + get { return ResourceManager.GetString("SelectorMatchTrace", resourceCulture); } } - - /// - /// Looks up a localized string similar to [PSRule][S][Trace] -- {0}: {1}. - /// - internal static string SelectorTrace - { - get - { - return ResourceManager.GetString("SelectorTrace", resourceCulture); - } - } - + /// /// Looks up a localized string similar to Can not serialize a null PSObject.. /// - internal static string SerializeNullPSObject - { - get - { + internal static string SerializeNullPSObject { + get { return ResourceManager.GetString("SerializeNullPSObject", resourceCulture); } } - + /// /// Looks up a localized string similar to Create path. /// - internal static string ShouldCreatePath - { - get - { + internal static string ShouldCreatePath { + get { return ResourceManager.GetString("ShouldCreatePath", resourceCulture); } } - + /// /// Looks up a localized string similar to Write file. /// - internal static string ShouldWriteFile - { - get - { + internal static string ShouldWriteFile { + get { return ResourceManager.GetString("ShouldWriteFile", resourceCulture); } } - + /// /// Looks up a localized string similar to The source was not found.. /// - internal static string SourceNotFound - { - get - { + internal static string SourceNotFound { + get { return ResourceManager.GetString("SourceNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to Using invariant culture may cause rule infomation to be displayed incorrectly. Consider using -Culture or set the Output.Culture option.. /// - internal static string UsingInvariantCulture - { - get - { + internal static string UsingInvariantCulture { + get { return ResourceManager.GetString("UsingInvariantCulture", resourceCulture); } } - + /// /// Looks up a localized string similar to The variable '${0}' can only be used within a Rule block.. /// - internal static string VariableConditionScope - { - get - { + internal static string VariableConditionScope { + get { return ResourceManager.GetString("VariableConditionScope", resourceCulture); } } - + /// /// Looks up a localized string similar to The version constraint '{0}' is not valid.. /// - internal static string VersionConstraintInvalid - { - get - { + internal static string VersionConstraintInvalid { + get { return ResourceManager.GetString("VersionConstraintInvalid", resourceCulture); } } - + /// /// Looks up a localized string similar to The Within parameter Value must be a string when the Like parameter is used.. /// - internal static string WithinLikeNotString - { - get - { + internal static string WithinLikeNotString { + get { return ResourceManager.GetString("WithinLikeNotString", resourceCulture); } } - + /// /// Looks up a localized string similar to Within: {0}. /// - internal static string WithinTrue - { - get - { + internal static string WithinTrue { + get { return ResourceManager.GetString("WithinTrue", resourceCulture); } } diff --git a/src/PSRule/Resources/PSRuleResources.resx b/src/PSRule/Resources/PSRuleResources.resx index 71168f0047..4097ade673 100644 --- a/src/PSRule/Resources/PSRuleResources.resx +++ b/src/PSRule/Resources/PSRuleResources.resx @@ -382,4 +382,7 @@ Read JSON failed because the token ({0}) was not expected. + + Target failed sub-selector pre-condition. + \ No newline at end of file diff --git a/src/PSRule/Runtime/LanguageScope.cs b/src/PSRule/Runtime/LanguageScope.cs index f94c4f4ccc..e9ad168350 100644 --- a/src/PSRule/Runtime/LanguageScope.cs +++ b/src/PSRule/Runtime/LanguageScope.cs @@ -12,6 +12,9 @@ namespace PSRule.Runtime /// internal interface ILanguageScope : IDisposable { + /// + /// The name of the scope. + /// string Name { get; } /// @@ -28,29 +31,42 @@ internal interface ILanguageScope : IDisposable IResourceFilter GetFilter(ResourceKind kind); + /// + /// Add a service to the scope. + /// void AddService(string name, object service); + /// + /// Get a previously added service. + /// object GetService(string name); + + bool TryGetType(object o, out string type, out string path); + + bool TryGetName(object o, out string name, out string path); } internal sealed class LanguageScope : ILanguageScope { private const string STANDALONE_SCOPENAME = "."; + private readonly RunspaceContext _Context; private readonly Dictionary _Configuration; private readonly Dictionary _Service; private readonly Dictionary _Filter; private bool _Disposed; - public LanguageScope(string name) + public LanguageScope(RunspaceContext context, string name) { + _Context = context; Name = Normalize(name); _Configuration = new Dictionary(); _Filter = new Dictionary(); _Service = new Dictionary(); } + /// public string Name { get; } public void Configure(Dictionary configuration) @@ -74,6 +90,7 @@ public IResourceFilter GetFilter(ResourceKind kind) return _Filter.TryGetValue(kind, out var filter) ? filter : null; } + /// public void AddService(string name, object service) { if (_Service.ContainsKey(name)) @@ -82,11 +99,54 @@ public void AddService(string name, object service) _Service.Add(name, service); } + /// public object GetService(string name) { return _Service.TryGetValue(name, out var service) ? service : null; } + public bool TryGetType(object o, out string type, out string path) + { + if (_Context != null && _Context.TargetObject.Value == o) + { + var binding = _Context.TargetBinder.Result(Name); + type = binding.TargetType; + path = binding.TargetTypePath; + return true; + } + else if (_Context != null) + { + var binding = _Context.TargetBinder.Using(Name).Bind(o); + type = binding.TargetType; + path = binding.TargetTypePath; + return true; + } + type = null; + path = null; + return false; + } + + public bool TryGetName(object o, out string name, out string path) + { + if (_Context != null && _Context.TargetObject.Value == o) + { + var binding = _Context.TargetBinder.Result(Name); + name = binding.TargetName; + path = binding.TargetNamePath; + return true; + } + else if (_Context != null) + { + var binding = _Context.TargetBinder.Using(Name).Bind(o); + name = binding.TargetName; + path = binding.TargetNamePath; + return true; + } + name = null; + path = null; + return false; + } + internal static string Normalize(string scope) { return string.IsNullOrEmpty(scope) ? STANDALONE_SCOPENAME : scope; @@ -123,13 +183,15 @@ public void Dispose() internal sealed class LanguageScopeSet : IDisposable { + private readonly RunspaceContext _Context; private readonly Dictionary _Scopes; private ILanguageScope _Current; private bool _Disposed; - public LanguageScopeSet() + public LanguageScopeSet(RunspaceContext context) { + _Context = context; _Scopes = new Dictionary(StringComparer.OrdinalIgnoreCase); Import(null, out _Current); } @@ -168,7 +230,7 @@ internal bool Import(string name, out ILanguageScope scope) if (_Scopes.TryGetValue(GetScopeName(name), out scope)) return false; - scope = new LanguageScope(name); + scope = new LanguageScope(_Context, name); Add(scope); return true; } diff --git a/src/PSRule/Runtime/ObjectPath/PathExpression.cs b/src/PSRule/Runtime/ObjectPath/PathExpression.cs index f8fd9fc9cb..bffc335af5 100644 --- a/src/PSRule/Runtime/ObjectPath/PathExpression.cs +++ b/src/PSRule/Runtime/ObjectPath/PathExpression.cs @@ -10,7 +10,7 @@ namespace PSRule.Runtime.ObjectPath /// /// An expression function that returns one or more values when successful. /// - internal delegate bool PathExpressionFn(IPathExpressionContext context, object input, out IEnumerable value); + internal delegate bool PathExpressionFn(IPathExpressionContext context, object input, out IEnumerable value, out bool enumerable); /// /// A function for filter objects that simply returns true or false. @@ -92,7 +92,7 @@ public static PathExpression Create(string path) public bool TryGet(object o, bool caseSensitive, out object[] value) { value = null; - if (!TryGet(o, caseSensitive, out IEnumerable result)) + if (!TryGet(o, caseSensitive, out var result, out var enumerable)) return false; value = result.ToArray(); @@ -106,10 +106,11 @@ public bool TryGet(object o, bool caseSensitive, out object[] value) public bool TryGet(object o, bool caseSensitive, out object value) { value = null; - if (!TryGet(o, caseSensitive, out object[] result)) + if (!TryGet(o, caseSensitive, out var result, out var enumerable)) return false; - value = IsArray ? result : result[0]; + var items = result.ToArray(); + value = IsArray || enumerable ? items : items[0]; return true; } @@ -120,10 +121,10 @@ public bool TryGet(object o, bool caseSensitive, out object value) /// Determines if member name matching is case-sensitive. /// The values selected from the object. /// Returns true when the path exists within the object. Returns false if the path does not exist. - private bool TryGet(object o, bool caseSensitive, out IEnumerable value) + private bool TryGet(object o, bool caseSensitive, out IEnumerable value, out bool enumerable) { var context = new PathExpressionContext(o, caseSensitive); - return _Expression.Invoke(context, o, out value); + return _Expression.Invoke(context, o, out value, out enumerable); } } } diff --git a/src/PSRule/Runtime/ObjectPath/PathExpressionBuilder.cs b/src/PSRule/Runtime/ObjectPath/PathExpressionBuilder.cs index 22f0a98319..a8a90d918f 100644 --- a/src/PSRule/Runtime/ObjectPath/PathExpressionBuilder.cs +++ b/src/PSRule/Runtime/ObjectPath/PathExpressionBuilder.cs @@ -95,7 +95,7 @@ private PathExpressionFn FilterSelector(ITokenReader reader) UseArray(); var filter = BuildExpression(reader, PathTokenType.EndFilter); var next = BuildSelector(reader); - return (IPathExpressionContext context, object input, out IEnumerable value) => + return (IPathExpressionContext context, object input, out IEnumerable value, out bool enumerable) => { var result = new List(); var success = 0; @@ -104,13 +104,14 @@ private PathExpressionFn FilterSelector(ITokenReader reader) if (!filter(context, i)) continue; - if (!next(context, i, out var items)) + if (!next(context, i, out var items, out _)) continue; success++; result.AddRange(items); } value = success > 0 ? result.ToArray() : null; + enumerable = value != null; return success > 0; }; } @@ -118,10 +119,11 @@ private PathExpressionFn FilterSelector(ITokenReader reader) private PathExpressionFn IndexSelector(ITokenReader reader, int index) { var next = BuildSelector(reader); - return (IPathExpressionContext context, object input, out IEnumerable value) => + return (IPathExpressionContext context, object input, out IEnumerable value, out bool enumerable) => { value = null; - return TryGetIndex(input, index, out var item) && next(context, item, out value); + enumerable = false; + return TryGetIndex(input, index, out var item) && next(context, item, out value, out enumerable); }; } @@ -129,19 +131,20 @@ private PathExpressionFn IndexWildSelector(ITokenReader reader) { UseArray(); var next = BuildSelector(reader); - return (IPathExpressionContext context, object input, out IEnumerable value) => + return (IPathExpressionContext context, object input, out IEnumerable value, out bool enumerable) => { var result = new List(); var success = 0; foreach (var i in GetAll(input)) { - if (!next(context, i, out var items)) + if (!next(context, i, out var items, out _)) continue; success++; result.AddRange(items); } value = success > 0 ? result.ToArray() : null; + enumerable = value != null; return success > 0; }; } @@ -153,18 +156,19 @@ private PathExpressionFn ArraySliceSelector(ITokenReader reader, int?[] arg) var step = arg[2].GetValueOrDefault(1); var start = arg[0].GetValueOrDefault(step >= 0 ? 0 : -1); var end = arg[1]; - return (IPathExpressionContext context, object input, out IEnumerable value) => + return (IPathExpressionContext context, object input, out IEnumerable value, out bool enumerable) => { var result = new List(); var currentIndex = start; while ((!end.HasValue || (step > 0 && currentIndex < end) || (step < 0 && currentIndex > end)) && TryGetIndex(input, currentIndex, out var slice)) { currentIndex += step; - if (!next(context, slice, out var items)) + if (!next(context, slice, out var items, out _)) continue; result.AddRange(items); } + enumerable = true; value = result.ToArray(); return true; }; @@ -174,11 +178,12 @@ private PathExpressionFn DotSelector(ITokenReader reader, string memberName, Pat { var caseSensitiveFlag = option == PathTokenOption.CaseSensitive; var next = BuildSelector(reader); - return (IPathExpressionContext context, object input, out IEnumerable value) => + return (IPathExpressionContext context, object input, out IEnumerable value, out bool enumerable) => { value = null; + enumerable = false; var caseSensitive = context.CaseSensitive != caseSensitiveFlag; - return TryGetField(input, memberName, caseSensitive, out var item) && next(context, item, out value); + return TryGetField(input, memberName, caseSensitive, out var item) && next(context, item, out value, out enumerable); }; } @@ -186,20 +191,21 @@ private PathExpressionFn DescendantSelector(ITokenReader reader, string memberNa { var caseSensitiveFlag = option == PathTokenOption.CaseSensitive; var next = BuildSelector(reader); - return (IPathExpressionContext context, object input, out IEnumerable value) => + return (IPathExpressionContext context, object input, out IEnumerable value, out bool enumerable) => { var caseSensitive = context.CaseSensitive != caseSensitiveFlag; var result = new List(); var success = 0; foreach (var i in GetAllRecurse(input, memberName, caseSensitive)) { - if (!next(context, i, out var items)) + if (!next(context, i, out var items, out _)) continue; success++; result.AddRange(items); } value = success > 0 ? result.ToArray() : null; + enumerable = value != null; return success > 0; }; } @@ -207,13 +213,13 @@ private PathExpressionFn DescendantSelector(ITokenReader reader, string memberNa private PathExpressionFn CurrentRef(ITokenReader reader) { var next = BuildSelector(reader); - return (IPathExpressionContext context, object input, out IEnumerable value) => next(context, input, out value); + return (IPathExpressionContext context, object input, out IEnumerable value, out bool enumerable) => next(context, input, out value, out enumerable); } private PathExpressionFn RootRef(ITokenReader reader) { var next = BuildSelector(reader); - return (IPathExpressionContext context, object input, out IEnumerable value) => next(context, context.Input, out value); + return (IPathExpressionContext context, object input, out IEnumerable value, out bool enumerable) => next(context, context.Input, out value, out enumerable); } private PathExpressionFilterFn BuildExpression(ITokenReader reader, PathTokenType stop) @@ -282,7 +288,7 @@ private PathExpressionFilterFn BuildRelationExpression(ITokenReader reader) private static PathExpressionFilterFn ExistCondition(PathExpressionFn next) { - return (IPathExpressionContext context, object input) => next(context, input, out _); + return (IPathExpressionContext context, object input) => next(context, input, out _, out _); } private static PathExpressionFilterFn NotCondition(PathExpressionFilterFn next) @@ -294,7 +300,7 @@ private static PathExpressionFilterFn BinaryCondition(PathExpressionFn left, Pat { return (IPathExpressionContext context, object input) => { - if (!left(context, input, out var leftValue) || !right(context, input, out var rightValue)) + if (!left(context, input, out var leftValue, out _) || !right(context, input, out var rightValue, out _)) return false; var operand1 = leftValue.FirstOrDefault(); @@ -328,22 +334,32 @@ private static PathExpressionFilterFn BinaryCondition(PathExpressionFn left, Pat }; } - private static bool Return(IPathExpressionContext context, object input, out IEnumerable value) + private static bool Return(IPathExpressionContext context, object input, out IEnumerable value, out bool enumerable) { // Unwrap primitive types if (input is JValue jValue && (jValue.Type == JTokenType.String || jValue.Type == JTokenType.Integer || jValue.Type == JTokenType.Boolean)) input = jValue.Value; - value = new object[] { input }; + enumerable = false; + if (input is object[] eo) + { + enumerable = true; + value = eo; + } + else + value = new object[] { input }; + return true; } private static PathExpressionFn Literal(object arg) { - var result = new object[] { arg }; - return (IPathExpressionContext context, object input, out IEnumerable value) => + var isEnumerable = arg is object[]; + var result = isEnumerable ? arg as object[] : new object[] { arg }; + return (IPathExpressionContext context, object input, out IEnumerable value, out bool enumerable) => { value = result; + enumerable = isEnumerable; return true; }; } diff --git a/src/PSRule/Runtime/RunspaceContext.cs b/src/PSRule/Runtime/RunspaceContext.cs index 31b5c2046e..f2a0a11508 100644 --- a/src/PSRule/Runtime/RunspaceContext.cs +++ b/src/PSRule/Runtime/RunspaceContext.cs @@ -70,7 +70,7 @@ internal sealed class RunspaceContext : IDisposable // Fields exposed to engine internal RuleRecord RuleRecord; internal RuleBlock RuleBlock; - internal ITargetBindingContext Binding; + internal ITargetBindingResult Binding; private readonly bool _InconclusiveWarning; private readonly bool _NotProcessedWarning; @@ -118,7 +118,7 @@ internal RunspaceContext(PipelineContext pipeline, IPipelineWriter writer) _RuleTimer = new Stopwatch(); _Reason = new List(); _Conventions = new List(); - _LanguageScopes = new LanguageScopeSet(); + _LanguageScopes = new LanguageScopeSet(this); _Scope = new Stack(); } @@ -643,7 +643,7 @@ public bool TrySelector(ResourceId id) /// public RuleRecord EnterRuleBlock(RuleBlock ruleBlock) { - Binding = TargetBinder.Using(ruleBlock.Info.ModuleName); + Binding = TargetBinder.Result(ruleBlock.Info.ModuleName); _RuleErrors = 0; RuleBlock = ruleBlock; @@ -791,10 +791,14 @@ public void Begin() Pipeline.BindField, Pipeline.Option.Input.TargetType); + HashSet _TypeFilter = null; + if (Pipeline.Option.Input.TargetType != null && Pipeline.Option.Input.TargetType.Length > 0) + _TypeFilter = new HashSet(Pipeline.Option.Input.TargetType, StringComparer.OrdinalIgnoreCase); + foreach (var languageScope in _LanguageScopes.Get()) { var targetBinding = Pipeline.Baseline.GetTargetBinding(); - builder.With(new TargetBinder.TargetBindingContext(languageScope.Name, targetBinding)); + builder.With(new TargetBinder.TargetBindingContext(languageScope.Name, targetBinding, Pipeline.BindTargetName, Pipeline.BindTargetType, Pipeline.BindField, _TypeFilter)); } TargetBinder = builder.Build(); RunConventionInitialize(); diff --git a/tests/PSRule.Tests/FromFileSubSelector.Rule.jsonc b/tests/PSRule.Tests/FromFileSubSelector.Rule.jsonc new file mode 100644 index 0000000000..f831aa0394 --- /dev/null +++ b/tests/PSRule.Tests/FromFileSubSelector.Rule.jsonc @@ -0,0 +1,66 @@ +[ + { + // Synopsis: A rule with sub-selector pre-condition. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "JsonRuleWithPrecondition" + }, + "spec": { + "where": { + "field": "kind", + "equals": "test" + }, + "condition": { + "field": "resources", + "count": 2 + } + } + }, + { + // Synopsis: A rule with sub-selector filter. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "JsonRuleWithSubselector" + }, + "spec": { + "condition": { + "field": "resources", + "where": { + "field": ".", + "isString": true + }, + "allOf": [ + { + "field": ".", + "equals": "abc" + } + ] + } + } + }, + { + // Synopsis: A rule with sub-selector filter. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "JsonRuleWithSubselectorReordered" + }, + "spec": { + "condition": { + "allOf": [ + { + "field": ".", + "equals": "abc" + } + ], + "field": "resources", + "where": { + "field": ".", + "equals": "abc" + } + } + } + } +] diff --git a/tests/PSRule.Tests/FromFileSubSelector.Rule.yaml b/tests/PSRule.Tests/FromFileSubSelector.Rule.yaml new file mode 100644 index 0000000000..9ff8d2b065 --- /dev/null +++ b/tests/PSRule.Tests/FromFileSubSelector.Rule.yaml @@ -0,0 +1,52 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# YAML-based rules for unit testing +# + +--- +# Synopsis: A rule with sub-selector pre-condition. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Rule +metadata: + name: YamlRuleWithPrecondition +spec: + where: + field: 'kind' + equals: 'test' + condition: + field: resources + count: 2 + +--- +# Synopsis: A rule with sub-selector filter. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Rule +metadata: + name: YamlRuleWithSubselector +spec: + condition: + field: resources + where: + field: '.' + isString: true + allOf: + - field: '.' + equals: abc + +--- +# Synopsis: A rule with sub-selector filter. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Rule +metadata: + name: YamlRuleWithSubselectorReordered +spec: + condition: + allOf: + - field: '.' + equals: abc + field: resources + where: + field: '.' + equals: 'abc' diff --git a/tests/PSRule.Tests/FunctionBuilderTests.cs b/tests/PSRule.Tests/FunctionBuilderTests.cs index 963551a2c1..b2e4646a55 100644 --- a/tests/PSRule.Tests/FunctionBuilderTests.cs +++ b/tests/PSRule.Tests/FunctionBuilderTests.cs @@ -52,7 +52,7 @@ private static SelectorVisitor GetSelectorVisitor(string name, Source[] source, context.Init(source); context.Begin(); var selector = HostHelper.GetSelector(source, context).ToArray().FirstOrDefault(s => s.Name == name); - return new SelectorVisitor(selector.Id, selector.Source, selector.Spec.If); + return new SelectorVisitor(context, selector.Id, selector.Source, selector.Spec.If); } private static string GetSourcePath(string fileName) diff --git a/tests/PSRule.Tests/FunctionTests.cs b/tests/PSRule.Tests/FunctionTests.cs index 8dedd43b18..87772ce402 100644 --- a/tests/PSRule.Tests/FunctionTests.cs +++ b/tests/PSRule.Tests/FunctionTests.cs @@ -264,7 +264,7 @@ private static ExpressionContext GetContext() targetObject.Properties.Add(new PSNoteProperty("name", "TestObject1")); var context = new Runtime.RunspaceContext(PipelineContext.New(GetOption(), null, null, PipelineHookActions.BindTargetName, PipelineHookActions.BindTargetType, PipelineHookActions.BindField, new OptionContextBuilder(GetOption(), null, null, null).Build(), null), null); var s = GetSource(); - var result = new ExpressionContext(s[0].File[0], Definitions.ResourceKind.Rule, targetObject); + var result = new ExpressionContext(context, s[0].File[0], Definitions.ResourceKind.Rule, targetObject); context.Init(s); context.Begin(); context.PushScope(Runtime.RunspaceScope.Precondition); diff --git a/tests/PSRule.Tests/PSRule.Tests.csproj b/tests/PSRule.Tests/PSRule.Tests.csproj index 3feade5467..d0bd6c0e44 100644 --- a/tests/PSRule.Tests/PSRule.Tests.csproj +++ b/tests/PSRule.Tests/PSRule.Tests.csproj @@ -70,6 +70,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest diff --git a/tests/PSRule.Tests/RulesTests.cs b/tests/PSRule.Tests/RulesTests.cs index ce11715c9d..abe32802c7 100644 --- a/tests/PSRule.Tests/RulesTests.cs +++ b/tests/PSRule.Tests/RulesTests.cs @@ -20,6 +20,9 @@ public sealed class RulesTests { #region Yaml rules + /// + /// Test that a YAML-based rule can be parsed. + /// [Fact] public void ReadYamlRule() { @@ -44,6 +47,61 @@ public void ReadYamlRule() Assert.Equal("tag", hashtable["feature"]); } + /// + /// Test that a YAML-based rule with sub-selectors can be parsed. + /// + [Fact] + public void ReadYamlSubSelectorRule() + { + var context = new RunspaceContext(PipelineContext.New(GetOption(), null, null, PipelineHookActions.BindTargetName, PipelineHookActions.BindTargetType, PipelineHookActions.BindField, new OptionContext(), null), new TestWriter(GetOption())); + context.Init(GetSource("FromFileSubSelector.Rule.yaml")); + context.Begin(); + + // From current path + var rule = HostHelper.GetRule(GetSource("FromFileSubSelector.Rule.yaml"), context, includeDependencies: false); + Assert.NotNull(rule); + Assert.Equal("YamlRuleWithPrecondition", rule[0].Name); + Assert.Equal("YamlRuleWithSubselector", rule[1].Name); + Assert.Equal("YamlRuleWithSubselectorReordered", rule[2].Name); + + context.Init(GetSource("FromFileSubSelector.Rule.yaml")); + context.Begin(); + var subselector1 = GetRuleVisitor(context, "YamlRuleWithPrecondition", GetSource("FromFileSubSelector.Rule.yaml")); + var subselector2 = GetRuleVisitor(context, "YamlRuleWithSubselector", GetSource("FromFileSubSelector.Rule.yaml")); + var subselector3 = GetRuleVisitor(context, "YamlRuleWithSubselectorReordered", GetSource("FromFileSubSelector.Rule.yaml")); + context.EnterSourceScope(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" })); + + // YamlRuleWithPrecondition + context.EnterTargetObject(actual1); + context.EnterRuleBlock(subselector1); + Assert.True(subselector1.Condition.If().AllOf()); + + context.EnterTargetObject(actual2); + context.EnterRuleBlock(subselector1); + Assert.True(subselector1.Condition.If().Skipped()); + + // YamlRuleWithSubselector + context.EnterTargetObject(actual1); + context.EnterRuleBlock(subselector2); + Assert.True(subselector2.Condition.If().AllOf()); + + context.EnterTargetObject(actual2); + context.EnterRuleBlock(subselector2); + Assert.False(subselector2.Condition.If().AllOf()); + + // YamlRuleWithSubselectorReordered + context.EnterTargetObject(actual1); + context.EnterRuleBlock(subselector3); + Assert.True(subselector3.Condition.If().AllOf()); + + context.EnterTargetObject(actual2); + context.EnterRuleBlock(subselector3); + Assert.True(subselector3.Condition.If().AllOf()); + } + [Fact] public void EvaluateYamlRule() { @@ -136,6 +194,9 @@ public void RuleWithObjectPath() #region Json rules + /// + /// Test that a JSON-based rule can be parsed. + /// [Fact] public void ReadJsonRule() { @@ -160,6 +221,61 @@ public void ReadJsonRule() Assert.Equal("tag", hashtable["feature"]); } + /// + /// Test that a JSON-based rule with sub-selectors can be parsed. + /// + [Fact] + public void ReadJsonSubSelectorRule() + { + var context = new RunspaceContext(PipelineContext.New(GetOption(), null, null, PipelineHookActions.BindTargetName, PipelineHookActions.BindTargetType, PipelineHookActions.BindField, new OptionContext(), null), new TestWriter(GetOption())); + context.Init(GetSource("FromFileSubSelector.Rule.jsonc")); + context.Begin(); + + // From current path + var rule = HostHelper.GetRule(GetSource("FromFileSubSelector.Rule.jsonc"), context, includeDependencies: false); + Assert.NotNull(rule); + Assert.Equal("JsonRuleWithPrecondition", rule[0].Name); + Assert.Equal("JsonRuleWithSubselector", rule[1].Name); + Assert.Equal("JsonRuleWithSubselectorReordered", rule[2].Name); + + context.Init(GetSource("FromFileSubSelector.Rule.yaml")); + context.Begin(); + var subselector1 = GetRuleVisitor(context, "JsonRuleWithPrecondition", GetSource("FromFileSubSelector.Rule.jsonc")); + var subselector2 = GetRuleVisitor(context, "JsonRuleWithSubselector", GetSource("FromFileSubSelector.Rule.jsonc")); + var subselector3 = GetRuleVisitor(context, "JsonRuleWithSubselectorReordered", GetSource("FromFileSubSelector.Rule.jsonc")); + context.EnterSourceScope(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()); + } + #endregion Json rules #region Helper methods @@ -190,17 +306,17 @@ private static object[] GetObject(string path) return JsonConvert.DeserializeObject(File.ReadAllText(path)); } - private static RuleBlock GetRuleVisitor(RunspaceContext context, string name) + private static RuleBlock GetRuleVisitor(RunspaceContext context, string name, Source[] source = null) { - var block = HostHelper.GetRuleYamlBlocks(GetSource(), context); + var block = HostHelper.GetRuleBlockGraph(source ?? GetSource(), context).GetAll(); return block.FirstOrDefault(s => s.Name == name); } - private static void ImportSelectors(RunspaceContext context) + private static void ImportSelectors(RunspaceContext context, Source[] source = null) { - var selectors = HostHelper.GetSelector(GetSource(), context).ToArray(); + var selectors = HostHelper.GetSelector(source ?? GetSource(), context).ToArray(); foreach (var selector in selectors) - context.Pipeline.Import(selector); + context.Pipeline.Import(context, selector); } private static string GetSourcePath(string path) diff --git a/tests/PSRule.Tests/SelectorTests.cs b/tests/PSRule.Tests/SelectorTests.cs index 1f1c087178..145c5090d1 100644 --- a/tests/PSRule.Tests/SelectorTests.cs +++ b/tests/PSRule.Tests/SelectorTests.cs @@ -1712,7 +1712,7 @@ private static SelectorVisitor GetSelectorVisitor(string name, Source[] source, context.Init(source); context.Begin(); var selector = HostHelper.GetSelector(source, context).ToArray().FirstOrDefault(s => s.Name == name); - return new SelectorVisitor(selector.Id, selector.Source, selector.Spec.If); + return new SelectorVisitor(context, selector.Id, selector.Source, selector.Spec.If); } private static string GetSourcePath(string fileName) diff --git a/tests/PSRule.Tests/TargetBinderTests.cs b/tests/PSRule.Tests/TargetBinderTests.cs index adaeac92c6..9ea3867ec1 100644 --- a/tests/PSRule.Tests/TargetBinderTests.cs +++ b/tests/PSRule.Tests/TargetBinderTests.cs @@ -7,7 +7,6 @@ using PSRule.Definitions.Baselines; using PSRule.Pipeline; using Xunit; -using static PSRule.Pipeline.TargetBinder; namespace PSRule { @@ -20,15 +19,15 @@ public void BindTargetObject() var targetObject = GetTargetObject(); binder.Bind(targetObject); - var m1 = binder.Using("Module1"); + var m1 = binder.Result("Module1"); Assert.Equal("Name1", m1.TargetName); Assert.Equal("Type1", m1.TargetType); - var m2 = binder.Using("Module2"); + var m2 = binder.Result("Module2"); Assert.Equal("Name2", m2.TargetName); Assert.Equal("Type1", m2.TargetType); - var m0 = binder.Using("."); + var m0 = binder.Result("."); Assert.Equal("Name1", m0.TargetName); Assert.Equal("System.Management.Automation.PSCustomObject", m0.TargetType); } @@ -40,15 +39,15 @@ public void BindJObject() var targetObject = new TargetObject(PSObject.AsPSObject(JToken.Parse("{ \"name\": \"Name1\", \"type\": \"Type1\", \"AlternativeName\": \"Name2\", \"AlternativeType\": \"Type2\" }"))); binder.Bind(targetObject); - var m1 = binder.Using("Module1"); + var m1 = binder.Result("Module1"); Assert.Equal("Name1", m1.TargetName); Assert.Equal("Type1", m1.TargetType); - var m2 = binder.Using("Module2"); + var m2 = binder.Result("Module2"); Assert.Equal("Name2", m2.TargetName); Assert.Equal("Type1", m2.TargetType); - var m0 = binder.Using("."); + var m0 = binder.Result("."); Assert.Equal("Name1", m0.TargetName); Assert.Equal("System.Management.Automation.PSCustomObject", m0.TargetType); } @@ -93,13 +92,13 @@ private ITargetBinder GetBinder() )); option.UseScope("Module1"); - builder.With(new TargetBindingContext("Module1", option.GetTargetBinding())); + builder.With("Module1", option.GetTargetBinding()); option.UseScope("Module2"); - builder.With(new TargetBindingContext("Module2", option.GetTargetBinding())); + builder.With("Module2", option.GetTargetBinding()); option.UseScope(null); - builder.With(new TargetBindingContext(".", option.GetTargetBinding())); + builder.With(".", option.GetTargetBinding()); return builder.Build(); } From 3f1920fbe2608a7fdc5249e4801c7e634692439d Mon Sep 17 00:00:00 2001 From: Bernie White Date: Fri, 26 Aug 2022 17:15:45 +1000 Subject: [PATCH 039/156] Updates to PSRule engine API documentation #1186 (#1239) --- docs/CHANGELOG-v2.md | 4 + src/PSRule/Configuration/BannerFormat.cs | 8 ++ src/PSRule/Configuration/BaselineOption.cs | 6 +- src/PSRule/Configuration/PSRuleOption.cs | 6 +- src/PSRule/Configuration/PipelineHook.cs | 7 + src/PSRule/Configuration/ResultFormat.cs | 9 +- src/PSRule/Data/ITargetInfo.cs | 12 ++ src/PSRule/Data/ITargetIssueCollection.cs | 32 ++++- src/PSRule/Data/InputFileInfo.cs | 24 ++++ src/PSRule/Definitions/Baselines/Baseline.cs | 35 ++++- src/PSRule/Definitions/Resource.cs | 10 +- src/PSRule/Definitions/ResourceId.cs | 40 +++++- src/PSRule/Definitions/Rules/Rule.cs | 5 +- src/PSRule/Definitions/Rules/RuleFilter.cs | 1 + src/PSRule/Definitions/Selectors/Selector.cs | 6 + src/PSRule/Definitions/Spec.cs | 19 +++ .../SuppressionGroups/SuppressionGroup.cs | 27 ++-- .../SuppressionGroupVisitor.cs | 2 +- src/PSRule/Pipeline/HostContext.cs | 69 ++++++++++ src/PSRule/Pipeline/InvokePipelineBuilder.cs | 3 + src/PSRule/Pipeline/OptionContext.cs | 2 +- src/PSRule/Pipeline/PipelineBuilder.cs | 6 + src/PSRule/Pipeline/PipelineHookActions.cs | 10 ++ src/PSRule/Pipeline/SourcePipeline.cs | 125 ++++++++++++------ src/PSRule/Rules/SuppressionFilter.cs | 21 +-- src/PSRule/Runtime/Assert.cs | 2 +- src/PSRule/Runtime/AssertResult.cs | 6 +- .../Runtime/ObjectPath/PathExpression.cs | 3 +- tests/PSRule.Tests/TargetBinderTests.cs | 2 +- 29 files changed, 414 insertions(+), 88 deletions(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 45bc126391..e78cdb6676 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -32,6 +32,9 @@ What's changed since pre-release v2.4.0-B0039: - Sub-selector pre-conditions add an additional expression to determine if a rule is executed. - Sub-selector object filters provide an way to filter items from list properties. - See [sub-selectors][4] for more information. +- Engineering: + - Improvements to PSRule engine API documentation by @BernieWhite. + [#1186](https://github.com/microsoft/PSRule/issues/1186) ## v2.4.0-B0039 (pre-release) @@ -40,6 +43,7 @@ What's changed since pre-release v2.4.0-B0022: - New features: - **Experimental**: Added support for functions within YAML and JSON expressions by @BernieWhite. [#1227](https://github.com/microsoft/PSRule/issues/1227) + [#1016](https://github.com/microsoft/PSRule/issues/1016) - Added conversion functions `boolean`, `string`, and `integer`. - Added lookup functions `configuration`, and `path`. - Added string functions `concat`, `substring`. diff --git a/src/PSRule/Configuration/BannerFormat.cs b/src/PSRule/Configuration/BannerFormat.cs index fffe9e53eb..eb891e4b69 100644 --- a/src/PSRule/Configuration/BannerFormat.cs +++ b/src/PSRule/Configuration/BannerFormat.cs @@ -9,6 +9,7 @@ namespace PSRule.Configuration { /// /// The information displayed for Assert-PSRule banner. + /// See help. /// [Flags] [JsonConverter(typeof(StringEnumConverter))] @@ -39,7 +40,14 @@ public enum BannerFormat /// RepositoryInfo = 8, + /// + /// The default information shown in the assert banner. + /// Default = Title | Source | SupportLinks | RepositoryInfo, + + /// + /// A minimal set of information shown in the assert banner. + /// Minimal = Source } } diff --git a/src/PSRule/Configuration/BaselineOption.cs b/src/PSRule/Configuration/BaselineOption.cs index bc3c20633b..e55512957c 100644 --- a/src/PSRule/Configuration/BaselineOption.cs +++ b/src/PSRule/Configuration/BaselineOption.cs @@ -23,7 +23,7 @@ public BaselineRef(string name) } } - internal sealed class BaselineInline : BaselineOption, IBaselineSpec + internal sealed class BaselineInline : BaselineOption, IBaselineV1Spec { public BaselineInline() { @@ -88,7 +88,7 @@ public static BaselineOption FromString(string value) return string.IsNullOrEmpty(value) ? null : new BaselineRef(value); } - internal static void Load(IBaselineSpec option, EnvironmentHelper env) + internal static void Load(IBaselineV1Spec option, EnvironmentHelper env) { // Binding.Field - currently not supported @@ -133,7 +133,7 @@ internal static void Load(IBaselineSpec option, EnvironmentHelper env) /// /// A baseline options object to load. /// One or more indexed properties. - internal static void Load(IBaselineSpec option, Dictionary properties) + internal static void Load(IBaselineV1Spec option, Dictionary properties) { if (properties.TryPopValue("Binding.Field", out Hashtable map)) option.Binding.Field = new FieldMap(map); diff --git a/src/PSRule/Configuration/PSRuleOption.cs b/src/PSRule/Configuration/PSRuleOption.cs index 7b782a3151..6b9b4d3168 100644 --- a/src/PSRule/Configuration/PSRuleOption.cs +++ b/src/PSRule/Configuration/PSRuleOption.cs @@ -28,7 +28,7 @@ namespace PSRule.Configuration /// /// See . /// - public sealed class PSRuleOption : IEquatable, IBaselineSpec + public sealed class PSRuleOption : IEquatable, IBaselineV1Spec { private const string DEFAULT_FILENAME = "ps-rule.yaml"; @@ -104,7 +104,7 @@ private PSRuleOption(string sourcePath, PSRuleOption option) } /// - /// Options that affect property binding of TargetName. + /// Options that affect property binding. /// public BindingOption Binding { get; set; } @@ -248,6 +248,7 @@ public void ToFile(string path) /// /// This method is called from PowerShell. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "Avoid nested conditional expressions that increase complexity.")] public static PSRuleOption FromFile(string path) { // Get a rooted file path instead of directory or relative path @@ -298,6 +299,7 @@ public static PSRuleOption FromFileOrEmpty() /// /// This method is called from PowerShell. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "Avoid nested conditional expressions that increase complexity.")] public static PSRuleOption FromFileOrEmpty(PSRuleOption option, string path) { if (option == null) diff --git a/src/PSRule/Configuration/PipelineHook.cs b/src/PSRule/Configuration/PipelineHook.cs index 3875a1eb80..11bf22589a 100644 --- a/src/PSRule/Configuration/PipelineHook.cs +++ b/src/PSRule/Configuration/PipelineHook.cs @@ -18,12 +18,19 @@ namespace PSRule.Configuration /// public sealed class PipelineHook { + /// + /// Create an empty set of pipeline hooks. + /// public PipelineHook() { BindTargetName = new List(); BindTargetType = new List(); } + /// + /// Create pipeline hooks based on an existing option instance. + /// + /// An existing pipeline hook option. public PipelineHook(PipelineHook option) { BindTargetName = option?.BindTargetName ?? new List(); diff --git a/src/PSRule/Configuration/ResultFormat.cs b/src/PSRule/Configuration/ResultFormat.cs index a3c26e2479..bd70c9970e 100644 --- a/src/PSRule/Configuration/ResultFormat.cs +++ b/src/PSRule/Configuration/ResultFormat.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Newtonsoft.Json; @@ -8,12 +8,19 @@ namespace PSRule.Configuration { /// /// The format to return to the pipeline after executing rules. + /// See help. /// [JsonConverter(typeof(StringEnumConverter))] public enum ResultFormat { + /// + /// Return a record per rule per object. + /// Detail = 1, + /// + /// Return summary results. + /// Summary = 2 } } diff --git a/src/PSRule/Data/ITargetInfo.cs b/src/PSRule/Data/ITargetInfo.cs index cbb02502d0..f9de815481 100644 --- a/src/PSRule/Data/ITargetInfo.cs +++ b/src/PSRule/Data/ITargetInfo.cs @@ -3,12 +3,24 @@ namespace PSRule.Data { + /// + /// An interface implemented by objects that automatically provide binding and source information. + /// internal interface ITargetInfo { + /// + /// The target name provided by the object. + /// string TargetName { get; } + /// + /// The target type provided by the object. + /// string TargetType { get; } + /// + /// The source information provided by the object. + /// TargetSourceInfo Source { get; } } } diff --git a/src/PSRule/Data/ITargetIssueCollection.cs b/src/PSRule/Data/ITargetIssueCollection.cs index 7235046759..71a6b44bcd 100644 --- a/src/PSRule/Data/ITargetIssueCollection.cs +++ b/src/PSRule/Data/ITargetIssueCollection.cs @@ -7,24 +7,43 @@ namespace PSRule.Data { + /// + /// A collection of issues reported by a downstream tool. + /// public interface ITargetIssueCollection { + /// + /// Get any issues from the collection that match the specified type. + /// + /// The type of the issue. + /// Returns issues that match the specified . TargetIssueInfo[] Get(string type = null); + /// + /// Check if the collection contains any of the specified issue type. + /// + /// The type of the issue. + /// Returns true if any the collection contains any issues matching the specified . bool Any(string type = null); } + /// + /// A collection of issues reported by a downstream tool. + /// internal sealed class TargetIssueCollection : ITargetIssueCollection { private List _Items; internal TargetIssueCollection() { } + /// public bool Any(string type = null) { return Get(type).Length > 0; } + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "Avoid nested conditional expressions that increase complexity.")] public TargetIssueInfo[] Get(string type = null) { if (_Items == null) @@ -33,6 +52,10 @@ public TargetIssueInfo[] Get(string type = null) return type == null ? _Items.ToArray() : _Items.Where(i => StringComparer.OrdinalIgnoreCase.Equals(i.Type, type)).ToArray(); } + /// + /// Add one or more issues into the collection. + /// + /// An array of instance to add to the collection. internal void AddRange(TargetIssueInfo[] issueInfo) { for (var i = 0; issueInfo != null && i < issueInfo.Length; i++) @@ -41,15 +64,10 @@ internal void AddRange(TargetIssueInfo[] issueInfo) private void Add(TargetIssueInfo issueInfo) { - if (issueInfo == null) + if (issueInfo == null || string.IsNullOrEmpty(issueInfo.Type)) return; - if (string.IsNullOrEmpty(issueInfo.Type)) - return; - - if (_Items == null) - _Items = new List(); - + _Items ??= new List(); _Items.Add(issueInfo); } } diff --git a/src/PSRule/Data/InputFileInfo.cs b/src/PSRule/Data/InputFileInfo.cs index aa3e976bef..b935e3bade 100644 --- a/src/PSRule/Data/InputFileInfo.cs +++ b/src/PSRule/Data/InputFileInfo.cs @@ -5,6 +5,9 @@ namespace PSRule.Data { + /// + /// An input file information structure. + /// public sealed class InputFileInfo : ITargetInfo { private readonly string _TargetType; @@ -30,18 +33,39 @@ internal InputFileInfo(string basePath, string path) _TargetType = string.IsNullOrEmpty(Extension) ? System.IO.Path.GetFileNameWithoutExtension(path) : Extension; } + /// + /// The fully qualified path to the file. + /// public string FullName { get; } + /// + /// The path to the parent directory containing the file. + /// public string BasePath { get; } + /// + /// The file name. + /// public string Name { get; } + /// + /// The file extension. + /// public string Extension { get; } + /// + /// The name of the directory containing the file. + /// public string DirectoryName { get; } + /// + /// The normalized path to the file. + /// public string Path { get; } + /// + /// The friendly display name for the file. + /// public string DisplayName { get; } string ITargetInfo.TargetName => DisplayName; diff --git a/src/PSRule/Definitions/Baselines/Baseline.cs b/src/PSRule/Definitions/Baselines/Baseline.cs index 5e2c699ab0..ad463c7846 100644 --- a/src/PSRule/Definitions/Baselines/Baseline.cs +++ b/src/PSRule/Definitions/Baselines/Baseline.cs @@ -12,23 +12,47 @@ namespace PSRule.Definitions.Baselines { - internal interface IBaselineSpec + /// + /// A specification for a V1 baseline resource. + /// + internal interface IBaselineV1Spec { + /// + /// Options that affect property binding. + /// BindingOption Binding { get; set; } + /// + /// Allows configuration key/ values to be specified that can be used within rule definitions. + /// ConfigurationOption Configuration { get; set; } + /// + /// Options that configure conventions. + /// ConventionOption Convention { get; set; } + /// + /// Options for that affect which rules are executed by including and filtering discovered rules. + /// RuleOption Rule { get; set; } } + /// + /// A baseline resource V1. + /// [Spec(Specs.V1, Specs.Baseline)] public sealed class Baseline : InternalResource, IResource { + /// + /// Create a baseline instance. + /// public Baseline(string apiVersion, SourceFile source, ResourceMetadata metadata, IResourceHelpInfo info, ISourceExtent extent, BaselineSpec spec) : base(ResourceKind.Baseline, apiVersion, source, metadata, info, extent, spec) { } + /// + /// The unique identifier for the baseline. + /// [YamlIgnore()] public string BaselineId => Name; @@ -40,14 +64,21 @@ public Baseline(string apiVersion, SourceFile source, ResourceMetadata metadata, public string Synopsis => Info.Synopsis.Text; } - public sealed class BaselineSpec : Spec, IBaselineSpec + /// + /// A specification for a V1 baseline resource. + /// + public sealed class BaselineSpec : Spec, IBaselineV1Spec { + /// public BindingOption Binding { get; set; } + /// public ConfigurationOption Configuration { get; set; } + /// public ConventionOption Convention { get; set; } + /// public RuleOption Rule { get; set; } } diff --git a/src/PSRule/Definitions/Resource.cs b/src/PSRule/Definitions/Resource.cs index 408f455bc2..113045b5ce 100644 --- a/src/PSRule/Definitions/Resource.cs +++ b/src/PSRule/Definitions/Resource.cs @@ -281,7 +281,7 @@ public Hashtable ToHashtable() /// internal bool Contains(object key, object value) { - if (key == null || value == null || !(key is string k) || !ContainsKey(k)) + if (key == null || value == null || key is not string k || !ContainsKey(k)) return false; if (TryArray(value, out var values)) @@ -473,6 +473,10 @@ internal protected Resource(ResourceKind kind, string apiVersion, SourceFile sou public ISourceExtent Extent { get; } } + /// + /// A base class for built-in resource types. + /// + /// The type of the related for the resource. public abstract class InternalResource : Resource, IResource, IAnnotated where TSpec : Spec, new() { private readonly Dictionary _Annotations; @@ -558,9 +562,11 @@ internal static void ParseIdString(string id, out string scope, out string name) } /// - /// Checks each RuleName and converts each to a RuleId. + /// Checks each resource name and converts each into a full qualified . /// + /// The default scope to use if the resource name if not fully qualified. /// An array of names. Qualified names (RuleIds) supplied are left intact. + /// The of the . /// An array of RuleIds. internal static ResourceId[] GetRuleId(string defaultScope, string[] name, ResourceIdKind kind) { diff --git a/src/PSRule/Definitions/ResourceId.cs b/src/PSRule/Definitions/ResourceId.cs index 4497fe9c77..9aad18aad9 100644 --- a/src/PSRule/Definitions/ResourceId.cs +++ b/src/PSRule/Definitions/ResourceId.cs @@ -13,14 +13,29 @@ namespace PSRule.Definitions /// internal enum ResourceIdKind { + /// + /// Not specified. + /// None = 0, + /// + /// Unknown. + /// Unknown = 1, + /// + /// The identifier is a primary resource identifier. + /// Id = 2, + /// + /// The identifier is a opaque reference resource identifier. + /// Ref = 3, + /// + /// The identifier is an alias resource identifier. + /// Alias = 4, } @@ -46,18 +61,33 @@ private ResourceId(string id, string scope, string name, ResourceIdKind kind) internal ResourceId(string scope, string name, ResourceIdKind kind) : this(GetIdString(scope, name), LanguageScope.Normalize(scope), name, kind) { } + /// + /// A string representation of the resource identifier. + /// public string Value { get; } + /// + /// The scope of the resource. + /// public string Scope { get; } + /// + /// A unique name for the resource within the specified . + /// public string Name { get; } + /// + /// The type of resource identifier. + /// internal ResourceIdKind Kind { get; } /// /// Converts the resource identifier to a string. /// - /// + /// + /// This is the same as . + /// + /// A string representation of the resource identifier. public override string ToString() { return Value; @@ -92,11 +122,17 @@ public bool Equals(string id) EqualOrNull(Name, name); } + /// + /// Compare two resource identifiers to determine if they are equal. + /// public static bool operator ==(ResourceId left, ResourceId right) { return left.Equals(right); } + /// + /// Compare two resource identifiers to determine if they are not equal. + /// public static bool operator !=(ResourceId left, ResourceId right) { return !left.Equals(right); @@ -155,7 +191,7 @@ private static bool TryParse(string id, out string scope, out string name) /// internal sealed class ResourceIdEqualityComparer : IEqualityComparer, IEqualityComparer { - public readonly static ResourceIdEqualityComparer Default = new ResourceIdEqualityComparer(); + public readonly static ResourceIdEqualityComparer Default = new(); public static bool IdEquals(ResourceId x, ResourceId y) { diff --git a/src/PSRule/Definitions/Rules/Rule.cs b/src/PSRule/Definitions/Rules/Rule.cs index 36dd2d9ec5..2862a5e664 100644 --- a/src/PSRule/Definitions/Rules/Rule.cs +++ b/src/PSRule/Definitions/Rules/Rule.cs @@ -25,7 +25,7 @@ public enum SeverityLevel Error = 1, /// - /// A fiailure generates a warning. + /// A failure generates a warning. /// Warning = 2, @@ -101,6 +101,9 @@ internal interface IRuleSpec LanguageIf Where { get; } } + /// + /// A rule resource V1. + /// [Spec(Specs.V1, Specs.Rule)] internal sealed class RuleV1 : InternalResource, IResource, IRuleV1 { diff --git a/src/PSRule/Definitions/Rules/RuleFilter.cs b/src/PSRule/Definitions/Rules/RuleFilter.cs index 4e4d017e18..2926a6b5ed 100644 --- a/src/PSRule/Definitions/Rules/RuleFilter.cs +++ b/src/PSRule/Definitions/Rules/RuleFilter.cs @@ -27,6 +27,7 @@ internal sealed class RuleFilter : IResourceFilter /// Only accept these rules by name. /// Only accept rules that have these tags. /// Rule that are always excluded by name. + /// Determine if local rules are automatically included. public RuleFilter(string[] include, Hashtable tag, string[] exclude, bool? includeLocal) { _Include = include == null || include.Length == 0 ? null : include; diff --git a/src/PSRule/Definitions/Selectors/Selector.cs b/src/PSRule/Definitions/Selectors/Selector.cs index da43eea871..3235bda342 100644 --- a/src/PSRule/Definitions/Selectors/Selector.cs +++ b/src/PSRule/Definitions/Selectors/Selector.cs @@ -6,6 +6,9 @@ namespace PSRule.Definitions.Selectors { + /// + /// A selector resource V1. + /// [Spec(Specs.V1, Specs.Selector)] internal sealed class SelectorV1 : InternalResource { @@ -13,6 +16,9 @@ public SelectorV1(string apiVersion, SourceFile source, ResourceMetadata metadat : base(ResourceKind.Selector, apiVersion, source, metadata, info, extent, spec) { } } + /// + /// A specification for a V1 selector resource. + /// internal sealed class SelectorV1Spec : Spec { public LanguageIf If { get; set; } diff --git a/src/PSRule/Definitions/Spec.cs b/src/PSRule/Definitions/Spec.cs index dbca9acba3..734869656d 100644 --- a/src/PSRule/Definitions/Spec.cs +++ b/src/PSRule/Definitions/Spec.cs @@ -9,12 +9,24 @@ namespace PSRule.Definitions { + /// + /// The base class for a resource specification. + /// public abstract class Spec { private const string FullNameSeparator = "/"; + /// + /// Create an instance of the resource specication. + /// protected Spec() { } + /// + /// Get a fully qualified name for the resource type. + /// + /// The specific API version of the resource. + /// The type name of the resource. + /// A fully qualified type name string. public static string GetFullName(string apiVersion, string name) { return string.Concat(apiVersion, FullNameSeparator, name); @@ -23,14 +35,21 @@ public static string GetFullName(string apiVersion, string name) internal static class Specs { + /// + /// The API version for V1 resources. + /// internal const string V1 = "github.com/microsoft/PSRule/v1"; + // Resource names for different types of resources. internal const string Rule = "Rule"; internal const string Baseline = "Baseline"; internal const string ModuleConfig = "ModuleConfig"; internal const string Selector = "Selector"; internal const string SuppressionGroup = "SuppressionGroup"; + /// + /// The built-in resource types. + /// public readonly static ISpecDescriptor[] BuiltinTypes = new ISpecDescriptor[] { new SpecDescriptor(V1, Rule), diff --git a/src/PSRule/Definitions/SuppressionGroups/SuppressionGroup.cs b/src/PSRule/Definitions/SuppressionGroups/SuppressionGroup.cs index 03b2e5b44a..f0b59ce517 100644 --- a/src/PSRule/Definitions/SuppressionGroups/SuppressionGroup.cs +++ b/src/PSRule/Definitions/SuppressionGroups/SuppressionGroup.cs @@ -6,6 +6,19 @@ namespace PSRule.Definitions.SuppressionGroups { + /// + /// A specification for a V1 suppression group resource. + /// + internal interface ISuppressionGroupV1Spec + { + string[] Rule { get; } + + LanguageIf If { get; } + } + + /// + /// A suppression group resource V1. + /// [Spec(Specs.V1, Specs.SuppressionGroup)] internal sealed class SuppressionGroupV1 : InternalResource { @@ -13,17 +26,13 @@ public SuppressionGroupV1(string apiVersion, SourceFile source, ResourceMetadata : base(ResourceKind.SuppressionGroup, apiVersion, source, metadata, info, extent, spec) { } } - internal interface ISuppressionGroupSpec - { - string[] Rule { get; } - - LanguageIf If { get; } - } - - internal sealed class SuppressionGroupV1Spec : Spec, ISuppressionGroupSpec + /// + /// A specification for a V1 suppression group resource. + /// + internal sealed class SuppressionGroupV1Spec : Spec, ISuppressionGroupV1Spec { public string[] Rule { get; set; } public LanguageIf If { get; set; } } -} \ No newline at end of file +} diff --git a/src/PSRule/Definitions/SuppressionGroups/SuppressionGroupVisitor.cs b/src/PSRule/Definitions/SuppressionGroups/SuppressionGroupVisitor.cs index edda73213d..3e5c0a7785 100644 --- a/src/PSRule/Definitions/SuppressionGroups/SuppressionGroupVisitor.cs +++ b/src/PSRule/Definitions/SuppressionGroups/SuppressionGroupVisitor.cs @@ -15,7 +15,7 @@ internal sealed class SuppressionGroupVisitor private readonly SuppressionInfo _Info; private readonly RunspaceContext _Context; - public SuppressionGroupVisitor(RunspaceContext context, ResourceId id, SourceFile source, ISuppressionGroupSpec spec, IResourceHelpInfo info) + public SuppressionGroupVisitor(RunspaceContext context, ResourceId id, SourceFile source, ISuppressionGroupV1Spec spec, IResourceHelpInfo info) { _Context = context; Id = id; diff --git a/src/PSRule/Pipeline/HostContext.cs b/src/PSRule/Pipeline/HostContext.cs index 36674ebff2..ba32ea22de 100644 --- a/src/PSRule/Pipeline/HostContext.cs +++ b/src/PSRule/Pipeline/HostContext.cs @@ -6,28 +6,64 @@ namespace PSRule.Pipeline { + /// + /// A host context for handling input and output emitted from the pipeline. + /// public interface IHostContext { + /// + /// Determines if the pipeline is executing in a remote PowerShell session. + /// bool InSession { get; } + /// + /// Get the value of a PowerShell preference variable. + /// ActionPreference GetPreferenceVariable(string variableName); + /// + /// Get the value of a named variable. + /// T GetVariable(string variableName); + /// + /// Set the value of a named variable. + /// void SetVariable(string variableName, T value); + /// + /// Handle an error reported by the pipeline. + /// void Error(ErrorRecord errorRecord); + /// + /// Handle a warning reported by the pipeline. + /// void Warning(string text); + /// + /// Handle an informational record reported by the pipeline. + /// void Information(InformationRecord informationRecord); + /// + /// Handle a verbose message reported by the pipeline. + /// void Verbose(string text); + /// + /// Handle a debug message reported by the pipeline. + /// void Debug(string text); + /// + /// Handle an object emitted from the pipeline. + /// void Object(object sendToPipeline, bool enumerateCollection); + /// + /// Determines if a destructive action such as overwriting a file should be processed. + /// bool ShouldProcess(string target, string action); } @@ -71,39 +107,49 @@ public static PSModuleAutoLoadingPreference GetAutoLoadingPreference(this IHostC } } + /// + /// A base class for custom host context instances. + /// public abstract class HostContext : IHostContext { private const string ErrorPreference = "ErrorActionPreference"; private const string WarningPreference = "WarningPreference"; + /// public virtual bool InSession => false; + /// public virtual void Debug(string text) { } + /// public virtual void Error(ErrorRecord errorRecord) { } + /// public virtual ActionPreference GetPreferenceVariable(string variableName) { return variableName == ErrorPreference || variableName == WarningPreference ? ActionPreference.Continue : ActionPreference.Ignore; } + /// public virtual T GetVariable(string variableName) { return default; } + /// public virtual void Information(InformationRecord informationRecord) { } + /// public virtual void Object(object sendToPipeline, bool enumerateCollection) { if (sendToPipeline is IResultRecord record) @@ -112,29 +158,39 @@ public virtual void Object(object sendToPipeline, bool enumerateCollection) // foreach (var item in record) } + /// public virtual void SetVariable(string variableName, T value) { } + /// public abstract bool ShouldProcess(string target, string action); + /// public virtual void Verbose(string text) { } + /// public virtual void Warning(string text) { } + /// + /// Handle record objects emitted from the pipeline. + /// public virtual void Record(IResultRecord record) { } } + /// + /// The host context used for PowerShell-based pipelines. + /// public sealed class PSHostContext : IHostContext { internal readonly PSCmdlet CmdletContext; @@ -145,6 +201,9 @@ public sealed class PSHostContext : IHostContext /// public bool InSession { get; } + /// + /// Create an instance of a PowerShell-based host context. + /// public PSHostContext(PSCmdlet commandRuntime, EngineIntrinsics executionContext) { InSession = false; @@ -153,6 +212,7 @@ public PSHostContext(PSCmdlet commandRuntime, EngineIntrinsics executionContext) InSession = executionContext != null && executionContext.SessionState.PSVariable.GetValue("PSSenderInfo") != null; } + /// public ActionPreference GetPreferenceVariable(string variableName) { return ExecutionContext == null @@ -160,46 +220,55 @@ public ActionPreference GetPreferenceVariable(string variableName) : (ActionPreference)ExecutionContext.SessionState.PSVariable.GetValue(variableName); } + /// public T GetVariable(string variableName) { return ExecutionContext == null ? default : (T)ExecutionContext.SessionState.PSVariable.GetValue(variableName); } + /// public void SetVariable(string variableName, T value) { CmdletContext.SessionState.PSVariable.Set(variableName, value); } + /// public bool ShouldProcess(string target, string action) { return CmdletContext == null || CmdletContext.ShouldProcess(target, action); } + /// public void Error(ErrorRecord errorRecord) { CmdletContext.WriteError(errorRecord); } + /// public void Warning(string text) { CmdletContext.WriteWarning(text); } + /// public void Information(InformationRecord informationRecord) { CmdletContext.WriteInformation(informationRecord); } + /// public void Verbose(string text) { CmdletContext.WriteVerbose(text); } + /// public void Debug(string text) { CmdletContext.WriteDebug(text); } + /// public void Object(object sendToPipeline, bool enumerateCollection) { CmdletContext.WriteObject(sendToPipeline, enumerateCollection); diff --git a/src/PSRule/Pipeline/InvokePipelineBuilder.cs b/src/PSRule/Pipeline/InvokePipelineBuilder.cs index 438106d877..96519c9c13 100644 --- a/src/PSRule/Pipeline/InvokePipelineBuilder.cs +++ b/src/PSRule/Pipeline/InvokePipelineBuilder.cs @@ -6,6 +6,9 @@ namespace PSRule.Pipeline { + /// + /// A helper to build a pipeline for executing rules and conventions within a PSRule sandbox. + /// public interface IInvokePipelineBuilder : IPipelineBuilder { /// diff --git a/src/PSRule/Pipeline/OptionContext.cs b/src/PSRule/Pipeline/OptionContext.cs index 79e02eb63a..38cdf726cc 100644 --- a/src/PSRule/Pipeline/OptionContext.cs +++ b/src/PSRule/Pipeline/OptionContext.cs @@ -106,7 +106,7 @@ internal sealed class BaselineScope : OptionScope public string[] TargetType; public bool? UseQualifiedName; - public BaselineScope(ScopeType type, string baselineId, string moduleName, IBaselineSpec option, bool obsolete) + public BaselineScope(ScopeType type, string baselineId, string moduleName, IBaselineV1Spec option, bool obsolete) : base(type, moduleName) { Id = baselineId; diff --git a/src/PSRule/Pipeline/PipelineBuilder.cs b/src/PSRule/Pipeline/PipelineBuilder.cs index 4564975132..8633c5d4eb 100644 --- a/src/PSRule/Pipeline/PipelineBuilder.cs +++ b/src/PSRule/Pipeline/PipelineBuilder.cs @@ -78,6 +78,12 @@ public static IHelpPipelineBuilder GetHelp(Source[] source, PSRuleOption option, return pipeline; } + /// + /// Create a builder to define a list of rule sources. + /// + /// >Options that configure PSRule. + /// >An implementation of a host context that will recieve output and results. + /// A builder object to configure the source pipeline. public static ISourcePipelineBuilder RuleSource(PSRuleOption option, IHostContext hostContext) { var pipeline = new SourcePipelineBuilder(hostContext, option); diff --git a/src/PSRule/Pipeline/PipelineHookActions.cs b/src/PSRule/Pipeline/PipelineHookActions.cs index 0d3fcd82f2..3bee828cd3 100644 --- a/src/PSRule/Pipeline/PipelineHookActions.cs +++ b/src/PSRule/Pipeline/PipelineHookActions.cs @@ -15,11 +15,15 @@ namespace PSRule.Pipeline { internal delegate bool ShouldProcess(string target, string action); + /// + /// Define built-in binding hooks. + /// internal static class PipelineHookActions { private const string Property_TargetName = "TargetName"; private const string Property_Name = "Name"; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "Avoid nested conditional expressions that increase complexity.")] public static string BindTargetName(string[] propertyNames, bool caseSensitive, bool preferTargetInfo, object targetObject, out string path) { path = null; @@ -37,6 +41,7 @@ public static string BindTargetName(string[] propertyNames, bool caseSensitive, return DefaultTargetNameBinding(targetObject); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "Avoid nested conditional expressions that increase complexity.")] public static string BindTargetType(string[] propertyNames, bool caseSensitive, bool preferTargetInfo, object targetObject, out string path) { path = null; @@ -54,6 +59,8 @@ public static string BindTargetType(string[] propertyNames, bool caseSensitive, return DefaultTargetTypeBinding(targetObject); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Parameter preferTargetInfo is required for matching the delegate type.")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "Avoid nested conditional expressions that increase complexity.")] public static string BindField(string[] propertyNames, bool caseSensitive, bool preferTargetInfo, object targetObject, out string path) { path = null; @@ -89,6 +96,7 @@ private static string DefaultTargetNameBinding(object targetObject) /// Determines if binding properties are case-sensitive. /// A PSObject to bind. /// The next delegate function to check if all of the property names can not be found. + /// The object path that was used for binding. /// The TargetName of the object. private static string CustomTargetPropertyBinding(string[] propertyNames, bool caseSensitive, object targetObject, BindTargetName next, out string path) { @@ -111,6 +119,7 @@ private static string CustomTargetPropertyBinding(string[] propertyNames, bool c /// Determines if binding properties are case-sensitive. /// A PSObject to bind. /// The next delegate function to check if all of the property names can not be found. + /// The object path that was used for binding. /// The TargetName of the object. private static string NestedTargetPropertyBinding(string[] propertyNames, bool caseSensitive, object targetObject, BindTargetName next, out string path) { @@ -174,6 +183,7 @@ private static string DefaultTargetTypeBinding(object targetObject) return TryGetInfoTargetType(targetObject, out var targetType) ? targetType : GetTypeNames(targetObject); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "Avoid nested conditional expressions that increase complexity.")] private static string GetTypeNames(object targetObject) { if (targetObject == null) diff --git a/src/PSRule/Pipeline/SourcePipeline.cs b/src/PSRule/Pipeline/SourcePipeline.cs index 44c1deccbd..d83cf9d611 100644 --- a/src/PSRule/Pipeline/SourcePipeline.cs +++ b/src/PSRule/Pipeline/SourcePipeline.cs @@ -15,33 +15,87 @@ namespace PSRule.Pipeline { + /// + /// A helper to build a list of sources for a PowerShell pipeline. + /// public interface ISourcePipelineBuilder { + /// + /// Determines if PowerShell should automatically load the module. + /// bool ShouldLoadModule { get; } + /// + /// Log a verbose message for scanning sources. + /// void VerboseScanSource(string path); + /// + /// Log a verbose message for source modules. + /// void VerboseFoundModules(int count); + /// + /// Log a verbose message for scanning for modules. + /// void VerboseScanModule(string moduleName); + /// + /// Add loose files as a source. + /// + /// An array of file or directory paths containing one or more rule files. + /// Determine if the default rule path is excluded. When set to true the default rule path is excluded. void Directory(string[] path, bool excludeDefaultRulePath = false); + /// + /// Add loose files as a source. + /// + /// A file or directory path containing one or more rule files. + /// Determine if the default rule path is excluded. When set to true the default rule path is excluded. void Directory(string path, bool excludeDefaultRulePath = false); + /// + /// Add a module source. + /// + /// The module info. void Module(PSModuleInfo[] module); + /// + /// Build a list of sources for executing within PSRule. + /// + /// A list of sources. Source[] Build(); } + /// + /// A helper to build a list of sources for a command-line tool pipeline. + /// public interface ISourceCommandlineBuilder { + /// + /// Add loose files as a source. + /// + /// An array of file or directory paths containing one or more rule files. + /// Determine if the default rule path is excluded. When set to true the default rule path is excluded. void Directory(string[] path, bool excludeDefaultRulePath = false); + /// + /// Add loose files as a source. + /// + /// A file or directory path containing one or more rule files. + /// Determine if the default rule path is excluded. When set to true the default rule path is excluded. void Directory(string path, bool excludeDefaultRulePath = false); + /// + /// Add a module source. + /// + /// The name of the module. void ModuleByName(string name); + /// + /// Build a list of sources for executing within PSRule. + /// + /// A list of sources. Source[] Build(); } @@ -50,13 +104,14 @@ public interface ISourceCommandlineBuilder /// public sealed class SourcePipelineBuilder : ISourcePipelineBuilder, ISourceCommandlineBuilder { - private const string SourceFileExtension_YAML = ".yaml"; - private const string SourceFileExtension_YML = ".yml"; - private const string SourceFileExtension_JSON = ".json"; - private const string SourceFileExtension_JSONC = ".jsonc"; - private const string SourceFileExtension_PS1 = ".ps1"; - private const string RuleModuleTag = "PSRule-rules"; - private const string DefaultRulePath = ".ps-rule/"; + private const string SOURCE_FILE_EXTENSION_YAML = ".yaml"; + private const string SOURCE_FILE_EXTENSION_YML = ".yml"; + private const string SOURCE_FILE_EXTENSION_JSON = ".json"; + private const string SOURCE_FILE_EXTENSION_JSONC = ".jsonc"; + private const string SOURCE_FILE_EXTENSION_PS1 = ".ps1"; + private const string SOURCE_FILE_PATTERN = "*.Rule.*"; + private const string RULE_MODULE_TAG = "PSRule-rules"; + private const string DEFAULT_RULE_PATH = ".ps-rule/"; private readonly Dictionary _Source; private readonly IHostContext _HostContext; @@ -78,20 +133,24 @@ internal SourcePipelineBuilder(IHostContext hostContext, PSRuleOption option) Directory(option.Include.Path); } + /// public bool ShouldLoadModule => _HostContext.GetAutoLoadingPreference() == PSModuleAutoLoadingPreference.All; #region Logging + /// public void VerboseScanSource(string path) { Log(PSRuleResources.ScanSource, path); } + /// public void VerboseFoundModules(int count) { Log(PSRuleResources.FoundModules, count); } + /// public void VerboseScanModule(string moduleName) { Log(PSRuleResources.ScanModule, moduleName); @@ -115,10 +174,7 @@ private void Debug(string message, params object[] args) #endregion Logging - /// - /// Add loose files as a source. - /// - /// A file or directory path containing one or more rule files. + /// public void Directory(string[] path, bool excludeDefaultRulePath = false) { if (path == null || path.Length == 0) @@ -128,6 +184,7 @@ public void Directory(string[] path, bool excludeDefaultRulePath = false) Directory(path[i], excludeDefaultRulePath); } + /// public void Directory(string path, bool excludeDefaultRulePath = false) { if (string.IsNullOrEmpty(path)) @@ -142,10 +199,7 @@ public void Directory(string path, bool excludeDefaultRulePath = false) Source(new Source(path, files)); } - /// - /// Add a module source. - /// - /// The module info. + /// public void Module(PSModuleInfo[] module) { if (module == null || module.Length == 0) @@ -155,10 +209,7 @@ public void Module(PSModuleInfo[] module) Module(module[i], dependency: false); } - /// - /// Add a module source. - /// - /// The name of the module. + /// public void ModuleByName(string name) { var basePath = FindModule(name); @@ -250,8 +301,7 @@ private static Source.ModuleInfo LoadManifest(string basePath) var data = reader.ReadToEnd(); var ast = System.Management.Automation.Language.Parser.ParseInput(data, out _, out _); var hashtable = ast.FindAll(item => item is System.Management.Automation.Language.HashtableAst, false).FirstOrDefault(); - var manifest = hashtable.SafeGetValue() as Hashtable; - if (manifest == null) + if (hashtable.SafeGetValue() is not Hashtable manifest) return null; var version = manifest["ModuleVersion"] as string; @@ -261,9 +311,8 @@ private static Source.ModuleInfo LoadManifest(string basePath) var psData = privateData["PSData"] as Hashtable; var projectUri = psData["ProjectUri"] as string; var prerelease = psData["Prerelease"] as string; - var requiredAssemblies = manifest["RequiredAssemblies"] as Array; - if (requiredAssemblies != null) + if (manifest["RequiredAssemblies"] is Array requiredAssemblies) { foreach (var a in requiredAssemblies.OfType()) Assembly.LoadFile(Path.Combine(basePath, a)); @@ -304,12 +353,13 @@ private static bool IsRuleModule(PSModuleInfo module) return false; foreach (var tag in module.Tags) - if (StringComparer.OrdinalIgnoreCase.Equals(RuleModuleTag, tag)) + if (StringComparer.OrdinalIgnoreCase.Equals(RULE_MODULE_TAG, tag)) return true; return false; } + /// public Source[] Build() { Default(); @@ -319,7 +369,7 @@ public Source[] Build() private void Default() { if (_UseDefaultPath) - Directory(DefaultRulePath); + Directory(DEFAULT_RULE_PATH); } private void Source(Source source) @@ -364,9 +414,7 @@ private static SourceFile[] IncludeFile(string path, string helpPath) if (!File.Exists(path)) throw new FileNotFoundException(PSRuleResources.SourceNotFound, path); - if (helpPath == null) - helpPath = Path.GetDirectoryName(path); - + helpPath ??= Path.GetDirectoryName(path); return new SourceFile[] { new SourceFile(path, null, GetSourceType(path), helpPath) }; } @@ -374,30 +422,26 @@ private static SourceFile[] IncludePath(string path, string helpPath, string mod { if (!excludeDefaultRulePath) { - var allFiles = System.IO.Directory.EnumerateFiles(path, "*.Rule.*", SearchOption.AllDirectories); + var allFiles = System.IO.Directory.EnumerateFiles(path, SOURCE_FILE_PATTERN, SearchOption.AllDirectories); return GetSourceFiles(allFiles, helpPath, moduleName); } - - var filteredFiles = FilterFiles(path, "*.Rule.*", dir => !PathContainsDefaultRulePath(dir)); + var filteredFiles = FilterFiles(path, SOURCE_FILE_PATTERN, dir => !PathContainsDefaultRulePath(dir)); return GetSourceFiles(filteredFiles, helpPath, moduleName); } private static bool PathContainsDefaultRulePath(string path) { - return path.Contains(DefaultRulePath.TrimEnd(Path.AltDirectorySeparatorChar), StringComparison.OrdinalIgnoreCase); + return path.Contains(DEFAULT_RULE_PATH.TrimEnd(Path.AltDirectorySeparatorChar), StringComparison.OrdinalIgnoreCase); } private static SourceFile[] GetSourceFiles(IEnumerable files, string helpPath, string moduleName) { var result = new List(); - foreach (var file in files) { if (ShouldInclude(file)) { - if (helpPath == null) - helpPath = Path.GetDirectoryName(file); - + helpPath ??= Path.GetDirectoryName(file); result.Add(new SourceFile(file, moduleName, GetSourceType(file), helpPath)); } } @@ -423,33 +467,30 @@ private static IEnumerable FilterFiles(string path, string filePattern, private static SourceType GetSourceType(string path) { var extension = Path.GetExtension(path); - if (IsYamlFile(extension)) { return SourceType.Yaml; } - else if (IsJsonFile(extension)) { return SourceType.Json; } - return SourceType.Script; } private static bool IsSourceFile(string extension) { - return extension == SourceFileExtension_PS1 || IsYamlFile(extension) || IsJsonFile(extension); + return extension == SOURCE_FILE_EXTENSION_PS1 || IsYamlFile(extension) || IsJsonFile(extension); } private static bool IsYamlFile(string extension) { - return extension == SourceFileExtension_YAML || extension == SourceFileExtension_YML; + return extension == SOURCE_FILE_EXTENSION_YAML || extension == SOURCE_FILE_EXTENSION_YML; } private static bool IsJsonFile(string extension) { - return extension == SourceFileExtension_JSON || extension == SourceFileExtension_JSONC; + return extension == SOURCE_FILE_EXTENSION_JSON || extension == SOURCE_FILE_EXTENSION_JSONC; } } } diff --git a/src/PSRule/Rules/SuppressionFilter.cs b/src/PSRule/Rules/SuppressionFilter.cs index 56d0babaa7..3c492cf55b 100644 --- a/src/PSRule/Rules/SuppressionFilter.cs +++ b/src/PSRule/Rules/SuppressionFilter.cs @@ -47,7 +47,8 @@ private sealed class SuppressionKey { public readonly string RuleName; public readonly string TargetName; - private readonly int HashCode; + + private readonly int _HashCode; public SuppressionKey(string ruleName, string targetName) { @@ -59,21 +60,23 @@ public SuppressionKey(string ruleName, string targetName) RuleName = ruleName; TargetName = targetName; - HashCode = CombineHashCode(); + _HashCode = CombineHashCode(); } + /// public override int GetHashCode() { - return HashCode; + return _HashCode; } + /// public override bool Equals(object obj) { - if (!(obj is SuppressionKey)) + if (obj is not SuppressionKey) return false; var k2 = obj as SuppressionKey; - return HashCode == k2.HashCode && + return _HashCode == k2._HashCode && StringComparer.OrdinalIgnoreCase.Equals(TargetName, k2.TargetName) && StringComparer.OrdinalIgnoreCase.Equals(RuleName, k2.RuleName); } @@ -125,10 +128,10 @@ private HashSet Index(RunspaceContext context, SuppressionOption /// /// Attempts to fetch suppression group from rule suppression group index. /// - /// The key rule id which indexes suppression groups - /// Th target object we are invoking - /// The Id of the matched suppression group - /// Boolean indicating if suppression group has been found + /// The key rule id which indexes suppression groups. + /// The we are evaluating. + /// Information about a matching suppression group. + /// Boolean indicating if suppression group has been found. public bool TrySuppressionGroup(string ruleId, TargetObject targetObject, out ISuppressionInfo suppression) { suppression = null; diff --git a/src/PSRule/Runtime/Assert.cs b/src/PSRule/Runtime/Assert.cs index 5712f44862..319ba023a8 100644 --- a/src/PSRule/Runtime/Assert.cs +++ b/src/PSRule/Runtime/Assert.cs @@ -70,7 +70,7 @@ internal AssertResult Create(IOperand operand, bool condition, string reason, pa if (!(RunspaceContext.CurrentThread.IsScope(RunspaceScope.Rule) || RunspaceContext.CurrentThread.IsScope(RunspaceScope.Precondition))) throw new RuleException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.VariableConditionScope, VARIABLE_NAME)); - return new AssertResult(this, operand, condition, reason, args); + return new AssertResult(operand, condition, reason, args); } /// diff --git a/src/PSRule/Runtime/AssertResult.cs b/src/PSRule/Runtime/AssertResult.cs index d8c78768f3..9c5f79dd6f 100644 --- a/src/PSRule/Runtime/AssertResult.cs +++ b/src/PSRule/Runtime/AssertResult.cs @@ -15,12 +15,10 @@ namespace PSRule.Runtime /// public sealed class AssertResult : IEquatable { - private readonly Assert _Assert; private readonly List _Reason; - internal AssertResult(Assert assert, IOperand operand, bool value, string reason, object[] args) + internal AssertResult(IOperand operand, bool value, string reason, object[] args) { - _Assert = assert; Result = value; if (!Result) { @@ -65,7 +63,9 @@ internal void AddReason(AssertResult result) /// /// Add a reason. /// + /// Indentifies the operand that was the reason for the failure. /// The text of a reason to add. This text should already be localized for the currently culture. + /// Replacement arguments for the format string. internal void AddReason(IOperand operand, string text, params object[] args) { // Ignore reasons if this is a pass. diff --git a/src/PSRule/Runtime/ObjectPath/PathExpression.cs b/src/PSRule/Runtime/ObjectPath/PathExpression.cs index bffc335af5..f42ce4abc0 100644 --- a/src/PSRule/Runtime/ObjectPath/PathExpression.cs +++ b/src/PSRule/Runtime/ObjectPath/PathExpression.cs @@ -92,7 +92,7 @@ public static PathExpression Create(string path) public bool TryGet(object o, bool caseSensitive, out object[] value) { value = null; - if (!TryGet(o, caseSensitive, out var result, out var enumerable)) + if (!TryGet(o, caseSensitive, out var result, out _)) return false; value = result.ToArray(); @@ -120,6 +120,7 @@ public bool TryGet(object o, bool caseSensitive, out object value) /// The object to navigate the path for. /// Determines if member name matching is case-sensitive. /// The values selected from the object. + /// Determines if is enumerable. /// Returns true when the path exists within the object. Returns false if the path does not exist. private bool TryGet(object o, bool caseSensitive, out IEnumerable value, out bool enumerable) { diff --git a/tests/PSRule.Tests/TargetBinderTests.cs b/tests/PSRule.Tests/TargetBinderTests.cs index 9ea3867ec1..daec377b5d 100644 --- a/tests/PSRule.Tests/TargetBinderTests.cs +++ b/tests/PSRule.Tests/TargetBinderTests.cs @@ -102,7 +102,7 @@ private ITargetBinder GetBinder() return builder.Build(); } - private static IBaselineSpec GetOption(string[] targetName, string[] targetType) + private static IBaselineV1Spec GetOption(string[] targetName, string[] targetType) { var result = new BaselineOption.BaselineInline(); result.Binding.TargetName = targetName; From 10573cec156ca170e47e9e96b2665b1202226127 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Fri, 26 Aug 2022 19:26:36 +1000 Subject: [PATCH 040/156] Additional minor updates to documentation (#1240) * Minor updates to documentation --- docs/about.md | 1 + docs/analysis-output.md | 2 +- docs/authoring/testing-infrastructure.md | 1 + docs/authoring/using-expressions.md | 2 +- docs/creating-your-pipeline.md | 4 ++-- docs/expressions/functions.md | 2 +- docs/expressions/sub-selectors.md | 2 +- docs/faq.md | 1 + docs/features.md | 1 + docs/install-instructions.md | 3 ++- docs/license-contributing.md | 5 +++++ docs/support.md | 7 +++++-- docs/upgrade-notes.md | 5 +++++ docs/validating-locally.md | 17 +++++++++++------ mkdocs.yml | 11 +++-------- overrides/main.html | 13 ++++++++++--- 16 files changed, 51 insertions(+), 26 deletions(-) diff --git a/docs/about.md b/docs/about.md index febda88d68..3c840ac37f 100644 --- a/docs/about.md +++ b/docs/about.md @@ -1,4 +1,5 @@ --- +title: What is PSRule and why should I use it? author: BernieWhite --- diff --git a/docs/analysis-output.md b/docs/analysis-output.md index 7857fa7ba0..7c537946f1 100644 --- a/docs/analysis-output.md +++ b/docs/analysis-output.md @@ -59,7 +59,7 @@ The output format can be configuring by setting the `Output.Format` option to on === "Options file" - ```yaml hl_lines="2-3" + ```yaml title="ps-rule.yaml" hl_lines="2-3" output: format: 'Sarif' path: reports/ps-rule-results.sarif diff --git a/docs/authoring/testing-infrastructure.md b/docs/authoring/testing-infrastructure.md index 63e703e7a2..15a0e2ddc4 100644 --- a/docs/authoring/testing-infrastructure.md +++ b/docs/authoring/testing-infrastructure.md @@ -1,4 +1,5 @@ --- +title: Testing Infrastructure as Code author: BernieWhite --- diff --git a/docs/authoring/using-expressions.md b/docs/authoring/using-expressions.md index 17844e8cf0..b453c92278 100644 --- a/docs/authoring/using-expressions.md +++ b/docs/authoring/using-expressions.md @@ -78,4 +78,4 @@ _n/a_ | NullOrEmpty _n/a_ | TypeOf WithinPath | WithinPath -[^1]: The `Equals`, `HasValue` and expression and `HasFieldValue` are similar. +[^1]: The `Equals`, `HasValue` expressions and `HasFieldValue` are similar. diff --git a/docs/creating-your-pipeline.md b/docs/creating-your-pipeline.md index b18a646352..942aabaf4c 100644 --- a/docs/creating-your-pipeline.md +++ b/docs/creating-your-pipeline.md @@ -83,7 +83,7 @@ To prevent a rule executing you can either: [:octicons-book-24: Docs][3] - ```yaml + ```yaml title="ps-rule.yaml" rule: exclude: # Ignore the following rules for all objects @@ -97,7 +97,7 @@ To prevent a rule executing you can either: [:octicons-book-24: Docs][4] - ```yaml + ```yaml title="ps-rule.yaml" suppression: Azure.AKS.AuthorizedIPs: # Exclude the following externally managed AKS clusters diff --git a/docs/expressions/functions.md b/docs/expressions/functions.md index deab0deda1..312dc77a06 100644 --- a/docs/expressions/functions.md +++ b/docs/expressions/functions.md @@ -44,7 +44,7 @@ The conditions that are supported are: ## Examples -```yaml +```yaml title="YAML" --- # Synopsis: An expression function example. apiVersion: github.com/microsoft/PSRule/v1 diff --git a/docs/expressions/sub-selectors.md b/docs/expressions/sub-selectors.md index c502f90f20..995a21a538 100644 --- a/docs/expressions/sub-selectors.md +++ b/docs/expressions/sub-selectors.md @@ -186,7 +186,7 @@ In the example: Given the example, is important to understand what happens if: -- The `resources` property doesn't exist. +- The `resources` property doesn't exist. **OR** - The `resources` property doesn't contain any items that match the sub-selector condition. In either of these two cases, the sub-selector will return `false` and fail the rule. diff --git a/docs/faq.md b/docs/faq.md index c33596d5a4..06653f3031 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -1,4 +1,5 @@ --- +title: Frequently Asked Questions author: BernieWhite --- diff --git a/docs/features.md b/docs/features.md index 9dfc4bdbc1..c4988b9e4a 100644 --- a/docs/features.md +++ b/docs/features.md @@ -1,4 +1,5 @@ --- +title: Key features of PSRule author: BernieWhite --- diff --git a/docs/install-instructions.md b/docs/install-instructions.md index 934a737868..9444803dbd 100644 --- a/docs/install-instructions.md +++ b/docs/install-instructions.md @@ -1,4 +1,5 @@ --- +title: Instructions for installing PSRule author: BernieWhite --- @@ -16,7 +17,7 @@ It is shipped as a PowerShell module which makes it easy to install and distribu [:octicons-workflow-24: GitHub Action][1] -Install and use PSRule with GitHub Actions by referencing the `Microsoft/ps-rule` action. +Install and use PSRule with GitHub Actions by referencing the `microsoft/ps-rule` action. ```yaml - name: Analyze Azure template files diff --git a/docs/license-contributing.md b/docs/license-contributing.md index e8c62efd34..7e24a164bf 100755 --- a/docs/license-contributing.md +++ b/docs/license-contributing.md @@ -1,3 +1,8 @@ +--- +title: License and contributing to PSRule +author: BernieWhite +--- + # License and contributing PSRule is licensed with an [:octicons-law-24: MIT License][1], which means it's free to use and modify. diff --git a/docs/support.md b/docs/support.md index 7458009b59..e79bfe08af 100644 --- a/docs/support.md +++ b/docs/support.md @@ -1,7 +1,11 @@ +--- +title: Support for PSRule +author: BernieWhite +--- + # Support This project uses GitHub Issues to track bugs and feature requests. - Please search the existing issues before filing new issues to avoid duplicates. @@ -14,4 +18,3 @@ Support for this project/ product is limited to the resources listed above. [issue]: https://github.com/microsoft/PSRule/issues [discussion]: https://github.com/microsoft/PSRule/discussions - diff --git a/docs/upgrade-notes.md b/docs/upgrade-notes.md index c346bca7f6..abbd1b130e 100644 --- a/docs/upgrade-notes.md +++ b/docs/upgrade-notes.md @@ -1,3 +1,8 @@ +--- +title: Notes for upgrading between PSRule versions +author: BernieWhite +--- + # Upgrade notes This document contains notes to help upgrade from previous versions of PSRule. diff --git a/docs/validating-locally.md b/docs/validating-locally.md index d6c589cd4e..f564b5ee3f 100644 --- a/docs/validating-locally.md +++ b/docs/validating-locally.md @@ -5,28 +5,33 @@ author: BernieWhite # Validating locally PSRule can be installed locally on MacOS, Linux, and Windows for local validation. +This allows you to test Infrastructure as Code (IaC) artifacts before pushing changes to a repository. !!! Tip If you haven't already, follow the instructions on [installing locally][1] before continuing. - [1]: install-instructions.md#installinglocally + [1]: install-instructions.md#installing-locally ## With Visual Studio Code [:octicons-download-24: Extension][2] An extension for Visual Studio Code is available for an integrated experience using PSRule. -The Visual Studio Code extension includes a built-in `PSRule: Run analysis` task. +The Visual Studio Code extension includes a built-in task _PSRule: Run analysis_ task.

Built-in tasks shown in task list

-To learn about tasks in Visual Studio Code see [Integrate with External Tools via Tasks][3]. +!!! Info + To learn about tasks in Visual Studio Code see [Integrate with External Tools via Tasks][3]. -To use PSRule for Azure with the built-in `PSRule: Run analysis` task, insert the following into `.vscode/tasks.json`. +### Customizing the task -```json +The _PSRule: Run analysis_ task will be available automatically after you install the PSRule extension. +You can customize the defaults of the task by editing or inserting the task into `.vscode/tasks.json` within your workspace. + +```json title="JSON" { "type": "PSRule", "problemMatcher": [ @@ -46,7 +51,7 @@ To use PSRule for Azure with the built-in `PSRule: Run analysis` task, insert th !!! Example A complete `.vscode/tasks.json` might look like the following: - ```json + ```json title=".vscode/tasks.json" { "version": "2.0.0", "tasks": [ diff --git a/mkdocs.yml b/mkdocs.yml index 2da2e1241e..950683fb45 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + site_name: PSRule site_url: https://microsoft.github.io/PSRule site_description: Validate infrastructure as code (IaC) and objects using PowerShell rules. @@ -133,7 +136,6 @@ plugins: - mkdocs-simple-hooks: hooks: on_page_markdown: "docs.hooks:replace_maml" - # on_nav: "docs.hooks:build_reference_nav" - search - git-revision-date - redirects: @@ -153,10 +155,3 @@ extra: social_preview: https://repository-images.githubusercontent.com/125832556/d6685d9f-ba70-44a1-b11f-6534831143d1 repo_issue: https://github.com/microsoft/PSRule/issues repo_discussion: https://github.com/microsoft/PSRule/discussions - - # alternate: - - # # Switch to English - # - name: English - # link: /en/ - # lang: en diff --git a/overrides/main.html b/overrides/main.html index 8855a238d8..dfa5a0f5a7 100644 --- a/overrides/main.html +++ b/overrides/main.html @@ -9,25 +9,32 @@ {% set title = config.site_name ~ " - " ~ page.title | striptags %} {% endif %} + + {% set description = config.site_description %} + {% if page and page.meta and page.meta.description %} + {% set description = page.meta.description %} + {% endif %} + {% set image = config.extra.local.social_preview %} + - + - + - + {% endblock %} From 7716858114f1046c6873eb66293a75defd7652f0 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sat, 27 Aug 2022 01:20:12 +1000 Subject: [PATCH 041/156] Pre-release v2.4.0-B0063 (#1243) --- docs/CHANGELOG-v2.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index e78cdb6676..21228b5a69 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -23,10 +23,12 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.4.0-B0063 (pre-release) + What's changed since pre-release v2.4.0-B0039: - New features: - - **Experimental**: Added support for sub-selectors YAML and JSON expressions by @BernieWhite. + - **Experimental**: Added support for sub-selector YAML and JSON expressions by @BernieWhite. [#1024](https://github.com/microsoft/PSRule/issues/1024) [#1045](https://github.com/microsoft/PSRule/issues/1045) - Sub-selector pre-conditions add an additional expression to determine if a rule is executed. From 50252fd382236463e31e5626b37bc1c3547563e9 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sun, 28 Aug 2022 14:28:58 +1000 Subject: [PATCH 042/156] Minor snippet improvement (#1244) --- schemas/PSRule-options.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schemas/PSRule-options.schema.json b/schemas/PSRule-options.schema.json index e1f435ef85..8978218ee6 100644 --- a/schemas/PSRule-options.schema.json +++ b/schemas/PSRule-options.schema.json @@ -811,7 +811,7 @@ { "label": "Version constraint", "body": { - "${1:Module}": "${2:'>=1.0.0'}" + "${1:Module}": "'>=${2:1.0.0}'" } } ] From 4868a11c4ef5e1b52152016d445fc0d633cb41b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 28 Aug 2022 14:37:30 +1000 Subject: [PATCH 043/156] Bump BenchmarkDotNet from 0.13.1 to 0.13.2 (#1241) Bumps [BenchmarkDotNet](https://github.com/dotnet/BenchmarkDotNet) from 0.13.1 to 0.13.2. - [Release notes](https://github.com/dotnet/BenchmarkDotNet/releases) - [Commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.13.1...v0.13.2) --- updated-dependencies: - dependency-name: BenchmarkDotNet dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/PSRule.Benchmark/PSRule.Benchmark.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PSRule.Benchmark/PSRule.Benchmark.csproj b/src/PSRule.Benchmark/PSRule.Benchmark.csproj index 7599ff38ed..ecacff5da5 100644 --- a/src/PSRule.Benchmark/PSRule.Benchmark.csproj +++ b/src/PSRule.Benchmark/PSRule.Benchmark.csproj @@ -15,7 +15,7 @@ - + From 9e5bd1be6477adbd5cd381e035c4ae5cfaa89c13 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 28 Aug 2022 15:23:41 +1000 Subject: [PATCH 044/156] Bump BenchmarkDotNet.Diagnostics.Windows from 0.13.1 to 0.13.2 (#1242) * Bump BenchmarkDotNet.Diagnostics.Windows from 0.13.1 to 0.13.2 Bumps [BenchmarkDotNet.Diagnostics.Windows](https://github.com/dotnet/BenchmarkDotNet) from 0.13.1 to 0.13.2. - [Release notes](https://github.com/dotnet/BenchmarkDotNet/releases) - [Commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.13.1...v0.13.2) --- updated-dependencies: - dependency-name: BenchmarkDotNet.Diagnostics.Windows dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Bump change log Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bernie White --- docs/CHANGELOG-v2.md | 8 ++++++++ src/PSRule.Benchmark/PSRule.Benchmark.csproj | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 21228b5a69..bf74a1f31c 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -23,6 +23,14 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since pre-release v2.4.0-B0063: + +- Engineering: + - Bump BenchmarkDotNet to v0.13.2. + [#1241](https://github.com/microsoft/PSRule/pull/1241) + - Bump BenchmarkDotNet.Diagnostics.Windows to v0.13.2. + [#1242](https://github.com/microsoft/PSRule/pull/1242) + ## v2.4.0-B0063 (pre-release) What's changed since pre-release v2.4.0-B0039: diff --git a/src/PSRule.Benchmark/PSRule.Benchmark.csproj b/src/PSRule.Benchmark/PSRule.Benchmark.csproj index ecacff5da5..5457dc99a5 100644 --- a/src/PSRule.Benchmark/PSRule.Benchmark.csproj +++ b/src/PSRule.Benchmark/PSRule.Benchmark.csproj @@ -20,7 +20,7 @@ - + From 80df5c866a063ec1ef61bf5a2219aa6b94178df5 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sun, 28 Aug 2022 22:08:18 +1000 Subject: [PATCH 045/156] Pre-release v2.4.0-B0091 (#1245) --- docs/CHANGELOG-v2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index bf74a1f31c..62304d508a 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -23,6 +23,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.4.0-B0091 (pre-release) + What's changed since pre-release v2.4.0-B0063: - Engineering: From 23abf21e7ee193af9382c67f8668973c9f9ecdd0 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Mon, 29 Aug 2022 16:32:58 +1000 Subject: [PATCH 046/156] Release v2.4.0 (#1246) --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- docs/CHANGELOG-v2.md | 55 ++++++++++++++++++++++++---- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 580d472496..b65ffebb0b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -30,7 +30,7 @@ Steps to reproduce the issue: **Module in use and version:** - Module: PSRule -- Version: **[e.g. 2.0.0]** +- Version: **[e.g. 2.4.0]** Captured output from `$PSVersionTable`: diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 62304d508a..a555834ccb 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -23,6 +23,54 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.4.0 + +What's changed since v2.3.2: + +- New features: + - **Experimental**: Added support for functions within YAML and JSON expressions by @BernieWhite. + [#1227](https://github.com/microsoft/PSRule/issues/1227) + [#1016](https://github.com/microsoft/PSRule/issues/1016) + - Added conversion functions `boolean`, `string`, and `integer`. + - Added lookup functions `configuration`, and `path`. + - Added string functions `concat`, `substring`. + - See [functions][3] for more information. + - **Experimental**: Added support for sub-selector YAML and JSON expressions by @BernieWhite. + [#1024](https://github.com/microsoft/PSRule/issues/1024) + [#1045](https://github.com/microsoft/PSRule/issues/1045) + - Sub-selector pre-conditions add an additional expression to determine if a rule is executed. + - Sub-selector object filters provide an way to filter items from list properties. + - See [sub-selectors][4] for more information. +- Engineering: + - Improvements to PSRule engine API documentation by @BernieWhite. + [#1186](https://github.com/microsoft/PSRule/issues/1186) + - Updates to PSRule engine API by @BernieWhite. + [#1152](https://github.com/microsoft/PSRule/issues/1152) + - Added tool support for baselines parameter. + - Added module path discovery. + - Added output for verbose and debug messages. + - Bump support projects to .NET 6 by @BernieWhite. + [#1209](https://github.com/microsoft/PSRule/issues/1209) + - Bump Microsoft.NET.Test.Sdk to v17.3.0. + [#1213](https://github.com/microsoft/PSRule/pull/1213) + - Bump BenchmarkDotNet to v0.13.2. + [#1241](https://github.com/microsoft/PSRule/pull/1241) + - Bump BenchmarkDotNet.Diagnostics.Windows to v0.13.2. + [#1242](https://github.com/microsoft/PSRule/pull/1242) +- Bug fixes: + - Fixed reporting of duplicate identifiers which were not generating an error for all cases by @BernieWhite. + [#1229](https://github.com/microsoft/PSRule/issues/1229) + - Added `Execution.DuplicateResourceId` option to configure PSRule behaviour. + - By default, duplicate resource identifiers return an error. + - Fixed exception on JSON baseline without a synopsis by @BernieWhite. + [#1230](https://github.com/microsoft/PSRule/issues/1230) + - Fixed repository information not in output by @BernieWhite. + [#1219](https://github.com/microsoft/PSRule/issues/1219) + +What's changed since pre-release v2.4.0-B0091: + +- No additional changes. + ## v2.4.0-B0091 (pre-release) What's changed since pre-release v2.4.0-B0063: @@ -756,13 +804,6 @@ What's changed since v1.11.0: - Resources that do not specify an `apiVersion` will be ignored. - See [upgrade notes][1] for details. -[Assert-PSRule]: commands/PSRule/en-US/Assert-PSRule.md [about_PSRule_Assert]: concepts/PSRule/en-US/about_PSRule_Assert.md [about_PSRule_Options]: concepts/PSRule/en-US/about_PSRule_Options.md -[about_PSRule_Variables]: concepts/PSRule/en-US/about_PSRule_Variables.md -[about_PSRule_Conventions]: concepts/PSRule/en-US/about_PSRule_Conventions.md -[about_PSRule_Selectors]: concepts/PSRule/en-US/about_PSRule_Selectors.md -[about_PSRule_Rules]: concepts/PSRule/en-US/about_PSRule_Rules.md -[about_PSRule_Badges]: concepts/PSRule/en-US/about_PSRule_Badges.md -[about_PSRule_Expressions]: concepts/PSRule/en-US/about_PSRule_Expressions.md [about_PSRule_SuppressionGroups]: concepts/PSRule/en-US/about_PSRule_SuppressionGroups.md From ad7c5e6c4ea50b7d6bbe73eb94fb5ce7dbbd7cf0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 Aug 2022 16:24:54 +1000 Subject: [PATCH 047/156] Bump mkdocs-material from 8.4.1 to 8.4.2 (#1247) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.4.1 to 8.4.2. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.4.1...8.4.2) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 89cffd2bed..abd49d1aed 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.3.1 -mkdocs-material==8.4.1 +mkdocs-material==8.4.2 pymdown-extensions==9.5 mike==1.1.2 mkdocs-simple-hooks==0.1.5 From 86557bb9d6c2a4a6b4f75348c78860507dd2508b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 Aug 2022 17:54:15 +1000 Subject: [PATCH 048/156] Bump Microsoft.NET.Test.Sdk from 17.3.0 to 17.3.1 (#1248) * Bump Microsoft.NET.Test.Sdk from 17.3.0 to 17.3.1 Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 17.3.0 to 17.3.1. - [Release notes](https://github.com/microsoft/vstest/releases) - [Commits](https://github.com/microsoft/vstest/compare/v17.3.0...v17.3.1) --- updated-dependencies: - dependency-name: Microsoft.NET.Test.Sdk dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Bump change log Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bernie White --- docs/CHANGELOG-v2.md | 6 ++++++ tests/PSRule.Tests/PSRule.Tests.csproj | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index a555834ccb..a15e7e9f57 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -23,6 +23,12 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since v2.4.0: + +- Engineering: + - Bump Microsoft.NET.Test.Sdk to v17.3.1. + [#1248](https://github.com/microsoft/PSRule/pull/1248) + ## v2.4.0 What's changed since v2.3.2: diff --git a/tests/PSRule.Tests/PSRule.Tests.csproj b/tests/PSRule.Tests/PSRule.Tests.csproj index d0bd6c0e44..a2d1284c2d 100644 --- a/tests/PSRule.Tests/PSRule.Tests.csproj +++ b/tests/PSRule.Tests/PSRule.Tests.csproj @@ -10,7 +10,7 @@ - + From cb799e156fb07a66827ac5ed2f5c47c6b5c108cb Mon Sep 17 00:00:00 2001 From: Bernie White Date: Wed, 31 Aug 2022 22:36:02 +1000 Subject: [PATCH 049/156] Fixes could not load Microsoft.Management.Infrastructure #1249 (#1250) --- README.md | 1 + docs/CHANGELOG-v2.md | 4 + .../commands/PSRule/en-US/New-PSRuleOption.md | 111 +++++++++++++----- .../commands/PSRule/en-US/Set-PSRuleOption.md | 75 ++++++++++-- .../PSRule/en-US/about_PSRule_Options.md | 50 ++++++++ schemas/PSRule-options.schema.json | 15 ++- src/PSRule/Configuration/ExecutionOption.cs | 24 +++- src/PSRule/Configuration/SessionState.cs | 23 ++++ src/PSRule/Host/Host.cs | 22 ++-- src/PSRule/PSRule.psm1 | 20 ++++ src/PSRule/Pipeline/PipelineBuilder.cs | 62 +++++++++- src/PSRule/Pipeline/PipelineContext.cs | 5 +- src/PSRule/Pipeline/PipelineWriter.cs | 2 +- tests/PSRule.Tests/PSRule.Options.Tests.ps1 | 39 ++++++ tests/PSRule.Tests/PSRule.Tests.yml | 1 + 15 files changed, 394 insertions(+), 60 deletions(-) create mode 100644 src/PSRule/Configuration/SessionState.cs diff --git a/README.md b/README.md index 8e1577b4af..648534e7f2 100644 --- a/README.md +++ b/README.md @@ -257,6 +257,7 @@ The following conceptual topics exist in the `PSRule` module: - [Execution.NotProcessedWarning](https://aka.ms/ps-rule/options#executionnotprocessedwarning) - [Execution.SuppressedRuleWarning](https://aka.ms/ps-rule/options#executionsuppressedrulewarning) - [Execution.InvariantCultureWarning](https://aka.ms/ps-rule/options#executioninvariantculturewarning) + - [Execution.InitialSessionState](https://aka.ms/ps-rule/options#executioninitialsessionstate) - [Include.Module](https://aka.ms/ps-rule/options#includemodule) - [Include.Path](https://aka.ms/ps-rule/options#includepath) - [Input.Format](https://aka.ms/ps-rule/options#inputformat) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index a15e7e9f57..3e5cd6548a 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -28,6 +28,10 @@ What's changed since v2.4.0: - Engineering: - Bump Microsoft.NET.Test.Sdk to v17.3.1. [#1248](https://github.com/microsoft/PSRule/pull/1248) +- Bug fixes: + - Fixed could not load Microsoft.Management.Infrastructure by @BernieWhite. + [#1249](https://github.com/microsoft/PSRule/issues/1249) + - To use minimal initial session state set `Execution.InitialSessionState` to `Minimal`. ## v2.4.0 diff --git a/docs/commands/PSRule/en-US/New-PSRuleOption.md b/docs/commands/PSRule/en-US/New-PSRuleOption.md index 4b84cedb7f..f98d352da3 100644 --- a/docs/commands/PSRule/en-US/New-PSRuleOption.md +++ b/docs/commands/PSRule/en-US/New-PSRuleOption.md @@ -21,16 +21,18 @@ New-PSRuleOption [[-Path] ] [-Configuration ] [-BindTargetType ] [-BindingIgnoreCase ] [-BindingField ] [-BindingNameSeparator ] [-BindingPreferTargetInfo ] [-TargetName ] [-TargetType ] [-BindingUseQualifiedName ] [-Convention ] + [-AliasReferenceWarning ] [-DuplicateResourceId ] [-InconclusiveWarning ] [-NotProcessedWarning ] [-SuppressedRuleWarning ] - [-AliasReferenceWarning ] [-InvariantCultureWarning ] [-IncludeModule ] + [-InvariantCultureWarning ] [-InitialSessionState ] [-IncludeModule ] [-IncludePath ] [-Format ] [-InputIgnoreGitPath ] - [-InputIgnoreRepositoryCommon ] [-ObjectPath ] [-InputTargetType ] - [-InputPathIgnore ] [-LoggingLimitDebug ] [-LoggingLimitVerbose ] - [-LoggingRuleFail ] [-LoggingRulePass ] [-OutputAs ] - [-OutputBanner ] [-OutputCulture ] [-OutputEncoding ] - [-OutputFooter ] [-OutputFormat ] [-OutputOutcome ] - [-OutputPath ] [-OutputSarifProblemsOnly ] [-OutputStyle ] - [-OutputJsonIndent ] [-RepositoryUrl ] [-RuleIncludeLocal ] [] + [-InputIgnoreRepositoryCommon ] [-InputIgnoreObjectSource ] [-ObjectPath ] + [-InputTargetType ] [-InputPathIgnore ] [-LoggingLimitDebug ] + [-LoggingLimitVerbose ] [-LoggingRuleFail ] [-LoggingRulePass ] + [-OutputAs ] [-OutputBanner ] [-OutputCulture ] + [-OutputEncoding ] [-OutputFooter ] [-OutputFormat ] + [-OutputOutcome ] [-OutputPath ] [-OutputSarifProblemsOnly ] + [-OutputStyle ] [-OutputJsonIndent ] [-RepositoryUrl ] + [-RuleIncludeLocal ] [] ``` ### FromOption @@ -41,16 +43,18 @@ New-PSRuleOption [-Option] [-Configuration ] [-BindTargetType ] [-BindingIgnoreCase ] [-BindingField ] [-BindingNameSeparator ] [-BindingPreferTargetInfo ] [-TargetName ] [-TargetType ] [-BindingUseQualifiedName ] [-Convention ] + [-AliasReferenceWarning ] [-DuplicateResourceId ] [-InconclusiveWarning ] [-NotProcessedWarning ] [-SuppressedRuleWarning ] - [-AliasReferenceWarning ] [-InvariantCultureWarning ] [-IncludeModule ] + [-InvariantCultureWarning ] [-InitialSessionState ] [-IncludeModule ] [-IncludePath ] [-Format ] [-InputIgnoreGitPath ] - [-InputIgnoreRepositoryCommon ] [-ObjectPath ] [-InputTargetType ] - [-InputPathIgnore ] [-LoggingLimitDebug ] [-LoggingLimitVerbose ] - [-LoggingRuleFail ] [-LoggingRulePass ] [-OutputAs ] - [-OutputBanner ] [-OutputCulture ] [-OutputEncoding ] - [-OutputFooter ] [-OutputFormat ] [-OutputOutcome ] - [-OutputPath ] [-OutputSarifProblemsOnly ] [-OutputStyle ] - [-OutputJsonIndent ] [-RepositoryUrl ] [-RuleIncludeLocal ] [] + [-InputIgnoreRepositoryCommon ] [-InputIgnoreObjectSource ] [-ObjectPath ] + [-InputTargetType ] [-InputPathIgnore ] [-LoggingLimitDebug ] + [-LoggingLimitVerbose ] [-LoggingRuleFail ] [-LoggingRulePass ] + [-OutputAs ] [-OutputBanner ] [-OutputCulture ] + [-OutputEncoding ] [-OutputFooter ] [-OutputFormat ] + [-OutputOutcome ] [-OutputPath ] [-OutputSarifProblemsOnly ] + [-OutputStyle ] [-OutputJsonIndent ] [-RepositoryUrl ] + [-RuleIncludeLocal ] [] ``` ### FromDefault @@ -60,16 +64,18 @@ New-PSRuleOption [-Default] [-Configuration ] [-SuppressTar [-BindTargetName ] [-BindTargetType ] [-BindingIgnoreCase ] [-BindingField ] [-BindingNameSeparator ] [-BindingPreferTargetInfo ] [-TargetName ] [-TargetType ] [-BindingUseQualifiedName ] - [-Convention ] [-InconclusiveWarning ] [-NotProcessedWarning ] - [-SuppressedRuleWarning ] [-AliasReferenceWarning ] [-InvariantCultureWarning ] - [-IncludeModule ] [-IncludePath ] [-Format ] [-InputIgnoreGitPath ] - [-InputIgnoreRepositoryCommon ] [-ObjectPath ] [-InputTargetType ] - [-InputPathIgnore ] [-LoggingLimitDebug ] [-LoggingLimitVerbose ] - [-LoggingRuleFail ] [-LoggingRulePass ] [-OutputAs ] - [-OutputBanner ] [-OutputCulture ] [-OutputEncoding ] - [-OutputFooter ] [-OutputFormat ] [-OutputOutcome ] - [-OutputPath ] [-OutputSarifProblemsOnly ] [-OutputStyle ] - [-OutputJsonIndent ] [-RepositoryUrl ] [-RuleIncludeLocal ] [] + [-Convention ] [-AliasReferenceWarning ] [-DuplicateResourceId ] + [-InconclusiveWarning ] [-NotProcessedWarning ] [-SuppressedRuleWarning ] + [-InvariantCultureWarning ] [-InitialSessionState ] [-IncludeModule ] + [-IncludePath ] [-Format ] [-InputIgnoreGitPath ] + [-InputIgnoreRepositoryCommon ] [-InputIgnoreObjectSource ] [-ObjectPath ] + [-InputTargetType ] [-InputPathIgnore ] [-LoggingLimitDebug ] + [-LoggingLimitVerbose ] [-LoggingRuleFail ] [-LoggingRulePass ] + [-OutputAs ] [-OutputBanner ] [-OutputCulture ] + [-OutputEncoding ] [-OutputFooter ] [-OutputFormat ] + [-OutputOutcome ] [-OutputPath ] [-OutputSarifProblemsOnly ] + [-OutputStyle ] [-OutputJsonIndent ] [-RepositoryUrl ] + [-RuleIncludeLocal ] [] ``` ## DESCRIPTION @@ -588,6 +594,23 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -InputIgnoreObjectSource + +Sets the option `Input.IgnoreObjectSource`. +The `Input.IgnoreObjectSource` option determines if objects will be skipped if the source path has been ignored. + +```yaml +Type: Boolean +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -LoggingLimitDebug Sets the `Logging.LimitDebug` option to limit debug messages to a list of named debug scopes. @@ -912,7 +935,7 @@ Accept wildcard characters: False ### -InvariantCultureWarning Sets the option `Execution.InvariantCultureWarning`. -The `Execution.InvariantCultureWarning` option set if a warning is logged when invarient culture is detected. +The `Execution.InvariantCultureWarning` option sets if a warning is logged when invarient culture is detected. ```yaml Type: Boolean @@ -926,6 +949,40 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -DuplicateResourceId + +Sets the option `Execution.DuplicateResourceId`. +The `Execution.DuplicateResourceId` option determines how to handle duplicate resources identifiers during execution. + +```yaml +Type: ExecutionActionPreference +Parameter Sets: (All) +Aliases: ExecutionDuplicateResourceId + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -InitialSessionState + +Sets the option `Execution.InitialSessionState`. +The `Execution.InitialSessionState` option determines how the initial session state for executing PowerShell code is created. + +```yaml +Type: SessionState +Parameter Sets: (All) +Aliases: ExecutionInitialSessionState + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### CommonParameters This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](https://go.microsoft.com/fwlink/?LinkID=113216). diff --git a/docs/commands/PSRule/en-US/Set-PSRuleOption.md b/docs/commands/PSRule/en-US/Set-PSRuleOption.md index 01ad2c8f81..089996bc93 100644 --- a/docs/commands/PSRule/en-US/Set-PSRuleOption.md +++ b/docs/commands/PSRule/en-US/Set-PSRuleOption.md @@ -17,17 +17,19 @@ Sets options that configure PSRule execution. Set-PSRuleOption [[-Path] ] [-Option ] [-PassThru] [-Force] [-AllowClobber] [-BindingIgnoreCase ] [-BindingField ] [-BindingNameSeparator ] [-BindingPreferTargetInfo ] [-TargetName ] [-TargetType ] - [-BindingUseQualifiedName ] [-Convention ] [-InconclusiveWarning ] - [-NotProcessedWarning ] [-SuppressedRuleWarning ] [-AliasReferenceWarning ] - [-InvariantCultureWarning ] [-IncludeModule ] [-IncludePath ] - [-Format ] [-InputIgnoreGitPath ] [-InputIgnoreRepositoryCommon ] - [-ObjectPath ] [-InputPathIgnore ] [-InputTargetType ] - [-LoggingLimitDebug ] [-LoggingLimitVerbose ] [-LoggingRuleFail ] - [-LoggingRulePass ] [-OutputAs ] [-OutputBanner ] - [-OutputCulture ] [-OutputEncoding ] [-OutputFooter ] - [-OutputFormat ] [-OutputOutcome ] [-OutputPath ] - [-OutputSarifProblemsOnly ] [-OutputStyle ] [-OutputJsonIndent ] - [-RepositoryUrl ] [-RuleIncludeLocal ] [-WhatIf] [-Confirm] [] + [-BindingUseQualifiedName ] [-Convention ] [-AliasReferenceWarning ] + [-DuplicateResourceId ] [-InconclusiveWarning ] + [-NotProcessedWarning ] [-SuppressedRuleWarning ] [-InvariantCultureWarning ] + [-InitialSessionState ] [-IncludeModule ] [-IncludePath ] + [-Format ] [-InputIgnoreGitPath ] [-InputIgnoreObjectSource ] + [-InputIgnoreRepositoryCommon ] [-ObjectPath ] [-InputPathIgnore ] + [-InputTargetType ] [-LoggingLimitDebug ] [-LoggingLimitVerbose ] + [-LoggingRuleFail ] [-LoggingRulePass ] [-OutputAs ] + [-OutputBanner ] [-OutputCulture ] [-OutputEncoding ] + [-OutputFooter ] [-OutputFormat ] [-OutputOutcome ] + [-OutputPath ] [-OutputSarifProblemsOnly ] [-OutputStyle ] + [-OutputJsonIndent ] [-RepositoryUrl ] [-RuleIncludeLocal ] [-WhatIf] [-Confirm] + [] ``` ## DESCRIPTION @@ -482,6 +484,23 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -InputIgnoreObjectSource + +Sets the option `Input.IgnoreObjectSource`. +The `Input.IgnoreObjectSource` option determines if objects will be skipped if the source path has been ignored. + +```yaml +Type: Boolean +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -LoggingLimitDebug Sets the `Logging.LimitDebug` option to limit debug messages to a list of named debug scopes. @@ -849,6 +868,40 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -DuplicateResourceId + +Sets the option `Execution.DuplicateResourceId`. +The `Execution.DuplicateResourceId` option determines how to handle duplicate resources identifiers during execution. + +```yaml +Type: ExecutionActionPreference +Parameter Sets: (All) +Aliases: ExecutionDuplicateResourceId + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -InitialSessionState + +Sets the option `Execution.InitialSessionState`. +The `Execution.InitialSessionState` option determines how the initial session state for executing PowerShell code is created. + +```yaml +Type: SessionState +Parameter Sets: (All) +Aliases: ExecutionInitialSessionState + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### CommonParameters This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](https://go.microsoft.com/fwlink/?LinkID=113216). diff --git a/docs/concepts/PSRule/en-US/about_PSRule_Options.md b/docs/concepts/PSRule/en-US/about_PSRule_Options.md index 5e07cb7f48..26275f56f4 100644 --- a/docs/concepts/PSRule/en-US/about_PSRule_Options.md +++ b/docs/concepts/PSRule/en-US/about_PSRule_Options.md @@ -21,6 +21,7 @@ The following workspace options are available for use: - [Execution.NotProcessedWarning](#executionnotprocessedwarning) - [Execution.SuppressedRuleWarning](#executionsuppressedrulewarning) - [Execution.InvariantCultureWarning](#executioninvariantculturewarning) +- [Execution.InitialSessionState](#executioninitialsessionstate) - [Include.Module](#includemodule) - [Include.Path](#includepath) - [Input.Format](#inputformat) @@ -1024,6 +1025,55 @@ variables: value: false ``` +### Execution.InitialSessionState + +Determines how the initial session state for executing PowerShell code is created. + +The following preferences are available: + +- `BuiltIn` (0) - Create the initial session state with all built-in cmdlets loaded. + This is the default. +- `Minimal` (1) - Create the initial session state with only a minimum set of cmdlets loaded. + +```powershell +# PowerShell: Using the InitialSessionState parameter +$option = New-PSRuleOption -InitialSessionState 'Minimal'; +``` + +```powershell +# PowerShell: Using the Execution.InitialSessionState hashtable key +$option = New-PSRuleOption -Option @{ 'Execution.InitialSessionState' = 'Minimal' }; +``` + +```powershell +# PowerShell: Using the InitialSessionState parameter to set YAML +Set-PSRuleOption -InitialSessionState 'Minimal'; +``` + +```yaml +# YAML: Using the execution/initialSessionState property +execution: + initialSessionState: Minimal +``` + +```bash +# Bash: Using environment variable +export PSRULE_EXECUTION_INITIALSESSIONSTATE=Minimal +``` + +```yaml +# GitHub Actions: Using environment variable +env: + PSRULE_EXECUTION_INITIALSESSIONSTATE: Minimal +``` + +```yaml +# Azure Pipelines: Using environment variable +variables: +- name: PSRULE_EXECUTION_INITIALSESSIONSTATE + value: Minimal +``` + ### Include.Module Automatically include rules and resources from the specified module. diff --git a/schemas/PSRule-options.schema.json b/schemas/PSRule-options.schema.json index 8978218ee6..97e5e25b54 100644 --- a/schemas/PSRule-options.schema.json +++ b/schemas/PSRule-options.schema.json @@ -307,16 +307,27 @@ "suppressedRuleWarning": { "type": "boolean", "title": "Warn on suppressed rules", - "description": "Enable or disable warnings for suppressed rules. The default is `true`.", + "description": "Enable or disable warnings for suppressed rules. The default is true.", "markdownDescription": "Enable or disable warnings for suppressed rules. The default is `true`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executionsuppressedrulewarning)", "default": true }, "invariantCultureWarning": { "type": "boolean", "title": "Warn on invariant culture", - "description": "Enable or disable warning when invariant culture is used. The default is `true`.", + "description": "Enable or disable warning when invariant culture is used. The default is true.", "markdownDescription": "Enable or disable warning when invariant culture is used. The default is `true`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executioninvariantculturewarning)", "default": true + }, + "initialSessionState": { + "type": "string", + "title": "Initial Session State", + "description": "Determines how the initial session state for executing PowerShell code is created. The default is BuiltIn.", + "markdownDescription": "Determines how the initial session state for executing PowerShell code is created. The default is `BuiltIn`.\n- When set to `BuiltIn` all built-in cmdlets are loaded.\n- When set to `Minimal` only cmdlets for hosting PowerShell will be loaded.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executioninitialsessionstate)", + "enum": [ + "BuiltIn", + "Minimal" + ], + "default": "BuiltIn" } }, "additionalProperties": false diff --git a/src/PSRule/Configuration/ExecutionOption.cs b/src/PSRule/Configuration/ExecutionOption.cs index 6d688b6cea..dce24bce4c 100644 --- a/src/PSRule/Configuration/ExecutionOption.cs +++ b/src/PSRule/Configuration/ExecutionOption.cs @@ -22,6 +22,7 @@ public sealed class ExecutionOption : IEquatable private const bool DEFAULT_ALIASREFERENCEWARNING = true; private const bool DEFAULT_INVARIANTCULTUREWARNING = true; private const ExecutionActionPreference DEFAULT_DUPLICATERESOURCEID = ExecutionActionPreference.Error; + private const SessionState DEFAULT_INITIALSESSIONSTATE = SessionState.BuiltIn; internal static readonly ExecutionOption Default = new() { @@ -32,6 +33,7 @@ public sealed class ExecutionOption : IEquatable NotProcessedWarning = DEFAULT_NOTPROCESSEDWARNING, SuppressedRuleWarning = DEFAULT_SUPPRESSEDRULEWARNING, InvariantCultureWarning = DEFAULT_INVARIANTCULTUREWARNING, + InitialSessionState = DEFAULT_INITIALSESSIONSTATE, }; /// @@ -46,6 +48,7 @@ public ExecutionOption() NotProcessedWarning = null; SuppressedRuleWarning = null; InvariantCultureWarning = null; + InitialSessionState = null; } /// @@ -64,6 +67,7 @@ public ExecutionOption(ExecutionOption option) NotProcessedWarning = option.NotProcessedWarning; SuppressedRuleWarning = option.SuppressedRuleWarning; InvariantCultureWarning = option.InvariantCultureWarning; + InitialSessionState = option.InitialSessionState; } /// @@ -82,7 +86,8 @@ public bool Equals(ExecutionOption other) InconclusiveWarning == other.InconclusiveWarning && NotProcessedWarning == other.NotProcessedWarning && SuppressedRuleWarning == other.NotProcessedWarning && - InvariantCultureWarning == other.InvariantCultureWarning; + InvariantCultureWarning == other.InvariantCultureWarning && + InitialSessionState == other.InitialSessionState; } /// @@ -98,6 +103,7 @@ public override int GetHashCode() hash = hash * 23 + (NotProcessedWarning.HasValue ? NotProcessedWarning.Value.GetHashCode() : 0); hash = hash * 23 + (SuppressedRuleWarning.HasValue ? SuppressedRuleWarning.Value.GetHashCode() : 0); hash = hash * 23 + (InvariantCultureWarning.HasValue ? InvariantCultureWarning.Value.GetHashCode() : 0); + hash = hash * 23 + (InitialSessionState.HasValue ? InitialSessionState.Value.GetHashCode() : 0); return hash; } } @@ -116,7 +122,8 @@ internal static ExecutionOption Combine(ExecutionOption o1, ExecutionOption o2) InconclusiveWarning = o1.InconclusiveWarning ?? o2.InconclusiveWarning, NotProcessedWarning = o1.NotProcessedWarning ?? o2.NotProcessedWarning, SuppressedRuleWarning = o1.SuppressedRuleWarning ?? o2.SuppressedRuleWarning, - InvariantCultureWarning = o1.InvariantCultureWarning ?? o2.InvariantCultureWarning + InvariantCultureWarning = o1.InvariantCultureWarning ?? o2.InvariantCultureWarning, + InitialSessionState = o1.InitialSessionState ?? o2.InitialSessionState, }; return result; } @@ -167,6 +174,13 @@ internal static ExecutionOption Combine(ExecutionOption o1, ExecutionOption o2) [DefaultValue(null)] public bool? InvariantCultureWarning { get; set; } + /// + /// Determines how the initial session state for executing PowerShell code is created. + /// The default is . + /// + [DefaultValue(null)] + public SessionState? InitialSessionState { get; set; } + internal void Load(EnvironmentHelper env) { if (env.TryBool("PSRULE_EXECUTION_ALIASREFERENCEWARNING", out var bvalue)) @@ -189,6 +203,9 @@ internal void Load(EnvironmentHelper env) if (env.TryBool("PSRULE_EXECUTION_INVARIANTCULTUREWARNING", out bvalue)) InvariantCultureWarning = bvalue; + + if (env.TryEnum("PSRULE_EXECUTION_INITIALSESSIONSTATE", out SessionState initialSessionState)) + InitialSessionState = initialSessionState; } internal void Load(Dictionary index) @@ -213,6 +230,9 @@ internal void Load(Dictionary index) if (index.TryPopBool("Execution.InvariantCultureWarning", out bvalue)) InvariantCultureWarning = bvalue; + + if (index.TryPopEnum("Execution.InitialSessionState", out SessionState initialSessionState)) + InitialSessionState = initialSessionState; } } } diff --git a/src/PSRule/Configuration/SessionState.cs b/src/PSRule/Configuration/SessionState.cs new file mode 100644 index 0000000000..e330b1afd8 --- /dev/null +++ b/src/PSRule/Configuration/SessionState.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Configuration +{ + /// + /// Configures how the initial PowerShell sandbox for executing rules is created. + /// + public enum SessionState + { + /// + /// Create the initial session state with all built-in cmdlets loaded. + /// See CreateDefault. + /// + BuiltIn = 0, + + /// + /// Create the initial session state with only cmdlets loaded for hosting PowerShell. + /// See CreateDefault2. + /// + Minimal = 1 + } +} diff --git a/src/PSRule/Host/Host.cs b/src/PSRule/Host/Host.cs index dd6d63269c..5e54d9b242 100644 --- a/src/PSRule/Host/Host.cs +++ b/src/PSRule/Host/Host.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; @@ -34,12 +34,12 @@ internal sealed class RuleVariable : PSVariable { private const string VARIABLE_NAME = "Rule"; - private readonly Runtime.Rule _Value; + private readonly Rule _Value; public RuleVariable() : base(VARIABLE_NAME, null, ScopedItemOptions.ReadOnly) { - _Value = new Runtime.Rule(); + _Value = new Rule(); } public override object Value => _Value; @@ -52,12 +52,12 @@ internal sealed class LocalizedDataVariable : PSVariable { private const string VARIABLE_NAME = "LocalizedData"; - private readonly Runtime.LocalizedData _Value; + private readonly LocalizedData _Value; public LocalizedDataVariable() : base(VARIABLE_NAME, null, ScopedItemOptions.ReadOnly) { - _Value = new Runtime.LocalizedData(); + _Value = new LocalizedData(); } public override object Value => _Value; @@ -69,12 +69,12 @@ public LocalizedDataVariable() internal sealed class AssertVariable : PSVariable { private const string VARIABLE_NAME = "Assert"; - private readonly Runtime.Assert _Value; + private readonly Assert _Value; public AssertVariable() : base(VARIABLE_NAME, null, ScopedItemOptions.ReadOnly) { - _Value = new Runtime.Assert(); + _Value = new Assert(); } public override object Value => _Value; @@ -148,10 +148,10 @@ internal static class HostState /// /// Create a default session state. /// - /// - public static InitialSessionState CreateSessionState() + public static InitialSessionState CreateSessionState(Configuration.SessionState initialSessionState) { - var state = InitialSessionState.CreateDefault(); + var state = initialSessionState == Configuration.SessionState.Minimal ? + InitialSessionState.CreateDefault2() : InitialSessionState.CreateDefault(); // Add in language elements state.Commands.Add(BuiltInCmdlets); @@ -161,7 +161,7 @@ public static InitialSessionState CreateSessionState() state.ThreadOptions = PSThreadOptions.UseCurrentThread; // Set execution policy - SetExecutionPolicy(state: state, executionPolicy: Microsoft.PowerShell.ExecutionPolicy.RemoteSigned); + SetExecutionPolicy(state, executionPolicy: Microsoft.PowerShell.ExecutionPolicy.RemoteSigned); return state; } diff --git a/src/PSRule/PSRule.psm1 b/src/PSRule/PSRule.psm1 index 6f5577e196..32718fddbd 100644 --- a/src/PSRule/PSRule.psm1 +++ b/src/PSRule/PSRule.psm1 @@ -1195,6 +1195,11 @@ function New-PSRuleOption { [Alias('ExecutionInvariantCultureWarning')] [System.Boolean]$InvariantCultureWarning = $True, + # Sets the Execution.InitialSessionState option + [Parameter(Mandatory = $False)] + [Alias('ExecutionInitialSessionState')] + [PSRule.Configuration.SessionState]$InitialSessionState = [PSRule.Configuration.SessionState]::BuiltIn, + # Sets the Include.Module option [Parameter(Mandatory = $False)] [String[]]$IncludeModule, @@ -1474,6 +1479,11 @@ function Set-PSRuleOption { [Alias('ExecutionInvariantCultureWarning')] [System.Boolean]$InvariantCultureWarning = $True, + # Sets the Execution.InitialSessionState option + [Parameter(Mandatory = $False)] + [Alias('ExecutionInitialSessionState')] + [PSRule.Configuration.SessionState]$InitialSessionState = [PSRule.Configuration.SessionState]::BuiltIn, + # Sets the Include.Module option [Parameter(Mandatory = $False)] [String[]]$IncludeModule, @@ -2196,6 +2206,11 @@ function SetOptions { [Alias('ExecutionInvariantCultureWarning')] [System.Boolean]$InvariantCultureWarning = $True, + # Sets the Execution.InitialSessionState option + [Parameter(Mandatory = $False)] + [Alias('ExecutionInitialSessionState')] + [PSRule.Configuration.SessionState]$InitialSessionState = [PSRule.Configuration.SessionState]::BuiltIn, + # Sets the Include.Module option [Parameter(Mandatory = $False)] [String[]]$IncludeModule, @@ -2386,6 +2401,11 @@ function SetOptions { $Option.Execution.InvariantCultureWarning = $InvariantCultureWarning; } + # Sets option Execution.InitialSessionState + if ($PSBoundParameters.ContainsKey('InitialSessionState')) { + $Option.Execution.InitialSessionState = $InitialSessionState; + } + # Sets option Include.Module if ($PSBoundParameters.ContainsKey('IncludeModule')) { $Option.Include.Module = $IncludeModule; diff --git a/src/PSRule/Pipeline/PipelineBuilder.cs b/src/PSRule/Pipeline/PipelineBuilder.cs index 8633c5d4eb..1b232e0416 100644 --- a/src/PSRule/Pipeline/PipelineBuilder.cs +++ b/src/PSRule/Pipeline/PipelineBuilder.cs @@ -25,6 +25,7 @@ public static class PipelineBuilder { /// /// Create a builder for an Assert pipeline. + /// Used by Assert-PSRule. /// /// /// Assert pipelines process objects with rules and produce text-based output suitable for output to a CI pipeline. @@ -42,6 +43,7 @@ public static IInvokePipelineBuilder Assert(Source[] source, PSRuleOption option /// /// Create a builder for an Invoke pipeline. + /// Used by Invoke-PSRule. /// /// /// Invoke piplines process objects and produce records indicating the outcome of each rule. @@ -57,6 +59,17 @@ public static IInvokePipelineBuilder Invoke(Source[] source, PSRuleOption option return pipeline; } + /// + /// Create a builder for a Test pipeline. + /// Used by Test-PSRule. + /// + /// + /// Test piplines process objects and true or false the outcome of each rule. + /// + /// An array of sources. + /// Options that configure PSRule. + /// An implementation of a host context that will recieve output and results. + /// A builder object to configure the pipeline. public static IInvokePipelineBuilder Test(Source[] source, PSRuleOption option, IHostContext hostContext) { var pipeline = new TestPipelineBuilder(source, hostContext); @@ -64,6 +77,17 @@ public static IInvokePipelineBuilder Test(Source[] source, PSRuleOption option, return pipeline; } + /// + /// Create a builder for a Get pipeline. + /// Used by Get-PSRule. + /// + /// + /// Get pipelines list rules that are discovered by PSRule either in modules or as standalone rules. + /// + /// An array of sources. + /// Options that configure PSRule. + /// An implementation of a host context that will recieve output and results. + /// A builder object to configure the pipeline. public static IGetPipelineBuilder Get(Source[] source, PSRuleOption option, IHostContext hostContext) { var pipeline = new GetRulePipelineBuilder(source, hostContext); @@ -71,6 +95,17 @@ public static IGetPipelineBuilder Get(Source[] source, PSRuleOption option, IHos return pipeline; } + /// + /// Create a builder for a help pipeline. + /// Used by Get-PSRuleHelp. + /// + /// + /// Gets command lines help content for all or specific rules. + /// + /// An array of sources. + /// Options that configure PSRule. + /// An implementation of a host context that will recieve output and results. + /// A builder object to configure the pipeline. public static IHelpPipelineBuilder GetHelp(Source[] source, PSRuleOption option, IHostContext hostContext) { var pipeline = new GetRuleHelpPipelineBuilder(source, hostContext); @@ -90,6 +125,14 @@ public static ISourcePipelineBuilder RuleSource(PSRuleOption option, IHostContex return pipeline; } + /// + /// Create a builder for a get baseline pipeline. + /// Used by Get-PSRuleBaseline. + /// + /// An array of sources. + /// Options that configure PSRule. + /// An implementation of a host context that will recieve output and results. + /// A builder object to configure the pipeline. public static IPipelineBuilder GetBaseline(Source[] source, PSRuleOption option, IHostContext hostContext) { var pipeline = new GetBaselinePipelineBuilder(source, hostContext); @@ -97,6 +140,14 @@ public static IPipelineBuilder GetBaseline(Source[] source, PSRuleOption option, return pipeline; } + /// + /// Create a builder for an export baseline pipeline. + /// Used by Export-PSRuleBaseline. + /// + /// An array of sources. + /// Options that configure PSRule. + /// An implementation of a host context that will recieve output and results. + /// A builder object to configure the pipeline. public static IPipelineBuilder ExportBaseline(Source[] source, PSRuleOption option, IHostContext hostContext) { var pipeline = new ExportBaselinePipelineBuilder(source, hostContext); @@ -104,6 +155,13 @@ public static IPipelineBuilder ExportBaseline(Source[] source, PSRuleOption opti return pipeline; } + /// + /// Create a builder for a target pipeline. + /// Used by Get-PSRuleTarget. + /// + /// Options that configure PSRule. + /// An implementation of a host context that will recieve output and results. + /// A builder object to configure the pipeline. public static IGetTargetPipelineBuilder GetTarget(PSRuleOption option, IHostContext hostContext) { var pipeline = new GetTargetPipelineBuilder(null, hostContext); @@ -420,10 +478,6 @@ protected static RepositoryOption GetRepository(RepositoryOption option) protected static ExecutionOption GetExecutionOption(ExecutionOption option) { var result = ExecutionOption.Combine(option, ExecutionOption.Default); - //result.InconclusiveWarning ??= ExecutionOption.Default.InconclusiveWarning; - //result.NotProcessedWarning ??= ExecutionOption.Default.NotProcessedWarning; - //result.SuppressedRuleWarning ??= ExecutionOption.Default.SuppressedRuleWarning; - //result.InvariantCultureWarning ??= ExecutionOption.Default.InvariantCultureWarning; result.DuplicateResourceId = result.DuplicateResourceId == ExecutionActionPreference.None ? ExecutionOption.Default.DuplicateResourceId.Value : result.DuplicateResourceId; return result; } diff --git a/src/PSRule/Pipeline/PipelineContext.cs b/src/PSRule/Pipeline/PipelineContext.cs index c769f1d713..8d1b2e6f11 100644 --- a/src/PSRule/Pipeline/PipelineContext.cs +++ b/src/PSRule/Pipeline/PipelineContext.cs @@ -138,7 +138,8 @@ internal Runspace GetRunspace() { if (_Runspace == null) { - var state = HostState.CreateSessionState(); + var initialSessionState = Option.Execution.InitialSessionState.GetValueOrDefault(ExecutionOption.Default.InitialSessionState.Value); + var state = HostState.CreateSessionState(initialSessionState); state.LanguageMode = _LanguageMode == LanguageMode.FullLanguage ? PSLanguageMode.FullLanguage : PSLanguageMode.ConstrainedLanguage; _Runspace = RunspaceFactory.CreateRunspace(state); @@ -203,7 +204,7 @@ private bool TryBaselineRef(ResourceId resourceId, out BaselineRef baselineRef) { baselineRef = null; var r = _Unresolved.FirstOrDefault(i => ResourceIdEqualityComparer.IdEquals(i.Id, resourceId.Value)); - if (!(r is BaselineRef br)) + if (r is not BaselineRef br) return false; baselineRef = br; diff --git a/src/PSRule/Pipeline/PipelineWriter.cs b/src/PSRule/Pipeline/PipelineWriter.cs index 4b620b2f2b..c19e817cff 100644 --- a/src/PSRule/Pipeline/PipelineWriter.cs +++ b/src/PSRule/Pipeline/PipelineWriter.cs @@ -205,7 +205,7 @@ protected void WriteErrorInfo(RuleRecord record) WriteError(errorRecord); } - protected static ActionPreference GetPreferenceVariable(SessionState sessionState, string variableName) + protected static ActionPreference GetPreferenceVariable(System.Management.Automation.SessionState sessionState, string variableName) { return (ActionPreference)sessionState.PSVariable.GetValue(variableName); } diff --git a/tests/PSRule.Tests/PSRule.Options.Tests.ps1 b/tests/PSRule.Tests/PSRule.Options.Tests.ps1 index d58cc4b0c7..91aff50efa 100644 --- a/tests/PSRule.Tests/PSRule.Options.Tests.ps1 +++ b/tests/PSRule.Tests/PSRule.Options.Tests.ps1 @@ -864,6 +864,45 @@ Describe 'New-PSRuleOption' -Tag 'Option','New-PSRuleOption' { } } + Context 'Read Execution.InitialSessionState' { + It 'from default' { + $option = New-PSRuleOption -Default; + $option.Execution.InitialSessionState | Should -Be 'BuiltIn'; + } + + It 'from Hashtable' { + $option = New-PSRuleOption -Option @{ 'Execution.InitialSessionState' = 'Minimal' }; + $option.Execution.InitialSessionState | Should -Be 'Minimal'; + } + + It 'from YAML' { + $option = New-PSRuleOption -Option (Join-Path -Path $here -ChildPath 'PSRule.Tests.yml'); + $option.Execution.InitialSessionState | Should -Be 'Minimal'; + } + + It 'from Environment' { + try { + # With bool + $Env:PSRULE_EXECUTION_INITIALSESSIONSTATE = 'minimal'; + $option = New-PSRuleOption; + $option.Execution.InitialSessionState | Should -Be 'Minimal'; + + # With int + $Env:PSRULE_EXECUTION_INITIALSESSIONSTATE = '1'; + $option = New-PSRuleOption; + $option.Execution.InitialSessionState | Should -Be 'Minimal'; + } + finally { + Remove-Item 'Env:PSRULE_EXECUTION_INITIALSESSIONSTATE' -Force; + } + } + + It 'from parameter' { + $option = New-PSRuleOption -InitialSessionState 'Minimal' -Path $emptyOptionsFilePath; + $option.Execution.InitialSessionState | Should -Be 'Minimal'; + } + } + Context 'Read Include.Path' { It 'from default' { $option = New-PSRuleOption -Default; diff --git a/tests/PSRule.Tests/PSRule.Tests.yml b/tests/PSRule.Tests/PSRule.Tests.yml index d151c1d77a..ffa750e1bf 100644 --- a/tests/PSRule.Tests/PSRule.Tests.yml +++ b/tests/PSRule.Tests/PSRule.Tests.yml @@ -55,6 +55,7 @@ execution: notProcessedWarning: false suppressedRuleWarning: false invariantCultureWarning: false + initialSessionState: Minimal # Configure input options input: From 7906d68284f593bfb361bed76828fc1198d887c1 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Thu, 1 Sep 2022 03:05:39 +1000 Subject: [PATCH 050/156] Fixes unhandled exception with GetRootedPath #1251 (#1252) * Fixes unhandled exception with GetRootedPath #1251 * Update build version --- PSRule.sln | 26 ++++++---- docs/CHANGELOG-v2.md | 2 + pipeline.build.ps1 | 5 +- src/PSRule.BuildTask/Generator.props | 5 ++ .../Generators/EngineVersionGenerator.cs | 49 +++++++++++++++++++ src/PSRule.BuildTask/PSRule.BuildTask.csproj | 17 +++++++ src/PSRule.BuildTool/PSRule.BuildTool.csproj | 4 -- src/PSRule.Tool/ClientHelper.cs | 1 + src/PSRule/Common/Engine.cs | 22 +++++++++ src/PSRule/PSRule.csproj | 12 +++++ .../Pipeline/Formatters/AssertFormatter.cs | 10 ++-- .../Pipeline/Output/SarifOutputWriter.cs | 2 +- src/PSRule/Pipeline/PipelineBuilder.cs | 2 +- src/PSRule/Pipeline/SourcePipeline.cs | 2 +- 14 files changed, 135 insertions(+), 24 deletions(-) create mode 100644 src/PSRule.BuildTask/Generator.props create mode 100644 src/PSRule.BuildTask/Generators/EngineVersionGenerator.cs create mode 100644 src/PSRule.BuildTask/PSRule.BuildTask.csproj create mode 100644 src/PSRule/Common/Engine.cs diff --git a/PSRule.sln b/PSRule.sln index 283bdf4383..5db1fb557c 100644 --- a/PSRule.sln +++ b/PSRule.sln @@ -13,7 +13,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSRule.Badges", "src\PSRule EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSRule.BuildTool", "src\PSRule.BuildTool\PSRule.BuildTool.csproj", "{20DDCC65-8A9A-4BDC-91EC-C3BE6F32E52E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSRule.SDK", "src\PSRule.SDK\PSRule.SDK.csproj", "{6B21D558-BFC3-4BC6-963E-83B65353196F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSRule.SDK", "src\PSRule.SDK\PSRule.SDK.csproj", "{07A84E67-1CA3-4766-B9EA-1FDD9DF6516F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSRule.Tool", "src\PSRule.Tool\PSRule.Tool.csproj", "{F6CFCA60-72D5-474E-8B8B-1AB973434569}" EndProject @@ -22,7 +22,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution src\PSRule.Common.props = src\PSRule.Common.props EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSRule.Types", "src\PSRule.Types\PSRule.Types.csproj", "{6E48E1BA-9A94-4412-BB3F-F5F569E05A4E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSRule.Types", "src\PSRule.Types\PSRule.Types.csproj", "{5FE4DB0B-63D1-4DDB-9762-9C0D29168BC9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSRule.BuildTask", "src\PSRule.BuildTask\PSRule.BuildTask.csproj", "{872D2648-2F00-475E-84B5-F08BE07385B7}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -50,18 +52,22 @@ Global {20DDCC65-8A9A-4BDC-91EC-C3BE6F32E52E}.Debug|Any CPU.Build.0 = Debug|Any CPU {20DDCC65-8A9A-4BDC-91EC-C3BE6F32E52E}.Release|Any CPU.ActiveCfg = Release|Any CPU {20DDCC65-8A9A-4BDC-91EC-C3BE6F32E52E}.Release|Any CPU.Build.0 = Release|Any CPU - {6B21D558-BFC3-4BC6-963E-83B65353196F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6B21D558-BFC3-4BC6-963E-83B65353196F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6B21D558-BFC3-4BC6-963E-83B65353196F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6B21D558-BFC3-4BC6-963E-83B65353196F}.Release|Any CPU.Build.0 = Release|Any CPU + {07A84E67-1CA3-4766-B9EA-1FDD9DF6516F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07A84E67-1CA3-4766-B9EA-1FDD9DF6516F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07A84E67-1CA3-4766-B9EA-1FDD9DF6516F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07A84E67-1CA3-4766-B9EA-1FDD9DF6516F}.Release|Any CPU.Build.0 = Release|Any CPU {F6CFCA60-72D5-474E-8B8B-1AB973434569}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F6CFCA60-72D5-474E-8B8B-1AB973434569}.Debug|Any CPU.Build.0 = Debug|Any CPU {F6CFCA60-72D5-474E-8B8B-1AB973434569}.Release|Any CPU.ActiveCfg = Release|Any CPU {F6CFCA60-72D5-474E-8B8B-1AB973434569}.Release|Any CPU.Build.0 = Release|Any CPU - {6E48E1BA-9A94-4412-BB3F-F5F569E05A4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6E48E1BA-9A94-4412-BB3F-F5F569E05A4E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6E48E1BA-9A94-4412-BB3F-F5F569E05A4E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6E48E1BA-9A94-4412-BB3F-F5F569E05A4E}.Release|Any CPU.Build.0 = Release|Any CPU + {5FE4DB0B-63D1-4DDB-9762-9C0D29168BC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5FE4DB0B-63D1-4DDB-9762-9C0D29168BC9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5FE4DB0B-63D1-4DDB-9762-9C0D29168BC9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5FE4DB0B-63D1-4DDB-9762-9C0D29168BC9}.Release|Any CPU.Build.0 = Release|Any CPU + {872D2648-2F00-475E-84B5-F08BE07385B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {872D2648-2F00-475E-84B5-F08BE07385B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {872D2648-2F00-475E-84B5-F08BE07385B7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {872D2648-2F00-475E-84B5-F08BE07385B7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 3e5cd6548a..1af273942d 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -32,6 +32,8 @@ What's changed since v2.4.0: - Fixed could not load Microsoft.Management.Infrastructure by @BernieWhite. [#1249](https://github.com/microsoft/PSRule/issues/1249) - To use minimal initial session state set `Execution.InitialSessionState` to `Minimal`. + - Fixed unhandled exception with GetRootedPath by @BernieWhite. + [#1251](https://github.com/microsoft/PSRule/issues/1251) ## v2.4.0 diff --git a/pipeline.build.ps1 b/pipeline.build.ps1 index f2fb935922..f79baddf08 100644 --- a/pipeline.build.ps1 +++ b/pipeline.build.ps1 @@ -129,11 +129,12 @@ task TestDotNet { task BuildCLI BuildModule, { exec { # Build library - dotnet publish src/PSRule.Tool -c $Configuration --no-self-contained -r win-x64 -o ./out/cli/win-x64/ -p:version=$Build + dotnet publish src/PSRule.Tool -c $Configuration --self-contained /p:DebugType=None /p:DebugSymbols=false -r win-x64 -o ./out/cli/win-x64/ -p:version=$Build + # dotnet publish src/PSRule.Tool -c $Configuration --no-self-contained -r win-x64 -o ./out/cli/win-x64/ -p:version=$Build # dotnet publish --self-contained true -p:PublishTrimmed=true -p:PublishSingleFile=true -r win-x64 .\src\PSRule.Tool\PSRule.Tool.csproj -o .\out\cli } - Copy-Item -Path out/modules/PSRule/ -Destination out/cli/win-x64/modules/ -Recurse -Force; + # Copy-Item -Path out/modules/PSRule/ -Destination out/cli/win-x64/modules/ -Recurse -Force; } task CopyModule { diff --git a/src/PSRule.BuildTask/Generator.props b/src/PSRule.BuildTask/Generator.props new file mode 100644 index 0000000000..4034969030 --- /dev/null +++ b/src/PSRule.BuildTask/Generator.props @@ -0,0 +1,5 @@ + + + + + diff --git a/src/PSRule.BuildTask/Generators/EngineVersionGenerator.cs b/src/PSRule.BuildTask/Generators/EngineVersionGenerator.cs new file mode 100644 index 0000000000..9aa1251dda --- /dev/null +++ b/src/PSRule.BuildTask/Generators/EngineVersionGenerator.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Microsoft.CodeAnalysis; + +namespace PSRule.BuildTask.Generators +{ + /// + /// Generator contants for PSRule engine version. + /// + [Generator] + public sealed class EngineVersionGenerator : ISourceGenerator + { + // Detailed from Roslyn SDK: https://docs.microsoft.com/dotnet/csharp/roslyn-sdk/source-generators-overview + + /// + public void Execute(GeneratorExecutionContext context) + { + var result = GetPartialContent(context); + context.AddSource("EngineVersion.g.cs", result); + } + + /// + public void Initialize(GeneratorInitializationContext context) + { + // Not required. + } + + private static string GetPartialContent(GeneratorExecutionContext context) + { + if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.PSRule_Version", out var productVersion)) + productVersion = "0.0.1"; + + // Build up the source code + return $@"// +using System; + +namespace PSRule +{{ + internal static partial class Engine + {{ + private const string _Version = ""{productVersion}""; + }} +}} +"; + } + } +} diff --git a/src/PSRule.BuildTask/PSRule.BuildTask.csproj b/src/PSRule.BuildTask/PSRule.BuildTask.csproj new file mode 100644 index 0000000000..4829f15289 --- /dev/null +++ b/src/PSRule.BuildTask/PSRule.BuildTask.csproj @@ -0,0 +1,17 @@ + + + + PSRule.BuildTask + false + false + + + + + + + + + diff --git a/src/PSRule.BuildTool/PSRule.BuildTool.csproj b/src/PSRule.BuildTool/PSRule.BuildTool.csproj index 02f5db9501..4cbb9ed573 100644 --- a/src/PSRule.BuildTool/PSRule.BuildTool.csproj +++ b/src/PSRule.BuildTool/PSRule.BuildTool.csproj @@ -14,10 +14,6 @@ - - Windows - - CmdStrings.resx diff --git a/src/PSRule.Tool/ClientHelper.cs b/src/PSRule.Tool/ClientHelper.cs index 4bcfd438ea..fe69da97b8 100644 --- a/src/PSRule.Tool/ClientHelper.cs +++ b/src/PSRule.Tool/ClientHelper.cs @@ -96,6 +96,7 @@ private static void InstallVersion(PowerShell pwsh, string name, string version) private static PSRuleOption GetOption() { var option = PSRuleOption.FromFileOrEmpty(); + option.Execution.InitialSessionState = Configuration.SessionState.Minimal; option.Input.Format = InputFormat.File; option.Output.Style = OutputStyle.Client; return option; diff --git a/src/PSRule/Common/Engine.cs b/src/PSRule/Common/Engine.cs new file mode 100644 index 0000000000..a8e497d549 --- /dev/null +++ b/src/PSRule/Common/Engine.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using PSRule.Configuration; + +namespace PSRule +{ + internal static partial class Engine + { + internal static string GetLocalPath() + { + return PSRuleOption.GetRootedBasePath(Path.GetDirectoryName(AppContext.BaseDirectory)); + } + + internal static string GetVersion() + { + return _Version; + } + } +} diff --git a/src/PSRule/PSRule.csproj b/src/PSRule/PSRule.csproj index 94b376f746..82e567e7fb 100644 --- a/src/PSRule/PSRule.csproj +++ b/src/PSRule/PSRule.csproj @@ -21,6 +21,18 @@ + + + + + + + + $(version) + true + + + diff --git a/src/PSRule/Pipeline/Formatters/AssertFormatter.cs b/src/PSRule/Pipeline/Formatters/AssertFormatter.cs index 27728ef4b5..3269a403a8 100644 --- a/src/PSRule/Pipeline/Formatters/AssertFormatter.cs +++ b/src/PSRule/Pipeline/Formatters/AssertFormatter.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Management.Automation; -using System.Reflection; using System.Threading; using PSRule.Configuration; using PSRule.Definitions; @@ -137,7 +135,7 @@ internal abstract class AssertFormatterBase : PipelineLoggerBase, IAssertFormatt private bool _UnbrokenInfo; private bool _UnbrokenObject; - private static readonly TerminalSupport DefaultTerminalSupport = new TerminalSupport(4); + private static readonly TerminalSupport DefaultTerminalSupport = new(4); protected AssertFormatterBase(Source[] source, IPipelineWriter writer, PSRuleOption option) { @@ -369,8 +367,10 @@ private void Source(Source[] source) if (!Option.Output.Banner.GetValueOrDefault(BannerFormat.Default).HasFlag(BannerFormat.Source)) return; - var version = FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion; - WriteLineFormat(FormatterStrings.PSRuleVersion, version); + var version = Engine.GetVersion(); + if (!string.IsNullOrEmpty(version)) + WriteLineFormat(FormatterStrings.PSRuleVersion, version); + var list = new HashSet(StringComparer.OrdinalIgnoreCase); for (var i = 0; source != null && i < source.Length; i++) { diff --git a/src/PSRule/Pipeline/Output/SarifOutputWriter.cs b/src/PSRule/Pipeline/Output/SarifOutputWriter.cs index 4007f491ba..ad322e31ba 100644 --- a/src/PSRule/Pipeline/Output/SarifOutputWriter.cs +++ b/src/PSRule/Pipeline/Output/SarifOutputWriter.cs @@ -288,7 +288,7 @@ private static FailureLevel GetLevel(RuleRecord record) private Tool GetTool(Source[] source) { - var version = FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion; + var version = Engine.GetVersion(); return new Tool { Driver = new ToolComponent diff --git a/src/PSRule/Pipeline/PipelineBuilder.cs b/src/PSRule/Pipeline/PipelineBuilder.cs index 1b232e0416..d4d87dae31 100644 --- a/src/PSRule/Pipeline/PipelineBuilder.cs +++ b/src/PSRule/Pipeline/PipelineBuilder.cs @@ -325,7 +325,7 @@ protected bool RequireModules() var result = true; if (Option.Requires.TryGetValue(ENGINE_MODULE_NAME, out var requiredVersion)) { - var engineVersion = FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion; + var engineVersion = Engine.GetVersion(); if (GuardModuleVersion(ENGINE_MODULE_NAME, engineVersion, requiredVersion)) result = false; } diff --git a/src/PSRule/Pipeline/SourcePipeline.cs b/src/PSRule/Pipeline/SourcePipeline.cs index d83cf9d611..ee6447223a 100644 --- a/src/PSRule/Pipeline/SourcePipeline.cs +++ b/src/PSRule/Pipeline/SourcePipeline.cs @@ -126,7 +126,7 @@ internal SourcePipelineBuilder(IHostContext hostContext, PSRuleOption option) _Writer = new HostPipelineWriter(hostContext, option); _Writer.EnterScope("[Discovery.Source]"); _UseDefaultPath = option == null || option.Include == null || option.Include.Path == null; - _LocalPath = PSRuleOption.GetRootedBasePath(Path.GetDirectoryName(typeof(SourcePipelineBuilder).Assembly.Location)); + _LocalPath = Engine.GetLocalPath(); // Include paths from options if (!_UseDefaultPath) From 63ec216d852c1ca9a960a0e3d2307aee55b1e36d Mon Sep 17 00:00:00 2001 From: Bernie White Date: Thu, 1 Sep 2022 03:17:36 +1000 Subject: [PATCH 051/156] Pre-release v2.5.0-B0004 (#1253) --- docs/CHANGELOG-v2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 1af273942d..690b621f72 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -23,6 +23,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.5.0-B0004 (pre-release) + What's changed since v2.4.0: - Engineering: From 2d78a2bda19820fe2618c429e1eac94d0766ce11 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 4 Sep 2022 22:48:36 +1000 Subject: [PATCH 052/156] Bump Microsoft.CodeAnalysis.Common from 4.2.0 to 4.3.0 (#1254) Bumps [Microsoft.CodeAnalysis.Common](https://github.com/dotnet/roslyn) from 4.2.0 to 4.3.0. - [Release notes](https://github.com/dotnet/roslyn/releases) - [Changelog](https://github.com/dotnet/roslyn/blob/main/docs/Breaking%20API%20Changes.md) - [Commits](https://github.com/dotnet/roslyn/compare/v4.2.0...Visual-Studio-2019-Version-16.0-Preview-4.3) --- updated-dependencies: - dependency-name: Microsoft.CodeAnalysis.Common dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/PSRule.BuildTask/PSRule.BuildTask.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PSRule.BuildTask/PSRule.BuildTask.csproj b/src/PSRule.BuildTask/PSRule.BuildTask.csproj index 4829f15289..24562a32ca 100644 --- a/src/PSRule.BuildTask/PSRule.BuildTask.csproj +++ b/src/PSRule.BuildTask/PSRule.BuildTask.csproj @@ -7,7 +7,7 @@ - + validate-files.ps1 + |--> template + |--> ARM template... +``` + +## Run PSRules in the container + +Now we are ready to go! Run the below docker command to test the ARM template. + +```powershell +docker run -it --rm -v $PWD/:/src psrule:latest pwsh -file /src/validate-files.ps1 +``` + +This command runs the container and the PSRule tests by mounting the directory to the /src path +and then executing the `validate-files.ps1` script. + +!!! Note + The volume mount option expects your current working directory to be the new directory created. + You can change this to an absolute or relative path if desired. + +## Clean up + +When you are ready to clean up the container image you can do so with the below command. + +```powershell +docker image rm psrule +``` diff --git a/docs/scenarios/containers/dockerfile b/docs/scenarios/containers/dockerfile new file mode 100644 index 0000000000..86aa399075 --- /dev/null +++ b/docs/scenarios/containers/dockerfile @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +FROM mcr.microsoft.com/powershell:7.2-ubuntu-22.04 +SHELL ["pwsh", "-command"] + +RUN Install-Module -Name 'PSRule','PSRule.Rules.Azure' -Force \ No newline at end of file diff --git a/docs/scenarios/containers/psrulesample/template/azuredeploy.json b/docs/scenarios/containers/psrulesample/template/azuredeploy.json new file mode 100644 index 0000000000..0f33ea5abd --- /dev/null +++ b/docs/scenarios/containers/psrulesample/template/azuredeploy.json @@ -0,0 +1,139 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.5.6.12127", + "templateHash": "1829424340775765027" + } + }, + "parameters": { + "webAppName": { + "type": "string", + "defaultValue": "[format('webApp-{0}', uniqueString(resourceGroup().id))]", + "minLength": 2, + "metadata": { + "description": "Web app name." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for all resources." + } + }, + "sku": { + "type": "string", + "defaultValue": "F1", + "allowedValues": [ + "F1", + "D1", + "B1", + "B2", + "B3", + "S1", + "S2", + "S3", + "P1", + "P2", + "P3", + "P4" + ], + "metadata": { + "description": "Describes plan's pricing tier and instance size. Check details at https://azure.microsoft.com/en-us/pricing/details/app-service/" + } + }, + "language": { + "type": "string", + "defaultValue": ".net", + "allowedValues": [ + ".net", + "php", + "node", + "html" + ], + "metadata": { + "description": "The language stack of the app." + } + }, + "repoUrl": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional Git Repo URL, if empty a 'hello world' app will be deploy from the Azure-Samples repo" + } + } + }, + "variables": { + "appServicePlanName": "[format('AppServicePlan-{0}', parameters('webAppName'))]", + "gitRepoReference": { + ".net": "https://github.com/Azure-Samples/app-service-web-dotnet-get-started", + "node": "https://github.com/Azure-Samples/nodejs-docs-hello-world", + "php": "https://github.com/Azure-Samples/php-docs-hello-world", + "html": "https://github.com/Azure-Samples/html-docs-hello-world" + }, + "gitRepoUrl": "[if(empty(parameters('repoUrl')), variables('gitRepoReference')[parameters('language')], parameters('repoUrl'))]", + "configReference": { + ".net": { + "comments": ".Net app. No additional configuration needed." + }, + "html": { + "comments": "HTML app. No additional configuration needed." + }, + "php": { + "phpVersion": "7.4" + }, + "node": { + "appSettings": [ + { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "12.15.0" + } + ] + } + } + }, + "resources": [ + { + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2021-03-01", + "name": "[variables('appServicePlanName')]", + "location": "[parameters('location')]", + "sku": { + "name": "[parameters('sku')]" + } + }, + { + "type": "Microsoft.Web/sites", + "apiVersion": "2021-03-01", + "name": "[parameters('webAppName')]", + "location": "[parameters('location')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "siteConfig": "[union(variables('configReference')[parameters('language')], createObject('minTlsVersion', '1.2', 'scmMinTlsVersion', '1.2', 'ftpsState', 'FtpsOnly'))]", + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]", + "httpsOnly": true + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]" + ] + }, + { + "type": "Microsoft.Web/sites/sourcecontrols", + "apiVersion": "2021-03-01", + "name": "[format('{0}/{1}', parameters('webAppName'), 'web')]", + "properties": { + "repoUrl": "[variables('gitRepoUrl')]", + "branch": "master", + "isManualIntegration": true + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', parameters('webAppName'))]" + ] + } + ] + } \ No newline at end of file diff --git a/docs/scenarios/containers/psrulesample/template/azuredeploy.parameters.json b/docs/scenarios/containers/psrulesample/template/azuredeploy.parameters.json new file mode 100644 index 0000000000..ae0841e926 --- /dev/null +++ b/docs/scenarios/containers/psrulesample/template/azuredeploy.parameters.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "webAppName": { + "value": "GEN-UNIQUE" + } + } +} \ No newline at end of file diff --git a/docs/scenarios/containers/psrulesample/validate-files.ps1 b/docs/scenarios/containers/psrulesample/validate-files.ps1 new file mode 100644 index 0000000000..4db818aeb6 --- /dev/null +++ b/docs/scenarios/containers/psrulesample/validate-files.ps1 @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +<# + .SYNOPSIS + Create a PSRule AzRuleTemplate data file and run the PSRule.Rules.Azure module rules against the output. +#> + +Get-AzRuleTemplateLink "$PSScriptRoot/template" | Export-AzRuleTemplateData -OutputPath "$PSScriptRoot/out" + +Assert-PSRule -InputPath "$PSScriptRoot/out/" -Module 'PSRule.Rules.Azure' -As Summary diff --git a/docs/scenarios/containers/run.ps1 b/docs/scenarios/containers/run.ps1 new file mode 100644 index 0000000000..ad25f6194d --- /dev/null +++ b/docs/scenarios/containers/run.ps1 @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +<# + .SYNOPSIS + Create a container, execute the validate-files.ps1 script in the child psrulesample directory + and then remove the container. +#> + +# build image +docker build --tag psrule:latest . + +# docker run --rm psrule:latest +docker run -it --rm -v $PSScriptRoot/psrulesample:/src psrule:latest pwsh -file /src/validate-files.ps1 + +# Remove the image +docker image rm psrule diff --git a/mkdocs.yml b/mkdocs.yml index 950683fb45..27ba2092e9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -58,6 +58,7 @@ nav: - Validate Azure resource configuration: scenarios/azure-resources/azure-resources.md - Azure resource tagging example: scenarios/azure-tags/azure-tags.md - Kubernetes resource validation example: scenarios/kubernetes-resources/kubernetes-resources.md + - Using PSRule from a Container: scenarios/containers/container-execution.md - Concepts: - Functions: expressions/functions.md - Sub-selectors: expressions/sub-selectors.md From 56b375af7ebeb33330908cc2e4e98d9ca417deab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Sep 2022 19:09:23 +1000 Subject: [PATCH 062/156] Bump mkdocs-material from 8.5.0 to 8.5.1 (#1267) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.5.0 to 8.5.1. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.5.0...8.5.1) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index c862459905..9b505229b7 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.3.1 -mkdocs-material==8.5.0 +mkdocs-material==8.5.1 pymdown-extensions==9.5 mike==1.1.2 mkdocs-simple-hooks==0.1.5 From 0cd67915f8a16d3ea900b5b84f4affcbc726a81e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Sep 2022 15:00:24 +1000 Subject: [PATCH 063/156] Bump mkdocs-material from 8.5.1 to 8.5.2 (#1276) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.5.1 to 8.5.2. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.5.1...8.5.2) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 9b505229b7..72ffb0e983 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.3.1 -mkdocs-material==8.5.1 +mkdocs-material==8.5.2 pymdown-extensions==9.5 mike==1.1.2 mkdocs-simple-hooks==0.1.5 From 44890e2cb79b19908ffeca3b1da72ae4454916a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Sep 2022 15:22:14 +1000 Subject: [PATCH 064/156] Bump mkdocs-redirects from 1.1.0 to 1.2.0 (#1275) Bumps [mkdocs-redirects](https://github.com/datarobot/mkdocs-redirects) from 1.1.0 to 1.2.0. - [Release notes](https://github.com/datarobot/mkdocs-redirects/releases) - [Commits](https://github.com/datarobot/mkdocs-redirects/compare/v1.1.0...v1.2.0) --- updated-dependencies: - dependency-name: mkdocs-redirects dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 72ffb0e983..acbfe5d545 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -5,4 +5,4 @@ mike==1.1.2 mkdocs-simple-hooks==0.1.5 mkdocs-git-revision-date-plugin==0.3.2 mdx-truly-sane-lists==1.3 -mkdocs-redirects==1.1.0 +mkdocs-redirects==1.2.0 From 584c30f69b94b7790ccd0e6b43e9f9c2466a20d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Sep 2022 23:09:53 +1000 Subject: [PATCH 065/156] Bump mkdocs-material from 8.5.2 to 8.5.3 (#1277) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.5.2 to 8.5.3. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.5.2...8.5.3) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index acbfe5d545..6b80da97be 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.3.1 -mkdocs-material==8.5.2 +mkdocs-material==8.5.3 pymdown-extensions==9.5 mike==1.1.2 mkdocs-simple-hooks==0.1.5 From 0db3785242785bba181841677bf3ffcd8f683dfc Mon Sep 17 00:00:00 2001 From: Bernie White Date: Thu, 22 Sep 2022 03:21:47 +1000 Subject: [PATCH 066/156] Added taxa for grouping rules #1272 (#1278) --- docs/CHANGELOG-v2.md | 8 ++ docs/concepts/grouping-rules.md | 92 +++++++++++++++++++ mkdocs.yml | 1 + schemas/PSRule-language.schema.json | 56 +++++++++++ .../Commands/NewRuleDefinitionCommand.cs | 13 ++- src/PSRule/Common/DictionaryExtensions.cs | 16 +--- src/PSRule/Common/ExpressionHelpers.cs | 51 +++++++--- src/PSRule/Common/HashtableExtensions.cs | 23 ++++- src/PSRule/Common/JsonConverters.cs | 36 ++++++++ src/PSRule/Common/JsonReaderExtensions.cs | 14 ++- src/PSRule/Common/YamlConverters.cs | 39 +++++++- src/PSRule/Configuration/RuleOption.cs | 16 +++- .../Conventions/ScriptBlockConvention.cs | 3 + src/PSRule/Definitions/Resource.cs | 60 ++++++++++++ src/PSRule/Definitions/Rules/RuleFilter.cs | 31 +++++-- src/PSRule/Host/HostHelper.cs | 7 +- src/PSRule/PSRule.psm1 | 4 + src/PSRule/Pipeline/OptionContext.cs | 5 +- src/PSRule/Rules/Rule.cs | 32 ++++--- src/PSRule/Rules/RuleBlock.cs | 11 ++- tests/PSRule.Tests/Baseline.Rule.jsonc | 18 ++++ tests/PSRule.Tests/Baseline.Rule.yaml | 11 +++ tests/PSRule.Tests/BaselineTests.cs | 22 ++++- tests/PSRule.Tests/FromFile.Rule.jsonc | 7 ++ tests/PSRule.Tests/FromFile.Rule.yaml | 5 + tests/PSRule.Tests/FromFileBaseline.Rule.ps1 | 4 +- tests/PSRule.Tests/PSRule.Baseline.Tests.ps1 | 7 +- tests/PSRule.Tests/RuleFilterTests.cs | 64 +++++++++---- 28 files changed, 578 insertions(+), 78 deletions(-) create mode 100644 docs/concepts/grouping-rules.md diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 061df0e448..a940e19d45 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -23,6 +23,14 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since pre-release v2.5.0-B0015: + +- General improvements: + - Added taxa metadata from grouping and filtering rules by @BernieWhite. + [#1272](https://github.com/microsoft/PSRule/issues/1272) + - Taxa is metadata that extends on tags to provide a more structured way to group rules. + - Rules can be classified by setting the `metadata.taxa` property or `-Taxa` parameter. + ## v2.5.0-B0015 (pre-release) What's changed since pre-release v2.5.0-B0004: diff --git a/docs/concepts/grouping-rules.md b/docs/concepts/grouping-rules.md new file mode 100644 index 0000000000..76d6d7e1c9 --- /dev/null +++ b/docs/concepts/grouping-rules.md @@ -0,0 +1,92 @@ +# Grouping rules + +!!! Abstract + _Taxa_ are additional metadata that can be used to classify rules. + Together with tags they can be used to group or filter rules. + +## Using taxa + +When defining a rule you can specify taxa to classify or link rules using a framework or standard. +A single rule can be can linked to multiple taxa. +For example: + +- The Azure Well-Architected Framework (WAF) defines pillars such as _Security_ and _Reliability_. +- The CIS Benchmarks define a number of control IDs such as _3.12_ and _13.4_. + +=== "YAML" + + To specify taxa in YAML, use the `taxa` property: + + ```yaml + --- + # Synopsis: A rule with taxa defined. + apiVersion: github.com/microsoft/PSRule/v1 + kind: Rule + metadata: + name: WithTaxa + taxa: + Azure.WAF/pillar: Security + Azure.ASB.v3/control: [ 'ID-1', 'ID-2' ] + spec: { } + ``` + +=== "JSON" + + To specify taxa in JSON, use the `taxa` property: + + ```json + { + // Synopsis: A rule with taxa defined. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "WithTaxa", + "taxa": { + "Azure.WAF/pillar": "Security", + "Azure.ASB.v3/control": [ "ID-1", "ID-2" ] + } + }, + "spec": { } + } + ``` + +=== "PowerShell" + + To specify taxa in PowerShell, use the `-Taxa` parameter: + + ```powershell + # Synopsis: A rule with taxa defined. + Rule 'WithTaxa' -Taxa @{ 'Azure.WAF/pillar' = 'Security'; 'Azure.ASB.v3/control' = @('ID-1', 'ID-2') } { + # Define conditions here + } + ``` + +## Filtering with taxa + +A reason for assigning taxa to rules is to perform filtering of rules to a specific subset. +This can be accomplished using baselines and the `spec.rule.taxa` property. +For example: + +```yaml +--- +# Synopsis: A baseline which returns only security rules. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Baseline +metadata: + name: TestBaseline6 +spec: + rule: + taxa: + Azure.WAF/pillar: [ 'Security' ] + +--- +# Synopsis: A baseline which returns any rules that are classified to Azure.WAF/pillar. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Baseline +metadata: + name: TestBaseline6 +spec: + rule: + taxa: + Azure.WAF/pillar: '*' +``` diff --git a/mkdocs.yml b/mkdocs.yml index 27ba2092e9..ebec3e3025 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -61,6 +61,7 @@ nav: - Using PSRule from a Container: scenarios/containers/container-execution.md - Concepts: - Functions: expressions/functions.md + - Grouping rules: concepts/grouping-rules.md - Sub-selectors: expressions/sub-selectors.md - Scenarios: - Using within continuous integration: scenarios/validation-pipeline/validation-pipeline.md diff --git a/schemas/PSRule-language.schema.json b/schemas/PSRule-language.schema.json index e7bc131ad2..a1e536f8db 100644 --- a/schemas/PSRule-language.schema.json +++ b/schemas/PSRule-language.schema.json @@ -162,6 +162,28 @@ } ] } + }, + "taxa": { + "type": "object", + "title": "Taxa", + "description": "Require rules to have the following associated taxa.", + "markdownDescription": "Require rules to have the following associated taxa.", + "additionalProperties": { + "oneOf": [ + { + "type": "string", + "description": "A required reference." + }, + { + "type": "array", + "description": "A required reference.", + "items": { + "type": "string" + }, + "uniqueItems": true + } + ] + } } }, "additionalProperties": false @@ -680,6 +702,40 @@ }, "tags": { "$ref": "#/definitions/resourceTags" + }, + "taxa": { + "type": "object", + "title": "Taxa", + "description": "Any taxonomy references.", + "additionalProperties": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "defaultSnippets": [ + { + "label": "Reference key/ value", + "body": { + "${1:Key}": "${2:Value}" + } + }, + { + "label": "Reference key/ multi-value", + "body": { + "${1:Key}": [ + "${2:Value}" + ] + } + } + ] } }, "required": [ diff --git a/src/PSRule/Commands/NewRuleDefinitionCommand.cs b/src/PSRule/Commands/NewRuleDefinitionCommand.cs index b58e1dd257..0f2af022a7 100644 --- a/src/PSRule/Commands/NewRuleDefinitionCommand.cs +++ b/src/PSRule/Commands/NewRuleDefinitionCommand.cs @@ -90,10 +90,19 @@ internal sealed class NewRuleDefinitionCommand : LanguageBlock [Parameter(Mandatory = false)] public string[] Alias { get; set; } + /// + /// An optional reference identifer for the resource. + /// [Parameter(Mandatory = false)] [ValidateLength(3, 128)] public string Ref { get; set; } + /// + /// Any taxonomy references. + /// + [Parameter(Mandatory = false)] + public Hashtable Taxa { get; set; } + protected override void ProcessRecord() { if (!IsSourceScope()) @@ -112,6 +121,7 @@ protected override void ProcessRecord() ); var flags = ResourceFlags.None; var id = new ResourceId(source.Module, Name, ResourceIdKind.Id); + var taxa = ResourceTaxa.FromHashtable(Taxa); context.VerboseFoundResource(name: Name, moduleName: source.Module, scriptName: MyInvocation.ScriptName); @@ -136,7 +146,8 @@ protected override void ProcessRecord() dependsOn: ResourceHelper.GetRuleId(source.Module, DependsOn, ResourceIdKind.Unknown), configuration: Configure, extent: extent, - flags: flags + flags: flags, + taxa: taxa ); #pragma warning restore CA2000 // Dispose objects before losing scope, needs to be passed to pipeline WriteObject(block); diff --git a/src/PSRule/Common/DictionaryExtensions.cs b/src/PSRule/Common/DictionaryExtensions.cs index fa65fdffca..ffc365b185 100644 --- a/src/PSRule/Common/DictionaryExtensions.cs +++ b/src/PSRule/Common/DictionaryExtensions.cs @@ -5,7 +5,6 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; namespace PSRule { @@ -59,7 +58,7 @@ public static bool TryPopString(this IDictionary dictionary, str public static bool TryPopStringArray(this IDictionary dictionary, string key, out string[] value) { value = default; - return TryPopValue(dictionary, key, out var v) && TryStringArray(v, out value); + return TryPopValue(dictionary, key, out var v) && ExpressionHelpers.TryConvertStringArray(v, out value); } [DebuggerStepThrough] @@ -141,7 +140,7 @@ public static bool TryGetEnumerable(this IDictionary dictionary, public static bool TryGetStringArray(this IDictionary dictionary, string key, out string[] value) { value = null; - return dictionary.TryGetValue(key, out var o) && TryStringArray(o, out value); + return dictionary.TryGetValue(key, out var o) && ExpressionHelpers.TryConvertStringArray(o, out value); } [DebuggerStepThrough] @@ -152,17 +151,6 @@ public static void AddUnique(this IDictionary dictionary, IEnume dictionary.Add(kv.Key, kv.Value); } - [DebuggerStepThrough] - private static bool TryStringArray(object o, out string[] value) - { - value = default; - if (o == null) - return false; - - value = o.GetType().IsArray ? ((object[])o).OfType().ToArray() : new string[] { o.ToString() }; - return true; - } - internal static SortedDictionary ToSortedDictionary(this IDictionary dictionary) { return new SortedDictionary(dictionary); diff --git a/src/PSRule/Common/ExpressionHelpers.cs b/src/PSRule/Common/ExpressionHelpers.cs index 8249200744..3f6e51f8c4 100644 --- a/src/PSRule/Common/ExpressionHelpers.cs +++ b/src/PSRule/Common/ExpressionHelpers.cs @@ -171,20 +171,49 @@ internal static bool TryArray(object o, out Array value) return false; } - internal static bool TryConvertStringArray(object[] o, out string[] value) + internal static bool TryConvertStringArray(object o, out string[] value) { - value = Array.Empty(); - if (o == null || o.Length == 0 || !TryString(o[0], convert: true, value: out var s)) - return false; + // Handle single string + if (TryString(o, convert: true, value: out var s)) + { + value = new string[] { s }; + return true; + } + + // Handle multiple strings + return TryStringArray(o, out value); + } - value = new string[o.Length]; - value[0] = s; - for (var i = 1; i < o.Length; i++) + internal static bool TryStringArray(object o, out string[] value) + { + value = null; + if (o is Array array) { - if (TryString(o[i], convert: true, value: out s)) - value[i] = s; + value = new string[array.Length]; + for (var i = 0; i < array.Length; i++) + { + if (TryString(array.GetValue(i), out var s)) + value[i] = s; + } } - return true; + else if (o is JArray jArray) + { + value = new string[jArray.Count]; + for (var i = 0; i < jArray.Count; i++) + { + if (TryString(jArray[i], out var s)) + value[i] = s; + } + } + else if (o is IEnumerable enumerable) + { + value = enumerable.ToArray(); + } + else if (o is IEnumerable e) + { + value = e.OfType().ToArray(); + } + return value != null; } /// @@ -408,7 +437,7 @@ internal static bool AnyValue(object actualValue, object expectedValue, bool cas { foundValue = actualValue; var expectedBase = GetBaseObject(expectedValue); - if (actualValue is IEnumerable items && !(actualValue is string)) + if (actualValue is IEnumerable items && actualValue is not string) { foreach (var item in items) { diff --git a/src/PSRule/Common/HashtableExtensions.cs b/src/PSRule/Common/HashtableExtensions.cs index 80e84e4f97..89a09f0a07 100644 --- a/src/PSRule/Common/HashtableExtensions.cs +++ b/src/PSRule/Common/HashtableExtensions.cs @@ -3,13 +3,14 @@ using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; namespace PSRule { internal static class HashtableExtensions { - internal static IDictionary ToDictionary(this Hashtable hashtable) + public static IDictionary ToDictionary(this Hashtable hashtable) { return hashtable .Cast() @@ -18,5 +19,23 @@ internal static IDictionary ToDictionary(this Hashtable hashtabl kvp => kvp.Value ); } + + [DebuggerStepThrough] + public static bool TryGetStringArray(this Hashtable hashtable, string key, out string[] value) + { + value = null; + return hashtable.TryGetValue(key, out var o) && ExpressionHelpers.TryConvertStringArray(o, out value); + } + + [DebuggerStepThrough] + public static bool TryGetValue(this Hashtable hashtable, object key, out object value) + { + value = null; + if (!hashtable.ContainsKey(key)) + return false; + + value = hashtable[key]; + return true; + } } -} \ No newline at end of file +} diff --git a/src/PSRule/Common/JsonConverters.cs b/src/PSRule/Common/JsonConverters.cs index 7f4c30afea..bc04263bd9 100644 --- a/src/PSRule/Common/JsonConverters.cs +++ b/src/PSRule/Common/JsonConverters.cs @@ -619,6 +619,42 @@ private static bool SkipComments(JsonReader reader) } } + /// + /// A JSON converter that handles string to string array. + /// + internal sealed class StringArrayJsonConverter : JsonConverter + { + public override bool CanRead => true; + public override bool CanWrite => false; + + public override bool CanConvert(Type objectType) + { + return typeof(string[]).IsAssignableFrom(objectType); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TryConsume(JsonToken.StartArray)) + { + var result = new List(); + while (reader.TryConsume(JsonToken.String, out var s_object) && s_object is string s) + result.Add(s); + + return result.ToArray(); + } + else if (reader.TokenType == JsonToken.String && reader.Value is string s) + { + return new string[] { s }; + } + return null; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + } + /// /// A custom converter for deserializing JSON into a language expression. /// diff --git a/src/PSRule/Common/JsonReaderExtensions.cs b/src/PSRule/Common/JsonReaderExtensions.cs index da140b3c9c..4b49263e05 100644 --- a/src/PSRule/Common/JsonReaderExtensions.cs +++ b/src/PSRule/Common/JsonReaderExtensions.cs @@ -27,7 +27,7 @@ public static bool TryLineInfo(this JsonReader reader, out int lineNumber, out i public static bool GetSourceExtent(this JsonReader reader, string file, out ISourceExtent extent) { extent = null; - if (string.IsNullOrEmpty(file) || !TryLineInfo(reader, out int lineNumber, out int linePosition)) + if (string.IsNullOrEmpty(file) || !TryLineInfo(reader, out var lineNumber, out var linePosition)) return false; extent = new SourceExtent(file, lineNumber, linePosition); @@ -44,6 +44,18 @@ public static bool TryConsume(this JsonReader reader, JsonToken token) return true; } + [DebuggerStepThrough] + public static bool TryConsume(this JsonReader reader, JsonToken token, out object value) + { + value = null; + if (reader.TokenType != token) + return false; + + value = reader.Value; + reader.Read(); + return true; + } + [DebuggerStepThrough] public static void Consume(this JsonReader reader, JsonToken token) { diff --git a/src/PSRule/Common/YamlConverters.cs b/src/PSRule/Common/YamlConverters.cs index c41a6d641c..1a8879b4c2 100644 --- a/src/PSRule/Common/YamlConverters.cs +++ b/src/PSRule/Common/YamlConverters.cs @@ -94,7 +94,7 @@ public object ReadYaml(IParser parser, Type type) var fields = new List(); while (!parser.Accept(out _)) { - if (parser.TryConsume(out scalar)) + if (parser.TryConsume(out scalar)) fields.Add(scalar.Value); } result.Set(fieldName, fields.ToArray()); @@ -133,6 +133,40 @@ public void WriteYaml(IEmitter emitter, object value, Type type) } } + /// + /// A YAML converter that handles string to string array. + /// + internal sealed class StringArrayYamlTypeConverter : IYamlTypeConverter + { + public bool Accepts(Type type) + { + return type == typeof(string[]); + } + + public object ReadYaml(IParser parser, Type type) + { + if (parser.TryConsume(out _)) + { + var result = new List(); + while (parser.TryConsume(out var scalar)) + result.Add(scalar.Value); + + parser.Consume(); + return result.ToArray(); + } + else if (parser.TryConsume(out var scalar)) + { + return new string[] { scalar.Value }; + } + return null; + } + + public void WriteYaml(IEmitter emitter, object value, Type type) + { + throw new NotImplementedException(); + } + } + /// /// A YAML converter to deserialize a map/ object as a PSObject. /// @@ -465,6 +499,9 @@ public IObjectDescriptor Read(object target) } } + /// + /// A custom deserializer to convert YAML into a . + /// internal sealed class ResourceNodeDeserializer : INodeDeserializer { private const string FIELD_APIVERSION = "apiVersion"; diff --git a/src/PSRule/Configuration/RuleOption.cs b/src/PSRule/Configuration/RuleOption.cs index cbd7b86f48..d1e809919e 100644 --- a/src/PSRule/Configuration/RuleOption.cs +++ b/src/PSRule/Configuration/RuleOption.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.ComponentModel; +using PSRule.Definitions; namespace PSRule.Configuration { @@ -29,6 +30,7 @@ public RuleOption() IncludeLocal = null; Include = null; Tag = null; + Taxa = null; } /// @@ -45,6 +47,7 @@ public RuleOption(RuleOption option) IncludeLocal = option.IncludeLocal; Include = option.Include; Tag = option.Tag; + Taxa = option.Taxa; } /// @@ -61,7 +64,8 @@ public bool Equals(RuleOption other) Exclude == other.Exclude && IncludeLocal == other.IncludeLocal && Include == other.Include && - Tag == other.Tag; + Tag == other.Tag && + Taxa == other.Taxa; } /// @@ -75,6 +79,7 @@ public override int GetHashCode() hash = hash * 23 + (IncludeLocal.HasValue ? IncludeLocal.Value.GetHashCode() : 0); hash = hash * 23 + (Include != null ? Include.GetHashCode() : 0); hash = hash * 23 + (Tag != null ? Tag.GetHashCode() : 0); + hash = hash * 23 + (Taxa != null ? Taxa.GetHashCode() : 0); return hash; } } @@ -91,7 +96,8 @@ internal static RuleOption Combine(RuleOption o1, RuleOption o2) Exclude = o1.Exclude ?? o2.Exclude, IncludeLocal = o1.IncludeLocal ?? o2.IncludeLocal, Include = o1.Include ?? o2.Include, - Tag = o1.Tag ?? o2.Tag + Tag = o1.Tag ?? o2.Tag, + Taxa = o1.Taxa ?? o2.Taxa, }; return result; } @@ -125,5 +131,11 @@ internal static RuleOption Combine(RuleOption o1, RuleOption o2) /// [DefaultValue(null)] public Hashtable Tag { get; set; } + + /// + /// A set of taxonomy references. + /// + [DefaultValue(null)] + public ResourceTaxa Taxa { get; set; } } } diff --git a/src/PSRule/Definitions/Conventions/ScriptBlockConvention.cs b/src/PSRule/Definitions/Conventions/ScriptBlockConvention.cs index 842e7d3de1..27a35c2ae3 100644 --- a/src/PSRule/Definitions/Conventions/ScriptBlockConvention.cs +++ b/src/PSRule/Definitions/Conventions/ScriptBlockConvention.cs @@ -59,6 +59,9 @@ internal ScriptBlockConvention( // Not supported with conventions. ResourceTags IResource.Tags => null; + // Not supported with conventions. + ResourceTaxa IResource.Taxa => null; + public override void Initialize(RunspaceContext context, IEnumerable input) { InvokeConventionBlock(_Initialize, input); diff --git a/src/PSRule/Definitions/Resource.cs b/src/PSRule/Definitions/Resource.cs index 113045b5ce..8550370c3f 100644 --- a/src/PSRule/Definitions/Resource.cs +++ b/src/PSRule/Definitions/Resource.cs @@ -111,6 +111,11 @@ public interface IResource : ILanguageBlock /// ResourceTags Tags { get; } + /// + /// Any taxonomy references. + /// + ResourceTaxa Taxa { get; } + /// /// Flags for the resource. /// @@ -190,6 +195,7 @@ internal ResourceBuilder() .IgnoreUnmatchedProperties() .WithNamingConvention(CamelCaseNamingConvention.Instance) .WithTypeConverter(new FieldMapYamlTypeConverter()) + .WithTypeConverter(new StringArrayYamlTypeConverter()) .WithNodeDeserializer( inner => new ResourceNodeDeserializer(new LanguageExpressionDeserializer(inner)), s => s.InsteadOf()) @@ -225,6 +231,51 @@ public sealed class ResourceAnnotations : Dictionary } + /// + /// Additional resource taxonomy references. + /// + public sealed class ResourceTaxa : Dictionary + { + /// + /// Create an empty set of resource taxa. + /// + public ResourceTaxa() : base(StringComparer.OrdinalIgnoreCase) { } + + /// + /// Convert from a hashtable to resource taxa. + /// + internal static ResourceTaxa FromHashtable(Hashtable hashtable) + { + if (hashtable == null || hashtable.Count == 0) + return null; + + var annotations = new ResourceTaxa(); + foreach (DictionaryEntry kv in hashtable) + { + var key = kv.Key.ToString(); + if (hashtable.TryGetStringArray(key, out var value)) + annotations[key] = value; + } + return annotations; + } + + internal bool Contains(string key, string[] value) + { + if (!TryGetValue(key, out var actual)) + return false; + + if (value == null || value.Length == 0 || (value.Length == 1 && value[0] == "*")) + return true; + + for (var i = 0; i < value.Length; i++) + { + if (Array.IndexOf(actual, value[i]) != -1) + return true; + } + return false; + } + } + /// /// Additional resource tags. /// @@ -354,6 +405,7 @@ public ResourceMetadata() { Annotations = new ResourceAnnotations(); Tags = new ResourceTags(); + Taxa = new ResourceTaxa(); } /// @@ -382,6 +434,12 @@ public ResourceMetadata() /// [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitEmptyCollections)] public ResourceTags Tags { get; set; } + + /// + /// Any taxonomy references. + /// + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitEmptyCollections)] + public ResourceTaxa Taxa { get; set; } } /// @@ -513,6 +571,8 @@ private protected InternalResource(ResourceKind kind, string apiVersion, SourceF ResourceTags IResource.Tags => Metadata.Tags; + ResourceTaxa IResource.Taxa => Metadata.Taxa; + ResourceFlags IResource.Flags => Flags; TAnnotation IAnnotated.GetAnnotation() diff --git a/src/PSRule/Definitions/Rules/RuleFilter.cs b/src/PSRule/Definitions/Rules/RuleFilter.cs index 2926a6b5ed..82e2ed6668 100644 --- a/src/PSRule/Definitions/Rules/RuleFilter.cs +++ b/src/PSRule/Definitions/Rules/RuleFilter.cs @@ -18,6 +18,7 @@ internal sealed class RuleFilter : IResourceFilter private readonly string[] _Include; private readonly string[] _Excluded; private readonly Hashtable _Tag; + private readonly ResourceTaxa _Taxa; private readonly bool _IncludeLocal; private readonly WildcardPattern _WildcardMatch; @@ -28,11 +29,13 @@ internal sealed class RuleFilter : IResourceFilter /// Only accept rules that have these tags. /// Rule that are always excluded by name. /// Determine if local rules are automatically included. - public RuleFilter(string[] include, Hashtable tag, string[] exclude, bool? includeLocal) + /// Only accept rules that have these taxa. + public RuleFilter(string[] include, Hashtable tag, string[] exclude, bool? includeLocal, ResourceTaxa taxa) { _Include = include == null || include.Length == 0 ? null : include; _Excluded = exclude == null || exclude.Length == 0 ? null : exclude; _Tag = tag ?? null; + _Taxa = taxa ?? null; _IncludeLocal = includeLocal ?? RuleOption.Default.IncludeLocal.Value; _WildcardMatch = null; @@ -47,10 +50,10 @@ public RuleFilter(string[] include, Hashtable tag, string[] exclude, bool? inclu ResourceKind IResourceFilter.Kind => ResourceKind.Rule; - internal bool Match(string name, ResourceTags tag) + internal bool Match(string name, ResourceTags tag, ResourceTaxa taxa) { return !IsExcluded(new ResourceId[] { ResourceId.Parse(name) }) && - IsIncluded(new ResourceId[] { ResourceId.Parse(name) }, tag); + IsIncluded(new ResourceId[] { ResourceId.Parse(name) }, tag, taxa); } /// @@ -60,7 +63,7 @@ internal bool Match(string name, ResourceTags tag) public bool Match(IResource resource) { var ids = resource.GetIds(); - return !IsExcluded(ids) && (_IncludeLocal && resource.IsLocalScope() || IsIncluded(ids, resource.Tags)); + return !IsExcluded(ids) && (_IncludeLocal && resource.IsLocalScope() || IsIncluded(ids, resource.Tags, resource.Taxa)); } private bool IsExcluded(IEnumerable ids) @@ -76,12 +79,12 @@ private bool IsExcluded(IEnumerable ids) return false; } - private bool IsIncluded(IEnumerable ids, ResourceTags tag) + private bool IsIncluded(IEnumerable ids, ResourceTags tag, ResourceTaxa taxa) { foreach (var id in ids) { if (_Include == null || Contains(id, _Include) || MatchWildcard(id.Name)) - return TagEquals(tag); + return TagEquals(tag) && TaxaEquals(taxa); } return false; } @@ -102,6 +105,22 @@ private bool TagEquals(ResourceTags tag) return true; } + private bool TaxaEquals(ResourceTaxa taxa) + { + if (_Taxa == null) + return true; + + if (taxa == null || _Taxa.Count > taxa.Count) + return false; + + foreach (var taxon in _Taxa) + { + if (!taxa.Contains(taxon.Key, taxon.Value)) + return false; + } + return true; + } + private static bool Contains(ResourceId id, string[] set) { for (var i = 0; set != null && i < set.Length; i++) diff --git a/src/PSRule/Host/HostHelper.cs b/src/PSRule/Host/HostHelper.cs index aad0ae8ed0..6dd5c34734 100644 --- a/src/PSRule/Host/HostHelper.cs +++ b/src/PSRule/Host/HostHelper.cs @@ -253,6 +253,7 @@ private static ILanguageBlock[] GetYamlLanguageBlocks(Source[] sources, Runspace .IgnoreUnmatchedProperties() .WithNamingConvention(CamelCaseNamingConvention.Instance) .WithTypeConverter(new FieldMapYamlTypeConverter()) + .WithTypeConverter(new StringArrayYamlTypeConverter()) .WithTypeConverter(new PSObjectYamlTypeConverter()) .WithNodeTypeResolver(new PSOptionYamlTypeResolver()) .WithNodeDeserializer( @@ -311,6 +312,7 @@ private static ILanguageBlock[] GetJsonLanguageBlocks(Source[] sources, Runspace var deserializer = new JsonSerializer(); deserializer.Converters.Add(new ResourceObjectJsonConverter()); deserializer.Converters.Add(new FieldMapJsonConverter()); + deserializer.Converters.Add(new StringArrayJsonConverter()); deserializer.Converters.Add(new LanguageExpressionJsonConverter()); try @@ -445,6 +447,7 @@ private static DependencyTargetCollection ToRuleV1(ILanguageBlock[] blo DependsOn = block.DependsOn, Flags = block.Flags, Extent = block.Extent, + Taxa = block.Taxa, }); knownRuleNames.AddNames(block.Id, block.Ref, block.Alias); knownRuleIds.AddIds(block.Id, block.Ref, block.Alias); @@ -475,6 +478,7 @@ private static DependencyTargetCollection ToRuleV1(ILanguageBlock[] blo DependsOn = null, // TODO: No support for DependsOn yet Flags = block.Flags, Extent = block.Extent, + Taxa = block.Metadata.Taxa, }); knownRuleNames.AddNames(block.Id, block.Ref, block.Alias); knownRuleIds.AddIds(block.Id, block.Ref, block.Alias); @@ -553,7 +557,8 @@ private static DependencyTargetCollection ToRuleBlockV1(ILanguageBloc dependsOn: null, // TODO: No support for DependsOn yet configuration: null, // TODO: No support for rule configuration use module or workspace config extent: null, - flags: block.Flags + flags: block.Flags, + taxa: block.Metadata.Taxa )); knownRuleNames.AddNames(block.Id, block.Ref, block.Alias); knownRuleIds.AddIds(block.Id, block.Ref, block.Alias); diff --git a/src/PSRule/PSRule.psm1 b/src/PSRule/PSRule.psm1 index 32718fddbd..90fc179bf7 100644 --- a/src/PSRule/PSRule.psm1 +++ b/src/PSRule/PSRule.psm1 @@ -1728,6 +1728,10 @@ function Rule { [Parameter(Mandatory = $False)] [Hashtable]$Tag, + # Any taxonomy references. + [Parameter(Mandatory = $False)] + [hashtable]$Taxa, + [Parameter(Mandatory = $False)] [ScriptBlock]$If, diff --git a/src/PSRule/Pipeline/OptionContext.cs b/src/PSRule/Pipeline/OptionContext.cs index 38cdf726cc..d769670bb4 100644 --- a/src/PSRule/Pipeline/OptionContext.cs +++ b/src/PSRule/Pipeline/OptionContext.cs @@ -91,6 +91,7 @@ internal sealed class BaselineScope : OptionScope public string[] Include; public string[] Exclude; public Hashtable Tag; + public ResourceTaxa Taxa; // Configuration public Dictionary Configuration; @@ -122,6 +123,7 @@ public BaselineScope(ScopeType type, string baselineId, string moduleName, IBase Include = option.Rule?.Include; Exclude = option.Rule?.Exclude; Tag = option.Rule?.Tag; + Taxa = option.Rule?.Taxa; Configuration = option.Configuration != null ? new Dictionary(option.Configuration, StringComparer.OrdinalIgnoreCase) : new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -280,8 +282,9 @@ private IResourceFilter GetRuleFilter() var include = _Parameter?.Include ?? _Explicit?.Include ?? _WorkspaceBaseline?.Include ?? _ModuleBaseline?.Include; var exclude = _Explicit?.Exclude ?? _WorkspaceBaseline?.Exclude ?? _ModuleBaseline?.Exclude; var tag = _Parameter?.Tag ?? _Explicit?.Tag ?? _WorkspaceBaseline?.Tag ?? _ModuleBaseline?.Tag; + var taxa = _Parameter?.Taxa ?? _Explicit?.Taxa ?? _WorkspaceBaseline?.Taxa ?? _ModuleBaseline?.Taxa; var includeLocal = _Explicit?.IncludeLocal ?? _WorkspaceBaseline?.IncludeLocal ?? _ModuleBaseline?.IncludeLocal; - return _Filter = new RuleFilter(include, tag, exclude, includeLocal); + return _Filter = new RuleFilter(include, tag, exclude, includeLocal, taxa); } private IResourceFilter GetConventionFilter() diff --git a/src/PSRule/Rules/Rule.cs b/src/PSRule/Rules/Rule.cs index 258857926e..7c1bba5ae7 100644 --- a/src/PSRule/Rules/Rule.cs +++ b/src/PSRule/Rules/Rule.cs @@ -69,15 +69,13 @@ public sealed class Rule : IDependencyTarget, ITargetInfo, IResource, IRuleV1 /// /// A human readable block of text, used to identify the purpose of the rule. /// - [JsonIgnore] - [YamlIgnore] + [JsonIgnore, YamlIgnore] public string Synopsis => Info.Synopsis; /// - /// Legacy. Alias to synopsis + /// Legacy. Alias to . /// - [JsonIgnore] - [YamlIgnore] + [JsonIgnore, YamlIgnore] [Obsolete("Use Synopsis instead.")] public string Description => Info.Synopsis; @@ -88,10 +86,12 @@ public sealed class Rule : IDependencyTarget, ITargetInfo, IResource, IRuleV1 [DefaultValue(null)] public ResourceTags Tag { get; set; } + /// [JsonProperty(PropertyName = "info")] [DefaultValue(null)] public RuleHelpInfo Info { get; set; } + /// [JsonProperty(PropertyName = "source")] [DefaultValue(null)] public SourceFile Source { get; set; } @@ -102,19 +102,23 @@ public sealed class Rule : IDependencyTarget, ITargetInfo, IResource, IRuleV1 [JsonProperty(PropertyName = "dependsOn")] public ResourceId[] DependsOn { get; set; } - [JsonIgnore] - [YamlIgnore] + /// + [JsonIgnore, YamlIgnore] public ResourceFlags Flags { get; set; } - [JsonIgnore] - [YamlIgnore] + /// + [JsonIgnore, YamlIgnore] public ISourceExtent Extent { get; set; } + /// + [JsonIgnore, YamlIgnore] + public ResourceTaxa Taxa { get; set; } + string ITargetInfo.TargetName => Name; string ITargetInfo.TargetType => typeof(Rule).FullName; - TargetSourceInfo ITargetInfo.Source => new TargetSourceInfo { File = Source.Path }; + TargetSourceInfo ITargetInfo.Source => new() { File = Source.Path }; bool IDependencyTarget.Dependency => Source.IsDependency(); @@ -134,12 +138,12 @@ public sealed class Rule : IDependencyTarget, ITargetInfo, IResource, IRuleV1 [Obsolete("Use Source property instead.")] string ILanguageBlock.Module => Source.Module; - [JsonIgnore] - [YamlIgnore] + /// + [JsonIgnore, YamlIgnore] public ResourceId? Ref { get; set; } - [JsonIgnore] - [YamlIgnore] + /// + [JsonIgnore, YamlIgnore] public ResourceId[] Alias { get; set; } } } diff --git a/src/PSRule/Rules/RuleBlock.cs b/src/PSRule/Rules/RuleBlock.cs index ffe5f407e6..aa8b12951e 100644 --- a/src/PSRule/Rules/RuleBlock.cs +++ b/src/PSRule/Rules/RuleBlock.cs @@ -23,7 +23,7 @@ namespace PSRule.Rules [DebuggerDisplay("{Id} @{Source.Path}")] internal sealed class RuleBlock : ILanguageBlock, IDependencyTarget, IDisposable, IResource, IRuleV1 { - internal RuleBlock(SourceFile source, ResourceId id, ResourceId? @ref, SeverityLevel level, RuleHelpInfo info, ICondition condition, ResourceTags tag, ResourceId[] alias, ResourceId[] dependsOn, Hashtable configuration, ISourceExtent extent, ResourceFlags flags) + internal RuleBlock(SourceFile source, ResourceId id, ResourceId? @ref, SeverityLevel level, RuleHelpInfo info, ICondition condition, ResourceTags tag, ResourceId[] alias, ResourceId[] dependsOn, Hashtable configuration, ISourceExtent extent, ResourceFlags flags, ResourceTaxa taxa) { Source = source; Name = id.Name; @@ -41,6 +41,7 @@ internal RuleBlock(SourceFile source, ResourceId id, ResourceId? @ref, SeverityL Configuration = configuration; Extent = extent; Flags = flags; + Taxa = taxa; } /// @@ -48,8 +49,10 @@ internal RuleBlock(SourceFile source, ResourceId id, ResourceId? @ref, SeverityL /// public ResourceId Id { get; } + /// public ResourceId? Ref { get; } + /// public ResourceId[] Alias { get; } /// @@ -79,6 +82,9 @@ internal RuleBlock(SourceFile source, ResourceId id, ResourceId? @ref, SeverityL /// public readonly ResourceTags Tag; + /// + public ResourceTaxa Taxa { get; } + /// /// Configuration defaults for the rule definition. /// @@ -89,10 +95,13 @@ internal RuleBlock(SourceFile source, ResourceId id, ResourceId? @ref, SeverityL public readonly RuleHelpInfo Info; + /// public SourceFile Source { get; } + /// public ISourceExtent Extent { get; } + /// [JsonIgnore] [YamlIgnore] public ResourceFlags Flags { get; } diff --git a/tests/PSRule.Tests/Baseline.Rule.jsonc b/tests/PSRule.Tests/Baseline.Rule.jsonc index a291d1ba82..928369dcbe 100644 --- a/tests/PSRule.Tests/Baseline.Rule.jsonc +++ b/tests/PSRule.Tests/Baseline.Rule.jsonc @@ -115,5 +115,23 @@ } }, "spec": {} + }, + { + // Synopsis: An example of a baseline with taxonomy defined + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Baseline", + "metadata": { + "name": "TestBaseline6" + }, + "spec": { + "rule": { + "taxa": { + "framework.v1/control": [ + "c-1", + "c-2" + ] + } + } + } } ] diff --git a/tests/PSRule.Tests/Baseline.Rule.yaml b/tests/PSRule.Tests/Baseline.Rule.yaml index d70503fd28..bfa543a634 100644 --- a/tests/PSRule.Tests/Baseline.Rule.yaml +++ b/tests/PSRule.Tests/Baseline.Rule.yaml @@ -79,3 +79,14 @@ metadata: annotations: obsolete: true spec: { } + +--- +# Synopsis: An example of a baseline with taxa defined +apiVersion: github.com/microsoft/PSRule/v1 +kind: Baseline +metadata: + name: TestBaseline6 +spec: + rule: + taxa: + framework.v1/control: [ 'c-1', 'c-2' ] diff --git a/tests/PSRule.Tests/BaselineTests.cs b/tests/PSRule.Tests/BaselineTests.cs index 25bd24d8f7..3f1adf62a2 100644 --- a/tests/PSRule.Tests/BaselineTests.cs +++ b/tests/PSRule.Tests/BaselineTests.cs @@ -29,7 +29,7 @@ public void ReadBaselineYaml() { var baseline = GetBaselines(GetSource(BaselineYamlFileName)); Assert.NotNull(baseline); - Assert.Equal(5, baseline.Length); + Assert.Equal(6, baseline.Length); // TestBaseline1 Assert.Equal("TestBaseline1", baseline[0].Name); @@ -56,6 +56,14 @@ public void ReadBaselineYaml() Assert.Equal("github.com/microsoft/PSRule/v1", baseline[4].ApiVersion); Assert.True(baseline[4].Obsolete); Assert.Equal("This is an example obsolete baseline", baseline[4].Info.Synopsis.Text); + + // TestBaseline6 + Assert.Equal("TestBaseline6", baseline[5].Name); + var taxa = baseline[5].Spec.Rule.Taxa; + Assert.True(taxa.Contains("framework.v1/control", new string[] { "*" })); + Assert.True(taxa.Contains("framework.v1/control", new string[] { "c-1" })); + Assert.False(taxa.Contains("framework.v1/control", new string[] { "c-3" })); + Assert.False(taxa.Contains("framework.v3/control", new string[] { "*" })); } [Fact] @@ -63,7 +71,7 @@ public void ReadBaselineJson() { var baseline = GetBaselines(GetSource(BaselineJsonFileName)); Assert.NotNull(baseline); - Assert.Equal(5, baseline.Length); + Assert.Equal(6, baseline.Length); // TestBaseline1 Assert.Equal("TestBaseline1", baseline[0].Name); @@ -89,6 +97,14 @@ public void ReadBaselineJson() Assert.Equal("github.com/microsoft/PSRule/v1", baseline[4].ApiVersion); Assert.True(baseline[4].Obsolete); Assert.Equal("This is an example obsolete baseline", baseline[4].Info.Synopsis.Text); + + // TestBaseline6 + Assert.Equal("TestBaseline6", baseline[5].Name); + var taxa = baseline[5].Spec.Rule.Taxa; + Assert.True(taxa.Contains("framework.v1/control", new string[] { "*" })); + Assert.True(taxa.Contains("framework.v1/control", new string[] { "c-1" })); + Assert.False(taxa.Contains("framework.v1/control", new string[] { "c-3" })); + Assert.False(taxa.Contains("framework.v3/control", new string[] { "*" })); } [Theory] @@ -98,7 +114,7 @@ public void ReadBaselineInModule(SourceType type, string path) { var baseline = GetBaselines(GetSourceInModule(path, "TestModule", type)); Assert.NotNull(baseline); - Assert.Equal(5, baseline.Length); + Assert.Equal(6, baseline.Length); // TestBaseline1 Assert.Equal("TestBaseline1", baseline[0].Name); diff --git a/tests/PSRule.Tests/FromFile.Rule.jsonc b/tests/PSRule.Tests/FromFile.Rule.jsonc index f1a6e50e88..111db099fd 100644 --- a/tests/PSRule.Tests/FromFile.Rule.jsonc +++ b/tests/PSRule.Tests/FromFile.Rule.jsonc @@ -13,6 +13,13 @@ "name": "JsonBasicRule", "tags": { "feature": "tag" + }, + "taxa": { + "single": "Value", + "multi": [ + "Value1", + "Value2" + ] } }, "spec": { diff --git a/tests/PSRule.Tests/FromFile.Rule.yaml b/tests/PSRule.Tests/FromFile.Rule.yaml index c5398e743b..7aaea8860c 100644 --- a/tests/PSRule.Tests/FromFile.Rule.yaml +++ b/tests/PSRule.Tests/FromFile.Rule.yaml @@ -13,6 +13,11 @@ metadata: name: YamlBasicRule tags: feature: tag + taxa: + single: Value + multi: + - Value1 + - Value2 spec: condition: allOf: diff --git a/tests/PSRule.Tests/FromFileBaseline.Rule.ps1 b/tests/PSRule.Tests/FromFileBaseline.Rule.ps1 index 02a1126fd6..0d700f52f9 100644 --- a/tests/PSRule.Tests/FromFileBaseline.Rule.ps1 +++ b/tests/PSRule.Tests/FromFileBaseline.Rule.ps1 @@ -10,9 +10,9 @@ Rule 'WithBaseline' -Tag @{ category = 'group2'; severity = 'high' } { $PSRule.TargetName -eq 'TestObject1' $PSRule.TargetType -eq 'TestObjectType' $PSRule.Field.kind -eq 'TestObjectType' -} +} -Taxa @{ 'framework.v1/control' = @('c-2') } # Synopsis: Test for baseline Rule 'NotInBaseline' -Tag @{ category = 'group2'; severity = 'low' } { $False; -} +} -Taxa @{ 'framework.v1/control' = @('c-3') } diff --git a/tests/PSRule.Tests/PSRule.Baseline.Tests.ps1 b/tests/PSRule.Tests/PSRule.Baseline.Tests.ps1 index c00ad38b52..b67ad6b8f6 100644 --- a/tests/PSRule.Tests/PSRule.Baseline.Tests.ps1 +++ b/tests/PSRule.Tests/PSRule.Baseline.Tests.ps1 @@ -369,7 +369,7 @@ Describe 'Get-PSRuleBaseline' -Tag 'Baseline','Get-PSRuleBaseline' { It 'With defaults' { $result = @(Get-PSRuleBaseline -Path $baselineFilePath); $result | Should -Not -BeNullOrEmpty; - $result.Length | Should -Be 5; + $result.Length | Should -Be 6; $result[0].Name | Should -Be 'TestBaseline1'; $result[0].Module | Should -BeNullOrEmpty; $result[3].Name | Should -Be 'TestBaseline4'; @@ -858,6 +858,11 @@ Describe 'Baseline' -Tag 'Baseline' { $result.Length | Should -Be 2; $result[0].RuleName | Should -Be 'WithBaseline'; $result[1].RuleName | Should -Be 'NotInBaseline'; + + $result = @(Get-PSRule -Path $ruleFilePath,$baselineFilePath -Baseline 'TestBaseline6'); + $result | Should -Not -BeNullOrEmpty; + $result.Length | Should -Be 1; + $result[0].RuleName | Should -Be 'WithBaseline'; } } } diff --git a/tests/PSRule.Tests/RuleFilterTests.cs b/tests/PSRule.Tests/RuleFilterTests.cs index e1f555137c..4d0f1bd6bf 100644 --- a/tests/PSRule.Tests/RuleFilterTests.cs +++ b/tests/PSRule.Tests/RuleFilterTests.cs @@ -8,45 +8,75 @@ namespace PSRule { + /// + /// Define tests to validate . + /// public sealed class RuleFilterTests { [Fact] public void MatchInclude() { - var filter = new RuleFilter(new string[] { "rule1", "rule2" }, null, null, null); - Assert.True(filter.Match("rule1", null)); - Assert.True(filter.Match("Rule2", null)); - Assert.False(filter.Match("rule3", null)); + var filter = new RuleFilter(new string[] { "rule1", "rule2" }, null, null, null, null); + Assert.True(filter.Match("rule1", null, null)); + Assert.True(filter.Match("Rule2", null, null)); + Assert.False(filter.Match("rule3", null, null)); } [Fact] public void MatchExclude() { - var filter = new RuleFilter(null, null, new string[] { "rule3" }, null); - Assert.True(filter.Match("rule1", null)); - Assert.True(filter.Match("rule2", null)); - Assert.False(filter.Match("Rule3", null)); + var filter = new RuleFilter(null, null, new string[] { "rule3" }, null, null); + Assert.True(filter.Match("rule1", null, null)); + Assert.True(filter.Match("rule2", null, null)); + Assert.False(filter.Match("Rule3", null, null)); } [Fact] public void MatchTag() { + // Set resource tags + var resourceTags = new Hashtable(); + + // Create a filter with category equal to group 1 or group 2. var tag = new Hashtable { ["category"] = new string[] { "group1", "group2" } }; - var filter = new RuleFilter(null, tag, null, null); + var filter = new RuleFilter(null, tag, null, null, null); + + // Check basic match + resourceTags["category"] = "group2"; + Assert.True(filter.Match("rule", ResourceTags.FromHashtable(resourceTags), null)); + resourceTags["category"] = "group1"; + Assert.True(filter.Match("rule", ResourceTags.FromHashtable(resourceTags), null)); + resourceTags["category"] = "group3"; + Assert.False(filter.Match("rule", ResourceTags.FromHashtable(resourceTags), null)); + Assert.False(filter.Match("rule", null, null)); + } - var ruleTags = new Hashtable + [Fact] + public void MatchTaxa() + { + // Set resource tags + var resourceTaxa = new Hashtable(); + + // Create a filter + var taxa = new ResourceTaxa { - ["category"] = "group2" + ["framework.v1/control"] = new string[] { "c-1", "c-2" } }; - Assert.True(filter.Match("rule1", ResourceTags.FromHashtable(ruleTags))); - ruleTags["category"] = "group1"; - Assert.True(filter.Match("rule2", ResourceTags.FromHashtable(ruleTags))); - ruleTags["category"] = "group3"; - Assert.False(filter.Match("rule3", ResourceTags.FromHashtable(ruleTags))); - Assert.False(filter.Match("rule4", null)); + var filter = new RuleFilter(null, null, null, null, taxa); + + resourceTaxa["framework.v1/control"] = new string[] { "c-2", "c-1" }; + Assert.True(filter.Match("rule", null, ResourceTaxa.FromHashtable(resourceTaxa))); + resourceTaxa["framework.v1/control"] = new string[] { "c-3", "c-1" }; + Assert.True(filter.Match("rule", null, ResourceTaxa.FromHashtable(resourceTaxa))); + resourceTaxa["framework.v1/control"] = new string[] { "c-1", "c-3" }; + Assert.True(filter.Match("rule", null, ResourceTaxa.FromHashtable(resourceTaxa))); + resourceTaxa["framework.v1/control"] = new string[] { "c-3", "c-4" }; + Assert.False(filter.Match("rule", null, ResourceTaxa.FromHashtable(resourceTaxa))); + resourceTaxa["framework.v1/control"] = System.Array.Empty(); + Assert.False(filter.Match("rule", null, ResourceTaxa.FromHashtable(resourceTaxa))); } } } From b196ddfec03df19ef0c54b58a71088dce742f8bd Mon Sep 17 00:00:00 2001 From: Bernie White Date: Thu, 22 Sep 2022 03:35:45 +1000 Subject: [PATCH 067/156] Fixes Dockerfile case senstivity #1269 (#1279) --- docs/CHANGELOG-v2.md | 3 +++ src/PSRule/Runtime/Assert.cs | 5 +++-- tests/PSRule.Tests/AssertTests.cs | 4 ++++ tests/PSRule.Tests/PSRule.Tests.csproj | 3 +++ tests/PSRule.Tests/jenkinsfile | 4 ++++ 5 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 tests/PSRule.Tests/jenkinsfile diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index a940e19d45..5bbbc3dbba 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,9 @@ What's changed since pre-release v2.5.0-B0015: [#1272](https://github.com/microsoft/PSRule/issues/1272) - Taxa is metadata that extends on tags to provide a more structured way to group rules. - Rules can be classified by setting the `metadata.taxa` property or `-Taxa` parameter. +- Bug fixes: + - Fixed Dockerfile case sensitivity by @BernieWhite. + [#1269](https://github.com/microsoft/PSRule/issues/1269) ## v2.5.0-B0015 (pre-release) diff --git a/src/PSRule/Runtime/Assert.cs b/src/PSRule/Runtime/Assert.cs index 319ba023a8..1c0cf1a7bc 100644 --- a/src/PSRule/Runtime/Assert.cs +++ b/src/PSRule/Runtime/Assert.cs @@ -1458,6 +1458,7 @@ private static string GetFileType(string value) /// private static string DetectLinePrefix(string extension) { + extension = extension?.ToLower(); switch (extension) { case ".bicep": @@ -1476,7 +1477,7 @@ private static string DetectLinePrefix(string extension) case ".json": case ".jsonc": case ".scala": - case "Jenkinsfile": + case "jenkinsfile": return "// "; case ".ps1": @@ -1492,7 +1493,7 @@ private static string DetectLinePrefix(string extension) case ".gitignore": case ".pl": case ".rb": - case "Dockerfile": + case "dockerfile": return "# "; case ".sql": diff --git a/tests/PSRule.Tests/AssertTests.cs b/tests/PSRule.Tests/AssertTests.cs index 9fca26166f..2b3f06e1dd 100644 --- a/tests/PSRule.Tests/AssertTests.cs +++ b/tests/PSRule.Tests/AssertTests.cs @@ -1284,6 +1284,10 @@ public void FileHeader() // Dockerfile value = GetObject((name: "FullName", value: GetSourcePath("Dockerfile"))); Assert.True(assert.FileHeader(value, "FullName", header).Result); + + // Jenkinsfile + value = GetObject((name: "FullName", value: GetSourcePath("jenkinsfile"))); + Assert.True(assert.FileHeader(value, "FullName", header).Result); } [Fact] diff --git a/tests/PSRule.Tests/PSRule.Tests.csproj b/tests/PSRule.Tests/PSRule.Tests.csproj index a2d1284c2d..d05dcd32af 100644 --- a/tests/PSRule.Tests/PSRule.Tests.csproj +++ b/tests/PSRule.Tests/PSRule.Tests.csproj @@ -82,6 +82,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/tests/PSRule.Tests/jenkinsfile b/tests/PSRule.Tests/jenkinsfile new file mode 100644 index 0000000000..e4334cd745 --- /dev/null +++ b/tests/PSRule.Tests/jenkinsfile @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Example file From 0de17f2823a6ebc2ec925b19c651915c67787e20 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Sep 2022 08:14:33 +1000 Subject: [PATCH 068/156] Bump actions/stale from 5 to 6 (#1280) Bumps [actions/stale](https://github.com/actions/stale) from 5 to 6. - [Release notes](https://github.com/actions/stale/releases) - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/stale/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/stale dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/stale.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 8d1aa9deb2..c22a202591 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -20,7 +20,7 @@ jobs: issues: write steps: - - uses: actions/stale@v5 + - uses: actions/stale@v6 with: stale-issue-message: > This issue has been automatically marked as stale because it has not had From 9cb99f059146a58c9f44ea69f51192e79a031410 Mon Sep 17 00:00:00 2001 From: Johnny Ruiz Date: Thu, 29 Sep 2022 06:48:20 -0600 Subject: [PATCH 069/156] - created DocumentStrings.es.resx (#1285) - created DocumentStrings.es-us.resx --- src/PSRule/Resources/.DS_Store | Bin 0 -> 8196 bytes .../Resources/DocumentStrings.es-us.resx | 144 ++++++++++++++++++ src/PSRule/Resources/DocumentStrings.es.resx | 144 ++++++++++++++++++ 3 files changed, 288 insertions(+) create mode 100644 src/PSRule/Resources/.DS_Store create mode 100644 src/PSRule/Resources/DocumentStrings.es-us.resx create mode 100644 src/PSRule/Resources/DocumentStrings.es.resx diff --git a/src/PSRule/Resources/.DS_Store b/src/PSRule/Resources/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..2f43a2c68e26762393f9011f553eb8f65a4a6aed GIT binary patch literal 8196 zcmeHM-AV#c5T0XR2!Ti#S{CdJB-9gZVnKJI&~<7eg1NCOb-DMwL-YncUsv|coI|YZ zZvI?E(hQvQ?caQ7_M6>3&XI^rWA<=PR3)MU8e?f6U5fE}ZcCPPM~XlLf1*d~)0n2z zMpIJ-Q~^~$6;K6KfxoB#=4`e?#=LLOYOe~Y0{>D0{yunUjK0IjqI^2gNeKYVhTYKTj?v<6G2T z6;K7172w*vMokK+N3(_cJ)|LO`2kI+j`Mxs5PM^6g*eiq$;~L}bw@!L4E)kvqSfbO z*djV!$;zB(7?no|WrUYSPIG}5IcGWNb8uGH;>==uPq39&vZY7$ib&#jdP3LGFNAs> ztb0sFAzlI5P(kyV;5}S}H#NfCAi-3lTdc{np!EpRwM6Rxdm-}3Sy(^AZ?O!QSF)x? zM<0kT&T}LYbr45@=W83~P7{m=KQg99WjL$LQ9Vg!?TXem`HVB#sInMSqmoR^>Es=zKOuxEp>UV%x)|?BhF(EaC`E7Xm~E K?NotZRp1MuD;_rh literal 0 HcmV?d00001 diff --git a/src/PSRule/Resources/DocumentStrings.es-us.resx b/src/PSRule/Resources/DocumentStrings.es-us.resx new file mode 100644 index 0000000000..f98ecd6356 --- /dev/null +++ b/src/PSRule/Resources/DocumentStrings.es-us.resx @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Descripción + + + Nombre para mostrar + + + Enlaces + + + Nombre del módulo + + + Nombre + + + Notas + + + Recomendación + + + Sinopsis + + \ No newline at end of file diff --git a/src/PSRule/Resources/DocumentStrings.es.resx b/src/PSRule/Resources/DocumentStrings.es.resx new file mode 100644 index 0000000000..f98ecd6356 --- /dev/null +++ b/src/PSRule/Resources/DocumentStrings.es.resx @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Descripción + + + Nombre para mostrar + + + Enlaces + + + Nombre del módulo + + + Nombre + + + Notas + + + Recomendación + + + Sinopsis + + \ No newline at end of file From 0ea097888a4dfcd6c06e2125cb0efc9a9593b9d9 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Fri, 30 Sep 2022 00:59:37 +1000 Subject: [PATCH 070/156] Fixes markdown parsing for Spanish #1286 (#1287) --- .gitignore | 1 + docs/CHANGELOG-v2.md | 3 + src/PSRule/Help/HelpLexer.cs | 22 +++-- src/PSRule/Help/ResourceHelpLexer.cs | 2 +- src/PSRule/Help/RuleHelpLexer.cs | 11 +-- src/PSRule/Host/HostHelper.cs | 17 ++-- src/PSRule/PSRule.csproj | 6 ++ src/PSRule/Resources/.DS_Store | Bin 8196 -> 0 bytes src/PSRule/Runtime/LocalizedData.cs | 4 +- src/PSRule/Runtime/RunspaceContext.cs | 14 ++- tests/PSRule.Tests/PSRule.Tests.csproj | 3 + tests/PSRule.Tests/RuleDocument-es.md | 123 ++++++++++++++++++++++++ tests/PSRule.Tests/RuleDocumentTests.cs | 84 +++++++++++++--- 13 files changed, 249 insertions(+), 41 deletions(-) delete mode 100644 src/PSRule/Resources/.DS_Store create mode 100644 tests/PSRule.Tests/RuleDocument-es.md diff --git a/.gitignore b/.gitignore index f70199d530..7c734f1420 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ BenchmarkDotNet.Artifacts/ .sonarqube/ TestResults/ *.diagsession +**/.DS_Store diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 5bbbc3dbba..e913bbcdae 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -33,6 +33,9 @@ What's changed since pre-release v2.5.0-B0015: - Bug fixes: - Fixed Dockerfile case sensitivity by @BernieWhite. [#1269](https://github.com/microsoft/PSRule/issues/1269) + - Fixed markdown parsing of Spanish translated help fails by @BernieWhite @jonathanruiz. + [#1286](https://github.com/microsoft/PSRule/issues/1286) + [#1285](https://github.com/microsoft/PSRule/pull/1285) ## v2.5.0-B0015 (pre-release) diff --git a/src/PSRule/Help/HelpLexer.cs b/src/PSRule/Help/HelpLexer.cs index 0e03c1a709..98616cb609 100644 --- a/src/PSRule/Help/HelpLexer.cs +++ b/src/PSRule/Help/HelpLexer.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; +using System.Globalization; +using System.Resources; using System.Text; using System.Threading; using PSRule.Definitions; @@ -17,12 +19,20 @@ internal abstract class HelpLexer : MarkdownLexer private const string Space = " "; + protected readonly ResourceSet _Strings; + + protected HelpLexer(string culture) + { + var cultureInfo = string.IsNullOrEmpty(culture) ? Thread.CurrentThread.CurrentCulture : CultureInfo.GetCultureInfo(culture); + _Strings = DocumentStrings.ResourceManager.GetResourceSet(cultureInfo, createIfNotExists: true, tryParents: true); + } + /// /// Read synopsis. /// - protected static bool Synopsis(TokenStream stream, IHelpDocument doc) + protected bool Synopsis(TokenStream stream, IHelpDocument doc) { - if (!IsHeading(stream.Current, RULE_ENTRIES_HEADING_LEVEL, DocumentStrings.Synopsis)) + if (!IsHeading(stream.Current, RULE_ENTRIES_HEADING_LEVEL, _Strings.GetString("Synopsis"))) return false; doc.Synopsis = InfoString(stream); @@ -33,9 +43,9 @@ protected static bool Synopsis(TokenStream stream, IHelpDocument doc) /// /// Read description. /// - protected static bool Description(TokenStream stream, IHelpDocument doc) + protected bool Description(TokenStream stream, IHelpDocument doc) { - if (!IsHeading(stream.Current, RULE_ENTRIES_HEADING_LEVEL, DocumentStrings.Description)) + if (!IsHeading(stream.Current, RULE_ENTRIES_HEADING_LEVEL, _Strings.GetString("Description"))) return false; doc.Description = InfoString(stream, includeNonYamlFencedBlocks: true); @@ -46,9 +56,9 @@ protected static bool Description(TokenStream stream, IHelpDocument doc) /// /// Read links. /// - protected static bool Links(TokenStream stream, IHelpDocument doc) + protected bool Links(TokenStream stream, IHelpDocument doc) { - if (!IsHeading(stream.Current, RULE_ENTRIES_HEADING_LEVEL, DocumentStrings.Links)) + if (!IsHeading(stream.Current, RULE_ENTRIES_HEADING_LEVEL, _Strings.GetString("Links"))) return false; var links = new List(); diff --git a/src/PSRule/Help/ResourceHelpLexer.cs b/src/PSRule/Help/ResourceHelpLexer.cs index 1dd38ba052..dfe159fbc5 100644 --- a/src/PSRule/Help/ResourceHelpLexer.cs +++ b/src/PSRule/Help/ResourceHelpLexer.cs @@ -5,7 +5,7 @@ namespace PSRule.Help { internal sealed class ResourceHelpLexer : HelpLexer { - public ResourceHelpLexer() { } + public ResourceHelpLexer(string culture) : base(culture) { } public ResourceHelpDocument Process(TokenStream stream) { diff --git a/src/PSRule/Help/RuleHelpLexer.cs b/src/PSRule/Help/RuleHelpLexer.cs index 3472e9bb74..3cbca5f7e2 100644 --- a/src/PSRule/Help/RuleHelpLexer.cs +++ b/src/PSRule/Help/RuleHelpLexer.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using PSRule.Definitions; -using PSRule.Resources; namespace PSRule.Help { @@ -11,7 +10,7 @@ namespace PSRule.Help /// internal sealed class RuleHelpLexer : HelpLexer { - public RuleHelpLexer() { } + public RuleHelpLexer(string culture) : base(culture) { } public RuleDocument Process(TokenStream stream) { @@ -51,9 +50,9 @@ public RuleDocument Process(TokenStream stream) /// /// Read recommendation. /// - private static bool Recommendation(TokenStream stream, RuleDocument doc) + private bool Recommendation(TokenStream stream, RuleDocument doc) { - if (!IsHeading(stream.Current, RULE_ENTRIES_HEADING_LEVEL, DocumentStrings.Recommendation)) + if (!IsHeading(stream.Current, RULE_ENTRIES_HEADING_LEVEL, _Strings.GetString("Recommendation"))) return false; doc.Recommendation = InfoString(stream); @@ -64,9 +63,9 @@ private static bool Recommendation(TokenStream stream, RuleDocument doc) /// /// Read notes. /// - private static bool Notes(TokenStream stream, RuleDocument doc) + private bool Notes(TokenStream stream, RuleDocument doc) { - if (!IsHeading(stream.Current, RULE_ENTRIES_HEADING_LEVEL, DocumentStrings.Notes)) + if (!IsHeading(stream.Current, RULE_ENTRIES_HEADING_LEVEL, _Strings.GetString("Notes"))) return false; doc.Notes = TextBlock(stream, includeNonYamlFencedBlocks: true); diff --git a/src/PSRule/Host/HostHelper.cs b/src/PSRule/Host/HostHelper.cs index 6dd5c34734..0cb2d53d16 100644 --- a/src/PSRule/Host/HostHelper.cs +++ b/src/PSRule/Host/HostHelper.cs @@ -791,7 +791,7 @@ private static IConvention[] Sort(RunspaceContext context, IConvention[] convent internal static RuleHelpInfo GetRuleHelpInfo(RunspaceContext context, string name, string defaultSynopsis) { - return !TryHelpPath(context, name, out var path) || !TryDocument(path, out var document) + return !TryHelpPath(context, name, out var path, out var culture) || !TryDocument(path, culture, out var document) ? new RuleHelpInfo( name: name, displayName: name, @@ -815,24 +815,25 @@ internal static RuleHelpInfo GetRuleHelpInfo(RunspaceContext context, string nam internal static void UpdateHelpInfo(RunspaceContext context, IResource resource) { - if (context == null || resource == null || !TryHelpPath(context, resource.Name, out var path) || !TryHelpInfo(path, out var info)) + 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(RunspaceContext context, string name, out string path) + private static bool TryHelpPath(RunspaceContext context, string name, out string path, out string culture) { path = null; + culture = null; if (string.IsNullOrEmpty(context.Source.File.HelpPath)) return false; var helpFileName = string.Concat(name, Markdown_Extension); - path = context.GetLocalizedPath(helpFileName); + path = context.GetLocalizedPath(helpFileName, out culture); return path != null; } - private static bool TryDocument(string path, out RuleDocument document) + private static bool TryDocument(string path, string culture, out RuleDocument document) { document = null; var markdown = File.ReadAllText(path); @@ -841,12 +842,12 @@ private static bool TryDocument(string path, out RuleDocument document) var reader = new MarkdownReader(yamlHeaderOnly: false); var stream = reader.Read(markdown, path); - var lexer = new RuleHelpLexer(); + var lexer = new RuleHelpLexer(culture); document = lexer.Process(stream); return document != null; } - private static bool TryHelpInfo(string path, out IResourceHelpInfo info) + private static bool TryHelpInfo(string path, string culture, out IResourceHelpInfo info) { info = null; var markdown = File.ReadAllText(path); @@ -855,7 +856,7 @@ private static bool TryHelpInfo(string path, out IResourceHelpInfo info) var reader = new MarkdownReader(yamlHeaderOnly: false); var stream = reader.Read(markdown, path); - var lexer = new ResourceHelpLexer(); + var lexer = new ResourceHelpLexer(culture); info = lexer.Process(stream).ToInfo(); return info != null; } diff --git a/src/PSRule/PSRule.csproj b/src/PSRule/PSRule.csproj index 82e567e7fb..e28d594891 100644 --- a/src/PSRule/PSRule.csproj +++ b/src/PSRule/PSRule.csproj @@ -72,6 +72,12 @@ + + ResXFileCodeGenerator + + + ResXFileCodeGenerator + ResXFileCodeGenerator DocumentStrings.Designer.cs diff --git a/src/PSRule/Resources/.DS_Store b/src/PSRule/Resources/.DS_Store deleted file mode 100644 index 2f43a2c68e26762393f9011f553eb8f65a4a6aed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHM-AV#c5T0XR2!Ti#S{CdJB-9gZVnKJI&~<7eg1NCOb-DMwL-YncUsv|coI|YZ zZvI?E(hQvQ?caQ7_M6>3&XI^rWA<=PR3)MU8e?f6U5fE}ZcCPPM~XlLf1*d~)0n2z zMpIJ-Q~^~$6;K6KfxoB#=4`e?#=LLOYOe~Y0{>D0{yunUjK0IjqI^2gNeKYVhTYKTj?v<6G2T z6;K7172w*vMokK+N3(_cJ)|LO`2kI+j`Mxs5PM^6g*eiq$;~L}bw@!L4E)kvqSfbO z*djV!$;zB(7?no|WrUYSPIG}5IcGWNb8uGH;>==uPq39&vZY7$ib&#jdP3LGFNAs> ztb0sFAzlI5P(kyV;5}S}H#NfCAi-3lTdc{np!EpRwM6Rxdm-}3Sy(^AZ?O!QSF)x? zM<0kT&T}LYbr45@=W83~P7{m=KQg99WjL$LQ9Vg!?TXem`HVB#sInMSqmoR^>Es=zKOuxEp>UV%x)|?BhF(EaC`E7Xm~E K?NotZRp1MuD;_rh diff --git a/src/PSRule/Runtime/LocalizedData.cs b/src/PSRule/Runtime/LocalizedData.cs index c9234fa095..d3df755430 100644 --- a/src/PSRule/Runtime/LocalizedData.cs +++ b/src/PSRule/Runtime/LocalizedData.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Collections; @@ -47,7 +47,7 @@ private static Hashtable TryGetLocalized() private static string GetFilePath() { - return RunspaceContext.CurrentThread.GetLocalizedPath(DATA_FILENAME); + return RunspaceContext.CurrentThread.GetLocalizedPath(DATA_FILENAME, out _); } } } diff --git a/src/PSRule/Runtime/RunspaceContext.cs b/src/PSRule/Runtime/RunspaceContext.cs index abbcdb7a56..0b248e14ea 100644 --- a/src/PSRule/Runtime/RunspaceContext.cs +++ b/src/PSRule/Runtime/RunspaceContext.cs @@ -809,14 +809,15 @@ public void End(IEnumerable output) RunConventionEnd(); } - public string GetLocalizedPath(string file) + public string GetLocalizedPath(string file, out string culture) { + culture = null; if (string.IsNullOrEmpty(Source.File.HelpPath)) return null; - var culture = Pipeline.Baseline.GetCulture(); + var cultures = Pipeline.Baseline.GetCulture(); if (!_RaisedUsingInvariantCulture && - (culture == null || culture.Length == 0) && + (cultures == null || cultures.Length == 0) && _InvariantCultureWarning) { Writer.WarnUsingInvariantCulture(); @@ -824,11 +825,14 @@ public string GetLocalizedPath(string file) return null; } - for (var i = 0; culture != null && i < culture.Length; i++) + for (var i = 0; cultures != null && i < cultures.Length; i++) { - var path = Path.Combine(Source.File.HelpPath, culture[i], file); + var path = Path.Combine(Source.File.HelpPath, cultures[i], file); if (File.Exists(path)) + { + culture = cultures[i]; return path; + } } return null; } diff --git a/tests/PSRule.Tests/PSRule.Tests.csproj b/tests/PSRule.Tests/PSRule.Tests.csproj index d05dcd32af..ebd34c56d4 100644 --- a/tests/PSRule.Tests/PSRule.Tests.csproj +++ b/tests/PSRule.Tests/PSRule.Tests.csproj @@ -124,6 +124,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/tests/PSRule.Tests/RuleDocument-es.md b/tests/PSRule.Tests/RuleDocument-es.md new file mode 100644 index 0000000000..3af6165f32 --- /dev/null +++ b/tests/PSRule.Tests/RuleDocument-es.md @@ -0,0 +1,123 @@ +--- +reviewed: 2022-09-22 +severity: Critico +pillar: Seguridad +category: Autenticación +resource: Container Registry +online version: https://azure.github.io/PSRule.Rules.Azure/es/rules/Azure.ACR.AdminUser/ +--- + +# Deshabilitar el usuario adminstrador para ACR + +## Sinopsis + +Usar identidades de Azure AD en lugar de usar el usuario administrador del registro. + +## Descripción + +Azure Container Registry (ACR) incluye una cuenta de usuario administrador incorporada. +La cuenta de usuario administrador es una cuenta de usuario única con acceso administrativo al registro. +Esta cuenta proporciona acceso de usuario único para pruebas y desarrollo tempranos. +La cuenta de usuario administrador no está diseñada para usarse con registros de contenedores de producción. + +En su lugar, utilice el control de acceso basado en roles (RBAC). +RBAC se puede usar para delegar permisos de registro a una identidad de Azure AD (AAD). + +## Recomendación + +Considere deshabilitar la cuenta de usuario administrador y solo use la autenticación basada en identidad para las operaciones de registro. + +## Ejemplos + +### Configurar con plantilla de ARM + +Para implementar Container Registries, pasa la siguiente regla: + +- Establezca `properties.adminUserEnabled` a `false`. + +Por ejemplo: + +```json +{ + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2021-06-01-preview", + "name": "[parameters('registryName')]", + "location": "[parameters('location')]", + "sku": { + "name": "Premium" + }, + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "adminUserEnabled": false, + "policies": { + "quarantinePolicy": { + "status": "enabled" + }, + "trustPolicy": { + "status": "enabled", + "type": "Notary" + }, + "retentionPolicy": { + "status": "enabled", + "days": 30 + } + } + } +} +``` + +### Configurar con Bicep + +Para implementar Container Registries, pasa la siguiente regla: + +- Establezca `properties.adminUserEnabled` a `false`. + +Por ejemplo: + +```bicep +resource acr 'Microsoft.ContainerRegistry/registries@2021-06-01-preview' = { + name: registryName + location: location + sku: { + name: 'Premium' + } + identity: { + type: 'SystemAssigned' + } + properties: { + adminUserEnabled: false + policies: { + quarantinePolicy: { + status: 'enabled' + } + trustPolicy: { + status: 'enabled' + type: 'Notary' + } + retentionPolicy: { + status: 'enabled' + days: 30 + } + } + } +} +``` + +### Configurar con Azure CLI + +```bash +az acr update --admin-enabled false -n '' -g '' +``` + +### Configurar con Azure PowerShell + +```powershell +Update-AzContainerRegistry -ResourceGroupName '' -Name '' -DisableAdminUser +``` + +## Enlaces + +- [Uso de la autenticación basada en identidad](https://docs.microsoft.com/azure/architecture/framework/security/design-identity-authentication#use-identity-based-authentication) +- [Autenticación con un registro de contenedor de Azure](https://docs.microsoft.com/azure/container-registry/container-registry-authentication?tabs=azure-cli) diff --git a/tests/PSRule.Tests/RuleDocumentTests.cs b/tests/PSRule.Tests/RuleDocumentTests.cs index c4308bb327..81a9e4d216 100644 --- a/tests/PSRule.Tests/RuleDocumentTests.cs +++ b/tests/PSRule.Tests/RuleDocumentTests.cs @@ -13,11 +13,14 @@ namespace PSRule { public sealed class RuleDocumentTests { + /// + /// Try to parse the markdown document using English with Windows line endings. + /// [Fact] - public void ReadDocument_Windows() + public void ReadDocument_Windows_en() { - var document = GetDocument(GetToken(nx: false)); - var expected = GetExpected(); + var document = GetDocument(GetToken(nx: false, suffix: ""), culture: null); + var expected = GetExpected_en(); Assert.Equal(expected.Name, document.Name); Assert.Equal(expected.Synopsis.Text, document.Synopsis.Text); @@ -33,11 +36,36 @@ public void ReadDocument_Windows() Assert.Equal(expected.Links[1].Uri, document.Links[1].Uri); } + /// + /// Try to parse the markdown document using Spanish with Windows line endings. + /// [Fact] - public void ReadDocument_Linux() + public void ReadDocument_Windows_es() { - var document = GetDocument(GetToken(nx: true)); - var expected = GetExpected(); + var document = GetDocument(GetToken(nx: false, suffix: "-es"), culture: "es"); + var expected = GetExpected_es(); + + Assert.Equal(expected.Name, document.Name); + Assert.Equal(expected.Synopsis.Text, document.Synopsis.Text); + Assert.Equal(expected.Description.Text, document.Description.Text); + Assert.Equal(expected.Recommendation.Text, document.Recommendation.Text); + Assert.Equal(expected.Annotations["severity"], document.Annotations["severity"]); + Assert.Equal(expected.Annotations["category"], document.Annotations["category"]); + Assert.Equal(expected.Links.Length, document.Links.Length); + Assert.Equal(expected.Links[0].Name, document.Links[0].Name); + Assert.Equal(expected.Links[0].Uri, document.Links[0].Uri); + Assert.Equal(expected.Links[1].Name, document.Links[1].Name); + Assert.Equal(expected.Links[1].Uri, document.Links[1].Uri); + } + + /// + /// Try to parse the markdown document using English with Linux line endings. + /// + [Fact] + public void ReadDocument_Linux_en() + { + var document = GetDocument(GetToken(nx: true, suffix: ""), culture: null); + var expected = GetExpected_en(); Assert.Equal(expected.Name, document.Name); Assert.Equal(expected.Synopsis.Text, document.Synopsis.Text); @@ -53,7 +81,11 @@ public void ReadDocument_Linux() Assert.Equal(expected.Links[1].Uri, document.Links[1].Uri); } - private RuleDocument GetExpected() + #region Helper methods + + #endregion Helper methods + + private static RuleDocument GetExpected_en() { var annotations = new Hashtable { @@ -91,23 +123,49 @@ private RuleDocument GetExpected() return result; } - private RuleDocument GetDocument(TokenStream stream) + private static RuleDocument GetExpected_es() + { + var annotations = new Hashtable + { + ["severity"] = "Critico", + ["category"] = "Autenticación" + }; + + var links = new List + { + new Link { Name = "Uso de la autenticación basada en identidad", Uri = "https://docs.microsoft.com/azure/architecture/framework/security/design-identity-authentication#use-identity-based-authentication" }, + new Link { Name = "Autenticación con un registro de contenedor de Azure", Uri = "https://docs.microsoft.com/azure/container-registry/container-registry-authentication?tabs=azure-cli" } + }; + + var result = new RuleDocument(name: "Deshabilitar el usuario adminstrador para ACR") + { + Synopsis = new InfoString("Usar identidades de Azure AD en lugar de usar el usuario administrador del registro."), + Description = new InfoString(@"Azure Container Registry (ACR) incluye una cuenta de usuario administrador incorporada. La cuenta de usuario administrador es una cuenta de usuario única con acceso administrativo al registro. Esta cuenta proporciona acceso de usuario único para pruebas y desarrollo tempranos. La cuenta de usuario administrador no está diseñada para usarse con registros de contenedores de producción. +En su lugar, utilice el control de acceso basado en roles (RBAC). RBAC se puede usar para delegar permisos de registro a una identidad de Azure AD (AAD)."), + Annotations = ResourceTags.FromHashtable(annotations), + Recommendation = new InfoString(@"Considere deshabilitar la cuenta de usuario administrador y solo use la autenticación basada en identidad para las operaciones de registro."), + Links = links.ToArray() + }; + return result; + } + + private static RuleDocument GetDocument(TokenStream stream, string culture) { - var lexer = new RuleHelpLexer(); + var lexer = new RuleHelpLexer(culture); return lexer.Process(stream); } - private TokenStream GetToken(bool nx) + private static TokenStream GetToken(bool nx, string suffix) { var reader = new MarkdownReader(yamlHeaderOnly: false); - var content = GetMarkdownContent(); + var content = GetMarkdownContent(suffix); content = nx ? content.Replace("\r\n", "\n") : content.Replace("\r\n", "\n").Replace("\n", "\r\n"); return reader.Read(content, Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "RuleDocument.md")); } - private string GetMarkdownContent() + private static string GetMarkdownContent(string suffix) { - var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "RuleDocument.md"); + var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"RuleDocument{suffix}.md"); return File.ReadAllText(path); } } From b39378179903995cada0b7e7d37f7e51e45f4bfc Mon Sep 17 00:00:00 2001 From: Bernie White Date: Fri, 30 Sep 2022 01:37:53 +1000 Subject: [PATCH 071/156] Rename taxa to labels #1272 (#1290) --- docs/CHANGELOG-v2.md | 6 +-- docs/concepts/grouping-rules.md | 40 +++++++++---------- schemas/PSRule-language.schema.json | 12 +++--- .../Commands/NewRuleDefinitionCommand.cs | 6 +-- src/PSRule/Configuration/RuleOption.cs | 12 +++--- .../Conventions/ScriptBlockConvention.cs | 2 +- src/PSRule/Definitions/Resource.cs | 20 +++++----- src/PSRule/Definitions/Rules/RuleFilter.cs | 28 ++++++------- src/PSRule/Host/HostHelper.cs | 6 +-- src/PSRule/PSRule.psm1 | 2 +- src/PSRule/Pipeline/OptionContext.cs | 8 ++-- src/PSRule/Rules/Rule.cs | 2 +- src/PSRule/Rules/RuleBlock.cs | 6 +-- tests/PSRule.Tests/Baseline.Rule.jsonc | 2 +- tests/PSRule.Tests/Baseline.Rule.yaml | 4 +- tests/PSRule.Tests/BaselineTests.cs | 20 +++++----- tests/PSRule.Tests/FromFile.Rule.jsonc | 2 +- tests/PSRule.Tests/FromFile.Rule.yaml | 2 +- tests/PSRule.Tests/FromFileBaseline.Rule.ps1 | 4 +- tests/PSRule.Tests/RuleFilterTests.cs | 28 ++++++------- 20 files changed, 106 insertions(+), 106 deletions(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index e913bbcdae..0b6e6a80c4 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -26,10 +26,10 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers What's changed since pre-release v2.5.0-B0015: - General improvements: - - Added taxa metadata from grouping and filtering rules by @BernieWhite. + - Added labels metadata from grouping and filtering rules by @BernieWhite. [#1272](https://github.com/microsoft/PSRule/issues/1272) - - Taxa is metadata that extends on tags to provide a more structured way to group rules. - - Rules can be classified by setting the `metadata.taxa` property or `-Taxa` parameter. + - Labels are metadata that extends on tags to provide a more structured way to group rules. + - Rules can be classified by setting the `metadata.labels` property or `-Labels` parameter. - Bug fixes: - Fixed Dockerfile case sensitivity by @BernieWhite. [#1269](https://github.com/microsoft/PSRule/issues/1269) diff --git a/docs/concepts/grouping-rules.md b/docs/concepts/grouping-rules.md index 76d6d7e1c9..9a09b4d2ff 100644 --- a/docs/concepts/grouping-rules.md +++ b/docs/concepts/grouping-rules.md @@ -1,13 +1,13 @@ # Grouping rules !!! Abstract - _Taxa_ are additional metadata that can be used to classify rules. + _Labels_ are additional metadata that can be used to classify rules. Together with tags they can be used to group or filter rules. -## Using taxa +## Using labels -When defining a rule you can specify taxa to classify or link rules using a framework or standard. -A single rule can be can linked to multiple taxa. +When defining a rule you can specify labels to classify or link rules using a framework or standard. +A single rule can be can linked to multiple labels. For example: - The Azure Well-Architected Framework (WAF) defines pillars such as _Security_ and _Reliability_. @@ -15,16 +15,16 @@ For example: === "YAML" - To specify taxa in YAML, use the `taxa` property: + To specify labels in YAML, use the `labels` property: ```yaml --- - # Synopsis: A rule with taxa defined. + # Synopsis: A rule with labels defined. apiVersion: github.com/microsoft/PSRule/v1 kind: Rule metadata: - name: WithTaxa - taxa: + name: WithLabels + labels: Azure.WAF/pillar: Security Azure.ASB.v3/control: [ 'ID-1', 'ID-2' ] spec: { } @@ -32,16 +32,16 @@ For example: === "JSON" - To specify taxa in JSON, use the `taxa` property: + To specify labels in JSON, use the `labels` property: ```json { - // Synopsis: A rule with taxa defined. + // Synopsis: A rule with labels defined. "apiVersion": "github.com/microsoft/PSRule/v1", "kind": "Rule", "metadata": { - "name": "WithTaxa", - "taxa": { + "name": "WithLabels", + "labels": { "Azure.WAF/pillar": "Security", "Azure.ASB.v3/control": [ "ID-1", "ID-2" ] } @@ -52,19 +52,19 @@ For example: === "PowerShell" - To specify taxa in PowerShell, use the `-Taxa` parameter: + To specify labels in PowerShell, use the `-Labels` parameter: ```powershell - # Synopsis: A rule with taxa defined. - Rule 'WithTaxa' -Taxa @{ 'Azure.WAF/pillar' = 'Security'; 'Azure.ASB.v3/control' = @('ID-1', 'ID-2') } { + # Synopsis: A rule with labels defined. + Rule 'WithLabels' -Labels @{ 'Azure.WAF/pillar' = 'Security'; 'Azure.ASB.v3/control' = @('ID-1', 'ID-2') } { # Define conditions here } ``` -## Filtering with taxa +## Filtering with labels -A reason for assigning taxa to rules is to perform filtering of rules to a specific subset. -This can be accomplished using baselines and the `spec.rule.taxa` property. +A reason for assigning labels to rules is to perform filtering of rules to a specific subset. +This can be accomplished using baselines and the `spec.rule.labels` property. For example: ```yaml @@ -76,7 +76,7 @@ metadata: name: TestBaseline6 spec: rule: - taxa: + labels: Azure.WAF/pillar: [ 'Security' ] --- @@ -87,6 +87,6 @@ metadata: name: TestBaseline6 spec: rule: - taxa: + labels: Azure.WAF/pillar: '*' ``` diff --git a/schemas/PSRule-language.schema.json b/schemas/PSRule-language.schema.json index a1e536f8db..4bba0a6b6e 100644 --- a/schemas/PSRule-language.schema.json +++ b/schemas/PSRule-language.schema.json @@ -163,11 +163,11 @@ ] } }, - "taxa": { + "labels": { "type": "object", - "title": "Taxa", - "description": "Require rules to have the following associated taxa.", - "markdownDescription": "Require rules to have the following associated taxa.", + "title": "Labels", + "description": "Require rules to have the following associated labels.", + "markdownDescription": "Require rules to have the following associated labels.", "additionalProperties": { "oneOf": [ { @@ -703,9 +703,9 @@ "tags": { "$ref": "#/definitions/resourceTags" }, - "taxa": { + "labels": { "type": "object", - "title": "Taxa", + "title": "Labels", "description": "Any taxonomy references.", "additionalProperties": { "oneOf": [ diff --git a/src/PSRule/Commands/NewRuleDefinitionCommand.cs b/src/PSRule/Commands/NewRuleDefinitionCommand.cs index 0f2af022a7..616878c12c 100644 --- a/src/PSRule/Commands/NewRuleDefinitionCommand.cs +++ b/src/PSRule/Commands/NewRuleDefinitionCommand.cs @@ -101,7 +101,7 @@ internal sealed class NewRuleDefinitionCommand : LanguageBlock /// Any taxonomy references. /// [Parameter(Mandatory = false)] - public Hashtable Taxa { get; set; } + public Hashtable Labels { get; set; } protected override void ProcessRecord() { @@ -121,7 +121,7 @@ protected override void ProcessRecord() ); var flags = ResourceFlags.None; var id = new ResourceId(source.Module, Name, ResourceIdKind.Id); - var taxa = ResourceTaxa.FromHashtable(Taxa); + var labels = ResourceLabels.FromHashtable(Labels); context.VerboseFoundResource(name: Name, moduleName: source.Module, scriptName: MyInvocation.ScriptName); @@ -147,7 +147,7 @@ protected override void ProcessRecord() configuration: Configure, extent: extent, flags: flags, - taxa: taxa + labels: labels ); #pragma warning restore CA2000 // Dispose objects before losing scope, needs to be passed to pipeline WriteObject(block); diff --git a/src/PSRule/Configuration/RuleOption.cs b/src/PSRule/Configuration/RuleOption.cs index d1e809919e..84a471d5da 100644 --- a/src/PSRule/Configuration/RuleOption.cs +++ b/src/PSRule/Configuration/RuleOption.cs @@ -30,7 +30,7 @@ public RuleOption() IncludeLocal = null; Include = null; Tag = null; - Taxa = null; + Labels = null; } /// @@ -47,7 +47,7 @@ public RuleOption(RuleOption option) IncludeLocal = option.IncludeLocal; Include = option.Include; Tag = option.Tag; - Taxa = option.Taxa; + Labels = option.Labels; } /// @@ -65,7 +65,7 @@ public bool Equals(RuleOption other) IncludeLocal == other.IncludeLocal && Include == other.Include && Tag == other.Tag && - Taxa == other.Taxa; + Labels == other.Labels; } /// @@ -79,7 +79,7 @@ public override int GetHashCode() hash = hash * 23 + (IncludeLocal.HasValue ? IncludeLocal.Value.GetHashCode() : 0); hash = hash * 23 + (Include != null ? Include.GetHashCode() : 0); hash = hash * 23 + (Tag != null ? Tag.GetHashCode() : 0); - hash = hash * 23 + (Taxa != null ? Taxa.GetHashCode() : 0); + hash = hash * 23 + (Labels != null ? Labels.GetHashCode() : 0); return hash; } } @@ -97,7 +97,7 @@ internal static RuleOption Combine(RuleOption o1, RuleOption o2) IncludeLocal = o1.IncludeLocal ?? o2.IncludeLocal, Include = o1.Include ?? o2.Include, Tag = o1.Tag ?? o2.Tag, - Taxa = o1.Taxa ?? o2.Taxa, + Labels = o1.Labels ?? o2.Labels, }; return result; } @@ -136,6 +136,6 @@ internal static RuleOption Combine(RuleOption o1, RuleOption o2) /// A set of taxonomy references. /// [DefaultValue(null)] - public ResourceTaxa Taxa { get; set; } + public ResourceLabels Labels { get; set; } } } diff --git a/src/PSRule/Definitions/Conventions/ScriptBlockConvention.cs b/src/PSRule/Definitions/Conventions/ScriptBlockConvention.cs index 27a35c2ae3..a809785332 100644 --- a/src/PSRule/Definitions/Conventions/ScriptBlockConvention.cs +++ b/src/PSRule/Definitions/Conventions/ScriptBlockConvention.cs @@ -60,7 +60,7 @@ internal ScriptBlockConvention( ResourceTags IResource.Tags => null; // Not supported with conventions. - ResourceTaxa IResource.Taxa => null; + ResourceLabels IResource.Labels => null; public override void Initialize(RunspaceContext context, IEnumerable input) { diff --git a/src/PSRule/Definitions/Resource.cs b/src/PSRule/Definitions/Resource.cs index 8550370c3f..b591033df5 100644 --- a/src/PSRule/Definitions/Resource.cs +++ b/src/PSRule/Definitions/Resource.cs @@ -114,7 +114,7 @@ public interface IResource : ILanguageBlock /// /// Any taxonomy references. /// - ResourceTaxa Taxa { get; } + ResourceLabels Labels { get; } /// /// Flags for the resource. @@ -234,22 +234,22 @@ public sealed class ResourceAnnotations : Dictionary /// /// Additional resource taxonomy references. /// - public sealed class ResourceTaxa : Dictionary + public sealed class ResourceLabels : Dictionary { /// - /// Create an empty set of resource taxa. + /// Create an empty set of resource labels. /// - public ResourceTaxa() : base(StringComparer.OrdinalIgnoreCase) { } + public ResourceLabels() : base(StringComparer.OrdinalIgnoreCase) { } /// - /// Convert from a hashtable to resource taxa. + /// Convert from a hashtable to resource labels. /// - internal static ResourceTaxa FromHashtable(Hashtable hashtable) + internal static ResourceLabels FromHashtable(Hashtable hashtable) { if (hashtable == null || hashtable.Count == 0) return null; - var annotations = new ResourceTaxa(); + var annotations = new ResourceLabels(); foreach (DictionaryEntry kv in hashtable) { var key = kv.Key.ToString(); @@ -405,7 +405,7 @@ public ResourceMetadata() { Annotations = new ResourceAnnotations(); Tags = new ResourceTags(); - Taxa = new ResourceTaxa(); + Labels = new ResourceLabels(); } /// @@ -439,7 +439,7 @@ public ResourceMetadata() /// Any taxonomy references. /// [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitEmptyCollections)] - public ResourceTaxa Taxa { get; set; } + public ResourceLabels Labels { get; set; } } /// @@ -571,7 +571,7 @@ private protected InternalResource(ResourceKind kind, string apiVersion, SourceF ResourceTags IResource.Tags => Metadata.Tags; - ResourceTaxa IResource.Taxa => Metadata.Taxa; + ResourceLabels IResource.Labels => Metadata.Labels; ResourceFlags IResource.Flags => Flags; diff --git a/src/PSRule/Definitions/Rules/RuleFilter.cs b/src/PSRule/Definitions/Rules/RuleFilter.cs index 82e2ed6668..1d373f58e5 100644 --- a/src/PSRule/Definitions/Rules/RuleFilter.cs +++ b/src/PSRule/Definitions/Rules/RuleFilter.cs @@ -18,7 +18,7 @@ internal sealed class RuleFilter : IResourceFilter private readonly string[] _Include; private readonly string[] _Excluded; private readonly Hashtable _Tag; - private readonly ResourceTaxa _Taxa; + private readonly ResourceLabels _Labels; private readonly bool _IncludeLocal; private readonly WildcardPattern _WildcardMatch; @@ -29,13 +29,13 @@ internal sealed class RuleFilter : IResourceFilter /// Only accept rules that have these tags. /// Rule that are always excluded by name. /// Determine if local rules are automatically included. - /// Only accept rules that have these taxa. - public RuleFilter(string[] include, Hashtable tag, string[] exclude, bool? includeLocal, ResourceTaxa taxa) + /// Only accept rules that have these labels. + public RuleFilter(string[] include, Hashtable tag, string[] exclude, bool? includeLocal, ResourceLabels labels) { _Include = include == null || include.Length == 0 ? null : include; _Excluded = exclude == null || exclude.Length == 0 ? null : exclude; _Tag = tag ?? null; - _Taxa = taxa ?? null; + _Labels = labels ?? null; _IncludeLocal = includeLocal ?? RuleOption.Default.IncludeLocal.Value; _WildcardMatch = null; @@ -50,10 +50,10 @@ public RuleFilter(string[] include, Hashtable tag, string[] exclude, bool? inclu ResourceKind IResourceFilter.Kind => ResourceKind.Rule; - internal bool Match(string name, ResourceTags tag, ResourceTaxa taxa) + internal bool Match(string name, ResourceTags tag, ResourceLabels labels) { return !IsExcluded(new ResourceId[] { ResourceId.Parse(name) }) && - IsIncluded(new ResourceId[] { ResourceId.Parse(name) }, tag, taxa); + IsIncluded(new ResourceId[] { ResourceId.Parse(name) }, tag, labels); } /// @@ -63,7 +63,7 @@ internal bool Match(string name, ResourceTags tag, ResourceTaxa taxa) public bool Match(IResource resource) { var ids = resource.GetIds(); - return !IsExcluded(ids) && (_IncludeLocal && resource.IsLocalScope() || IsIncluded(ids, resource.Tags, resource.Taxa)); + return !IsExcluded(ids) && (_IncludeLocal && resource.IsLocalScope() || IsIncluded(ids, resource.Tags, resource.Labels)); } private bool IsExcluded(IEnumerable ids) @@ -79,12 +79,12 @@ private bool IsExcluded(IEnumerable ids) return false; } - private bool IsIncluded(IEnumerable ids, ResourceTags tag, ResourceTaxa taxa) + private bool IsIncluded(IEnumerable ids, ResourceTags tag, ResourceLabels labels) { foreach (var id in ids) { if (_Include == null || Contains(id, _Include) || MatchWildcard(id.Name)) - return TagEquals(tag) && TaxaEquals(taxa); + return TagEquals(tag) && LabelEquals(labels); } return false; } @@ -105,17 +105,17 @@ private bool TagEquals(ResourceTags tag) return true; } - private bool TaxaEquals(ResourceTaxa taxa) + private bool LabelEquals(ResourceLabels labels) { - if (_Taxa == null) + if (_Labels == null) return true; - if (taxa == null || _Taxa.Count > taxa.Count) + if (labels == null || _Labels.Count > labels.Count) return false; - foreach (var taxon in _Taxa) + foreach (var taxon in _Labels) { - if (!taxa.Contains(taxon.Key, taxon.Value)) + if (!labels.Contains(taxon.Key, taxon.Value)) return false; } return true; diff --git a/src/PSRule/Host/HostHelper.cs b/src/PSRule/Host/HostHelper.cs index 0cb2d53d16..1a7309fb06 100644 --- a/src/PSRule/Host/HostHelper.cs +++ b/src/PSRule/Host/HostHelper.cs @@ -447,7 +447,7 @@ private static DependencyTargetCollection ToRuleV1(ILanguageBlock[] blo DependsOn = block.DependsOn, Flags = block.Flags, Extent = block.Extent, - Taxa = block.Taxa, + Labels = block.Labels, }); knownRuleNames.AddNames(block.Id, block.Ref, block.Alias); knownRuleIds.AddIds(block.Id, block.Ref, block.Alias); @@ -478,7 +478,7 @@ private static DependencyTargetCollection ToRuleV1(ILanguageBlock[] blo DependsOn = null, // TODO: No support for DependsOn yet Flags = block.Flags, Extent = block.Extent, - Taxa = block.Metadata.Taxa, + Labels = block.Metadata.Labels, }); knownRuleNames.AddNames(block.Id, block.Ref, block.Alias); knownRuleIds.AddIds(block.Id, block.Ref, block.Alias); @@ -558,7 +558,7 @@ private static DependencyTargetCollection ToRuleBlockV1(ILanguageBloc configuration: null, // TODO: No support for rule configuration use module or workspace config extent: null, flags: block.Flags, - taxa: block.Metadata.Taxa + labels: block.Metadata.Labels )); knownRuleNames.AddNames(block.Id, block.Ref, block.Alias); knownRuleIds.AddIds(block.Id, block.Ref, block.Alias); diff --git a/src/PSRule/PSRule.psm1 b/src/PSRule/PSRule.psm1 index 90fc179bf7..f6195ddeb6 100644 --- a/src/PSRule/PSRule.psm1 +++ b/src/PSRule/PSRule.psm1 @@ -1730,7 +1730,7 @@ function Rule { # Any taxonomy references. [Parameter(Mandatory = $False)] - [hashtable]$Taxa, + [hashtable]$Labels, [Parameter(Mandatory = $False)] [ScriptBlock]$If, diff --git a/src/PSRule/Pipeline/OptionContext.cs b/src/PSRule/Pipeline/OptionContext.cs index d769670bb4..934687807b 100644 --- a/src/PSRule/Pipeline/OptionContext.cs +++ b/src/PSRule/Pipeline/OptionContext.cs @@ -91,7 +91,7 @@ internal sealed class BaselineScope : OptionScope public string[] Include; public string[] Exclude; public Hashtable Tag; - public ResourceTaxa Taxa; + public ResourceLabels Labels; // Configuration public Dictionary Configuration; @@ -123,7 +123,7 @@ public BaselineScope(ScopeType type, string baselineId, string moduleName, IBase Include = option.Rule?.Include; Exclude = option.Rule?.Exclude; Tag = option.Rule?.Tag; - Taxa = option.Rule?.Taxa; + Labels = option.Rule?.Labels; Configuration = option.Configuration != null ? new Dictionary(option.Configuration, StringComparer.OrdinalIgnoreCase) : new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -282,9 +282,9 @@ private IResourceFilter GetRuleFilter() var include = _Parameter?.Include ?? _Explicit?.Include ?? _WorkspaceBaseline?.Include ?? _ModuleBaseline?.Include; var exclude = _Explicit?.Exclude ?? _WorkspaceBaseline?.Exclude ?? _ModuleBaseline?.Exclude; var tag = _Parameter?.Tag ?? _Explicit?.Tag ?? _WorkspaceBaseline?.Tag ?? _ModuleBaseline?.Tag; - var taxa = _Parameter?.Taxa ?? _Explicit?.Taxa ?? _WorkspaceBaseline?.Taxa ?? _ModuleBaseline?.Taxa; + var labels = _Parameter?.Labels ?? _Explicit?.Labels ?? _WorkspaceBaseline?.Labels ?? _ModuleBaseline?.Labels; var includeLocal = _Explicit?.IncludeLocal ?? _WorkspaceBaseline?.IncludeLocal ?? _ModuleBaseline?.IncludeLocal; - return _Filter = new RuleFilter(include, tag, exclude, includeLocal, taxa); + return _Filter = new RuleFilter(include, tag, exclude, includeLocal, labels); } private IResourceFilter GetConventionFilter() diff --git a/src/PSRule/Rules/Rule.cs b/src/PSRule/Rules/Rule.cs index 7c1bba5ae7..76a4395752 100644 --- a/src/PSRule/Rules/Rule.cs +++ b/src/PSRule/Rules/Rule.cs @@ -112,7 +112,7 @@ public sealed class Rule : IDependencyTarget, ITargetInfo, IResource, IRuleV1 /// [JsonIgnore, YamlIgnore] - public ResourceTaxa Taxa { get; set; } + public ResourceLabels Labels { get; set; } string ITargetInfo.TargetName => Name; diff --git a/src/PSRule/Rules/RuleBlock.cs b/src/PSRule/Rules/RuleBlock.cs index aa8b12951e..a82d3b3423 100644 --- a/src/PSRule/Rules/RuleBlock.cs +++ b/src/PSRule/Rules/RuleBlock.cs @@ -23,7 +23,7 @@ namespace PSRule.Rules [DebuggerDisplay("{Id} @{Source.Path}")] internal sealed class RuleBlock : ILanguageBlock, IDependencyTarget, IDisposable, IResource, IRuleV1 { - internal RuleBlock(SourceFile source, ResourceId id, ResourceId? @ref, SeverityLevel level, RuleHelpInfo info, ICondition condition, ResourceTags tag, ResourceId[] alias, ResourceId[] dependsOn, Hashtable configuration, ISourceExtent extent, ResourceFlags flags, ResourceTaxa taxa) + internal RuleBlock(SourceFile source, ResourceId id, ResourceId? @ref, SeverityLevel level, RuleHelpInfo info, ICondition condition, ResourceTags tag, ResourceId[] alias, ResourceId[] dependsOn, Hashtable configuration, ISourceExtent extent, ResourceFlags flags, ResourceLabels labels) { Source = source; Name = id.Name; @@ -41,7 +41,7 @@ internal RuleBlock(SourceFile source, ResourceId id, ResourceId? @ref, SeverityL Configuration = configuration; Extent = extent; Flags = flags; - Taxa = taxa; + Labels = labels; } /// @@ -83,7 +83,7 @@ internal RuleBlock(SourceFile source, ResourceId id, ResourceId? @ref, SeverityL public readonly ResourceTags Tag; /// - public ResourceTaxa Taxa { get; } + public ResourceLabels Labels { get; } /// /// Configuration defaults for the rule definition. diff --git a/tests/PSRule.Tests/Baseline.Rule.jsonc b/tests/PSRule.Tests/Baseline.Rule.jsonc index 928369dcbe..6138cbb525 100644 --- a/tests/PSRule.Tests/Baseline.Rule.jsonc +++ b/tests/PSRule.Tests/Baseline.Rule.jsonc @@ -125,7 +125,7 @@ }, "spec": { "rule": { - "taxa": { + "labels": { "framework.v1/control": [ "c-1", "c-2" diff --git a/tests/PSRule.Tests/Baseline.Rule.yaml b/tests/PSRule.Tests/Baseline.Rule.yaml index bfa543a634..64b9c050fd 100644 --- a/tests/PSRule.Tests/Baseline.Rule.yaml +++ b/tests/PSRule.Tests/Baseline.Rule.yaml @@ -81,12 +81,12 @@ metadata: spec: { } --- -# Synopsis: An example of a baseline with taxa defined +# Synopsis: An example of a baseline with labels defined apiVersion: github.com/microsoft/PSRule/v1 kind: Baseline metadata: name: TestBaseline6 spec: rule: - taxa: + labels: framework.v1/control: [ 'c-1', 'c-2' ] diff --git a/tests/PSRule.Tests/BaselineTests.cs b/tests/PSRule.Tests/BaselineTests.cs index 3f1adf62a2..59497f7fef 100644 --- a/tests/PSRule.Tests/BaselineTests.cs +++ b/tests/PSRule.Tests/BaselineTests.cs @@ -59,11 +59,11 @@ public void ReadBaselineYaml() // TestBaseline6 Assert.Equal("TestBaseline6", baseline[5].Name); - var taxa = baseline[5].Spec.Rule.Taxa; - Assert.True(taxa.Contains("framework.v1/control", new string[] { "*" })); - Assert.True(taxa.Contains("framework.v1/control", new string[] { "c-1" })); - Assert.False(taxa.Contains("framework.v1/control", new string[] { "c-3" })); - Assert.False(taxa.Contains("framework.v3/control", new string[] { "*" })); + var labels = baseline[5].Spec.Rule.Labels; + Assert.True(labels.Contains("framework.v1/control", new string[] { "*" })); + Assert.True(labels.Contains("framework.v1/control", new string[] { "c-1" })); + Assert.False(labels.Contains("framework.v1/control", new string[] { "c-3" })); + Assert.False(labels.Contains("framework.v3/control", new string[] { "*" })); } [Fact] @@ -100,11 +100,11 @@ public void ReadBaselineJson() // TestBaseline6 Assert.Equal("TestBaseline6", baseline[5].Name); - var taxa = baseline[5].Spec.Rule.Taxa; - Assert.True(taxa.Contains("framework.v1/control", new string[] { "*" })); - Assert.True(taxa.Contains("framework.v1/control", new string[] { "c-1" })); - Assert.False(taxa.Contains("framework.v1/control", new string[] { "c-3" })); - Assert.False(taxa.Contains("framework.v3/control", new string[] { "*" })); + var labels = baseline[5].Spec.Rule.Labels; + Assert.True(labels.Contains("framework.v1/control", new string[] { "*" })); + Assert.True(labels.Contains("framework.v1/control", new string[] { "c-1" })); + Assert.False(labels.Contains("framework.v1/control", new string[] { "c-3" })); + Assert.False(labels.Contains("framework.v3/control", new string[] { "*" })); } [Theory] diff --git a/tests/PSRule.Tests/FromFile.Rule.jsonc b/tests/PSRule.Tests/FromFile.Rule.jsonc index 111db099fd..b6c15bda82 100644 --- a/tests/PSRule.Tests/FromFile.Rule.jsonc +++ b/tests/PSRule.Tests/FromFile.Rule.jsonc @@ -14,7 +14,7 @@ "tags": { "feature": "tag" }, - "taxa": { + "labels": { "single": "Value", "multi": [ "Value1", diff --git a/tests/PSRule.Tests/FromFile.Rule.yaml b/tests/PSRule.Tests/FromFile.Rule.yaml index 7aaea8860c..7680f131e7 100644 --- a/tests/PSRule.Tests/FromFile.Rule.yaml +++ b/tests/PSRule.Tests/FromFile.Rule.yaml @@ -13,7 +13,7 @@ metadata: name: YamlBasicRule tags: feature: tag - taxa: + labels: single: Value multi: - Value1 diff --git a/tests/PSRule.Tests/FromFileBaseline.Rule.ps1 b/tests/PSRule.Tests/FromFileBaseline.Rule.ps1 index 0d700f52f9..3514c3bb1e 100644 --- a/tests/PSRule.Tests/FromFileBaseline.Rule.ps1 +++ b/tests/PSRule.Tests/FromFileBaseline.Rule.ps1 @@ -10,9 +10,9 @@ Rule 'WithBaseline' -Tag @{ category = 'group2'; severity = 'high' } { $PSRule.TargetName -eq 'TestObject1' $PSRule.TargetType -eq 'TestObjectType' $PSRule.Field.kind -eq 'TestObjectType' -} -Taxa @{ 'framework.v1/control' = @('c-2') } +} -Labels @{ 'framework.v1/control' = @('c-2') } # Synopsis: Test for baseline Rule 'NotInBaseline' -Tag @{ category = 'group2'; severity = 'low' } { $False; -} -Taxa @{ 'framework.v1/control' = @('c-3') } +} -Labels @{ 'framework.v1/control' = @('c-3') } diff --git a/tests/PSRule.Tests/RuleFilterTests.cs b/tests/PSRule.Tests/RuleFilterTests.cs index 4d0f1bd6bf..a93547cbdb 100644 --- a/tests/PSRule.Tests/RuleFilterTests.cs +++ b/tests/PSRule.Tests/RuleFilterTests.cs @@ -55,28 +55,28 @@ public void MatchTag() } [Fact] - public void MatchTaxa() + public void MatchLabels() { // Set resource tags - var resourceTaxa = new Hashtable(); + var resourceLabels = new Hashtable(); // Create a filter - var taxa = new ResourceTaxa + var labels = new ResourceLabels { ["framework.v1/control"] = new string[] { "c-1", "c-2" } }; - var filter = new RuleFilter(null, null, null, null, taxa); + var filter = new RuleFilter(null, null, null, null, labels); - resourceTaxa["framework.v1/control"] = new string[] { "c-2", "c-1" }; - Assert.True(filter.Match("rule", null, ResourceTaxa.FromHashtable(resourceTaxa))); - resourceTaxa["framework.v1/control"] = new string[] { "c-3", "c-1" }; - Assert.True(filter.Match("rule", null, ResourceTaxa.FromHashtable(resourceTaxa))); - resourceTaxa["framework.v1/control"] = new string[] { "c-1", "c-3" }; - Assert.True(filter.Match("rule", null, ResourceTaxa.FromHashtable(resourceTaxa))); - resourceTaxa["framework.v1/control"] = new string[] { "c-3", "c-4" }; - Assert.False(filter.Match("rule", null, ResourceTaxa.FromHashtable(resourceTaxa))); - resourceTaxa["framework.v1/control"] = System.Array.Empty(); - Assert.False(filter.Match("rule", null, ResourceTaxa.FromHashtable(resourceTaxa))); + resourceLabels["framework.v1/control"] = new string[] { "c-2", "c-1" }; + Assert.True(filter.Match("rule", null, ResourceLabels.FromHashtable(resourceLabels))); + resourceLabels["framework.v1/control"] = new string[] { "c-3", "c-1" }; + Assert.True(filter.Match("rule", null, ResourceLabels.FromHashtable(resourceLabels))); + resourceLabels["framework.v1/control"] = new string[] { "c-1", "c-3" }; + Assert.True(filter.Match("rule", null, ResourceLabels.FromHashtable(resourceLabels))); + resourceLabels["framework.v1/control"] = new string[] { "c-3", "c-4" }; + Assert.False(filter.Match("rule", null, ResourceLabels.FromHashtable(resourceLabels))); + resourceLabels["framework.v1/control"] = System.Array.Empty(); + Assert.False(filter.Match("rule", null, ResourceLabels.FromHashtable(resourceLabels))); } } } From 2d9ae08edaf3c9b017d6928ee905e42b8259c720 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Fri, 30 Sep 2022 06:58:30 +1000 Subject: [PATCH 072/156] Merge change log from patch v2.4.1 (#1289) (#1291) --- docs/CHANGELOG-v2.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 0b6e6a80c4..d34cea4fc1 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -59,6 +59,15 @@ What's changed since v2.4.0: - Fixed unhandled exception with GetRootedPath by @BernieWhite. [#1251](https://github.com/microsoft/PSRule/issues/1251) +## v2.4.1 + +What's changed since v2.4.0: + +- Bug fixes: + - Fixed markdown parsing of Spanish translated help fails by @BernieWhite @jonathanruiz. + [#1286](https://github.com/microsoft/PSRule/issues/1286) + [#1285](https://github.com/microsoft/PSRule/pull/1285) + ## v2.4.0 What's changed since v2.3.2: From f24d2048a771c08d7a8ff2d99686dd2e9291b8c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Oct 2022 15:59:52 +1000 Subject: [PATCH 073/156] Bump mkdocs-material from 8.5.3 to 8.5.5 (#1292) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.5.3 to 8.5.5. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.5.3...8.5.5) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 6b80da97be..20aaf948ba 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.3.1 -mkdocs-material==8.5.3 +mkdocs-material==8.5.5 pymdown-extensions==9.5 mike==1.1.2 mkdocs-simple-hooks==0.1.5 From 8614cf0146a56cb7738de237537beb18fcfaa584 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Oct 2022 16:45:04 +1000 Subject: [PATCH 074/156] Bump mkdocs from 1.3.1 to 1.4.0 (#1284) Bumps [mkdocs](https://github.com/mkdocs/mkdocs) from 1.3.1 to 1.4.0. - [Release notes](https://github.com/mkdocs/mkdocs/releases) - [Commits](https://github.com/mkdocs/mkdocs/compare/1.3.1...1.4.0) --- updated-dependencies: - dependency-name: mkdocs dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 20aaf948ba..f94f768c2b 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,4 +1,4 @@ -mkdocs==1.3.1 +mkdocs==1.4.0 mkdocs-material==8.5.5 pymdown-extensions==9.5 mike==1.1.2 From 207ca9fada3b52531796d798e04e727562f09662 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Oct 2022 17:00:21 +1000 Subject: [PATCH 075/156] Bump pymdown-extensions from 9.5 to 9.6 (#1293) Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 9.5 to 9.6. - [Release notes](https://github.com/facelessuser/pymdown-extensions/releases) - [Commits](https://github.com/facelessuser/pymdown-extensions/compare/9.5...9.6) --- updated-dependencies: - dependency-name: pymdown-extensions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index f94f768c2b..70ecda4d38 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,6 +1,6 @@ mkdocs==1.4.0 mkdocs-material==8.5.5 -pymdown-extensions==9.5 +pymdown-extensions==9.6 mike==1.1.2 mkdocs-simple-hooks==0.1.5 mkdocs-git-revision-date-plugin==0.3.2 From 855560d8b25e9ee17e113f358e8f7396e1ad384b Mon Sep 17 00:00:00 2001 From: Bernie White Date: Mon, 3 Oct 2022 20:50:18 +1000 Subject: [PATCH 076/156] Added support for changed files only #688 (#1295) * Added support for changed files only #688 * Added comments --- README.md | 3 + docs/CHANGELOG-v2.md | 8 + .../PSRule/en-US/about_PSRule_Options.md | 99 +++++++++- .../PSRule/en-US/about_PSRule_Variables.md | 4 +- docs/creating-your-pipeline.md | 22 ++- ps-rule.yaml | 4 + schemas/PSRule-options.schema.json | 13 ++ src/PSRule/Common/EnvironmentHelper.cs | 12 +- src/PSRule/Common/ExternalToolHelper.cs | 182 ++++++++++++++++++ src/PSRule/Common/GitHelper.cs | 31 +++ src/PSRule/Configuration/InputOption.cs | 19 ++ src/PSRule/Configuration/RepositoryOption.cs | 17 ++ src/PSRule/Data/InputFileInfoCollection.cs | 64 ++++++ src/PSRule/PSRule.psm1 | 34 ++++ src/PSRule/Pipeline/GetTargetPipeline.cs | 14 +- src/PSRule/Pipeline/InputPathBuilder.cs | 6 +- src/PSRule/Pipeline/InvokePipelineBuilder.cs | 14 +- src/PSRule/Pipeline/PathBuilder.cs | 26 ++- src/PSRule/Pipeline/PipelineBuilder.cs | 26 ++- src/PSRule/Pipeline/PipelineReader.cs | 25 ++- .../Pipeline/PipelineWriterExtensions.cs | 11 ++ src/PSRule/Pipeline/RulePipeline.cs | 2 +- .../Resources/PSRuleResources.Designer.cs | 18 ++ src/PSRule/Resources/PSRuleResources.resx | 6 + src/PSRule/Runtime/IInputCollection.cs | 17 ++ src/PSRule/Runtime/IRepositoryRuntimeInfo.cs | 29 +++ src/PSRule/Runtime/PSRule.cs | 83 +++++++- tests/PSRule.Tests/FromFileGit.Rule.ps1 | 20 ++ tests/PSRule.Tests/InputPathBuilderTests.cs | 24 ++- tests/PSRule.Tests/PSRule.Options.Tests.ps1 | 86 +++++++++ tests/PSRule.Tests/PSRule.Tests.yml | 2 + 31 files changed, 868 insertions(+), 53 deletions(-) create mode 100644 src/PSRule/Common/ExternalToolHelper.cs create mode 100644 src/PSRule/Data/InputFileInfoCollection.cs create mode 100644 src/PSRule/Runtime/IInputCollection.cs create mode 100644 src/PSRule/Runtime/IRepositoryRuntimeInfo.cs create mode 100644 tests/PSRule.Tests/FromFileGit.Rule.ps1 diff --git a/README.md b/README.md index 648534e7f2..0911ceee15 100644 --- a/README.md +++ b/README.md @@ -264,6 +264,7 @@ The following conceptual topics exist in the `PSRule` module: - [Input.IgnoreGitPath](https://aka.ms/ps-rule/options#inputignoregitpath) - [Input.IgnoreObjectSource](https://aka.ms/ps-rule/options#inputignoreobjectsource) - [Input.IgnoreRepositoryCommon](https://aka.ms/ps-rule/options#inputignorerepositorycommon) + - [Input.IgnoreUnchangedPath](https://aka.ms/ps-rule/options#inputignoreunchangedpath) - [Input.ObjectPath](https://aka.ms/ps-rule/options#inputobjectpath) - [Input.PathIgnore](https://aka.ms/ps-rule/options#inputpathignore) - [Input.TargetType](https://aka.ms/ps-rule/options#inputtargettype) @@ -282,6 +283,8 @@ The following conceptual topics exist in the `PSRule` module: - [Output.Path](https://aka.ms/ps-rule/options#outputpath) - [Output.SarifProblemsOnly](https://aka.ms/ps-rule/options#outputsarifproblemsonly) - [Output.Style](https://aka.ms/ps-rule/options#outputstyle) + - [Repository.BaseRef](https://aka.ms/ps-rule/options#repositorybaseref) + - [Repository.Url](https://aka.ms/ps-rule/options#repositoryurl) - [Requires](https://aka.ms/ps-rule/options#requires) - [Rule.Baseline](https://aka.ms/ps-rule/options#rulebaseline) - [Rule.Include](https://aka.ms/ps-rule/options#ruleinclude) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index d34cea4fc1..4c3807b701 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -17,14 +17,22 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers See [functions][3] for more information. - Sub-selectors within YAML and JSON expressions can be used to filter rules and list properties. See [sub-selectors][4] for more information. +- Processing of changes files only within a pipeline. + See [creating your pipeline][5] for more information. [3]: expressions/functions.md [4]: expressions/sub-selectors.md + [5]: creating-your-pipeline.md#processing-changed-files-only ## Unreleased What's changed since pre-release v2.5.0-B0015: +- New features: + - **Experimental**: Added support for only processing changed files by @BernieWhite. + [#688](https://github.com/microsoft/PSRule/issues/688) + - To ignore unchanged files, set the `Input.IgnoreUnchangedPath` option to `true`. + - See [creating your pipeline][5] for more information. - General improvements: - Added labels metadata from grouping and filtering rules by @BernieWhite. [#1272](https://github.com/microsoft/PSRule/issues/1272) diff --git a/docs/concepts/PSRule/en-US/about_PSRule_Options.md b/docs/concepts/PSRule/en-US/about_PSRule_Options.md index 26275f56f4..99a7f5fd73 100644 --- a/docs/concepts/PSRule/en-US/about_PSRule_Options.md +++ b/docs/concepts/PSRule/en-US/about_PSRule_Options.md @@ -28,6 +28,7 @@ The following workspace options are available for use: - [Input.IgnoreGitPath](#inputignoregitpath) - [Input.IgnoreObjectSource](#inputignoreobjectsource) - [Input.IgnoreRepositoryCommon](#inputignorerepositorycommon) +- [Input.IgnoreUnchangedPath](#inputignoreunchangedpath) - [Input.ObjectPath](#inputobjectpath) - [Input.PathIgnore](#inputpathignore) - [Input.TargetType](#inputtargettype) @@ -46,6 +47,7 @@ The following workspace options are available for use: - [Output.Path](#outputpath) - [Output.SarifProblemsOnly](#outputsarifproblemsonly) - [Output.Style](#outputstyle) +- [Repository.BaseRef](#repositorybaseref) - [Repository.Url](#repositoryurl) - [Requires](#requires) - [Suppression](#suppression) @@ -1436,12 +1438,61 @@ variables: value: false ``` +### Input.IgnoreUnchangedPath + +By default, PSRule will process all files within an input path. +For large repositories, this can result in a large number of files being processed. +Additionally, for a pull request you may only be interested in files that have changed. + +When set to `true`, files that have not changed will be ignored. +This option can be specified using: + +```powershell +# PowerShell: Using the InputIgnoreUnchangedPath parameter +$option = New-PSRuleOption -InputIgnoreUnchangedPath $True; +``` + +```powershell +# PowerShell: Using the Input.IgnoreUnchangedPath hashtable key +$option = New-PSRuleOption -Option @{ 'Input.IgnoreUnchangedPath' = $True }; +``` + +```powershell +# PowerShell: Using the InputIgnoreUnchangedPath parameter to set YAML +Set-PSRuleOption -InputIgnoreUnchangedPath $True; +``` + +```yaml +# YAML: Using the input/ignoreUnchangedPath property +input: + ignoreUnchangedPath: true +``` + +```bash +# Bash: Using environment variable +export PSRULE_INPUT_IGNOREUNCHANGEDPATH=true +``` + +```yaml +# GitHub Actions: Using environment variable +env: + PSRULE_INPUT_IGNOREUNCHANGEDPATH: true +``` + +```yaml +# Azure Pipelines: Using environment variable +variables: +- name: PSRULE_INPUT_IGNOREUNCHANGEDPATH + value: true +``` + ### Input.ObjectPath The object path to a property to use instead of the pipeline object. By default, PSRule processes objects passed from the pipeline against selected rules. -When this option is set, instead of evaluating the pipeline object, PSRule looks for a property of the pipeline object specified by `ObjectPath` and uses that instead. +When this option is set, instead of evaluating the pipeline object, +PSRule looks for a property of the pipeline object specified by `ObjectPath` and uses that instead. If the property specified by `ObjectPath` is a collection/ array, then each item is evaluated separately. If the property specified by `ObjectPath` does not exist, PSRule skips the object. @@ -2428,6 +2479,45 @@ variables: value: 2 ``` +### Repository.BaseRef + +This option is used for specify the base branch for pull requests. +When evaluating changes files only PSRule uses this option for comparison with the current branch. +By default, the base ref is detected from environment variables set by the build system. + +This option can be specified using: + +```powershell +# PowerShell: Using the RepositoryBaseRef parameter +$option = New-PSRuleOption -RepositoryBaseRef 'main'; +``` + +```powershell +# PowerShell: Using the Repository.BaseRef hashtable key +$option = New-PSRuleOption -Option @{ 'Repository.BaseRef' = 'main' }; +``` + +```powershell +# PowerShell: Using the RepositoryBaseRef parameter to set YAML +Set-PSRuleOption -RepositoryBaseRef 'main'; +``` + +```yaml +# YAML: Using the repository/baseRef property +repository: + baseRef: main +``` + +```bash +# Bash: Using environment variable +export PSRULE_REPOSITORY_BASEREF='main' +``` + +```powershell +# PowerShell: Using environment variable +$env:PSRULE_REPOSITORY_BASEREF = 'main'; +``` + ### Repository.Url This option can be configured to set the repository URL reported in output. @@ -2775,6 +2865,11 @@ Rule 'isFruit' -If { $TargetObject.Category -eq 'Produce' } { # PSRule example configuration # +# Configures the repository +repository: + url: https://github.com/microsoft/PSRule + baseRef: main + # Configure required module versions requires: PSRule.Rules.Azure: '>=1.1.0' @@ -2804,6 +2899,7 @@ input: ignoreGitPath: false ignoreObjectSource: true ignoreRepositoryCommon: false + ignoreUnchangedPath: true objectPath: items pathIgnore: - '*.Designer.cs' @@ -2911,6 +3007,7 @@ input: ignoreGitPath: true ignoreObjectSource: false ignoreRepositoryCommon: true + ignoreUnchangedPath: false objectPath: null pathIgnore: [ ] targetType: [ ] diff --git a/docs/concepts/PSRule/en-US/about_PSRule_Variables.md b/docs/concepts/PSRule/en-US/about_PSRule_Variables.md index 1ef8610b84..d433c4b37f 100644 --- a/docs/concepts/PSRule/en-US/about_PSRule_Variables.md +++ b/docs/concepts/PSRule/en-US/about_PSRule_Variables.md @@ -170,12 +170,14 @@ The following properties are available for read access: This property can only be called within the `-End` block of a convention. - `Field` - A hashtable of custom bound fields. See option `Binding.Field` for more information. +- `Input` - Allows adding additional input paths to the pipeline. +- `Repository` - Provides access to information about the current repository. +- `Source` - A collection of sources for the object currently being processed on the pipeline. - `TargetObject` - The object currently being processed on the pipeline. - `TargetName` - The name of the object currently being processed on the pipeline. This property will automatically default to `TargetName` or `Name` properties of the object if they exist. - `TargetType` - The type of the object currently being processed on the pipeline. This property will automatically bind to `PSObject.TypeNames[0]` by default. -- `Source` - A collection of sources for the object currently being processed on the pipeline. - `Output` - The output of all rules. This property can only be called within the `-End` block of a convention. diff --git a/docs/creating-your-pipeline.md b/docs/creating-your-pipeline.md index 942aabaf4c..c952c51314 100644 --- a/docs/creating-your-pipeline.md +++ b/docs/creating-your-pipeline.md @@ -132,7 +132,7 @@ To prevent a rule executing you can either: - 'TestObject2' ``` -!!! tip +!!! Tip Use comments within `ps-rule.yaml` to describe the reason why rules are excluded or suppressed. Meaningful comments help during peer review within a Pull Request (PR). Also consider including a date if the exclusions or suppressions are temporary. @@ -142,3 +142,23 @@ To prevent a rule executing you can either: [5]: concepts/PSRule/en-US/about_PSRule_SuppressionGroups.md [6]: addon-modules.md [7]: authoring/packaging-rules.md + +### Processing changed files only + +[:octicons-book-24: Docs][8] + +To only process files that have changed within a pull request, set the `Input.IgnoreUnchangedPath` option. + +```yaml title="ps-rule.yaml" +repository: + baseRef: main + +input: + ignoreUnchangedPath: true +``` + +!!! Tip + In some cases it may be nessessary to set `Repository.BaseRef` to the default branch of your repository. + By default, PSRule will detect the default branch of the repository from the build system environment variables. + + [8]: concepts/PSRule/en-US/about_PSRule_Options.md#inputignoreunchangedpath diff --git a/ps-rule.yaml b/ps-rule.yaml index 6c57a48f81..9829454240 100644 --- a/ps-rule.yaml +++ b/ps-rule.yaml @@ -5,6 +5,10 @@ # Please see the documentation for all configuration options: # https://microsoft.github.io/PSRule/ +repository: + url: https://github.com/microsoft/PSRule + baseRef: main + output: culture: - 'en-US' diff --git a/schemas/PSRule-options.schema.json b/schemas/PSRule-options.schema.json index 97e5e25b54..9f7367e709 100644 --- a/schemas/PSRule-options.schema.json +++ b/schemas/PSRule-options.schema.json @@ -409,6 +409,13 @@ "markdownDescription": "Determines if objects are ignore based on their file source path. When set, objects from the pipeline or read from files will be exclude based on their source path and the configuration of `pathIgnore`, `ignoreGitPath`, and `ignoreRepositoryCommon` options. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#inputignoreobjectsource)", "default": false }, + "ignoreUnchangedPath": { + "type": "boolean", + "title": "Ignore unchanged path", + "description": "Determine if unchanged files are ignored. By default, PSRule will process all files within an input path. For large repositories, this can result in a large number of files being processed. Additionally, for a pull request you may only be interested in files that have changed.", + "markdownDescription": "Determine if unchanged files are ignored. By default, PSRule will process all files within an input path. For large repositories, this can result in a large number of files being processed. Additionally, for a pull request you may only be interested in files that have changed. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#inputignoreunchangedpath)", + "default": false + }, "objectPath": { "type": "string", "title": "Object path", @@ -797,6 +804,12 @@ "description": "Configures repository options.", "markdownDescription": "Configures repository options.", "properties": { + "baseRef": { + "type": "string", + "title": "Base Reference", + "description": "Sets the repository base ref used for comparisons of changed files. By default, the base ref is detected from environment vairables set by the build system.", + "markdownDescription": "Sets the repository base ref used for comparisons of changed files. By default, the base ref is detected from environment vairables set by the build system. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#repositorybaseref)" + }, "url": { "type": "string", "title": "Repository URL", diff --git a/src/PSRule/Common/EnvironmentHelper.cs b/src/PSRule/Common/EnvironmentHelper.cs index 71d40f2cae..89eecc5ae3 100644 --- a/src/PSRule/Common/EnvironmentHelper.cs +++ b/src/PSRule/Common/EnvironmentHelper.cs @@ -39,10 +39,11 @@ public static string GetRunId(this EnvironmentHelper helper) internal sealed class EnvironmentHelper { - private readonly static char[] STRINGARRAY_SEPARATOR = new char[] { ';' }; - private readonly static char[] WINDOWS_PATH_ENV_SEPARATOR = new char[] { ';' }; - private readonly static char[] LINUX_PATH_ENV_SEPARATOR = new char[] { ':' }; + private static readonly char[] STRINGARRAY_SEPARATOR = new char[] { ';' }; + private static readonly char[] LINUX_PATH_ENV_SEPARATOR = new char[] { ':' }; + private static readonly char[] WINDOWS_PATH_ENV_SEPARATOR = new char[] { ';' }; + private const string PATH_ENV = "PATH"; private const string DEFAULT_CREDENTIAL_USERNAME = "na"; public static readonly EnvironmentHelper Default = new EnvironmentHelper(); @@ -90,6 +91,11 @@ internal bool TryStringArray(string key, out string[] value) return value != null; } + internal bool TryPathEnvironmentVariable(out string[] value) + { + return TryPathEnvironmentVariable(PATH_ENV, out value); + } + internal bool TryPathEnvironmentVariable(string key, out string[] value) { value = default; diff --git a/src/PSRule/Common/ExternalToolHelper.cs b/src/PSRule/Common/ExternalToolHelper.cs new file mode 100644 index 0000000000..fa0bebdaaf --- /dev/null +++ b/src/PSRule/Common/ExternalToolHelper.cs @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading; +using PSRule.Configuration; + +namespace PSRule +{ + internal sealed class ExternalTool : IDisposable + { + private 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 readonly string _BinaryPath; + private bool _Disposed; + + private ExternalTool(string binaryPath, int timeout, string version = null) + { + _Output = new StringBuilder(); + _Error = new StringBuilder(); + _Interval = 1000; + _Timeout = timeout; + _BinaryPath = binaryPath; + + _ErrorWait = new AutoResetEvent(false); + _OutputWait = new AutoResetEvent(false); + } + + public bool HasExited => _Process.HasExited; + + internal static ExternalTool Get(string defaultPath, string binary) + { + if (!TryPathFromDefault(defaultPath, binary, out var binaryPath) && !TryPathFromEnvironment(binary, out binaryPath)) + return null; + + return new ExternalTool(binaryPath, 0, null); + } + + private static bool TryPathFromDefault(string defaultPath, string binary, out string binaryPath) + { + return TryPath(binary, defaultPath, out binaryPath); + } + + public bool WaitForExit(string args, out int exitCode) + { + var startInfo = new ProcessStartInfo(_BinaryPath, args) + { + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + WorkingDirectory = PSRuleOption.GetWorkingPath(), + }; + _Process = Process.Start(startInfo); + _Process.ErrorDataReceived += Bicep_ErrorDataReceived; + _Process.OutputDataReceived += Bicep_OutputDataReceived; + + _Process.BeginErrorReadLine(); + _Process.BeginOutputReadLine(); + + _ErrorWait.Reset(); + _OutputWait.Reset(); + + 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(); + } + + public string GetOutput() + { + return _Output.ToString(); + } + + public string GetError() + { + return _Error.ToString(); + } + + private void Bicep_OutputDataReceived(object sender, DataReceivedEventArgs e) + { + if (e.Data == null) + { + _OutputWait.Set(); + } + else + { + _Output.AppendLine(e.Data); + } + } + + private void Bicep_ErrorDataReceived(object sender, DataReceivedEventArgs e) + { + if (e.Data == null) + { + _ErrorWait.Set(); + } + else + { + var errors = GetErrorLine(e.Data); + for (var i = 0; i < errors.Length; i++) + _Error.AppendLine(errors[i]); + } + } + + private static string[] GetErrorLine(string input) + { + 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(); + } + + private static bool TryPathFromEnvironment(string binary, out string binaryPath) + { + binaryPath = null; + if (!EnvironmentHelper.Default.TryPathEnvironmentVariable(out var path)) + return false; + + for (var i = 0; path != null && i < path.Length; i++) + if (TryPath(binary, path[i], out binaryPath)) + return true; + + binaryPath = null; + return false; + } + + private static bool TryPath(string binary, string path, out string binaryPath) + { + binaryPath = null; + if (string.IsNullOrEmpty(path)) + return false; + + binaryPath = Path.Combine(path, binary); + if (File.Exists(binaryPath)) + return true; + + binaryPath = null; + return false; + } + + private void Dispose(bool disposing) + { + if (!_Disposed) + { + if (disposing) + { + _ErrorWait.Dispose(); + _OutputWait.Dispose(); + _Process.Dispose(); + } + _Error.Clear(); + _Output.Clear(); + _Disposed = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/PSRule/Common/GitHelper.cs b/src/PSRule/Common/GitHelper.cs index 844d8e4d1d..58c0febdac 100644 --- a/src/PSRule/Common/GitHelper.cs +++ b/src/PSRule/Common/GitHelper.cs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.IO; +using System.Runtime.InteropServices; using PSRule.Configuration; namespace PSRule @@ -134,6 +136,35 @@ public static bool TryRepository(out string value, string path = null) return false; } + public static bool TryGetChangedFiles(string baseRef, string filter, string options, out string[] files) + { + // Get current tip + var source = TryRevision(out var source_sha) ? source_sha : "HEAD"; + var target = !string.IsNullOrEmpty(baseRef) ? baseRef : "HEAD^"; + + var bin = GetGitBinary(); + var args = GetGitArgs(target, source, filter, options); + var tool = ExternalTool.Get(null, bin); + + files = Array.Empty(); + if (!tool.WaitForExit(args, out var exitCode) || exitCode != 0) + return false; + + files = tool.GetOutput().Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + return true; + } + + private static string GetGitBinary() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || + RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "git" : "git.exe"; + } + + private static string GetGitArgs(string target, string source, string filter, string options) + { + return $"diff --diff-filter={filter} --ignore-submodules=all --name-only --no-renames {target}"; + } + private static bool TryReadHead(string path, out string value) { value = null; diff --git a/src/PSRule/Configuration/InputOption.cs b/src/PSRule/Configuration/InputOption.cs index 3574d2829a..9758aaab42 100644 --- a/src/PSRule/Configuration/InputOption.cs +++ b/src/PSRule/Configuration/InputOption.cs @@ -16,6 +16,7 @@ public sealed class InputOption : IEquatable private const bool DEFAULT_IGNOREGITPATH = true; private const bool DEFAULT_IGNOREOBJECTSOURCE = false; private const bool DEFAULT_IGNOREREPOSITORYCOMMON = true; + private const bool DEFAULT_IGNOREUNCHANGEDPATH = false; private const string DEFAULT_OBJECTPATH = null; private const string[] DEFAULT_PATHIGNORE = null; private const string[] DEFAULT_TARGETTYPE = null; @@ -26,6 +27,7 @@ public sealed class InputOption : IEquatable IgnoreGitPath = DEFAULT_IGNOREGITPATH, IgnoreObjectSource = DEFAULT_IGNOREOBJECTSOURCE, IgnoreRepositoryCommon = DEFAULT_IGNOREREPOSITORYCOMMON, + IgnoreUnchangedPath = DEFAULT_IGNOREUNCHANGEDPATH, ObjectPath = DEFAULT_OBJECTPATH, PathIgnore = DEFAULT_PATHIGNORE, TargetType = DEFAULT_TARGETTYPE, @@ -40,6 +42,7 @@ public InputOption() IgnoreGitPath = null; IgnoreObjectSource = null; IgnoreRepositoryCommon = null; + IgnoreUnchangedPath = null; ObjectPath = null; PathIgnore = null; TargetType = null; @@ -58,6 +61,7 @@ public InputOption(InputOption option) IgnoreGitPath = option.IgnoreGitPath; IgnoreObjectSource = option.IgnoreObjectSource; IgnoreRepositoryCommon = option.IgnoreRepositoryCommon; + IgnoreUnchangedPath = option.IgnoreUnchangedPath; ObjectPath = option.ObjectPath; PathIgnore = option.PathIgnore; TargetType = option.TargetType; @@ -77,6 +81,7 @@ public bool Equals(InputOption other) IgnoreGitPath == other.IgnoreGitPath && IgnoreObjectSource == other.IgnoreObjectSource && IgnoreRepositoryCommon == other.IgnoreRepositoryCommon && + IgnoreUnchangedPath == other.IgnoreUnchangedPath && ObjectPath == other.ObjectPath && PathIgnore == other.PathIgnore && TargetType == other.TargetType; @@ -92,6 +97,7 @@ public override int GetHashCode() hash = hash * 23 + (IgnoreGitPath.HasValue ? IgnoreGitPath.Value.GetHashCode() : 0); hash = hash * 23 + (IgnoreObjectSource.HasValue ? IgnoreObjectSource.Value.GetHashCode() : 0); hash = hash * 23 + (IgnoreRepositoryCommon.HasValue ? IgnoreRepositoryCommon.Value.GetHashCode() : 0); + hash = hash * 23 + (IgnoreUnchangedPath.HasValue ? IgnoreUnchangedPath.Value.GetHashCode() : 0); hash = hash * 23 + (ObjectPath != null ? ObjectPath.GetHashCode() : 0); hash = hash * 23 + (PathIgnore != null ? PathIgnore.GetHashCode() : 0); hash = hash * 23 + (TargetType != null ? TargetType.GetHashCode() : 0); @@ -111,6 +117,7 @@ internal static InputOption Combine(InputOption o1, InputOption o2) IgnoreGitPath = o1.IgnoreGitPath ?? o2.IgnoreGitPath, IgnoreObjectSource = o1.IgnoreObjectSource ?? o2.IgnoreObjectSource, IgnoreRepositoryCommon = o1.IgnoreRepositoryCommon ?? o2.IgnoreRepositoryCommon, + IgnoreUnchangedPath = o1.IgnoreUnchangedPath ?? o2.IgnoreUnchangedPath, ObjectPath = o1.ObjectPath ?? o2.ObjectPath, PathIgnore = o1.PathIgnore ?? o2.PathIgnore, TargetType = o1.TargetType ?? o2.TargetType @@ -142,6 +149,12 @@ internal static InputOption Combine(InputOption o1, InputOption o2) [DefaultValue(null)] public bool? IgnoreRepositoryCommon { get; set; } + /// + /// Determine if unchanged files are ignored. + /// + [DefaultValue(null)] + public bool? IgnoreUnchangedPath { get; set; } + /// /// The object path to a property to use instead of the pipeline object. /// @@ -174,6 +187,9 @@ internal void Load(EnvironmentHelper env) if (env.TryBool("PSRULE_INPUT_IGNOREREPOSITORYCOMMON", out var ignoreRepositoryCommon)) IgnoreRepositoryCommon = ignoreRepositoryCommon; + if (env.TryBool("PSRULE_INPUT_IGNOREUNCHANGEDPATH", out var ignoreUnchangedPath)) + IgnoreUnchangedPath = ignoreUnchangedPath; + if (env.TryString("PSRULE_INPUT_OBJECTPATH", out var objectPath)) ObjectPath = objectPath; @@ -198,6 +214,9 @@ internal void Load(Dictionary index) if (index.TryPopBool("Input.IgnoreRepositoryCommon", out var ignoreRepositoryCommon)) IgnoreRepositoryCommon = ignoreRepositoryCommon; + if (index.TryPopBool("Input.IgnoreUnchangedPath", out var ignoreUnchangedPath)) + IgnoreUnchangedPath = ignoreUnchangedPath; + if (index.TryPopString("Input.ObjectPath", out var objectPath)) ObjectPath = objectPath; diff --git a/src/PSRule/Configuration/RepositoryOption.cs b/src/PSRule/Configuration/RepositoryOption.cs index 157418ddb1..c9a5d0d1c1 100644 --- a/src/PSRule/Configuration/RepositoryOption.cs +++ b/src/PSRule/Configuration/RepositoryOption.cs @@ -22,6 +22,7 @@ public sealed class RepositoryOption : IEquatable /// public RepositoryOption() { + BaseRef = null; Url = null; } @@ -34,6 +35,7 @@ public RepositoryOption(RepositoryOption option) if (option == null) return; + BaseRef = option.BaseRef; Url = option.Url; } @@ -48,6 +50,7 @@ public override bool Equals(object obj) public bool Equals(RepositoryOption other) { return other != null && + BaseRef == other.BaseRef && Url == other.Url; } @@ -57,6 +60,7 @@ public override int GetHashCode() unchecked // Overflow is fine { var hash = 17; + hash = hash * 23 + (BaseRef != null ? BaseRef.GetHashCode() : 0); hash = hash * 23 + (Url != null ? Url.GetHashCode() : 0); return hash; } @@ -70,11 +74,18 @@ internal static RepositoryOption Combine(RepositoryOption o1, RepositoryOption o { var result = new RepositoryOption(o1) { + BaseRef = o1.BaseRef ?? o2.BaseRef, Url = o1.Url ?? o2.Url, }; return result; } + /// + /// Sets the repository base ref used for comparisons of changed files. + /// + [DefaultValue(null)] + public string BaseRef { get; set; } + /// /// Configures the repository URL to report in output. /// @@ -87,6 +98,9 @@ internal static RepositoryOption Combine(RepositoryOption o1, RepositoryOption o /// internal void Load(EnvironmentHelper env) { + if (env.TryString("PSRULE_REPOSITORY_BASEREF", out var baseRef)) + BaseRef = baseRef; + if (env.TryString("PSRULE_REPOSITORY_URL", out var url)) Url = url; } @@ -97,6 +111,9 @@ internal void Load(EnvironmentHelper env) /// internal void Load(Dictionary index) { + if (index.TryPopString("Repository.BaseRef", out var baseRef)) + BaseRef = baseRef; + if (index.TryPopString("Repository.Url", out var url)) Url = url; } diff --git a/src/PSRule/Data/InputFileInfoCollection.cs b/src/PSRule/Data/InputFileInfoCollection.cs new file mode 100644 index 0000000000..cc51259603 --- /dev/null +++ b/src/PSRule/Data/InputFileInfoCollection.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace PSRule.Data +{ + /// + /// A collection of . + /// + public interface IInputFileInfoCollection : IEnumerable + { + /// + /// Filters the collection to only include with a specific file extension. + /// + /// A file extension to filter the collection to. + /// A filtered collection. + IInputFileInfoCollection WithExtension(string extension); + } + + /// + /// A collection of . + /// + internal sealed class InputFileInfoCollection : IInputFileInfoCollection, IEnumerable + { + private readonly IEnumerable _Items; + + public InputFileInfoCollection(IEnumerable items) + { + _Items = items; + } + + public InputFileInfoCollection(string basePath, string[] items) + { + _Items = items != null && items.Length > 0 ? items.Select(i => new InputFileInfo(basePath, i)).ToArray() : Array.Empty(); + } + + public IInputFileInfoCollection WithExtension(string extension) + { + return new InputFileInfoCollection(_Items.Where(i => i.Extension == extension)); + } + + #region IEnumerable + + public IEnumerator GetEnumerator() + { + return _Items.GetEnumerator(); + } + + #endregion IEnumerable + + #region IEnumerable + + IEnumerator IEnumerable.GetEnumerator() + { + return _Items.GetEnumerator(); + } + + #endregion IEnumerable + } +} diff --git a/src/PSRule/PSRule.psm1 b/src/PSRule/PSRule.psm1 index f6195ddeb6..a7ac5b2ba4 100644 --- a/src/PSRule/PSRule.psm1 +++ b/src/PSRule/PSRule.psm1 @@ -1226,6 +1226,10 @@ function New-PSRuleOption { [Parameter(Mandatory = $False)] [System.Boolean]$InputIgnoreObjectSource = $False, + # Sets the Input.IgnoreUnchangedPath option + [Parameter(Mandatory = $False)] + [System.Boolean]$InputIgnoreUnchangedPath = $False, + # Sets the Input.ObjectPath option [Parameter(Mandatory = $False)] [Alias('InputObjectPath')] @@ -1309,6 +1313,10 @@ function New-PSRuleOption { [Alias('JsonIndent')] [int]$OutputJsonIndent = 0, + # Sets the Repository.BaseRef option + [Parameter(Mandatory = $False)] + [String]$RepositoryBaseRef, + # Sets the Repository.Url option [Parameter(Mandatory = $False)] [String]$RepositoryUrl, @@ -1510,6 +1518,10 @@ function Set-PSRuleOption { [Parameter(Mandatory = $False)] [System.Boolean]$InputIgnoreRepositoryCommon = $True, + # Sets the Input.IgnoreUnchangedPath option + [Parameter(Mandatory = $False)] + [System.Boolean]$InputIgnoreUnchangedPath = $False, + # Sets the Input.ObjectPath option [Parameter(Mandatory = $False)] [Alias('InputObjectPath')] @@ -1593,6 +1605,10 @@ function Set-PSRuleOption { [Alias('JsonIndent')] [Int]$OutputJsonIndent = 0, + # Sets the Repository.BaseRef option + [Parameter(Mandatory = $False)] + [String]$RepositoryBaseRef, + # Sets the Repository.Url option [Parameter(Mandatory = $False)] [String]$RepositoryUrl, @@ -2241,6 +2257,10 @@ function SetOptions { [Parameter(Mandatory = $False)] [System.Boolean]$InputIgnoreRepositoryCommon = $True, + # Sets the Input.IgnoreUnchangedPath option + [Parameter(Mandatory = $False)] + [System.Boolean]$InputIgnoreUnchangedPath = $False, + # Sets the Input.ObjectPath option [Parameter(Mandatory = $False)] [Alias('InputObjectPath')] @@ -2324,6 +2344,10 @@ function SetOptions { [Alias('JsonIndent')] [Int]$OutputJsonIndent = 0, + # Sets the Repository.BaseRef option + [Parameter(Mandatory = $False)] + [String]$RepositoryBaseRef, + # Sets the Repository.Url option [Parameter(Mandatory = $False)] [String]$RepositoryUrl, @@ -2440,6 +2464,11 @@ function SetOptions { $Option.Input.IgnoreRepositoryCommon = $InputIgnoreRepositoryCommon; } + # Sets option Input.IgnoreUnchangedPath + if ($PSBoundParameters.ContainsKey('InputIgnoreUnchangedPath')) { + $Option.Input.IgnoreUnchangedPath = $InputIgnoreUnchangedPath; + } + # Sets option Input.ObjectPath if ($PSBoundParameters.ContainsKey('ObjectPath')) { $Option.Input.ObjectPath = $ObjectPath; @@ -2530,6 +2559,11 @@ function SetOptions { $Option.Output.Style = $OutputStyle; } + # Sets option Repository.BaseRef + if ($PSBoundParameters.ContainsKey('RepositoryBaseRef')) { + $Option.Repository.BaseRef = $RepositoryBaseRef; + } + # Sets option Repository.Url if ($PSBoundParameters.ContainsKey('RepositoryUrl')) { $Option.Repository.Url = $RepositoryUrl; diff --git a/src/PSRule/Pipeline/GetTargetPipeline.cs b/src/PSRule/Pipeline/GetTargetPipeline.cs index 11bf67c4d4..1ae7e2782e 100644 --- a/src/PSRule/Pipeline/GetTargetPipeline.cs +++ b/src/PSRule/Pipeline/GetTargetPipeline.cs @@ -4,7 +4,6 @@ using System; using System.Management.Automation; using PSRule.Configuration; -using PSRule.Data; namespace PSRule.Pipeline { @@ -18,7 +17,7 @@ public interface IGetTargetPipelineBuilder : IPipelineBuilder /// internal sealed class GetTargetPipelineBuilder : PipelineBuilderBase, IGetTargetPipelineBuilder { - private InputFileInfo[] _InputPath; + private InputPathBuilder _InputPath; internal GetTargetPipelineBuilder(Source[] source, IHostContext hostContext) : base(source, hostContext) @@ -47,9 +46,16 @@ public void InputPath(string[] path) if (path == null || path.Length == 0) return; - var builder = new InputPathBuilder(GetOutput(), PSRuleOption.GetWorkingPath(), "*", GetInputFilter()); + PathFilter required = null; + if (TryChangedFiles(out var files)) + { + required = PathFilter.Create(PSRuleOption.GetWorkingPath(), path); + path = files; + } + + var builder = new InputPathBuilder(GetOutput(), PSRuleOption.GetWorkingPath(), "*", GetInputFilter(), required); builder.Add(path); - _InputPath = builder.Build(); + _InputPath = builder; } public override IPipeline Build(IPipelineWriter writer = null) diff --git a/src/PSRule/Pipeline/InputPathBuilder.cs b/src/PSRule/Pipeline/InputPathBuilder.cs index 633da9a816..7e3bf3e501 100644 --- a/src/PSRule/Pipeline/InputPathBuilder.cs +++ b/src/PSRule/Pipeline/InputPathBuilder.cs @@ -1,11 +1,11 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. namespace PSRule.Pipeline { internal sealed class InputPathBuilder : PathBuilder { - public InputPathBuilder(IPipelineWriter logger, string basePath, string searchPattern, PathFilter filter) - : base(logger, basePath, searchPattern, filter) { } + public InputPathBuilder(IPipelineWriter logger, string basePath, string searchPattern, PathFilter filter, PathFilter required) + : base(logger, basePath, searchPattern, filter, required) { } } } diff --git a/src/PSRule/Pipeline/InvokePipelineBuilder.cs b/src/PSRule/Pipeline/InvokePipelineBuilder.cs index 53d9afd023..84943be8e8 100644 --- a/src/PSRule/Pipeline/InvokePipelineBuilder.cs +++ b/src/PSRule/Pipeline/InvokePipelineBuilder.cs @@ -6,7 +6,6 @@ using System.IO; using System.Linq; using PSRule.Configuration; -using PSRule.Data; using PSRule.Host; namespace PSRule.Pipeline @@ -37,7 +36,7 @@ public interface IInvokePipelineBuilder : IPipelineBuilder internal abstract class InvokePipelineBuilderBase : PipelineBuilderBase, IInvokePipelineBuilder { - protected InputFileInfo[] _InputPath; + protected InputPathBuilder _InputPath; protected string _ResultVariableName; private List _TrustedPublishers; @@ -53,9 +52,16 @@ public void InputPath(string[] path) if (path == null || path.Length == 0) return; - var builder = new InputPathBuilder(GetOutput(), PSRuleOption.GetWorkingPath(), "*", GetInputFilter()); + PathFilter required = null; + if (TryChangedFiles(out var files)) + { + required = PathFilter.Create(PSRuleOption.GetWorkingPath(), path); + path = files; + } + + var builder = new InputPathBuilder(GetOutput(), PSRuleOption.GetWorkingPath(), "*", GetInputFilter(), required); builder.Add(path); - _InputPath = builder.Build(); + _InputPath = builder; } public void ResultVariable(string variableName) diff --git a/src/PSRule/Pipeline/PathBuilder.cs b/src/PSRule/Pipeline/PathBuilder.cs index c695612432..f0f223132d 100644 --- a/src/PSRule/Pipeline/PathBuilder.cs +++ b/src/PSRule/Pipeline/PathBuilder.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Collections.Generic; @@ -10,16 +10,16 @@ namespace PSRule.Pipeline { - public interface IPathBuilder - { - void Add(string path); + //public interface IPathBuilder + //{ + // void Add(string path); - void Add(FileInfo[] fileInfo); + // void Add(FileInfo[] fileInfo); - void Add(PathInfo[] pathInfo); + // void Add(PathInfo[] pathInfo); - InputFileInfo[] Build(); - } + // InputFileInfo[] Build(); + //} internal abstract class PathBuilder { @@ -40,8 +40,9 @@ internal abstract class PathBuilder private readonly string _BasePath; private readonly string _DefaultSearchPattern; private readonly PathFilter _GlobalFilter; + private readonly PathFilter _Required; - protected PathBuilder(IPipelineWriter logger, string basePath, string searchPattern, PathFilter filter) + protected PathBuilder(IPipelineWriter logger, string basePath, string searchPattern, PathFilter filter, PathFilter required) { _Logger = logger; _Files = new List(); @@ -49,8 +50,14 @@ protected PathBuilder(IPipelineWriter logger, string basePath, string searchPatt _BasePath = NormalizePath(PSRuleOption.GetRootedBasePath(basePath)); _DefaultSearchPattern = searchPattern; _GlobalFilter = filter; + _Required = required; } + /// + /// The number of files found. + /// + public int Count => _Files.Count; + public void Add(string[] path) { if (path == null || path.Length == 0) @@ -218,6 +225,7 @@ private static string TrimPath(string path, out bool relativeAnchor) private bool ShouldInclude(string file, PathFilter filter) { return (filter == null || filter.Match(file)) && + (_Required == null || _Required.Match(file)) && (_GlobalFilter == null || _GlobalFilter.Match(file)); } diff --git a/src/PSRule/Pipeline/PipelineBuilder.cs b/src/PSRule/Pipeline/PipelineBuilder.cs index 9333d903a8..4d93143fab 100644 --- a/src/PSRule/Pipeline/PipelineBuilder.cs +++ b/src/PSRule/Pipeline/PipelineBuilder.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.Linq; using System.Management.Automation; +using System.Threading; using PSRule.Configuration; using PSRule.Data; using PSRule.Definitions; @@ -290,7 +291,7 @@ public virtual IPipelineBuilder Configure(PSRuleOption option) Option.Output = new OutputOption(option.Output); Option.Output.Outcome ??= OutputOption.Default.Outcome; Option.Output.Banner ??= OutputOption.Default.Banner; - Option.Repository = GetRepository(Option.Repository); + Option.Repository = GetRepository(option.Repository); return this; } @@ -467,6 +468,9 @@ protected static RepositoryOption GetRepository(RepositoryOption option) if (string.IsNullOrEmpty(result.Url) && GitHelper.TryRepository(out var url)) result.Url = url; + if (string.IsNullOrEmpty(result.BaseRef) && GitHelper.TryBaseRef(out var baseRef)) + result.BaseRef = baseRef; + return result; } @@ -562,25 +566,33 @@ protected void AddVisitTargetObjectAction(VisitTargetObjectAction action) /// Normalizes JSON indent range between minimum 0 and maximum 4. /// /// - /// + /// The number of characters to indent. protected static int NormalizeJsonIndentRange(int? jsonIndent) { if (jsonIndent.HasValue) { if (jsonIndent < MIN_JSON_INDENT) - { return MIN_JSON_INDENT; - } else if (jsonIndent > MAX_JSON_INDENT) - { return MAX_JSON_INDENT; - } return jsonIndent.Value; } - return MIN_JSON_INDENT; } + + protected bool TryChangedFiles(out string[] files) + { + files = null; + if (!Option.Input.IgnoreUnchangedPath.GetValueOrDefault(InputOption.Default.IgnoreUnchangedPath.Value) || + !GitHelper.TryGetChangedFiles(Option.Repository.BaseRef, "d", null, out files)) + return false; + + for (var i = 0; i < files.Length; i++) + HostContext.Verbose(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.UsingChangedFile, files[i])); + + return true; + } } } diff --git a/src/PSRule/Pipeline/PipelineReader.cs b/src/PSRule/Pipeline/PipelineReader.cs index a4ba67bb97..e02b38dc60 100644 --- a/src/PSRule/Pipeline/PipelineReader.cs +++ b/src/PSRule/Pipeline/PipelineReader.cs @@ -4,18 +4,17 @@ using System; using System.Collections.Concurrent; using System.Management.Automation; -using PSRule.Data; namespace PSRule.Pipeline { internal sealed class PipelineReader { private readonly VisitTargetObject _Input; - private readonly InputFileInfo[] _InputPath; + private readonly InputPathBuilder _InputPath; private readonly PathFilter _InputFilter; private readonly ConcurrentQueue _Queue; - public PipelineReader(VisitTargetObject input, InputFileInfo[] inputPath, PathFilter inputFilter) + public PipelineReader(VisitTargetObject input, InputPathBuilder inputPath, PathFilter inputFilter) { _Input = input; _InputPath = inputPath; @@ -55,19 +54,20 @@ public bool TryDequeue(out TargetObject sourceObject) public void Open() { - if (_InputPath == null || _InputPath.Length == 0) + if (_InputPath == null || _InputPath.Count == 0) return; // Read each file - for (var i = 0; i < _InputPath.Length; i++) + var files = _InputPath.Build(); + for (var i = 0; i < files.Length; i++) { - if (_InputPath[i].IsUrl) + if (files[i].IsUrl) { - Enqueue(PSObject.AsPSObject(new Uri(_InputPath[i].FullName))); + Enqueue(PSObject.AsPSObject(new Uri(files[i].FullName))); } else { - Enqueue(PSObject.AsPSObject(_InputPath[i])); + Enqueue(PSObject.AsPSObject(files[i])); } } } @@ -88,5 +88,14 @@ private bool ShouldQueue(TargetObject targetObject) } return true; } + + /// + /// Add a path to the list of inputs. + /// + /// The path of files to add. + internal void Add(string path) + { + _InputPath.Add(path); + } } } diff --git a/src/PSRule/Pipeline/PipelineWriterExtensions.cs b/src/PSRule/Pipeline/PipelineWriterExtensions.cs index c8feb0cd1f..f4470e3461 100644 --- a/src/PSRule/Pipeline/PipelineWriterExtensions.cs +++ b/src/PSRule/Pipeline/PipelineWriterExtensions.cs @@ -8,6 +8,9 @@ namespace PSRule.Pipeline { + /// + /// Extensions for the . + /// public static class PipelineWriterExtensions { public static void WriteDebug(this IPipelineWriter writer, DebugRecord debugRecord) @@ -82,6 +85,14 @@ internal static void ErrorReadFileFailed(this IPipelineWriter writer, string pat ); } + internal static void VerboseInputAdded(this IPipelineWriter writer, string path) + { + if (writer == null || !writer.ShouldWriteVerbose() || string.IsNullOrEmpty(path)) + return; + + writer.WriteVerbose(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.InputAdded, path)); + } + internal static void WriteError(this IPipelineWriter writer, PipelineException exception, string errorId, ErrorCategory errorCategory) { if (writer == null) diff --git a/src/PSRule/Pipeline/RulePipeline.cs b/src/PSRule/Pipeline/RulePipeline.cs index b8b00ce61d..b78da6cd95 100644 --- a/src/PSRule/Pipeline/RulePipeline.cs +++ b/src/PSRule/Pipeline/RulePipeline.cs @@ -35,9 +35,9 @@ protected RulePipeline(PipelineContext context, Source[] source, PipelineReader /// public virtual void Begin() { - Reader.Open(); Writer.Begin(); Context.Begin(); + Reader.Open(); } /// diff --git a/src/PSRule/Resources/PSRuleResources.Designer.cs b/src/PSRule/Resources/PSRuleResources.Designer.cs index c3b501f2cb..9f40e83125 100644 --- a/src/PSRule/Resources/PSRuleResources.Designer.cs +++ b/src/PSRule/Resources/PSRuleResources.Designer.cs @@ -303,6 +303,15 @@ internal static string InfoOutputPath { } } + /// + /// Looks up a localized string similar to The input path was added: {0}. + /// + internal static string InputAdded { + get { + return ResourceManager.GetString("InputAdded", resourceCulture); + } + } + /// /// Looks up a localized string similar to An invalid ErrorAction ({0}) was specified for rule at {1}. Ignore | Stop are supported.. /// @@ -780,6 +789,15 @@ internal static string SourceNotFound { } } + /// + /// Looks up a localized string similar to Using changed files: {0}. + /// + internal static string UsingChangedFile { + get { + return ResourceManager.GetString("UsingChangedFile", resourceCulture); + } + } + /// /// Looks up a localized string similar to Using invariant culture may cause rule infomation to be displayed incorrectly. Consider using -Culture or set the Output.Culture option.. /// diff --git a/src/PSRule/Resources/PSRuleResources.resx b/src/PSRule/Resources/PSRuleResources.resx index 4097ade673..0d5e26d061 100644 --- a/src/PSRule/Resources/PSRuleResources.resx +++ b/src/PSRule/Resources/PSRuleResources.resx @@ -385,4 +385,10 @@ Target failed sub-selector pre-condition. + + The input path was added: {0} + + + Using changed files: {0} + \ No newline at end of file diff --git a/src/PSRule/Runtime/IInputCollection.cs b/src/PSRule/Runtime/IInputCollection.cs new file mode 100644 index 0000000000..4bc69cc1c8 --- /dev/null +++ b/src/PSRule/Runtime/IInputCollection.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Runtime +{ + /// + /// A collection of input passed to PSRule for anlaysis. + /// + public interface IInputCollection + { + /// + /// Add a path to the list of inputs. + /// + /// The path of files to add. + void Add(string path); + } +} diff --git a/src/PSRule/Runtime/IRepositoryRuntimeInfo.cs b/src/PSRule/Runtime/IRepositoryRuntimeInfo.cs new file mode 100644 index 0000000000..2fd3d64240 --- /dev/null +++ b/src/PSRule/Runtime/IRepositoryRuntimeInfo.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using PSRule.Data; + +namespace PSRule.Runtime +{ + /// + /// Display information about the current repository at runtime. + /// + public interface IRepositoryRuntimeInfo + { + /// + /// A URL to the current repository. + /// + string Url { get; } + + /// + /// The base ref for the current repository branch. + /// + string BaseRef { get; } + + /// + /// Get a list of changed files within the repository. + /// + /// A collection of files. + IInputFileInfoCollection GetChangedFiles(); + } +} diff --git a/src/PSRule/Runtime/PSRule.cs b/src/PSRule/Runtime/PSRule.cs index 83d0a53c39..ea4c04b7c3 100644 --- a/src/PSRule/Runtime/PSRule.cs +++ b/src/PSRule/Runtime/PSRule.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Management.Automation; using PSRule.Badges; +using PSRule.Configuration; using PSRule.Data; using PSRule.Pipeline; @@ -19,8 +20,10 @@ namespace PSRule.Runtime public sealed class PSRule : ScopedItem { private ITargetSourceCollection _Source; + private IInputCollection _Input; private ITargetIssueCollection _Issue; private IBadgeBuilder _BadgeBuilder; + private IRepositoryRuntimeInfo _Repository; /// /// Create an empty instance. @@ -60,6 +63,45 @@ public bool Any(string type = null) } } + private sealed class PSRuleInput : ScopedItem, IInputCollection + { + internal PSRuleInput(RunspaceContext context) + : base(context) { } + + /// + public void Add(string path) + { + var context = GetContext(); + context.Writer.VerboseInputAdded(path); + context.Pipeline.Reader.Add(path); + } + } + + private sealed class PSRuleRepository : ScopedItem, IRepositoryRuntimeInfo + { + private InputFileInfoCollection _ChangedFiles; + + internal PSRuleRepository(RunspaceContext context) + : base(context) + { + Url = GetContext().Pipeline.Option.Repository.Url; + BaseRef = GetContext().Pipeline.Option.Repository.BaseRef; + } + + /// + public string Url { get; } + + /// + public string BaseRef { get; } + + /// + public IInputFileInfoCollection GetChangedFiles() + { + _ChangedFiles ??= new InputFileInfoCollection(PSRuleOption.GetWorkingPath(), GitHelper.TryGetChangedFiles(BaseRef, "d", null, out var files) ? files : null); + return _ChangedFiles; + } + } + /// /// Exposes the badge API for used within conventions. /// @@ -68,9 +110,7 @@ public IBadgeBuilder Badges get { RequireScope(RunspaceScope.ConventionEnd); - if (_BadgeBuilder == null) - _BadgeBuilder = new BadgeBuilder(); - + _BadgeBuilder ??= new BadgeBuilder(); return _BadgeBuilder; } } @@ -105,6 +145,22 @@ public Hashtable Field } } + /// + /// A list of pre-defined input path that are included. + /// + /// + /// This property can only be accessed from an initialize convention block. + /// + /// + /// Thrown when accessing this property outside of an initialize convention block. + /// + public IInputCollection Input => GetInput(); + + /// + /// Information about the repository that is currently being used. + /// + public IRepositoryRuntimeInfo Repository => GetRepository(); + /// /// An aggregated set of results from executing PSRule rules. /// @@ -293,21 +349,30 @@ public object GetService(string id) private ITargetSourceCollection GetSource() { RequireScope(RunspaceScope.Target); - if (_Source == null) - _Source = new PSRuleSource(GetContext()); - + _Source ??= new PSRuleSource(GetContext()); return _Source; } + private IInputCollection GetInput() + { + RequireScope(RunspaceScope.ConventionInitialize); + _Input ??= new PSRuleInput(GetContext()); + return _Input; + } + private ITargetIssueCollection GetIssue() { RequireScope(RunspaceScope.Target); - if (_Issue == null) - _Issue = new PSRuleIssue(GetContext()); - + _Issue ??= new PSRuleIssue(GetContext()); return _Issue; } + private IRepositoryRuntimeInfo GetRepository() + { + _Repository ??= new PSRuleRepository(GetContext()); + return _Repository; + } + #endregion Helper methods } } diff --git a/tests/PSRule.Tests/FromFileGit.Rule.ps1 b/tests/PSRule.Tests/FromFileGit.Rule.ps1 new file mode 100644 index 0000000000..fbe993890d --- /dev/null +++ b/tests/PSRule.Tests/FromFileGit.Rule.ps1 @@ -0,0 +1,20 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Export-PSRuleConvention 'AddModuleFiles' -Initialize { + foreach ($inputFile in $PSRule.Repository.GetChangedFiles().WithExtension('.bicep')) { + # Get module path + $modulePath = $inputFile.AsFileInfo().Directory; + while (!$modulePath.Name.StartsWith('v')) { + $modulePath = $modulePath.Parent; + } + $moduleVersion = $modulePath.Name; + $moduleName = $modulePath.Parent.Name; + + # Add whole module path to input files + $PSRule.Input.Add($modulePath.FullName); + + # Add matching docs + $PSRule.Input.Add("docs/modules/$moduleName-$moduleVersion/"); + } +} diff --git a/tests/PSRule.Tests/InputPathBuilderTests.cs b/tests/PSRule.Tests/InputPathBuilderTests.cs index 1026aac94f..cbaf52263b 100644 --- a/tests/PSRule.Tests/InputPathBuilderTests.cs +++ b/tests/PSRule.Tests/InputPathBuilderTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; @@ -13,7 +13,7 @@ public sealed class InputPathBuilderTests [Fact] public void GetPath() { - var builder = new InputPathBuilder(null, GetWorkingPath(), "*", null); + var builder = new InputPathBuilder(null, GetWorkingPath(), "*", null, null); builder.Add("."); var actual = builder.Build(); Assert.True(actual.Length > 100); @@ -59,6 +59,26 @@ public void GetPath() Assert.True(actual.Length > 100); } + [Fact] + public void GetPathRequired() + { + var required = PathFilter.Create(GetWorkingPath(), new string[] { "README.md" }); + var builder = new InputPathBuilder(null, GetWorkingPath(), "*", null, required); + builder.Add("."); + var actual = builder.Build(); + Assert.True(actual.Length == 1); + + builder.Add(GetWorkingPath()); + actual = builder.Build(); + Assert.True(actual.Length == 1); + + required = PathFilter.Create(GetWorkingPath(), new string[] { "." }); + builder = new InputPathBuilder(null, GetWorkingPath(), "*", null, required); + builder.Add("."); + actual = builder.Build(); + Assert.True(actual.Length > 100); + } + private static string GetWorkingPath() { return Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../../..")); diff --git a/tests/PSRule.Tests/PSRule.Options.Tests.ps1 b/tests/PSRule.Tests/PSRule.Options.Tests.ps1 index 91aff50efa..a03f1685ae 100644 --- a/tests/PSRule.Tests/PSRule.Options.Tests.ps1 +++ b/tests/PSRule.Tests/PSRule.Options.Tests.ps1 @@ -1131,6 +1131,45 @@ Describe 'New-PSRuleOption' -Tag 'Option','New-PSRuleOption' { } } + Context 'Read Input.IgnoreUnchangedPath' { + It 'from default' { + $option = New-PSRuleOption -Default; + $option.Input.IgnoreUnchangedPath | Should -Be $False; + } + + It 'from Hashtable' { + $option = New-PSRuleOption -Option @{ 'Input.IgnoreUnchangedPath' = $True }; + $option.Input.IgnoreUnchangedPath | Should -Be $True; + } + + It 'from YAML' { + $option = New-PSRuleOption -Option (Join-Path -Path $here -ChildPath 'PSRule.Tests.yml'); + $option.Input.IgnoreUnchangedPath | Should -Be $True; + } + + It 'from Environment' { + try { + # With bool + $Env:PSRULE_INPUT_IGNOREUNCHANGEDPATH = 'true'; + $option = New-PSRuleOption; + $option.Input.IgnoreUnchangedPath | Should -Be $True; + + # With int + $Env:PSRULE_INPUT_IGNOREUNCHANGEDPATH = '1'; + $option = New-PSRuleOption; + $option.Input.IgnoreUnchangedPath | Should -Be $True; + } + finally { + Remove-Item 'Env:PSRULE_INPUT_IGNOREUNCHANGEDPATH' -Force; + } + } + + It 'from parameter' { + $option = New-PSRuleOption -InputIgnoreUnchangedPath $True -Path $emptyOptionsFilePath; + $option.Input.IgnoreUnchangedPath | Should -Be $True; + } + } + Context 'Read Input.ObjectPath' { It 'from default' { $option = New-PSRuleOption -Default; @@ -1752,6 +1791,39 @@ Describe 'New-PSRuleOption' -Tag 'Option','New-PSRuleOption' { } } + Context 'Read Repository.BaseRef' { + It 'from default' { + $option = New-PSRuleOption -Default; + $option.Repository.BaseRef | Should -BeNullOrEmpty; + } + + It 'from Hashtable' { + $option = New-PSRuleOption -Option @{ 'Repository.BaseRef' = 'dev' }; + $option.Repository.BaseRef | Should -Be 'dev'; + } + + It 'from YAML' { + $option = New-PSRuleOption -Option (Join-Path -Path $here -ChildPath 'PSRule.Tests.yml'); + $option.Repository.BaseRef | Should -Be 'dev'; + } + + It 'from Environment' { + try { + $Env:PSRULE_REPOSITORY_BASEREF = 'dev'; + $option = New-PSRuleOption; + $option.Repository.BaseRef | Should -Be 'dev'; + } + finally { + Remove-Item 'Env:PSRULE_REPOSITORY_BASEREF' -Force; + } + } + + It 'from parameter' { + $option = New-PSRuleOption -RepositoryBaseRef 'dev' -Path $emptyOptionsFilePath; + $option.Repository.BaseRef | Should -Be 'dev'; + } + } + Context 'Read Repository.Url' { It 'from default' { $option = New-PSRuleOption -Default; @@ -2009,6 +2081,13 @@ Describe 'Set-PSRuleOption' -Tag 'Option','Set-PSRuleOption' { } } + Context 'Read Input.IgnoreUnchangedPath' { + It 'from parameter' { + $option = Set-PSRuleOption -InputIgnoreUnchangedPath $True @optionParams; + $option.Input.IgnoreUnchangedPath | Should -Be $True; + } + } + Context 'Read Input.ObjectPath' { It 'from parameter' { $option = Set-PSRuleOption -ObjectPath 'items' @optionParams; @@ -2135,6 +2214,13 @@ Describe 'Set-PSRuleOption' -Tag 'Option','Set-PSRuleOption' { } } + Context 'Read Repository.BaseRef' { + It 'from parameter' { + $option = Set-PSRuleOption -RepositoryBaseRef 'dev' @optionParams; + $option.Repository.BaseRef | Should -Be 'dev'; + } + } + Context 'Read Repository.Url' { It 'from parameter' { $option = Set-PSRuleOption -RepositoryUrl 'https://github.com/microsoft/PSRule.UnitTest' @optionParams; diff --git a/tests/PSRule.Tests/PSRule.Tests.yml b/tests/PSRule.Tests/PSRule.Tests.yml index ffa750e1bf..36ad3679b8 100644 --- a/tests/PSRule.Tests/PSRule.Tests.yml +++ b/tests/PSRule.Tests/PSRule.Tests.yml @@ -2,6 +2,7 @@ repository: url: 'https://github.com/microsoft/PSRule.UnitTest' + baseRef: dev # Configure baseline rule: @@ -68,6 +69,7 @@ input: - '*.Designer.cs' targetType: - virtualMachine + ignoreUnchangedPath: true # Configure logging options logging: From f1706f88017f38a86ccac31f0c4789bd064d146c Mon Sep 17 00:00:00 2001 From: Bernie White Date: Mon, 3 Oct 2022 21:01:57 +1000 Subject: [PATCH 077/156] Pre-release v2.5.0-B0045 (#1296) --- docs/CHANGELOG-v2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 4c3807b701..08f0e1a93f 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -26,6 +26,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.5.0-B0045 (pre-release) + What's changed since pre-release v2.5.0-B0015: - New features: From 42acf1c48b82f9cf6546491639771e3b0c077437 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Tue, 4 Oct 2022 09:35:57 +1000 Subject: [PATCH 078/156] Update tasks and vscode settings (#1298) * Update tasks and vscode settings * Install for dev --- .devcontainer/Dockerfile | 10 ++ .devcontainer/container-build.ps1 | 4 + .devcontainer/container-start.ps1 | 2 +- .devcontainer/devcontainer.json | 46 +++--- .vscode/settings.json | 224 +++++++++++++++--------------- .vscode/tasks.json | 141 ++++++++++++++++++- pipeline.build.ps1 | 2 +- scripts/dependencies.psm1 | 21 +-- 8 files changed, 306 insertions(+), 144 deletions(-) create mode 100644 .devcontainer/Dockerfile diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000000..2e471b8f74 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Visual Studio Code image with .NET + +# NOTE: +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/dotnet/.devcontainer/base.Dockerfile + +ARG VARIANT="6.0-bullseye-slim" +FROM mcr.microsoft.com/vscode/devcontainers/dotnet:0-${VARIANT} diff --git a/.devcontainer/container-build.ps1 b/.devcontainer/container-build.ps1 index 8663147632..a01331b2b6 100644 --- a/.devcontainer/container-build.ps1 +++ b/.devcontainer/container-build.ps1 @@ -5,6 +5,7 @@ # This is run during container creation. # Install Python 3 dependencies +sudo apt install python3-pip -y sudo python3 -m pip install --upgrade pip sudo python3 -m pip install wheel @@ -19,3 +20,6 @@ if ($Null -eq (Get-InstalledModule -Name PowerShellGet -MinimumVersion 2.2.1 -Er if ($Null -eq (Get-InstalledModule -Name InvokeBuild -MinimumVersion 5.4.0 -ErrorAction Ignore)) { Install-Module InvokeBuild -MinimumVersion 5.4.0 -Scope CurrentUser -Force; } + +Import-Module ./scripts/dependencies.psm1; +Install-Dependencies -Dev; diff --git a/.devcontainer/container-start.ps1 b/.devcontainer/container-start.ps1 index d4dcec37a6..6835f148c7 100644 --- a/.devcontainer/container-start.ps1 +++ b/.devcontainer/container-start.ps1 @@ -5,7 +5,7 @@ # This is run during container startup. # Install Python packages -# pip install -r requirements-docs.txt +pip install -r requirements-docs.txt # Restore .NET packages dotnet restore diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f8f310782d..a821e5180e 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,25 +1,33 @@ { - "name": "PSRule dev", - "settings": { + "$schema": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainer.schema.json", + "name": "PSRule dev", + "customizations": { + "vscode": { + "settings": { "terminal.integrated.defaultProfile.linux": "pwsh", "terminal.integrated.profiles.linux": { - "pwsh": { - "path": "/bin/pwsh" - } + "pwsh": { + "path": "/opt/microsoft/powershell/7/pwsh" + } } - }, - "extensions": [ - "ms-azure-devops.azure-pipelines", - "davidanson.vscode-markdownlint", - "bewhite.psrule-vscode-preview", + }, + "extensions": [ "ms-dotnettools.csharp", - "eamodio.gitlens", - "github.vscode-pull-request-github", - "streetsidesoftware.code-spell-checker" - ], - "features": { - "github-cli": "latest" - }, - "onCreateCommand": "/bin/pwsh -f .devcontainer/container-build.ps1", - "postStartCommand": "/bin/pwsh -f .devcontainer/container-start.ps1" + "bewhite.psrule-vscode-preview" + ] + } + }, + "features": { + "github-cli": "latest", + "powershell": "latest" + }, + "onCreateCommand": "/opt/microsoft/powershell/7/pwsh -f .devcontainer/container-build.ps1", + "postStartCommand": "/opt/microsoft/powershell/7/pwsh -f .devcontainer/container-start.ps1", + "build": { + "dockerfile": "Dockerfile", + "args": { + "VARIANT": "6.0-bullseye-slim" + } + }, + "remoteUser": "vscode" } diff --git a/.vscode/settings.json b/.vscode/settings.json index 199be65f0d..de6a13bc03 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,116 +1,120 @@ { - "files.exclude": { - ".vs/": true, - "out/": true, - "**/bin/": true, - "**/obj/": true, - "TestResults/": true - }, - "search.exclude": { - "out/": true, - "reports/": true - }, - "editor.insertSpaces": true, - "editor.tabSize": 4, - "yaml.format.singleQuote": true, - "yaml.schemas": { - "./schemas/PSRule-options.schema.json": [ - "/tests/PSRule.Tests/PSRule.*.yml", - "/docs/scenarios/*/ps-rule.yaml", - "/ps-rule.yaml" - ], - "./schemas/PSRule-language.schema.json": [ - "/tests/PSRule.Tests/**.Rule.yaml", - "/tests/PSRule.Tests/**/**.Rule.yaml", - "/docs/scenarios/*/*.Rule.yaml", - "/docs/expressions/**/*.Rule.yaml" - ] - }, - "json.schemas": [ - { - "fileMatch": [ - "/tests/PSRule.Tests/**.Rule.jsonc", - "/tests/PSRule.Tests/**/**.Rule.jsonc", - "/docs/expressions/**/*.Rule.jsonc" - ], - "url": "./schemas/PSRule-resources.schema.json" - } + "files.exclude": { + ".vs/": true, + "out/": true, + "**/bin/": true, + "**/obj/": true, + "TestResults/": true + }, + "search.exclude": { + "out/": true, + "reports/": true + }, + "editor.insertSpaces": true, + "editor.tabSize": 4, + "yaml.format.singleQuote": true, + "yaml.schemas": { + "./schemas/PSRule-options.schema.json": [ + "/tests/PSRule.Tests/PSRule.*.yml", + "/docs/scenarios/*/ps-rule.yaml", + "/ps-rule.yaml" ], - "[xml]": { - "editor.tabSize": 2, - "editor.formatOnSave": true - }, - "[yaml]": { - "editor.tabSize": 2, - "files.insertFinalNewline": true - }, - "[markdown]": { - "editor.tabSize": 2, - "files.insertFinalNewline": true - }, - "[json]": { - "editor.tabSize": 4, - "editor.tabCompletion": "on", - "editor.quickSuggestions": { - "strings": true - }, - "editor.suggest.insertMode": "replace", - "gitlens.codeLens.scopes": [ - "document" - ], - "files.insertFinalNewline": true - }, - "[jsonc]": { - "editor.tabSize": 4, - "editor.tabCompletion": "on", - "editor.quickSuggestions": { - "strings": true - }, - "editor.suggest.insertMode": "replace", - "gitlens.codeLens.scopes": [ - "document" - ], - "files.insertFinalNewline": true - }, - "files.associations": { - "**/.azure-pipelines/*.yaml": "azure-pipelines", - "**/.azure-pipelines/jobs/*.yaml": "azure-pipelines", - "**/NuGet.config": "xml" + "./schemas/PSRule-language.schema.json": [ + "/tests/PSRule.Tests/**.Rule.yaml", + "/tests/PSRule.Tests/**/**.Rule.yaml", + "/docs/scenarios/*/*.Rule.yaml", + "/docs/expressions/**/*.Rule.yaml" + ] + }, + "json.schemas": [ + { + "fileMatch": [ + "/tests/PSRule.Tests/**.Rule.jsonc", + "/tests/PSRule.Tests/**/**.Rule.jsonc", + "/docs/expressions/**/*.Rule.jsonc" + ], + "url": "./schemas/PSRule-resources.schema.json" + } + ], + "[xml]": { + "editor.tabSize": 2, + "editor.formatOnSave": true + }, + "[yaml]": { + "editor.tabSize": 2, + "files.insertFinalNewline": true + }, + "[markdown]": { + "editor.tabSize": 2, + "files.insertFinalNewline": true + }, + "[json]": { + "editor.formatOnSave": true, + "editor.detectIndentation": false, + "editor.tabSize": 2, + "editor.tabCompletion": "on", + "editor.quickSuggestions": { + "strings": true }, - "cSpell.words": [ - "APPSERVICEMININSTANCECOUNT", - "cmdlet", - "cmdlets", - "concat", - "datetime", - "deserialize", - "deserialized", - "deserializes", - "Gitter", - "hashtable", - "Kubernetes", - "Newtonsoft", - "NOTCOUNT", - "proxied", - "PSRULE", - "quickstart", - "runspace", - "runspaces", - "SARIF", - "SBOM", - "unencrypted", - "xunit" + "editor.suggest.insertMode": "replace", + "gitlens.codeLens.scopes": [ + "document" ], - "[csharp]": { - "editor.formatOnSave": true, - "files.insertFinalNewline": true + "files.insertFinalNewline": true + }, + "[jsonc]": { + "editor.formatOnSave": true, + "editor.detectIndentation": false, + "editor.tabSize": 2, + "editor.tabCompletion": "on", + "editor.quickSuggestions": { + "strings": true }, - "omnisharp.enableImportCompletion": true, - "omnisharp.organizeImportsOnFormat": true, - "omnisharp.enableEditorConfigSupport": true, - "omnisharp.enableRoslynAnalyzers": true, - "git.branchProtection": [ - "main", - "release/*" - ] + "editor.suggest.insertMode": "replace", + "gitlens.codeLens.scopes": [ + "document" + ], + "files.insertFinalNewline": true + }, + "files.associations": { + "**/.azure-pipelines/*.yaml": "azure-pipelines", + "**/.azure-pipelines/jobs/*.yaml": "azure-pipelines", + "**/NuGet.config": "xml" + }, + "cSpell.words": [ + "APPSERVICEMININSTANCECOUNT", + "cmdlet", + "cmdlets", + "concat", + "datetime", + "deserialize", + "deserialized", + "deserializes", + "Gitter", + "hashtable", + "Kubernetes", + "Newtonsoft", + "NOTCOUNT", + "proxied", + "PSRULE", + "quickstart", + "runspace", + "runspaces", + "SARIF", + "SBOM", + "unencrypted", + "xunit" + ], + "[csharp]": { + "editor.formatOnSave": true, + "files.insertFinalNewline": true + }, + "omnisharp.enableImportCompletion": true, + "omnisharp.organizeImportsOnFormat": true, + "omnisharp.enableEditorConfigSupport": true, + "omnisharp.enableRoslynAnalyzers": true, + "git.branchProtection": [ + "main", + "release/*" + ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8563d71572..8f1aab1ccc 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -18,6 +18,16 @@ "presentation": { "clear": true, "panel": "dedicated" + }, + "linux": { + "options": { + "shell": { + "executable": "pwsh", + "args": [ + "-c" + ] + } + } } }, { @@ -32,6 +42,16 @@ "presentation": { "clear": true, "panel": "dedicated" + }, + "linux": { + "options": { + "shell": { + "executable": "pwsh", + "args": [ + "-c" + ] + } + } } }, { @@ -43,6 +63,16 @@ "presentation": { "clear": true, "panel": "dedicated" + }, + "linux": { + "options": { + "shell": { + "executable": "pwsh", + "args": [ + "-c" + ] + } + } } }, { @@ -54,7 +84,17 @@ "kind": "build", "isDefault": true }, - "problemMatcher": [] + "problemMatcher": [], + "linux": { + "options": { + "shell": { + "executable": "pwsh", + "args": [ + "-c" + ] + } + } + } }, { "label": "coverage", @@ -66,33 +106,83 @@ "presentation": { "clear": true, "panel": "dedicated" + }, + "linux": { + "options": { + "shell": { + "executable": "pwsh", + "args": [ + "-c" + ] + } + } } }, { "label": "build-docs", "type": "shell", "command": "Invoke-Build BuildHelp", - "problemMatcher": [] + "problemMatcher": [], + "linux": { + "options": { + "shell": { + "executable": "pwsh", + "args": [ + "-c" + ] + } + } + } }, { "label": "scaffold-docs", "detail": "Generate cmdlet markdown docs.", "type": "shell", "command": "Invoke-Build ScaffoldHelp", - "problemMatcher": [] + "problemMatcher": [], + "linux": { + "options": { + "shell": { + "executable": "pwsh", + "args": [ + "-c" + ] + } + } + } }, { "label": "clean", "detail": "Clean up temporary working paths.", "type": "shell", "command": "Invoke-Build Clean", - "problemMatcher": [] + "problemMatcher": [], + "linux": { + "options": { + "shell": { + "executable": "pwsh", + "args": [ + "-c" + ] + } + } + } }, { "label": "script-analyzer", "type": "shell", "command": "Invoke-Build Analyze", - "problemMatcher": [] + "problemMatcher": [], + "linux": { + "options": { + "shell": { + "executable": "pwsh", + "args": [ + "-c" + ] + } + } + } }, { "label": "benchmark", @@ -102,6 +192,16 @@ "presentation": { "clear": true, "panel": "dedicated" + }, + "linux": { + "options": { + "shell": { + "executable": "pwsh", + "args": [ + "-c" + ] + } + } } }, { @@ -112,6 +212,16 @@ "presentation": { "clear": true, "panel": "dedicated" + }, + "linux": { + "options": { + "shell": { + "executable": "pwsh", + "args": [ + "-c" + ] + } + } } }, { @@ -125,6 +235,17 @@ "panel": "dedicated" } }, + { + "label": "install python dependencies", + "detail": "Install or upgrade dependencies to build and debug mkdocs documentation locally.", + "type": "shell", + "command": "python3 -m pip install -r requirements-docs.txt", + "problemMatcher": [], + "presentation": { + "clear": true, + "panel": "dedicated" + } + }, { "type": "PSRule", "problemMatcher": [ @@ -154,6 +275,16 @@ "presentation": { "clear": true, "panel": "dedicated" + }, + "linux": { + "options": { + "shell": { + "executable": "pwsh", + "args": [ + "-c" + ] + } + } } } ], diff --git a/pipeline.build.ps1 b/pipeline.build.ps1 index f79baddf08..e7197694cb 100644 --- a/pipeline.build.ps1 +++ b/pipeline.build.ps1 @@ -256,7 +256,7 @@ task NuGet { task Dependencies NuGet, { Import-Module $PWD/scripts/dependencies.psm1; - Install-Dependencies -Path $PWD/modules.json; + Install-Dependencies -Path $PWD/modules.json -Dev; } # Synopsis: Test the module diff --git a/scripts/dependencies.psm1 b/scripts/dependencies.psm1 index 5584f65906..1010d4d3a4 100644 --- a/scripts/dependencies.psm1 +++ b/scripts/dependencies.psm1 @@ -7,8 +7,8 @@ function Update-Dependencies { [CmdletBinding()] param ( - [Parameter(Mandatory = $True)] - [String]$Path, + [Parameter(Mandatory = $False)] + [String]$Path = (Join-Path -Path $PWD -ChildPath 'modules.json'), [Parameter(Mandatory = $False)] [String]$Repository = 'PSGallery' @@ -19,7 +19,7 @@ function Update-Dependencies { $devDependencies = CheckVersion $modules.devDependencies -Repository $Repository -Dev; $modules = [Ordered]@{ - dependencies = $dependencies + dependencies = $dependencies devDependencies = $devDependencies } $modules | ConvertTo-Json -Depth 10 | Set-Content -Path $Path; @@ -45,16 +45,21 @@ function Update-Dependencies { function Install-Dependencies { [CmdletBinding()] param ( - [Parameter(Mandatory = $True)] - [String]$Path, + [Parameter(Mandatory = $False)] + [String]$Path = (Join-Path -Path $PWD -ChildPath 'modules.json'), [Parameter(Mandatory = $False)] - [String]$Repository = 'PSGallery' + [String]$Repository = 'PSGallery', + + [Parameter(Mandatory = $False)] + [Switch]$Dev ) process { $modules = Get-Content -Path $Path -Raw | ConvertFrom-Json; InstallVersion $modules.dependencies -Repository $Repository; - InstallVersion $modules.devDependencies -Repository $Repository -Dev; + if ($Dev) { + InstallVersion $modules.devDependencies -Repository $Repository -Dev; + } } } @@ -133,7 +138,7 @@ function InstallVersion { process { foreach ($module in $InputObject.PSObject.Properties.GetEnumerator()) { Write-Host -Object "[$group] -- Installing $($module.Name) v$($module.Value.version)"; - $installParams = @{ MinimumVersion = $module.Value.version }; + $installParams = @{ RequiredVersion = $module.Value.version }; if ($Null -eq (Get-InstalledModule -Name $module.Name @installParams -ErrorAction Ignore)) { Install-Module -Name $module.Name @installParams -Force -Repository $Repository; } From 446fbe7dc3b47564fef20182d73b076b2c347a5e Mon Sep 17 00:00:00 2001 From: Bernie White Date: Tue, 4 Oct 2022 09:49:09 +1000 Subject: [PATCH 079/156] Add feedback for pages with Giscus (#1299) --- docs/CHANGELOG-v0.md | 4 ++ docs/CHANGELOG-v1.md | 4 ++ docs/CHANGELOG-v2.md | 4 ++ docs/about.md | 1 + docs/addon-modules.md | 4 ++ docs/deprecations.md | 1 + docs/features.md | 1 + docs/license-contributing.md | 1 + docs/related-projects.md | 3 +- docs/support.md | 1 + docs/upgrade-notes.md | 1 + docs/versioning.md | 11 +++-- mkdocs.yml | 6 +++ overrides/main.html | 6 +++ overrides/partials/giscus.html | 76 ++++++++++++++++++++++++++++++++++ 15 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 overrides/partials/giscus.html diff --git a/docs/CHANGELOG-v0.md b/docs/CHANGELOG-v0.md index 46dafbdb4c..ef680b7ea5 100644 --- a/docs/CHANGELOG-v0.md +++ b/docs/CHANGELOG-v0.md @@ -1,3 +1,7 @@ +--- +discussion: false +--- + # Change log See [upgrade notes][1] for helpful information when upgrading from previous versions. diff --git a/docs/CHANGELOG-v1.md b/docs/CHANGELOG-v1.md index 7e3821e661..21d9347b47 100644 --- a/docs/CHANGELOG-v1.md +++ b/docs/CHANGELOG-v1.md @@ -1,3 +1,7 @@ +--- +discussion: false +--- + # Change log See [upgrade notes][1] for helpful information when upgrading from previous versions. diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 08f0e1a93f..8d7c5c6001 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -1,3 +1,7 @@ +--- +discussion: false +--- + # Change log See [upgrade notes][1] for helpful information when upgrading from previous versions. diff --git a/docs/about.md b/docs/about.md index 3c840ac37f..062b1de56e 100644 --- a/docs/about.md +++ b/docs/about.md @@ -1,6 +1,7 @@ --- title: What is PSRule and why should I use it? author: BernieWhite +discussion: false --- # What is PSRule? diff --git a/docs/addon-modules.md b/docs/addon-modules.md index 40f21e1be3..6649e99e5e 100644 --- a/docs/addon-modules.md +++ b/docs/addon-modules.md @@ -1,3 +1,7 @@ +--- +discussion: false +--- + # Additional modules ## Integrations diff --git a/docs/deprecations.md b/docs/deprecations.md index 60247abc0a..44237566ba 100644 --- a/docs/deprecations.md +++ b/docs/deprecations.md @@ -1,5 +1,6 @@ --- author: BernieWhite +discussion: false --- # Deprecations diff --git a/docs/features.md b/docs/features.md index c4988b9e4a..a55cd012aa 100644 --- a/docs/features.md +++ b/docs/features.md @@ -1,6 +1,7 @@ --- title: Key features of PSRule author: BernieWhite +discussion: false --- # Features diff --git a/docs/license-contributing.md b/docs/license-contributing.md index 7e24a164bf..c7fb58f3ca 100755 --- a/docs/license-contributing.md +++ b/docs/license-contributing.md @@ -1,6 +1,7 @@ --- title: License and contributing to PSRule author: BernieWhite +discussion: false --- # License and contributing diff --git a/docs/related-projects.md b/docs/related-projects.md index ee04a3c926..60713280df 100644 --- a/docs/related-projects.md +++ b/docs/related-projects.md @@ -1,5 +1,6 @@ --- author: BernieWhite +discussion: false --- # Related projects @@ -29,4 +30,4 @@ Name | Description [7]: https://github.com/microsoft/PSRule.Rules.GitHub [8]: https://github.com/microsoft/PSRule.Rules.Kubernetes [9]: https://github.com/microsoft/PSRule.Rules.MSFT.OSS - [10]: https://github.com/Azure/PSRule.Rules.Azure-quickstart \ No newline at end of file + [10]: https://github.com/Azure/PSRule.Rules.Azure-quickstart diff --git a/docs/support.md b/docs/support.md index e79bfe08af..bf7821a130 100644 --- a/docs/support.md +++ b/docs/support.md @@ -1,6 +1,7 @@ --- title: Support for PSRule author: BernieWhite +discussion: false --- # Support diff --git a/docs/upgrade-notes.md b/docs/upgrade-notes.md index abbd1b130e..8f7f428eae 100644 --- a/docs/upgrade-notes.md +++ b/docs/upgrade-notes.md @@ -1,6 +1,7 @@ --- title: Notes for upgrading between PSRule versions author: BernieWhite +discussion: false --- # Upgrade notes diff --git a/docs/versioning.md b/docs/versioning.md index 00bfa70464..ce46605601 100644 --- a/docs/versioning.md +++ b/docs/versioning.md @@ -1,3 +1,8 @@ +--- +author: BernieWhite +discussion: false +--- + # Changes and versioning PSRule uses [semantic versioning][1] to declare breaking changes. @@ -26,9 +31,9 @@ Experimental features may ship in stable releases, however to use them you may n - Enabled or explictly reference them. !!! Important - Experimental features should be considered work in progress. - These features may be incomplete and should not be used in production. - We may introduce breaking changes for experimental features as we work towards a general release for the feature. + Experimental features should be considered work in progress. + These features may be incomplete and should not be used in production. + We may introduce breaking changes for experimental features as we work towards a general release for the feature. ## Reporting bugs diff --git a/mkdocs.yml b/mkdocs.yml index ebec3e3025..5ac7d91daa 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -157,3 +157,9 @@ extra: social_preview: https://repository-images.githubusercontent.com/125832556/d6685d9f-ba70-44a1-b11f-6534831143d1 repo_issue: https://github.com/microsoft/PSRule/issues repo_discussion: https://github.com/microsoft/PSRule/discussions + + giscus: + repo: microsoft/PSRule + repo_id: MMDEwOlJlcG9zaXRvcnkxMjU4MzI1NTY= + category: Documentation + category_id: DIC_kwDOB4ANbM4CRxSO diff --git a/overrides/main.html b/overrides/main.html index dfa5a0f5a7..d31b340efd 100644 --- a/overrides/main.html +++ b/overrides/main.html @@ -38,3 +38,9 @@ {% endblock %} + +{% block content %} + {{ super() }} + + {% include "partials/giscus.html" %} +{% endblock %} diff --git a/overrides/partials/giscus.html b/overrides/partials/giscus.html new file mode 100644 index 0000000000..459029ae04 --- /dev/null +++ b/overrides/partials/giscus.html @@ -0,0 +1,76 @@ +{% set discussion = not page.meta.discussion is defined or page.meta.discussion == true %} +{% if not page.meta.generated and discussion and config.extra.giscus %} + + +

{{ lang.t("meta.comments") }}

+ {% if page and page.meta and page.meta.rule %} + {% set discus_term = page.meta.rule %} + {% endif %} + + {% if discus_term %} + + {% else %} + + {% endif %} + + + +{% endif %} \ No newline at end of file From 24b0fe0d96c0e7b1ce5c3b33fe622a36f6134367 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Tue, 4 Oct 2022 12:31:44 +1000 Subject: [PATCH 080/156] Fix CI test (#1300) --- tests/PSRule.Tests/InputPathBuilderTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PSRule.Tests/InputPathBuilderTests.cs b/tests/PSRule.Tests/InputPathBuilderTests.cs index cbaf52263b..15dfcc6c36 100644 --- a/tests/PSRule.Tests/InputPathBuilderTests.cs +++ b/tests/PSRule.Tests/InputPathBuilderTests.cs @@ -72,7 +72,7 @@ public void GetPathRequired() actual = builder.Build(); Assert.True(actual.Length == 1); - required = PathFilter.Create(GetWorkingPath(), new string[] { "." }); + required = PathFilter.Create(GetWorkingPath(), new string[] { "**" }); builder = new InputPathBuilder(null, GetWorkingPath(), "*", null, required); builder.Add("."); actual = builder.Build(); From d515845512820b0ab48cbd13bc3a06d7e20d342a Mon Sep 17 00:00:00 2001 From: Bernie White Date: Tue, 4 Oct 2022 23:59:41 +1000 Subject: [PATCH 081/156] Merge patch v2.4.2 (#1304) * Fixed PathExpressionBuilder.GetAllRecurse #1301 (#1302) * Release v2.4.2 (#1303) --- docs/CHANGELOG-v2.md | 14 ++++++ .../Resources/PSRuleResources.Designer.cs | 9 ++++ src/PSRule/Resources/PSRuleResources.resx | 3 ++ src/PSRule/Runtime/ObjectPath/Exceptions.cs | 47 +++++++++++++++++++ .../ObjectPath/PathExpressionBuilder.cs | 28 +++++++++-- tests/PSRule.Tests/PathExpressionTests.cs | 17 ++++++- 6 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 src/PSRule/Runtime/ObjectPath/Exceptions.cs diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 8d7c5c6001..8065f67506 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,12 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since pre-release v2.5.0-B0045: + +- Bug fixes: + - Fixed exception with `PathExpressionBuilder.GetAllRecurse` by @BernieWhite. + [#1301](https://github.com/microsoft/PSRule/issues/1301) + ## v2.5.0-B0045 (pre-release) What's changed since pre-release v2.5.0-B0015: @@ -73,6 +79,14 @@ What's changed since v2.4.0: - Fixed unhandled exception with GetRootedPath by @BernieWhite. [#1251](https://github.com/microsoft/PSRule/issues/1251) +## v2.4.2 + +What's changed since v2.4.1: + +- Bug fixes: + - Fixed exception with `PathExpressionBuilder.GetAllRecurse` by @BernieWhite. + [#1301](https://github.com/microsoft/PSRule/issues/1301) + ## v2.4.1 What's changed since v2.4.0: diff --git a/src/PSRule/Resources/PSRuleResources.Designer.cs b/src/PSRule/Resources/PSRuleResources.Designer.cs index 9f40e83125..63ad14b215 100644 --- a/src/PSRule/Resources/PSRuleResources.Designer.cs +++ b/src/PSRule/Resources/PSRuleResources.Designer.cs @@ -456,6 +456,15 @@ internal static string ObjectPathNotFound { } } + /// + /// Looks up a localized string similar to The object path expression reached the maximum depth of {0} evaluating the path '{1}'.. + /// + internal static string ObjectPathRecurseMaxDepth { + get { + return ResourceManager.GetString("ObjectPathRecurseMaxDepth", resourceCulture); + } + } + /// /// Looks up a localized string similar to Options file does not exist.. /// diff --git a/src/PSRule/Resources/PSRuleResources.resx b/src/PSRule/Resources/PSRuleResources.resx index 0d5e26d061..d86264c7ad 100644 --- a/src/PSRule/Resources/PSRuleResources.resx +++ b/src/PSRule/Resources/PSRuleResources.resx @@ -391,4 +391,7 @@ Using changed files: {0} + + The object path expression reached the maximum depth of {0} evaluating the path '{1}'. + \ No newline at end of file diff --git a/src/PSRule/Runtime/ObjectPath/Exceptions.cs b/src/PSRule/Runtime/ObjectPath/Exceptions.cs new file mode 100644 index 0000000000..f8918e6c38 --- /dev/null +++ b/src/PSRule/Runtime/ObjectPath/Exceptions.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Runtime.Serialization; +using System.Security.Permissions; +using PSRule.Pipeline; + +namespace PSRule.Runtime.ObjectPath +{ + /// + /// An exception thrown by PSRule when evaluating an object path. + /// + [Serializable] + public sealed class ObjectPathEvaluateException : PipelineException + { + /// + public ObjectPathEvaluateException() + { + } + + /// + public ObjectPathEvaluateException(string message) : base(message) + { + } + + /// + public ObjectPathEvaluateException(string message, Exception innerException) : base(message, innerException) + { + } + + /// + private ObjectPathEvaluateException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + /// + [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); + } + } +} diff --git a/src/PSRule/Runtime/ObjectPath/PathExpressionBuilder.cs b/src/PSRule/Runtime/ObjectPath/PathExpressionBuilder.cs index a8a90d918f..90ca0be0e7 100644 --- a/src/PSRule/Runtime/ObjectPath/PathExpressionBuilder.cs +++ b/src/PSRule/Runtime/ObjectPath/PathExpressionBuilder.cs @@ -11,6 +11,7 @@ using System.Reflection; using System.Threading; using Newtonsoft.Json.Linq; +using PSRule.Resources; namespace PSRule.Runtime.ObjectPath { @@ -19,6 +20,16 @@ namespace PSRule.Runtime.ObjectPath ///
internal sealed class PathExpressionBuilder { + private const int DEFAULT_RECURSE_MAX_DEPTH = 100; + private static readonly object[] DEFAULT_EMPTY_ARRAY = new object[] { }; + + private readonly int _RecurseMaxDepth; + + public PathExpressionBuilder() + { + _RecurseMaxDepth = DEFAULT_RECURSE_MAX_DEPTH; + } + private sealed class DynamicPropertyBinder : GetMemberBinder { internal DynamicPropertyBinder(string name, bool ignoreCase) @@ -196,7 +207,7 @@ private PathExpressionFn DescendantSelector(ITokenReader reader, string memberNa var caseSensitive = context.CaseSensitive != caseSensitiveFlag; var result = new List(); var success = 0; - foreach (var i in GetAllRecurse(input, memberName, caseSensitive)) + foreach (var i in GetAllRecurse(input, memberName, caseSensitive, 0)) { if (!next(context, i, out var items, out _)) continue; @@ -369,11 +380,22 @@ private static PathExpressionFn Literal(object arg) private static IEnumerable GetAll(object o) { var baseObject = ExpressionHelpers.GetBaseObject(o); + if (IsSimpleType(baseObject)) + return DEFAULT_EMPTY_ARRAY; + return baseObject is IEnumerable ? GetAllIndex(baseObject) : GetAllField(baseObject); } - private static IEnumerable GetAllRecurse(object o, string fieldName, bool caseSensitive) + private static bool IsSimpleType(object o) { + return o is string || (o != null && o.GetType().IsValueType); + } + + private IEnumerable GetAllRecurse(object o, string fieldName, bool caseSensitive, int depth) + { + if (depth > _RecurseMaxDepth) + throw new ObjectPathEvaluateException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.ObjectPathRecurseMaxDepth, _RecurseMaxDepth, fieldName)); + foreach (var i in GetAll(o)) { if (TryGetField(i, fieldName, caseSensitive, out var value)) @@ -382,7 +404,7 @@ private static IEnumerable GetAllRecurse(object o, string fieldName, boo } else { - foreach (var c in GetAllRecurse(i, fieldName, caseSensitive)) + foreach (var c in GetAllRecurse(i, fieldName, caseSensitive, depth + 1)) yield return c; } } diff --git a/tests/PSRule.Tests/PathExpressionTests.cs b/tests/PSRule.Tests/PathExpressionTests.cs index 8e1336f5e8..d8bfcccd26 100644 --- a/tests/PSRule.Tests/PathExpressionTests.cs +++ b/tests/PSRule.Tests/PathExpressionTests.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Management.Automation; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PSRule.Runtime.ObjectPath; @@ -299,11 +300,16 @@ public void WithDescendant() Assert.NotNull(actual); Assert.Single(actual); Assert.Equal("TestObject2", actual[0]); + + // Handle exception cases + var pso = GetPSObjectContent(); + expression = PathExpression.Create("$..value"); + Assert.False(expression.TryGet(pso, true, out object[] _)); } #region Helper methods - private object GetJsonContent() + private static object GetJsonContent() { var settings = new JsonLoadSettings { @@ -315,6 +321,15 @@ private object GetJsonContent() return JToken.Load(reader, settings); } + private static object GetPSObjectContent() + { + var result = new PSObject(); + result.Properties.Add(new PSNoteProperty("string", "")); + result.Properties.Add(new PSNoteProperty("date", DateTime.Now)); + result.Properties.Add(new PSNoteProperty("int", 0)); + return result; + } + #endregion Helper methods } } From fad3b68b55e0add9644c7a07fecde8e2ba3e1d6d Mon Sep 17 00:00:00 2001 From: Bernie White Date: Wed, 5 Oct 2022 00:43:13 +1000 Subject: [PATCH 082/156] Pre-release v2.5.0-B0080 (#1305) --- docs/CHANGELOG-v2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 8065f67506..a0fe6b5172 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.5.0-B0080 (pre-release) + What's changed since pre-release v2.5.0-B0045: - Bug fixes: From 4dd2ea96b3336daa15474f64ad8de4269bfd865f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vict=C3=B3ria=20Rose?= <70824102+victoriaquasar@users.noreply.github.com> Date: Thu, 6 Oct 2022 01:37:16 -0300 Subject: [PATCH 083/156] Improved readme accessibility and usability (#1306) * chore: improved readme accessibility and usability Implemented a summary in the repository readme to ease getting information and improve the readability. It will also be useful to improve the accessibility of screen readers, since in addition to the table of contents, options to return to the table of contents have been implemented at important points where information is finalized. * fix(readme): small typo --- README.md | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0911ceee15..763508a90f 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,28 @@ PSRule works great and integrates with popular continuous integration (CI) syste [![Open in vscode.dev](https://img.shields.io/badge/Open%20in-vscode.dev-blue)][1] -Features of PSRule include: +### Summary + +- [Introduction](#summary) +- [Project Objectives](#project-objectives) +- [Support](#support) +- [Getting the module](#getting-the-module) +- [Getting extensions](#getting-extensions) +- [Getting started](#getting-started) + - [Scenarios](#scenarios) +- [Language reference](#language-reference) + - [Keywords](#keywords) + - [Commands](#commands) + - [Concepts](#concepts) + - [Schemas](#schemas) +- [Related projects](#related-projects) +- [Changes and versioning](#changes-and-versioning) +- [Contributing](#contributing) +- [Code of conduct](#code-of-conduct) +- [Maintainers](#maintainers) +- [License](#license) + +### Features of PSRule include: - [DevOps][2] - Built to support DevOps culture and tools. - [Extensible][3] - Define tests using YAML, JSON, or PowerShell format. @@ -38,6 +59,8 @@ Rules must be able to be disabled where they are not applicable. Continue reading the [PSRule design specification][5]. [5]: docs/specs/design-spec.md + +> Back to the [summary](#summary) ## Support @@ -52,6 +75,8 @@ Support for this project/ product is limited to the resources listed above. [6]: https://github.com/Microsoft/PSRule/issues [7]: https://github.com/microsoft/PSRule/discussions +> Back to the [summary](#summary) + ## Getting the module You can download and install the PSRule module from the PowerShell Gallery. @@ -66,6 +91,8 @@ For rule and integration modules see [related projects][10]. [9]: https://microsoft.github.io/PSRule/v2/install-instructions/ [10]: https://microsoft.github.io/PSRule/v2/related-projects/ +> Back to the [summary](#summary) + ## Getting extensions Companion extensions are available for the following platforms. @@ -79,6 +106,8 @@ Visual Studio Code | Visual Studio Code extension for PSRule. | [latest][13] / [ [11]: https://marketplace.visualstudio.com/items?itemName=bewhite.ps-rule [12]: https://github.com/marketplace/actions/psrule [13]: https://marketplace.visualstudio.com/items?itemName=bewhite.psrule-vscode + +> Back to the [summary](#summary) ## Getting started @@ -87,6 +116,8 @@ For specific use cases see [scenarios](#scenarios). For frequently asked questions, see the [FAQ](https://microsoft.github.io/PSRule/v2/faq/). +> Back to the [summary](#summary) + ### Scenarios For walk through examples of PSRule usage see: @@ -98,6 +129,8 @@ For walk through examples of PSRule usage see: - [Packaging rules in a module](https://microsoft.github.io/PSRule/v2/authoring/packaging-rules/) - [Writing rule help](https://microsoft.github.io/PSRule/v2/authoring/writing-rule-help/) +> Back to the [summary](#summary) + ## Language reference PSRule extends PowerShell with domain specific language (DSL) keywords, cmdlets and automatic variables. @@ -116,6 +149,8 @@ The following language keywords are used by the `PSRule` module: - [Reason](https://microsoft.github.io/PSRule/v2/keywords/PSRule/en-US/about_PSRule_Keywords/#reason) - Return a reason for why the rule failed. - [Recommend](https://microsoft.github.io/PSRule/v2/keywords/PSRule/en-US/about_PSRule_Keywords/#recommend) - Return a recommendation to resolve the issue and pass the rule. +> Back to the [summary](#summary) + ### Commands The following commands exist in the `PSRule` module: @@ -131,6 +166,8 @@ The following commands exist in the `PSRule` module: - [Set-PSRuleOption](https://microsoft.github.io/PSRule/v2/commands/PSRule/en-US/Set-PSRuleOption/) - Sets options that configure PSRule execution. - [Test-PSRuleTarget](https://microsoft.github.io/PSRule/v2/commands/PSRule/en-US/Test-PSRuleTarget/) - Pass or fail objects against matching rules. +> Back to the [summary](#summary) + ### Concepts The following conceptual topics exist in the `PSRule` module: @@ -303,6 +340,8 @@ The following conceptual topics exist in the `PSRule` module: - [$Rule](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Variables/#rule) - [$TargetObject](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Variables/#targetobject) +> Back to the [summary](#summary) + ### Schemas PSRule uses the following schemas: @@ -311,6 +350,8 @@ PSRule uses the following schemas: - [Language](schemas/PSRule-language.schema.json) - Schema for PSRule resources such as baselines. - [Resources](schemas/PSRule-resources.schema.json) - Schema for PSRule resources documents used with JSON. +> Back to the [summary](#summary) + ## Related projects For a list of projects and integrations see [Related projects][10]. @@ -343,3 +384,5 @@ or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any addi ## License This project is [licensed under the MIT License](LICENSE). + +> Back to the [summary](#summary) From e1331312b88a73fc4a552d95f98e2e313d28d76d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Oct 2022 22:40:55 +1000 Subject: [PATCH 084/156] Bump mkdocs-material from 8.5.5 to 8.5.6 (#1297) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.5.5 to 8.5.6. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.5.5...8.5.6) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bernie White --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 70ecda4d38..29ca64f90d 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.4.0 -mkdocs-material==8.5.5 +mkdocs-material==8.5.6 pymdown-extensions==9.6 mike==1.1.2 mkdocs-simple-hooks==0.1.5 From e08c552ab26b7bb7c9079db8d070b2bc2b38e309 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Thu, 6 Oct 2022 23:30:55 +1000 Subject: [PATCH 085/156] Release v2.5.0 (#1307) --- docs/CHANGELOG-v2.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index a0fe6b5172..afd585b839 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,38 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.5.0 + +What's changed since v2.4.2: + +- New features: + - **Experimental**: Added support for only processing changed files by @BernieWhite. + [#688](https://github.com/microsoft/PSRule/issues/688) + - To ignore unchanged files, set the `Input.IgnoreUnchangedPath` option to `true`. + - See [creating your pipeline][5] for more information. +- General improvements: + - Added labels metadata from grouping and filtering rules by @BernieWhite. + [#1272](https://github.com/microsoft/PSRule/issues/1272) + - Labels are metadata that extends on tags to provide a more structured way to group rules. + - Rules can be classified by setting the `metadata.labels` property or `-Labels` parameter. + - Provide unblock for command line tools by @BernieWhite. + [#1261](https://github.com/microsoft/PSRule/issues/1261) +- Engineering: + - Bump Microsoft.NET.Test.Sdk to v17.3.1. + [#1248](https://github.com/microsoft/PSRule/pull/1248) +- Bug fixes: + - Fixed could not load Microsoft.Management.Infrastructure by @BernieWhite. + [#1249](https://github.com/microsoft/PSRule/issues/1249) + - To use minimal initial session state set `Execution.InitialSessionState` to `Minimal`. + - Fixed unhandled exception with GetRootedPath by @BernieWhite. + [#1251](https://github.com/microsoft/PSRule/issues/1251) + - Fixed Dockerfile case sensitivity by @BernieWhite. + [#1269](https://github.com/microsoft/PSRule/issues/1269) + +What's changed since pre-release v2.5.0-B0080: + +- No additional changes. + ## v2.5.0-B0080 (pre-release) What's changed since pre-release v2.5.0-B0045: From 6f4e1b74be0a006ec139e9bb70d4e3190cb96f0b Mon Sep 17 00:00:00 2001 From: Bernie White Date: Fri, 7 Oct 2022 00:14:18 +1000 Subject: [PATCH 086/156] Update creating your pipeline and docs (#1308) --- docs/analysis-output.md | 4 +- docs/creating-your-pipeline.md | 69 ++++++++++++++++--- docs/install-instructions.md | 2 +- .../containers/container-execution.md | 31 +++++---- 4 files changed, 80 insertions(+), 26 deletions(-) diff --git a/docs/analysis-output.md b/docs/analysis-output.md index 7c537946f1..b2c8077369 100644 --- a/docs/analysis-output.md +++ b/docs/analysis-output.md @@ -29,7 +29,7 @@ The output format can be configuring by setting the `Output.Format` option to on ```yaml hl_lines="5-6" # Analyze and save results - name: Analyze repository - uses: microsoft/ps-rule@v2.3.2 + uses: microsoft/ps-rule@v2.4.2 with: outputFormat: Sarif outputPath: reports/ps-rule-results.sarif @@ -237,7 +237,7 @@ To configure GitHub Actions, perform the following steps: uses: actions/checkout@v3 - name: Run PSRule analysis - uses: microsoft/ps-rule@v2.3.2 + uses: microsoft/ps-rule@v2.4.2 with: outputFormat: Sarif outputPath: reports/ps-rule-results.sarif diff --git a/docs/creating-your-pipeline.md b/docs/creating-your-pipeline.md index c952c51314..cc376415fc 100644 --- a/docs/creating-your-pipeline.md +++ b/docs/creating-your-pipeline.md @@ -28,7 +28,7 @@ Within the root directory of your IaC repository: # Analyze Azure resources using PSRule for Azure - name: Analyze Azure template files - uses: microsoft/ps-rule@v2.3.2 + uses: microsoft/ps-rule@v2.4.2 with: modules: 'PSRule.Rules.Azure' ``` @@ -57,8 +57,9 @@ Within the root directory of your IaC repository: Create a pipeline in any CI environment by using PowerShell. ```powershell - Install-Module -Name 'PSRule.Rules.Azure' -Scope CurrentUser -Force -ErrorAction Stop; - Assert-PSRule -InputPath '.' -Module 'PSRule.Rules.Azure' -Format File -ErrorAction Stop; + $modules = @('PSRule.Rules.Azure') + Install-Module -Name $modules -Scope CurrentUser -Force -ErrorAction Stop; + Assert-PSRule -InputPath '.' -Module $modules -Format File -ErrorAction Stop; ``` !!! Tip @@ -145,17 +146,65 @@ To prevent a rule executing you can either: ### Processing changed files only -[:octicons-book-24: Docs][8] +:octicons-milestone-24: v2.5.0 · [:octicons-book-24: Docs][8] To only process files that have changed within a pull request, set the `Input.IgnoreUnchangedPath` option. -```yaml title="ps-rule.yaml" -repository: - baseRef: main +=== "GitHub Actions" + + Update your GitHub Actions workflow by setting the `PSRULE_INPUT_IGNOREUNCHANGEDPATH` environment variable. -input: - ignoreUnchangedPath: true -``` + ```yaml title=".github/workflows/analyze-arm.yaml" + name: Analyze templates + on: + - pull_request + jobs: + analyze_arm: + name: Analyze templates + runs-on: ubuntu-latest + steps: + + - name: Checkout + uses: actions/checkout@v3 + + # Analyze Azure resources using PSRule for Azure + - name: Analyze Azure template files + uses: microsoft/ps-rule@v2.4.2 + with: + modules: 'PSRule.Rules.Azure' + env: + PSRULE_INPUT_IGNOREUNCHANGEDPATH: true + ``` + +=== "Azure Pipelines" + + Update your Azure DevOps YAML pipeline by setting the `PSRULE_INPUT_IGNOREUNCHANGEDPATH` environment variable. + + ```yaml title=".azure-pipelines/analyze-arm.yaml" + steps: + + # Analyze Azure resources using PSRule for Azure + - task: ps-rule-assert@2 + displayName: Analyze Azure template files + inputs: + inputType: repository + modules: 'PSRule.Rules.Azure' + env: + PSRULE_INPUT_IGNOREUNCHANGEDPATH: true + ``` + +=== "Generic with PowerShell" + + Update your PowerShell command-line to include the `Input.IgnoreUnchangedPath` option. + + ```powershell title="PowerShell" + $modules = @('PSRule.Rules.Azure') + $options = @{ + 'Input.IgnoreUnchangedPath' = $True + } + Install-Module -Name $modules -Scope CurrentUser -Force -ErrorAction Stop; + Assert-PSRule -Options $options -InputPath '.' -Module $modules -Format File -ErrorAction Stop; + ``` !!! Tip In some cases it may be nessessary to set `Repository.BaseRef` to the default branch of your repository. diff --git a/docs/install-instructions.md b/docs/install-instructions.md index 9444803dbd..0c4b168def 100644 --- a/docs/install-instructions.md +++ b/docs/install-instructions.md @@ -21,7 +21,7 @@ Install and use PSRule with GitHub Actions by referencing the `microsoft/ps-rule ```yaml - name: Analyze Azure template files - uses: microsoft/ps-rule@v2.3.2 + uses: microsoft/ps-rule@v2.4.2 ``` This will automatically install compatible versions of all dependencies. diff --git a/docs/scenarios/containers/container-execution.md b/docs/scenarios/containers/container-execution.md index 353110821b..5a46b0e7bb 100644 --- a/docs/scenarios/containers/container-execution.md +++ b/docs/scenarios/containers/container-execution.md @@ -1,14 +1,18 @@ # Using PSRule from a Container -Depending on your development or CI/CD process for your environment you may desire to use PSRules to validate your Infrastructure as Code (IaC) from a container. This document shows how you can use a simple container based on the [mcr.microsoft.com/powershell](https://hub.docker.com/_/microsoft-powershell) image from Microsoft. +Depending on your development or CI/CD process for your environment you may desire to use PSRules to validate your Infrastructure as Code (IaC) from a container. +This document shows how you can use a simple container based on the [mcr.microsoft.com/powershell](https://hub.docker.com/_/microsoft-powershell) image from Microsoft. -In this tutorial we are going to use a simple Ubuntu based PowerShell image to validate an ARM template. We will do this by creating a dockerfile to describe and create a container image that we can then run. When we run the container we will use a volume mount to share our ARM template and test code for the container to then execute the PSRule.Rules.Azure PSRules against our ARM template and output the results. +In this tutorial we are going to use a simple Ubuntu based PowerShell image to validate an ARM template. +We will do this by creating a dockerfile to describe and create a container image that we can then run. +When we run the container we will use a volume mount to share our ARM template and test code for the container to then execute the _PSRule for Azure_ against our ARM template and output the results. ## Creating the image -Creating an image ready to run PSRules first requires a dockerfile. The below example will use the latest powershell image released and install the PSRule and PSRule.Rules.Azure modules. +Creating an image ready to run PSRules first requires a dockerfile. +The below example will use the latest PowerShell image released and install the `PSRule` and `PSRule.Rules.Azure` modules. -```dockerfile +```dockerfile title="Dockerfile" # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. @@ -25,14 +29,14 @@ docker build --tag psrule:latest . ``` !!! Note - While fine for an example, it is common to always reference a container by a version - number and not the "latest" tag. Using the "latest" tag may lead to unexpected behavior as - version changes occur. + While fine for an example, it is common to always reference a container by a version number and not the `latest` tag. + Using the `latest` tag may lead to unexpected behavior as version changes occur. ## Create your test script -Create a new directory and add a new file named `validate-files.ps1`. This file will run the PSRule -test for us on our new container image. Add the below code to the file. +Create a new directory and add a new file named `validate-files.ps1`. +This file will run the PSRule test for us on our new container image. +Add the below code to the file. ```powershell # Copyright (c) Microsoft Corporation. @@ -48,12 +52,13 @@ Get-AzRuleTemplateLink "$PSScriptRoot/template" | Export-AzRuleTemplateData -Out Assert-PSRule -InputPath "$PSScriptRoot/out/" -Module 'PSRule.Rules.Azure' -As Summary ``` -Also, within the new directory add another directory named `template`. Add any ARM template you would like to -test in this directory. For a starting point you can get a template from [Azure Quickstart Templates.](https://azure.microsoft.com/resources/templates/) +Also, within the new directory add another directory named `template`. +Add any ARM template you would like to test in this directory. +For a starting point you can get a template from [Azure Quickstart Templates.](https://azure.microsoft.com/resources/templates/) Your directory should now look like the below. -``` +```text - Directory |--> validate-files.ps1 |--> template @@ -68,7 +73,7 @@ Now we are ready to go! Run the below docker command to test the ARM template. docker run -it --rm -v $PWD/:/src psrule:latest pwsh -file /src/validate-files.ps1 ``` -This command runs the container and the PSRule tests by mounting the directory to the /src path +This command runs the container and the PSRule tests by mounting the directory to the `/src` path and then executing the `validate-files.ps1` script. !!! Note From 4b778e2599492463963ee07ac871cda4cab8e952 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sun, 16 Oct 2022 16:01:49 +1000 Subject: [PATCH 087/156] Fixed In with array source #1314 (#1315) * Fixed In with array source #1314 * Test alternative alpine image * Disable alpine image --- .azure-pipelines/azure-pipelines.yaml | 12 ++++++------ docs/CHANGELOG-v2.md | 6 ++++++ .../Runtime/ObjectPath/PathExpressionBuilder.cs | 3 +++ tests/PSRule.Tests/AssertTests.cs | 7 ++++++- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/.azure-pipelines/azure-pipelines.yaml b/.azure-pipelines/azure-pipelines.yaml index 24e3d715c0..56dda80cd7 100644 --- a/.azure-pipelines/azure-pipelines.yaml +++ b/.azure-pipelines/azure-pipelines.yaml @@ -163,12 +163,12 @@ stages: displayName: 'PowerShell 7.2 - Windows 2022' imageName: 'windows-2022' - - template: jobs/testContainer.yaml - parameters: - name: alpine_3_14 - displayName: 'PowerShell 7.2 - alpine-3.14' - imageName: mcr.microsoft.com/powershell/test-deps - imageTag: alpine-3.14 + # - template: jobs/testContainer.yaml + # parameters: + # name: alpine_3_14 + # displayName: 'PowerShell 7.2 - alpine-3.14' + # imageName: mcr.microsoft.com/powershell/test-deps + # imageTag: alpine-3.14 - template: jobs/testContainer.yaml parameters: diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index afd585b839..df13d1a97f 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,12 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since v2.5.0: + +- Bug fixes: + - Fixed `In` with array source object and dot object path by @BernieWhite. + [#1314](https://github.com/microsoft/PSRule/issues/1314) + ## v2.5.0 What's changed since v2.4.2: diff --git a/src/PSRule/Runtime/ObjectPath/PathExpressionBuilder.cs b/src/PSRule/Runtime/ObjectPath/PathExpressionBuilder.cs index 90ca0be0e7..21fb404b30 100644 --- a/src/PSRule/Runtime/ObjectPath/PathExpressionBuilder.cs +++ b/src/PSRule/Runtime/ObjectPath/PathExpressionBuilder.cs @@ -351,6 +351,9 @@ private static bool Return(IPathExpressionContext context, object input, out IEn if (input is JValue jValue && (jValue.Type == JTokenType.String || jValue.Type == JTokenType.Integer || jValue.Type == JTokenType.Boolean)) input = jValue.Value; + if (input is PSObject pso && pso.BaseObject is IEnumerable e && pso.BaseObject is not string) + input = e.Cast().ToArray(); + enumerable = false; if (input is object[] eo) { diff --git a/tests/PSRule.Tests/AssertTests.cs b/tests/PSRule.Tests/AssertTests.cs index 2b3f06e1dd..bef6fa01a4 100644 --- a/tests/PSRule.Tests/AssertTests.cs +++ b/tests/PSRule.Tests/AssertTests.cs @@ -978,7 +978,7 @@ public void In() Assert.True(assert.In(value, "values", new float[] { 2f, 4f, 5f }).Result); // String - value = GetObject((name: "value", value: "value2"), (name: "values", value: new string[] { "value2", "value5" })); + value = GetObject((name: "value", value: "value2"), (name: "values", value: new string[] { "value2", "value5" }), (name: "objects", value: new object[] { "value2", "value5" })); Assert.True(assert.In(value, "value", new string[] { "Value2" }).Result); Assert.True(assert.In(value, "value", new string[] { "VALUE1", "VALUE2", "VALUE3" }).Result); Assert.False(assert.In(value, "value", new string[] { "Value3" }).Result); @@ -991,9 +991,14 @@ public void In() Assert.True(assert.In(value, "values", new string[] { "Value2" }).Result); Assert.True(assert.In(value, "values", new string[] { "VALUE1", "VALUE2", "VALUE3" }).Result); Assert.True(assert.In(value, "values", new string[] { "Value3", "Value5" }).Result); + Assert.True(assert.In(value, "values", new object[] { "Value3", "Value5" }).Result); Assert.False(assert.In(value, "values", new string[] { "Value1", "Value3" }).Result); Assert.False(assert.In(value, "values", new string[] { "VALUE1", "VALUE2", "VALUE3" }, true).Result); + Assert.True(assert.In(value, "objects", new object[] { "Value3", "Value5" }).Result); + Assert.True(assert.In(value, "objects", new string[] { "Value3", "Value5" }).Result); + Assert.True(assert.In(PSObject.AsPSObject(new object[] { "value2", "value5" }), ".", new string[] { "Value3", "Value5" }).Result); + Assert.Equal("value", assert.In(value, "value", new string[] { "Value3" }).ToResultReason().FirstOrDefault().Path); } From 432dc20a12cd21f124666eafbdcea3cbeef2d66d Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sun, 16 Oct 2022 16:11:26 +1000 Subject: [PATCH 088/156] Further scope workflow permissions (#1310) --- .github/workflows/analyze.yaml | 2 ++ .github/workflows/dependencies.yaml | 2 ++ .github/workflows/docs.yaml | 2 ++ .github/workflows/first-interaction.yaml | 2 ++ .github/workflows/stale.yaml | 2 ++ 5 files changed, 10 insertions(+) diff --git a/.github/workflows/analyze.yaml b/.github/workflows/analyze.yaml index 849180a962..350164458d 100644 --- a/.github/workflows/analyze.yaml +++ b/.github/workflows/analyze.yaml @@ -19,6 +19,8 @@ on: - cron: '24 22 * * 0' # At 10:24 PM, on Sunday each week workflow_dispatch: +permissions: {} + jobs: oss: name: Analyze with PSRule diff --git a/.github/workflows/dependencies.yaml b/.github/workflows/dependencies.yaml index 7a3c91ff63..ec990efa3d 100644 --- a/.github/workflows/dependencies.yaml +++ b/.github/workflows/dependencies.yaml @@ -14,6 +14,8 @@ on: env: WORKING_BRANCH: dependencies/powershell-bump +permissions: {} + jobs: dependencies: name: Bump dependencies diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index ea3d90924b..3c87e6a727 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -12,6 +12,8 @@ on: - main workflow_dispatch: +permissions: {} + jobs: deploy: name: Publish docs diff --git a/.github/workflows/first-interaction.yaml b/.github/workflows/first-interaction.yaml index 88dddcd321..fdaca780d4 100644 --- a/.github/workflows/first-interaction.yaml +++ b/.github/workflows/first-interaction.yaml @@ -9,6 +9,8 @@ name: First interaction on: [pull_request_target, issues] +permissions: {} + jobs: greeting: name: Greeting diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index c22a202591..fa5e64c685 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -11,6 +11,8 @@ on: - cron: '30 1 * * *' workflow_dispatch: +permissions: {} + jobs: issue: name: Close stale issues From 94bcd09896fec0a6c0573f975fcff380a7a263f1 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sun, 16 Oct 2022 16:22:24 +1000 Subject: [PATCH 089/156] Release v2.5.1 (#1317) --- docs/CHANGELOG-v2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index df13d1a97f..d2b6a69080 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.5.1 + What's changed since v2.5.0: - Bug fixes: From 9a96b98eebec0133b05b9d69aa3d6126d66a3552 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Oct 2022 23:12:20 +1000 Subject: [PATCH 090/156] Bump mkdocs from 1.4.0 to 1.4.1 (#1319) Bumps [mkdocs](https://github.com/mkdocs/mkdocs) from 1.4.0 to 1.4.1. - [Release notes](https://github.com/mkdocs/mkdocs/releases) - [Commits](https://github.com/mkdocs/mkdocs/compare/1.4.0...1.4.1) --- updated-dependencies: - dependency-name: mkdocs dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 29ca64f90d..4a6cbb153d 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,4 +1,4 @@ -mkdocs==1.4.0 +mkdocs==1.4.1 mkdocs-material==8.5.6 pymdown-extensions==9.6 mike==1.1.2 From 0b95647cebbce92e824f1b5909205889ab4849b0 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Wed, 19 Oct 2022 23:12:41 +1000 Subject: [PATCH 091/156] Fixed NUnit output escaping #1316 (#1320) * Fixed NUnit output escaping #1316 * Fix for line endings --- docs/CHANGELOG-v2.md | 6 ++ .../Pipeline/Output/NUnit3OutputWriter.cs | 100 ++++++++++++++---- tests/PSRule.Tests/OutputWriterTests.cs | 30 +++++- 3 files changed, 115 insertions(+), 21 deletions(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index d2b6a69080..f96c74f0c3 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,12 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since v2.5.1: + +- Bug fixes: + - Fixed NUnit output does not escape characters in all result properties by @BernieWhite. + [#1316](https://github.com/microsoft/PSRule/issues/1316) + ## v2.5.1 What's changed since v2.5.0: diff --git a/src/PSRule/Pipeline/Output/NUnit3OutputWriter.cs b/src/PSRule/Pipeline/Output/NUnit3OutputWriter.cs index b24f894185..7ddb19bdb2 100644 --- a/src/PSRule/Pipeline/Output/NUnit3OutputWriter.cs +++ b/src/PSRule/Pipeline/Output/NUnit3OutputWriter.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using System.Threading; +using System.Xml; using PSRule.Configuration; using PSRule.Resources; using PSRule.Rules; @@ -23,7 +24,7 @@ internal NUnit3OutputWriter(PipelineWriter inner, PSRuleOption option) public override void WriteObject(object sendToPipeline, bool enumerateCollection) { - if (!(sendToPipeline is InvokeResult result)) + if (sendToPipeline is not InvokeResult result) return; Add(result); @@ -31,16 +32,49 @@ public override void WriteObject(object sendToPipeline, bool enumerateCollection protected override string Serialize(InvokeResult[] o) { - _Builder.Append(""); + var settings = new XmlWriterSettings + { + Encoding = Encoding.UTF8, // Consider using: Option.Output.GetEncoding() + // Consider using: Indent = true, + }; + using var xml = XmlWriter.Create(_Builder, settings); + xml.WriteStartDocument(standalone: false); float time = o.Sum(r => r.Time); var total = o.Sum(r => r.Total); var error = o.Sum(r => r.Error); var fail = o.Sum(r => r.Fail); - _Builder.Append($""); - _Builder.Append($""); - _Builder.Append($""); + xml.WriteStartElement("test-results"); + xml.WriteAttributeString("xsi", "noNamespaceSchemaLocation", "http://www.w3.org/2001/XMLSchema-instance", "nunit_schema_2.5.xsd"); + xml.WriteAttributeString("name", "PSRule"); + xml.WriteAttributeString("total", total.ToString()); + xml.WriteAttributeString("errors", error.ToString()); + xml.WriteAttributeString("failures", fail.ToString()); + xml.WriteAttributeString("not-run", "0"); + xml.WriteAttributeString("inconclusive", "0"); + xml.WriteAttributeString("ignored", "0"); + xml.WriteAttributeString("skipped", "0"); + xml.WriteAttributeString("invalid", "0"); + xml.WriteAttributeString("date", DateTime.UtcNow.ToString("yyyy-MM-dd", Thread.CurrentThread.CurrentCulture)); + xml.WriteAttributeString("time", TimeSpan.FromMilliseconds(time).ToString()); + + xml.WriteStartElement("environment"); + xml.WriteAttributeString("user", Environment.UserName); + xml.WriteAttributeString("machine-name", Environment.MachineName); + xml.WriteAttributeString("cwd", PSRuleOption.GetWorkingPath()); + xml.WriteAttributeString("user-domain", Environment.UserDomainName); + xml.WriteAttributeString("platform", Environment.OSVersion.Platform.ToString()); + xml.WriteAttributeString("nunit-version", "2.5.8.0"); + xml.WriteAttributeString("os-version", Environment.OSVersion.Version.ToString()); + xml.WriteAttributeString("clr-version", Environment.Version.ToString()); + xml.WriteEndElement(); + + xml.WriteStartElement("culture-info"); + xml.WriteAttributeString("current-culture", Thread.CurrentThread.CurrentCulture.ToString()); + xml.WriteAttributeString("current-uiculture", Thread.CurrentThread.CurrentUICulture.ToString()); + xml.WriteEndElement(); + foreach (var result in o) { if (result.Total == 0) @@ -68,35 +102,63 @@ protected override string Serialize(InvokeResult[] o) asserts: failedCount, testCases: testCases ); - VisitFixture(fixture: fixture); + VisitFixture(xml, fixture); } - _Builder.Append(""); + xml.WriteEndElement(); + xml.WriteEndDocument(); + xml.Flush(); return _Builder.ToString(); } - private void VisitFixture(TestFixture fixture) + private static void VisitFixture(XmlWriter xml, TestFixture fixture) { - _Builder.Append($""); + xml.WriteStartElement("test-suite"); + xml.WriteAttributeString("type", "TestFixture"); + xml.WriteAttributeString("name", fixture.Name); + xml.WriteAttributeString("executed", fixture.Executed.ToString()); + xml.WriteAttributeString("result", fixture.Success ? "Success" : "Failure"); + xml.WriteAttributeString("success", fixture.Success.ToString()); + + xml.WriteAttributeString("time", fixture.Time.ToString(Thread.CurrentThread.CurrentCulture)); + xml.WriteAttributeString("asserts", fixture.Asserts.ToString()); + xml.WriteAttributeString("description", fixture.Description); + + xml.WriteStartElement("results"); foreach (var testCase in fixture.Results) - VisitTestCase(testCase: testCase); + VisitTestCase(xml, testCase); - _Builder.Append(""); + xml.WriteEndElement(); + xml.WriteEndElement(); } - private void VisitTestCase(TestCase testCase) + private static void VisitTestCase(XmlWriter xml, TestCase testCase) { - _Builder.Append($""); + xml.WriteStartElement("test-case"); + xml.WriteAttributeString("description", testCase.Description); + xml.WriteAttributeString("name", testCase.Name); + xml.WriteAttributeString("time", testCase.Time.ToString(Thread.CurrentThread.CurrentCulture)); + xml.WriteAttributeString("asserts", "0"); + xml.WriteAttributeString("success", testCase.Success.ToString()); + xml.WriteAttributeString("result", testCase.Success ? "Success" : "Failure"); + xml.WriteAttributeString("executed", testCase.Executed.ToString()); if (!testCase.Success) { - _Builder.Append(""); - _Builder.Append($""); - _Builder.Append($""); - _Builder.Append(""); + xml.WriteStartElement("failure"); + + xml.WriteStartElement("message"); + xml.WriteCData(testCase.FailureMessage); + xml.WriteEndElement(); + + xml.WriteStartElement("stack-trace"); + xml.WriteCData(testCase.ScriptStackTrace); + xml.WriteEndElement(); + + xml.WriteEndElement(); } - _Builder.Append(""); + xml.WriteEndElement(); } - private string FailureMessage(Rules.RuleRecord record) + private string FailureMessage(RuleRecord record) { var useMarkdown = Option.Output.Style == OutputStyle.AzurePipelines; var sb = new StringBuilder(); diff --git a/tests/PSRule.Tests/OutputWriterTests.cs b/tests/PSRule.Tests/OutputWriterTests.cs index 2136b180fc..fb7d415fec 100644 --- a/tests/PSRule.Tests/OutputWriterTests.cs +++ b/tests/PSRule.Tests/OutputWriterTests.cs @@ -1,9 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Collections; using System.Linq; using System.Management.Automation; +using System.Xml; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PSRule.Configuration; @@ -277,6 +279,30 @@ public void Json() ]", output.Output.OfType().FirstOrDefault()); } + [Fact] + public void NUnit3() + { + var option = GetOption(); + option.Repository.Url = "https://github.com/microsoft/PSRule.UnitTest"; + var output = new TestWriter(option); + var result = new InvokeResult(); + result.Add(GetPass()); + result.Add(GetFail()); + result.Add(GetFail("rid-003", SeverityLevel.Warning)); + result.Add(GetFail("rid-004", SeverityLevel.Information, "Synopsis \"with quotes\".")); + var writer = new NUnit3OutputWriter(output, option); + writer.Begin(); + writer.WriteObject(result, false); + writer.End(); + + var s = output.Output.OfType().FirstOrDefault(); + var doc = new XmlDocument(); + doc.LoadXml(s); + + var xml = doc["test-results"]["test-suite"].OuterXml.Replace(Environment.NewLine, "\r\n"); + Assert.Equal("", xml); + } + #region Helper methods private static RuleRecord GetPass() @@ -304,7 +330,7 @@ private static RuleRecord GetPass() ); } - private static RuleRecord GetFail(string ruleRef = "rid-002", SeverityLevel level = SeverityLevel.Error) + private static RuleRecord GetFail(string ruleRef = "rid-002", SeverityLevel level = SeverityLevel.Error, string synopsis = "This is rule 002.") { return new RuleRecord( runId: "run-001", @@ -318,7 +344,7 @@ private static RuleRecord GetFail(string ruleRef = "rid-002", SeverityLevel leve "rule-002", "Rule 002", "TestModule", - synopsis: new InfoString("This is rule 002."), + synopsis: new InfoString(synopsis), recommendation: new InfoString("Recommendation for rule 002") ), field: new Hashtable(), From 0034460f98abb77911212d4fcae859a5ed3e3b10 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Thu, 20 Oct 2022 03:25:42 +1000 Subject: [PATCH 092/156] Release v2.5.2 (#1321) --- docs/CHANGELOG-v2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index f96c74f0c3..a3da3f7c42 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.5.2 + What's changed since v2.5.1: - Bug fixes: From 8f48e9d54aa9637b99a5168d86c0c79e51c1bf6c Mon Sep 17 00:00:00 2001 From: Bernie White Date: Fri, 21 Oct 2022 18:55:58 +1000 Subject: [PATCH 093/156] Fixes incorrect XML header for encoding #1322 (#1323) --- docs/CHANGELOG-v2.md | 6 ++++++ .../Pipeline/Output/NUnit3OutputWriter.cs | 13 +++++------- .../Pipeline/Output/OutputStringWriter.cs | 21 +++++++++++++++++++ tests/PSRule.Tests/OutputWriterTests.cs | 2 ++ 4 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 src/PSRule/Pipeline/Output/OutputStringWriter.cs diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index a3da3f7c42..c927b41628 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,12 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since v2.5.2: + +- Bug fixes: + - Fixed incorrect XML header for encoding by @BernieWhite. + [#1322](https://github.com/microsoft/PSRule/issues/1322) + ## v2.5.2 What's changed since v2.5.1: diff --git a/src/PSRule/Pipeline/Output/NUnit3OutputWriter.cs b/src/PSRule/Pipeline/Output/NUnit3OutputWriter.cs index 7ddb19bdb2..d82f042aff 100644 --- a/src/PSRule/Pipeline/Output/NUnit3OutputWriter.cs +++ b/src/PSRule/Pipeline/Output/NUnit3OutputWriter.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.IO; using System.Linq; using System.Text; using System.Threading; @@ -14,13 +15,8 @@ namespace PSRule.Pipeline.Output { internal sealed class NUnit3OutputWriter : SerializationOutputWriter { - private readonly StringBuilder _Builder; - internal NUnit3OutputWriter(PipelineWriter inner, PSRuleOption option) - : base(inner, option) - { - _Builder = new StringBuilder(); - } + : base(inner, option) { } public override void WriteObject(object sendToPipeline, bool enumerateCollection) { @@ -37,7 +33,8 @@ protected override string Serialize(InvokeResult[] o) Encoding = Encoding.UTF8, // Consider using: Option.Output.GetEncoding() // Consider using: Indent = true, }; - using var xml = XmlWriter.Create(_Builder, settings); + using var writer = new OutputStringWriter(Option); + using var xml = XmlWriter.Create(writer, settings); xml.WriteStartDocument(standalone: false); float time = o.Sum(r => r.Time); @@ -107,7 +104,7 @@ protected override string Serialize(InvokeResult[] o) xml.WriteEndElement(); xml.WriteEndDocument(); xml.Flush(); - return _Builder.ToString(); + return writer.ToString(); } private static void VisitFixture(XmlWriter xml, TestFixture fixture) diff --git a/src/PSRule/Pipeline/Output/OutputStringWriter.cs b/src/PSRule/Pipeline/Output/OutputStringWriter.cs new file mode 100644 index 0000000000..8df6adfb0f --- /dev/null +++ b/src/PSRule/Pipeline/Output/OutputStringWriter.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO; +using System.Text; +using PSRule.Configuration; + +namespace PSRule.Pipeline.Output +{ + internal sealed class OutputStringWriter : StringWriter + { + private readonly Encoding _Encoding; + + public OutputStringWriter(PSRuleOption option) + { + _Encoding = option.Output.GetEncoding(); + } + + public override Encoding Encoding => _Encoding; + } +} diff --git a/tests/PSRule.Tests/OutputWriterTests.cs b/tests/PSRule.Tests/OutputWriterTests.cs index fb7d415fec..ce48b060d9 100644 --- a/tests/PSRule.Tests/OutputWriterTests.cs +++ b/tests/PSRule.Tests/OutputWriterTests.cs @@ -299,6 +299,8 @@ public void NUnit3() var doc = new XmlDocument(); doc.LoadXml(s); + var declaration = doc.ChildNodes.Item(0) as XmlDeclaration; + Assert.Equal("utf-8", declaration.Encoding); var xml = doc["test-results"]["test-suite"].OuterXml.Replace(Environment.NewLine, "\r\n"); Assert.Equal("", xml); } From 586900de2ef4f29f7c715997ed196a4053896359 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Fri, 21 Oct 2022 19:43:23 +1000 Subject: [PATCH 094/156] Release v2.5.3 (#1324) --- docs/CHANGELOG-v2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index c927b41628..bbfcf6d1c6 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.5.3 + What's changed since v2.5.2: - Bug fixes: From a39f64070b671b86c88805acd2bb39f080c69d0f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 22 Oct 2022 12:13:36 +1000 Subject: [PATCH 095/156] Bump Microsoft.NET.Test.Sdk from 17.3.1 to 17.3.2 (#1283) * Bump Microsoft.NET.Test.Sdk from 17.3.1 to 17.3.2 Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 17.3.1 to 17.3.2. - [Release notes](https://github.com/microsoft/vstest/releases) - [Commits](https://github.com/microsoft/vstest/compare/v17.3.1...v17.3.2) --- updated-dependencies: - dependency-name: Microsoft.NET.Test.Sdk dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Bump change log Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bernie White --- docs/CHANGELOG-v2.md | 6 ++++++ tests/PSRule.Tests/PSRule.Tests.csproj | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index bbfcf6d1c6..7440b4907f 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,12 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since v2.5.1: + +- Engineering: + - Bump Microsoft.NET.Test.Sdk to v17.3.2. + [#1283](https://github.com/microsoft/PSRule/pull/1283) + ## v2.5.3 What's changed since v2.5.2: diff --git a/tests/PSRule.Tests/PSRule.Tests.csproj b/tests/PSRule.Tests/PSRule.Tests.csproj index ebd34c56d4..41d688eacf 100644 --- a/tests/PSRule.Tests/PSRule.Tests.csproj +++ b/tests/PSRule.Tests/PSRule.Tests.csproj @@ -10,7 +10,7 @@ - + From 8516ddbf89be52446e1ac9c236a4adc83353c18c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 23 Oct 2022 12:40:07 +1000 Subject: [PATCH 096/156] Bump PowerShell dependencies (#1318) * Update ./modules.json * Bump change log Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Bernie White --- docs/CHANGELOG-v2.md | 4 +++- modules.json | 2 +- scripts/dependencies.psm1 | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 7440b4907f..54f02461bd 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,11 +30,13 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased -What's changed since v2.5.1: +What's changed since v2.5.3: - Engineering: - Bump Microsoft.NET.Test.Sdk to v17.3.2. [#1283](https://github.com/microsoft/PSRule/pull/1283) + - Bump PSScriptAnalyzer to v1.21.0. + [#1318](https://github.com/microsoft/PSRule/pull/1318) ## v2.5.3 diff --git a/modules.json b/modules.json index bf2f32c5c2..e82f14c092 100644 --- a/modules.json +++ b/modules.json @@ -8,7 +8,7 @@ "version": "0.14.2" }, "PSScriptAnalyzer": { - "version": "1.20.0" + "version": "1.21.0" } } } diff --git a/scripts/dependencies.psm1 b/scripts/dependencies.psm1 index 1010d4d3a4..5e230e8dfa 100644 --- a/scripts/dependencies.psm1 +++ b/scripts/dependencies.psm1 @@ -105,7 +105,7 @@ function CheckVersion { if (([Version]$found.Version) -gt ([Version]$module.Value.version)) { Write-Host -Object "[$group] -- Newer version found $($found.Version)"; $dependencies[$module.Name].version = $found.Version; - $Null = Add-Content -Path $changeNotes -Value "Bump $($module.Name) to $($found.Version)."; + $Null = Add-Content -Path $changeNotes -Value "Bump $($module.Name) to v$($found.Version)."; } else { Write-Host -Object "[$group] -- Already up to date."; From 644b014da87e951450529cdf5a1144171257fd2d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Oct 2022 12:05:26 +1000 Subject: [PATCH 097/156] Bump mkdocs-material from 8.5.6 to 8.5.7 (#1325) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.5.6 to 8.5.7. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.5.6...8.5.7) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 4a6cbb153d..e85cc2e686 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.4.1 -mkdocs-material==8.5.6 +mkdocs-material==8.5.7 pymdown-extensions==9.6 mike==1.1.2 mkdocs-simple-hooks==0.1.5 From 935d4853326c1eac39a82e66b0c0d467f7a0bb0c Mon Sep 17 00:00:00 2001 From: Bernie White Date: Thu, 27 Oct 2022 00:42:28 +1000 Subject: [PATCH 098/156] Class clean up and docs #1186 (#1327) --- docs/CHANGELOG-v2.md | 2 + src/PSRule.Types/Data/SemanticVersion.cs | 59 ++++++++++++++ src/PSRule/Definitions/IDependencyTarget.cs | 12 +++ .../Pipeline/Output/NUnit3OutputWriter.cs | 1 - src/PSRule/Pipeline/PipelineWriter.cs | 79 ++++++++++++++++++- 5 files changed, 148 insertions(+), 5 deletions(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 54f02461bd..e6346e605d 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -37,6 +37,8 @@ What's changed since v2.5.3: [#1283](https://github.com/microsoft/PSRule/pull/1283) - Bump PSScriptAnalyzer to v1.21.0. [#1318](https://github.com/microsoft/PSRule/pull/1318) + - Class clean up and documentation by @BernieWhite. + [#1186](https://github.com/microsoft/PSRule/issues/1186) ## v2.5.3 diff --git a/src/PSRule.Types/Data/SemanticVersion.cs b/src/PSRule.Types/Data/SemanticVersion.cs index 78a790e326..b51852ad9e 100644 --- a/src/PSRule.Types/Data/SemanticVersion.cs +++ b/src/PSRule.Types/Data/SemanticVersion.cs @@ -8,8 +8,14 @@ namespace PSRule.Data { + /// + /// A semantic version constraint. + /// public interface IVersionConstraint { + /// + /// Determines if the semantic version meets the requirments of the constraint. + /// bool Equals(SemanticVersion.Version version); } @@ -36,6 +42,9 @@ public static class SemanticVersion private const string FLAG_PRERELEASE = "@prerelease "; private const string FLAG_PRE = "@pre "; + /// + /// A comparison operation for a version constraint. + /// [Flags] internal enum ComparisonOperator { @@ -76,10 +85,14 @@ internal enum ConstraintModifier Prerelease = 1 } + /// + /// A semantic version constraint. + /// public sealed class VersionConstraint : IVersionConstraint { private List _Constraints; + /// public bool Equals(Version version) { if (_Constraints == null || _Constraints.Count == 0) @@ -311,12 +324,34 @@ private static bool IsStable(PR prid) } } + /// + /// A semantic version. + /// public sealed class Version : IComparable, IEquatable { + /// + /// The major part of the version. + /// public readonly int Major; + + /// + /// The minor part of the version. + /// public readonly int Minor; + + /// + /// The patch part of the version. + /// public readonly int Patch; + + /// + /// The pre-release part of the version. + /// public readonly PR Prerelease; + + /// + /// The build part of the version. + /// public readonly string Build; internal Version(int major, int minor, int patch, PR prerelease, string build) @@ -328,6 +363,9 @@ internal Version(int major, int minor, int patch, PR prerelease, string build) Build = build; } + /// + /// Try to parse a semantic version from a string. + /// public static bool TryParse(string value, out Version version) { return TryParseVersion(value, out version); @@ -360,12 +398,18 @@ public override int GetHashCode() } } + /// + /// Compare the version against another version. + /// public bool Equals(Version other) { return other != null && Equals(other.Major, other.Minor, other.Patch); } + /// + /// Compare the version against another version based on major.minor.patch. + /// public bool Equals(int major, int minor, int patch) { return major == Major && @@ -373,6 +417,9 @@ public bool Equals(int major, int minor, int patch) patch == Patch; } + /// + /// Compare the version against another version. + /// public int CompareTo(Version other) { if (other == null) @@ -391,6 +438,9 @@ public int CompareTo(Version other) } } + /// + /// A semantic version pre-release identifier. + /// [DebuggerDisplay("{Value}")] public sealed class PR { @@ -411,10 +461,19 @@ internal PR(string value) _Identifiers = string.IsNullOrEmpty(value) ? null : value.Split(SEPARATORS, StringSplitOptions.RemoveEmptyEntries); } + /// + /// The string value of a pre-release identifier. + /// public string Value { get; } + /// + /// Is the pre-release identifier empty, indicating a stable release. + /// public bool Stable => _Identifiers == null; + /// + /// Compare the pre-release identifer to another pre-release identifier. + /// public int CompareTo(PR pr) { if (pr == null || pr.Stable) diff --git a/src/PSRule/Definitions/IDependencyTarget.cs b/src/PSRule/Definitions/IDependencyTarget.cs index 8affee0c21..a4ecdec1b4 100644 --- a/src/PSRule/Definitions/IDependencyTarget.cs +++ b/src/PSRule/Definitions/IDependencyTarget.cs @@ -3,12 +3,24 @@ namespace PSRule.Definitions { + /// + /// An object that relies on a dependency chain. + /// public interface IDependencyTarget { + /// + /// The unique identifier of the resource. + /// ResourceId Id { get; } + /// + /// A unique reference for the resource. + /// ResourceId? Ref { get; } + /// + /// Additional aliases for the resource. + /// ResourceId[] Alias { get; } /// diff --git a/src/PSRule/Pipeline/Output/NUnit3OutputWriter.cs b/src/PSRule/Pipeline/Output/NUnit3OutputWriter.cs index d82f042aff..6fa0b0db3e 100644 --- a/src/PSRule/Pipeline/Output/NUnit3OutputWriter.cs +++ b/src/PSRule/Pipeline/Output/NUnit3OutputWriter.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using System.IO; using System.Linq; using System.Text; using System.Threading; diff --git a/src/PSRule/Pipeline/PipelineWriter.cs b/src/PSRule/Pipeline/PipelineWriter.cs index c19e817cff..d92f93e7b4 100644 --- a/src/PSRule/Pipeline/PipelineWriter.cs +++ b/src/PSRule/Pipeline/PipelineWriter.cs @@ -10,45 +10,97 @@ namespace PSRule.Pipeline { - public delegate void MessageHook(string message); - - public delegate void ErrorRecordHook(ErrorRecord errorRecord); - + /// + /// An writer which recieves output from PSRule. + /// public interface IPipelineWriter { + /// + /// Write a verbose message. + /// void WriteVerbose(string message); + /// + /// Determines if a verbose message should be written to output. + /// bool ShouldWriteVerbose(); + /// + /// Write a warning message. + /// void WriteWarning(string message); + /// + /// Determines if a warning message should be written to output. + /// bool ShouldWriteWarning(); + /// + /// Write an error message. + /// void WriteError(ErrorRecord errorRecord); + /// + /// Determines if an error message should be written to output. + /// bool ShouldWriteError(); + /// + /// Write an informational message. + /// void WriteInformation(InformationRecord informationRecord); + /// + /// Write a message to the host process. + /// void WriteHost(HostInformationMessage info); + /// + /// Determines if an informational message should be written to output. + /// bool ShouldWriteInformation(); + /// + /// Write a debug message. + /// void WriteDebug(string text, params object[] args); + /// + /// Determines if a debug message should be written to output. + /// bool ShouldWriteDebug(); + /// + /// Write an object to output. + /// + /// The object to write to the pipeline. + /// Determines when the object is enumerable if it should be enumerated as more then one object. void WriteObject(object sendToPipeline, bool enumerateCollection); + /// + /// Enter a logging scope. + /// void EnterScope(string scopeName); + /// + /// Exit a logging scope. + /// void ExitScope(); + /// + /// Start and initialize the writer. + /// void Begin(); + /// + /// Stop and finalized the writer. + /// void End(); } + /// + /// A base class for writers. + /// internal abstract class PipelineWriter : IPipelineWriter { protected const string ErrorPreference = "ErrorActionPreference"; @@ -67,6 +119,7 @@ protected PipelineWriter(IPipelineWriter inner, PSRuleOption option) Option = option; } + /// public virtual void Begin() { if (_Writer == null) @@ -75,6 +128,7 @@ public virtual void Begin() _Writer.Begin(); } + /// public virtual void WriteObject(object sendToPipeline, bool enumerateCollection) { if (_Writer == null || sendToPipeline == null) @@ -83,6 +137,7 @@ public virtual void WriteObject(object sendToPipeline, bool enumerateCollection) _Writer.WriteObject(sendToPipeline, enumerateCollection); } + /// public virtual void End() { if (_Writer == null) @@ -91,6 +146,7 @@ public virtual void End() _Writer.End(); } + /// public virtual void WriteVerbose(string message) { if (_Writer == null || string.IsNullOrEmpty(message)) @@ -99,11 +155,13 @@ public virtual void WriteVerbose(string message) _Writer.WriteVerbose(message); } + /// public virtual bool ShouldWriteVerbose() { return _Writer != null && _Writer.ShouldWriteVerbose(); } + /// public virtual void WriteWarning(string message) { if (_Writer == null || string.IsNullOrEmpty(message)) @@ -112,11 +170,13 @@ public virtual void WriteWarning(string message) _Writer.WriteWarning(message); } + /// public virtual bool ShouldWriteWarning() { return _Writer != null && _Writer.ShouldWriteWarning(); } + /// public virtual void WriteError(ErrorRecord errorRecord) { if (_Writer == null || errorRecord == null) @@ -125,11 +185,13 @@ public virtual void WriteError(ErrorRecord errorRecord) _Writer.WriteError(errorRecord); } + /// public virtual bool ShouldWriteError() { return _Writer != null && _Writer.ShouldWriteError(); } + /// public virtual void WriteInformation(InformationRecord informationRecord) { if (_Writer == null || informationRecord == null) @@ -138,6 +200,7 @@ public virtual void WriteInformation(InformationRecord informationRecord) _Writer.WriteInformation(informationRecord); } + /// public virtual void WriteHost(HostInformationMessage info) { if (_Writer == null) @@ -146,11 +209,13 @@ public virtual void WriteHost(HostInformationMessage info) _Writer.WriteHost(info); } + /// public virtual bool ShouldWriteInformation() { return _Writer != null && _Writer.ShouldWriteInformation(); } + /// public virtual void WriteDebug(string text, params object[] args) { if (_Writer == null || string.IsNullOrEmpty(text) || !ShouldWriteDebug()) @@ -160,11 +225,13 @@ public virtual void WriteDebug(string text, params object[] args) _Writer.WriteDebug(text); } + /// public virtual bool ShouldWriteDebug() { return _Writer != null && _Writer.ShouldWriteDebug(); } + /// public virtual void EnterScope(string scopeName) { if (_Writer == null) @@ -173,6 +240,7 @@ public virtual void EnterScope(string scopeName) _Writer.EnterScope(scopeName); } + /// public virtual void ExitScope() { if (_Writer == null) @@ -205,6 +273,9 @@ protected void WriteErrorInfo(RuleRecord record) WriteError(errorRecord); } + /// + /// Get the value of a preference variable. + /// protected static ActionPreference GetPreferenceVariable(System.Management.Automation.SessionState sessionState, string variableName) { return (ActionPreference)sessionState.PSVariable.GetValue(variableName); From 8ac78978707eecb0f3483b4de601820ced5d3495 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Thu, 27 Oct 2022 00:53:19 +1000 Subject: [PATCH 099/156] Pre-release v2.6.0-B0013 (#1328) --- docs/CHANGELOG-v2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index e6346e605d..62015035a9 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.6.0-B0013 (pre-release) + What's changed since v2.5.3: - Engineering: From cbdc7a63d30636dfe6b9d534f7cde1b3875b4027 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 5 Nov 2022 17:19:51 +1000 Subject: [PATCH 100/156] Bump mkdocs-material from 8.5.7 to 8.5.8 (#1330) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.5.7 to 8.5.8. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.5.7...8.5.8) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index e85cc2e686..2246ae3fbd 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.4.1 -mkdocs-material==8.5.7 +mkdocs-material==8.5.8 pymdown-extensions==9.6 mike==1.1.2 mkdocs-simple-hooks==0.1.5 From 3c44a794945916eade2c65f86562729a76bf479a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 5 Nov 2022 18:05:57 +1000 Subject: [PATCH 101/156] Bump mkdocs from 1.4.1 to 1.4.2 (#1329) * Bump mkdocs from 1.4.1 to 1.4.2 Bumps [mkdocs](https://github.com/mkdocs/mkdocs) from 1.4.1 to 1.4.2. - [Release notes](https://github.com/mkdocs/mkdocs/releases) - [Commits](https://github.com/mkdocs/mkdocs/compare/1.4.1...1.4.2) --- updated-dependencies: - dependency-name: mkdocs dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Bump requirements Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bernie White --- requirements-docs.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 2246ae3fbd..e97c40ad97 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,6 +1,6 @@ -mkdocs==1.4.1 +mkdocs==1.4.2 mkdocs-material==8.5.8 -pymdown-extensions==9.6 +pymdown-extensions==9.7 mike==1.1.2 mkdocs-simple-hooks==0.1.5 mkdocs-git-revision-date-plugin==0.3.2 From 281f712cc032f0601a887f493b3aa77ad312e170 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Nov 2022 21:53:05 +1000 Subject: [PATCH 102/156] Bump mkdocs-material from 8.5.8 to 8.5.9 (#1334) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.5.8 to 8.5.9. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.5.8...8.5.9) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index e97c40ad97..68582b85cc 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.4.2 -mkdocs-material==8.5.8 +mkdocs-material==8.5.9 pymdown-extensions==9.7 mike==1.1.2 mkdocs-simple-hooks==0.1.5 From 3db39e17a03a8ed72b8186fb129fae4faf00cc2e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Nov 2022 22:07:29 +1000 Subject: [PATCH 103/156] Bump pymdown-extensions from 9.7 to 9.8 (#1333) Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 9.7 to 9.8. - [Release notes](https://github.com/facelessuser/pymdown-extensions/releases) - [Commits](https://github.com/facelessuser/pymdown-extensions/compare/9.7...9.8) --- updated-dependencies: - dependency-name: pymdown-extensions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 68582b85cc..27d4392f3e 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,6 +1,6 @@ mkdocs==1.4.2 mkdocs-material==8.5.9 -pymdown-extensions==9.7 +pymdown-extensions==9.8 mike==1.1.2 mkdocs-simple-hooks==0.1.5 mkdocs-git-revision-date-plugin==0.3.2 From 2f2e2125c0c85b4f727b540977f8297c520bda6f Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sat, 12 Nov 2022 17:26:16 +1000 Subject: [PATCH 104/156] Added support for job summaries #1264 (#1337) --- README.md | 1 + docs/CHANGELOG-v2.md | 8 + .../commands/PSRule/en-US/New-PSRuleOption.md | 102 +- .../commands/PSRule/en-US/Set-PSRuleOption.md | 64 +- .../PSRule/en-US/about_PSRule_Options.md | 83 +- .../scenarios/benchmark/results-v2.0.0-pre.md | 29 - pipeline.build.ps1 | 6 +- schemas/PSRule-options.schema.json | 1754 +++++++++-------- src/PSRule.Types/Data/ModuleConstraint.cs | 14 + src/PSRule/Configuration/JobSummaryFormat.cs | 38 + src/PSRule/Configuration/OutputOption.cs | 41 +- src/PSRule/Data/ITargetSourceCollection.cs | 12 +- .../Definitions/Expressions/Exceptions.cs | 124 +- src/PSRule/Definitions/ICondition.cs | 22 + src/PSRule/Host/HostHelper.cs | 3 - src/PSRule/PSRule.csproj | 9 + src/PSRule/PSRule.psm1 | 53 +- src/PSRule/Pipeline/AssertPipeline.cs | 39 +- .../Pipeline/Formatters/AssertFormatter.cs | 2 +- src/PSRule/Pipeline/GetRuleHelpPipeline.cs | 5 +- src/PSRule/Pipeline/InvokeResult.cs | 30 +- src/PSRule/Pipeline/Output/CsvOutputWriter.cs | 4 +- .../Pipeline/Output/FileOutputWriter.cs | 10 +- .../Pipeline/Output/HostPipelineWriter.cs | 48 +- .../Pipeline/Output/JobSummaryWriter.cs | 242 +++ .../Pipeline/Output/JsonOutputWriter.cs | 4 +- .../Pipeline/Output/MarkdownOutputWriter.cs | 4 +- .../Pipeline/Output/NUnit3OutputWriter.cs | 4 +- .../Pipeline/Output/SarifOutputWriter.cs | 4 +- .../Pipeline/Output/WideOutputWriter.cs | 4 +- .../Pipeline/Output/YamlOutputWriter.cs | 4 +- src/PSRule/Pipeline/PipelineBuilder.cs | 38 +- src/PSRule/Pipeline/PipelineLogger.cs | 56 +- src/PSRule/Pipeline/PipelineWriter.cs | 101 +- .../Pipeline/PipelineWriterExtensions.cs | 3 + src/PSRule/Pipeline/RulePipeline.cs | 1 + src/PSRule/Pipeline/SourcePipeline.cs | 11 +- src/PSRule/Pipeline/TestPipeline.cs | 11 +- src/PSRule/Resources/Summaries.Designer.cs | 171 ++ src/PSRule/Resources/Summaries.resx | 156 ++ src/PSRule/Runtime/RuleConditionResult.cs | 5 +- tests/PSRule.Tests/AssertTests.cs | 16 +- tests/PSRule.Tests/OutputWriterTests.cs | 36 +- tests/PSRule.Tests/PSRule.Options.Tests.ps1 | 40 + tests/PSRule.Tests/PSRule.Tests.csproj | 2 + tests/PSRule.Tests/PSRule.Tests.yml | 1 + tests/PSRule.Tests/TestAssertWriter.cs | 2 +- tests/PSRule.Tests/TestWriter.cs | 8 +- 48 files changed, 2289 insertions(+), 1136 deletions(-) delete mode 100644 docs/scenarios/benchmark/results-v2.0.0-pre.md create mode 100644 src/PSRule/Configuration/JobSummaryFormat.cs create mode 100644 src/PSRule/Pipeline/Output/JobSummaryWriter.cs create mode 100644 src/PSRule/Resources/Summaries.Designer.cs create mode 100644 src/PSRule/Resources/Summaries.resx diff --git a/README.md b/README.md index 763508a90f..a26984e422 100644 --- a/README.md +++ b/README.md @@ -315,6 +315,7 @@ The following conceptual topics exist in the `PSRule` module: - [Output.Encoding](https://aka.ms/ps-rule/options#outputencoding) - [Output.Footer](https://aka.ms/ps-rule/options#outputfooter) - [Output.Format](https://aka.ms/ps-rule/options#outputformat) + - [Output.JobSummaryPath](https://aka.ms/ps-rule/options#outputjobsummarypath) - [Output.JsonIndent](https://aka.ms/ps-rule/options#outputjsonindent) - [Output.Outcome](https://aka.ms/ps-rule/options#outputoutcome) - [Output.Path](https://aka.ms/ps-rule/options#outputpath) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 62015035a9..c03c6d0edc 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,14 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since pre-release v2.6.0-B0013: + +- New features: + - Added support for generating job summaries by @BernieWhite. + [#1264](https://github.com/microsoft/PSRule/issues/1264) + - Job summaries provide a markdown output for pipelines in addition to other supported output formats. + - To use, configure the `Output.JobSummaryPath` option. + ## v2.6.0-B0013 (pre-release) What's changed since v2.5.3: diff --git a/docs/commands/PSRule/en-US/New-PSRuleOption.md b/docs/commands/PSRule/en-US/New-PSRuleOption.md index f98d352da3..fa3c24db22 100644 --- a/docs/commands/PSRule/en-US/New-PSRuleOption.md +++ b/docs/commands/PSRule/en-US/New-PSRuleOption.md @@ -25,14 +25,15 @@ New-PSRuleOption [[-Path] ] [-Configuration ] [-InconclusiveWarning ] [-NotProcessedWarning ] [-SuppressedRuleWarning ] [-InvariantCultureWarning ] [-InitialSessionState ] [-IncludeModule ] [-IncludePath ] [-Format ] [-InputIgnoreGitPath ] - [-InputIgnoreRepositoryCommon ] [-InputIgnoreObjectSource ] [-ObjectPath ] - [-InputTargetType ] [-InputPathIgnore ] [-LoggingLimitDebug ] - [-LoggingLimitVerbose ] [-LoggingRuleFail ] [-LoggingRulePass ] - [-OutputAs ] [-OutputBanner ] [-OutputCulture ] - [-OutputEncoding ] [-OutputFooter ] [-OutputFormat ] - [-OutputOutcome ] [-OutputPath ] [-OutputSarifProblemsOnly ] - [-OutputStyle ] [-OutputJsonIndent ] [-RepositoryUrl ] - [-RuleIncludeLocal ] [] + [-InputIgnoreRepositoryCommon ] [-InputIgnoreObjectSource ] + [-InputIgnoreUnchangedPath ] [-ObjectPath ] [-InputTargetType ] + [-InputPathIgnore ] [-LoggingLimitDebug ] [-LoggingLimitVerbose ] + [-LoggingRuleFail ] [-LoggingRulePass ] [-OutputAs ] + [-OutputBanner ] [-OutputCulture ] [-OutputEncoding ] + [-OutputFooter ] [-OutputFormat ] [-OutputJobSummaryPath ] + [-OutputJsonIndent ] [-OutputOutcome ] [-OutputPath ] + [-OutputSarifProblemsOnly ] [-OutputStyle ] [-RepositoryBaseRef ] + [-RepositoryUrl ] [-RuleIncludeLocal ] [] ``` ### FromOption @@ -47,14 +48,15 @@ New-PSRuleOption [-Option] [-Configuration ] [-InconclusiveWarning ] [-NotProcessedWarning ] [-SuppressedRuleWarning ] [-InvariantCultureWarning ] [-InitialSessionState ] [-IncludeModule ] [-IncludePath ] [-Format ] [-InputIgnoreGitPath ] - [-InputIgnoreRepositoryCommon ] [-InputIgnoreObjectSource ] [-ObjectPath ] - [-InputTargetType ] [-InputPathIgnore ] [-LoggingLimitDebug ] - [-LoggingLimitVerbose ] [-LoggingRuleFail ] [-LoggingRulePass ] - [-OutputAs ] [-OutputBanner ] [-OutputCulture ] - [-OutputEncoding ] [-OutputFooter ] [-OutputFormat ] - [-OutputOutcome ] [-OutputPath ] [-OutputSarifProblemsOnly ] - [-OutputStyle ] [-OutputJsonIndent ] [-RepositoryUrl ] - [-RuleIncludeLocal ] [] + [-InputIgnoreRepositoryCommon ] [-InputIgnoreObjectSource ] + [-InputIgnoreUnchangedPath ] [-ObjectPath ] [-InputTargetType ] + [-InputPathIgnore ] [-LoggingLimitDebug ] [-LoggingLimitVerbose ] + [-LoggingRuleFail ] [-LoggingRulePass ] [-OutputAs ] + [-OutputBanner ] [-OutputCulture ] [-OutputEncoding ] + [-OutputFooter ] [-OutputFormat ] [-OutputJobSummaryPath ] + [-OutputJsonIndent ] [-OutputOutcome ] [-OutputPath ] + [-OutputSarifProblemsOnly ] [-OutputStyle ] [-RepositoryBaseRef ] + [-RepositoryUrl ] [-RuleIncludeLocal ] [] ``` ### FromDefault @@ -68,14 +70,15 @@ New-PSRuleOption [-Default] [-Configuration ] [-SuppressTar [-InconclusiveWarning ] [-NotProcessedWarning ] [-SuppressedRuleWarning ] [-InvariantCultureWarning ] [-InitialSessionState ] [-IncludeModule ] [-IncludePath ] [-Format ] [-InputIgnoreGitPath ] - [-InputIgnoreRepositoryCommon ] [-InputIgnoreObjectSource ] [-ObjectPath ] - [-InputTargetType ] [-InputPathIgnore ] [-LoggingLimitDebug ] - [-LoggingLimitVerbose ] [-LoggingRuleFail ] [-LoggingRulePass ] - [-OutputAs ] [-OutputBanner ] [-OutputCulture ] - [-OutputEncoding ] [-OutputFooter ] [-OutputFormat ] - [-OutputOutcome ] [-OutputPath ] [-OutputSarifProblemsOnly ] - [-OutputStyle ] [-OutputJsonIndent ] [-RepositoryUrl ] - [-RuleIncludeLocal ] [] + [-InputIgnoreRepositoryCommon ] [-InputIgnoreObjectSource ] + [-InputIgnoreUnchangedPath ] [-ObjectPath ] [-InputTargetType ] + [-InputPathIgnore ] [-LoggingLimitDebug ] [-LoggingLimitVerbose ] + [-LoggingRuleFail ] [-LoggingRulePass ] [-OutputAs ] + [-OutputBanner ] [-OutputCulture ] [-OutputEncoding ] + [-OutputFooter ] [-OutputFormat ] [-OutputJobSummaryPath ] + [-OutputJsonIndent ] [-OutputOutcome ] [-OutputPath ] + [-OutputSarifProblemsOnly ] [-OutputStyle ] [-RepositoryBaseRef ] + [-RepositoryUrl ] [-RuleIncludeLocal ] [] ``` ## DESCRIPTION @@ -542,6 +545,23 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -InputIgnoreUnchangedPath + +Sets the option `Input.IgnoreUnchangedPath`. +The `Input.IgnoreUnchangedPath` option determine if unchanged files are ignored. + +```yaml +Type: Boolean +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -ObjectPath Sets the `Input.ObjectPath` option to use an object path to use instead of the pipeline object. @@ -788,6 +808,23 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -OutputJobSummaryPath + +Set the option `Output.JobSummaryPath`. +The `Output.JobSummaryPath` option configures the path to a job summary output file. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -OutputJsonIndent Sets the option `Output.JsonIndent`. @@ -880,6 +917,23 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -RepositoryBaseRef + +Sets the option `Repository.BaseRef`. +The `Repository.BaseRef` option sets the repository base ref used for comparisons of changed files. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -RepositoryUrl Sets the option `Repository.Url`. diff --git a/docs/commands/PSRule/en-US/Set-PSRuleOption.md b/docs/commands/PSRule/en-US/Set-PSRuleOption.md index 089996bc93..55cee7ad31 100644 --- a/docs/commands/PSRule/en-US/Set-PSRuleOption.md +++ b/docs/commands/PSRule/en-US/Set-PSRuleOption.md @@ -22,13 +22,14 @@ Set-PSRuleOption [[-Path] ] [-Option ] [-PassThru] [-Force [-NotProcessedWarning ] [-SuppressedRuleWarning ] [-InvariantCultureWarning ] [-InitialSessionState ] [-IncludeModule ] [-IncludePath ] [-Format ] [-InputIgnoreGitPath ] [-InputIgnoreObjectSource ] - [-InputIgnoreRepositoryCommon ] [-ObjectPath ] [-InputPathIgnore ] - [-InputTargetType ] [-LoggingLimitDebug ] [-LoggingLimitVerbose ] - [-LoggingRuleFail ] [-LoggingRulePass ] [-OutputAs ] - [-OutputBanner ] [-OutputCulture ] [-OutputEncoding ] - [-OutputFooter ] [-OutputFormat ] [-OutputOutcome ] + [-InputIgnoreRepositoryCommon ] [-InputIgnoreUnchangedPath ] [-ObjectPath ] + [-InputPathIgnore ] [-InputTargetType ] [-LoggingLimitDebug ] + [-LoggingLimitVerbose ] [-LoggingRuleFail ] [-LoggingRulePass ] + [-OutputAs ] [-OutputBanner ] [-OutputCulture ] + [-OutputEncoding ] [-OutputFooter ] [-OutputFormat ] + [-OutputJobSummaryPath ] [-OutputJsonIndent ] [-OutputOutcome ] [-OutputPath ] [-OutputSarifProblemsOnly ] [-OutputStyle ] - [-OutputJsonIndent ] [-RepositoryUrl ] [-RuleIncludeLocal ] [-WhatIf] [-Confirm] + [-RepositoryBaseRef ] [-RepositoryUrl ] [-RuleIncludeLocal ] [-WhatIf] [-Confirm] [] ``` @@ -433,6 +434,23 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -InputIgnoreUnchangedPath + +Sets the option `Input.IgnoreUnchangedPath`. +The `Input.IgnoreUnchangedPath` option determine if unchanged files are ignored. + +```yaml +Type: Boolean +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -ObjectPath Sets the `Input.ObjectPath` option to use an object path to use instead of the pipeline object. @@ -675,6 +693,23 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -OutputJobSummaryPath + +Set the option `Output.JobSummaryPath`. +The `Output.JobSummaryPath` option configures the path to a job summary output file. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -OutputJsonIndent Sets the option `Output.JsonIndent`. @@ -799,6 +834,23 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -RepositoryBaseRef + +Sets the option `Repository.BaseRef`. +The `Repository.BaseRef` option sets the repository base ref used for comparisons of changed files. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -RepositoryUrl Sets the option `Repository.Url`. diff --git a/docs/concepts/PSRule/en-US/about_PSRule_Options.md b/docs/concepts/PSRule/en-US/about_PSRule_Options.md index 99a7f5fd73..0c06d36890 100644 --- a/docs/concepts/PSRule/en-US/about_PSRule_Options.md +++ b/docs/concepts/PSRule/en-US/about_PSRule_Options.md @@ -42,6 +42,7 @@ The following workspace options are available for use: - [Output.Encoding](#outputencoding) - [Output.Footer](#outputfooter) - [Output.Format](#outputformat) +- [Output.JobSummaryPath](#outputjobsummarypath) - [Output.JsonIndent](#outputjsonindent) - [Output.Outcome](#outputoutcome) - [Output.Path](#outputpath) @@ -2352,13 +2353,13 @@ If specified, the `-Style` parameter takes precedence, over this option. The following styles are available: -- Client - Output is written to the host directly in green/ red to indicate outcome. -- Plain - Output is written as an unformatted string. +- `Client` - Output is written to the host directly in green/ red to indicate outcome. +- `Plain` - Output is written as an unformatted string. This option can be redirected to a file. -- AzurePipelines - Output is written for integration Azure Pipelines. -- GitHubActions - Output is written for integration GitHub Actions. -- VisualStudioCode - Output is written for integration with Visual Studio Code. -- Detect - Output style will be detected by checking the environment variables. +- `AzurePipelines` - Output is written for integration Azure Pipelines. +- `GitHubActions` - Output is written for integration GitHub Actions. +- `VisualStudioCode` - Output is written for integration with Visual Studio Code. +- `Detect` - Output style will be detected by checking the environment variables. This is the default. Detect uses the following logic: @@ -2423,6 +2424,72 @@ variables: value: AzurePipelines ``` +### Output.JobSummaryPath + +Configures the file path a job summary will be written to when using `Assert-PSRule`. +A job summary is a markdown file that summarizes the results of a job. +When not specified, a job summary will not be generated. + +Syntax: + +```yaml +output: + jobSummaryPath: string +``` + +Default: + +```yaml +output: + jobSummaryPath: null +``` + +This option can be specified using: + +```powershell +# PowerShell: Using the OutputJobSummaryPath parameter +$option = New-PSRuleOption -OutputJobSummaryPath 'reports/summary.md'; +``` + +```powershell +# PowerShell: Using the Output.JobSummaryPath hashtable key +$option = New-PSRuleOption -Option @{ 'Output.JobSummaryPath' = 'reports/summary.md' }; +``` + +```powershell +# PowerShell: Using the OutputJobSummaryPath parameter to set YAML +Set-PSRuleOption -OutputJobSummaryPath 'reports/summary.md'; +``` + +```yaml +# YAML: Using the output/jobSummaryPath property +output: + jobSummaryPath: 'reports/summary.md' +``` + +```bash +# Bash: Using environment variable +export PSRULE_OUTPUT_JOBSUMMARYPATH='reports/summary.md' +``` + +```powershell +# PowerShell: Using environment variable +$env:PSRULE_OUTPUT_JOBSUMMARYPATH = 'reports/summary.md'; +``` + +```yaml +# GitHub Actions: Using environment variable +env: + PSRULE_OUTPUT_JOBSUMMARYPATH: reports/summary.md +``` + +```yaml +# Azure Pipelines: Using environment variable +variables: +- name: PSRULE_OUTPUT_JOBSUMMARYPATH + value: reports/summary.md +``` + ### Output.JsonIndent Configures the number of spaces to indent JSON properties and elements. @@ -2446,7 +2513,7 @@ $option = New-PSRuleOption -Option @{ 'Output.JsonIndent' = 2 }; ``` ```powershell -# PowerShell: Using the OutputStyle parameter to set YAML +# PowerShell: Using the OutputJsonIndent parameter to set YAML Set-PSRuleOption -OutputJsonIndent 2; ``` @@ -2926,6 +2993,7 @@ output: encoding: UTF8 footer: RuleCount format: Json + jobSummaryPath: reports/summary.md outcome: Fail sarifProblemsOnly: false style: GitHubActions @@ -3026,6 +3094,7 @@ output: encoding: Default footer: Default format: None + jobSummaryPath: null outcome: Processed sarifProblemsOnly: true style: Detect diff --git a/docs/scenarios/benchmark/results-v2.0.0-pre.md b/docs/scenarios/benchmark/results-v2.0.0-pre.md deleted file mode 100644 index ffc7f7d41b..0000000000 --- a/docs/scenarios/benchmark/results-v2.0.0-pre.md +++ /dev/null @@ -1,29 +0,0 @@ -``` ini - -BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000 -Intel Core i7-1065G7 CPU 1.30GHz, 1 CPU, 8 logical and 4 physical cores -.NET SDK=6.0.101 - [Host] : .NET Core 3.1.22 (CoreCLR 4.700.21.56803, CoreFX 4.700.21.57101), X64 RyuJIT - DefaultJob : .NET Core 3.1.22 (CoreCLR 4.700.21.56803, CoreFX 4.700.21.57101), X64 RyuJIT - - -``` -| Method | Mean | Error | StdDev | Median | Gen 0 | Gen 1 | Allocated | -|------------------------- |-----------------:|----------------:|----------------:|-----------------:|-----------:|----------:|----------:| -| Invoke | 50,095,808.5 ns | 772,205.79 ns | 722,321.78 ns | 50,307,409.1 ns | 4272.7273 | 363.6364 | 17,696 KB | -| InvokeIf | 55,034,039.3 ns | 1,108,134.08 ns | 3,267,358.33 ns | 53,563,390.0 ns | 4500.0000 | 400.0000 | 19,946 KB | -| InvokeType | 50,531,706.1 ns | 547,514.40 ns | 512,145.31 ns | 50,618,872.7 ns | 4272.7273 | 363.6364 | 17,696 KB | -| InvokeSummary | 50,301,144.7 ns | 623,171.61 ns | 582,915.11 ns | 50,296,230.0 ns | 4300.0000 | 300.0000 | 17,696 KB | -| Assert | 53,474,003.3 ns | 476,358.06 ns | 445,585.63 ns | 53,438,930.0 ns | 4200.0000 | 300.0000 | 18,399 KB | -| Get | 5,898,009.3 ns | 100,257.34 ns | 93,780.78 ns | 5,903,140.6 ns | 85.9375 | - | 366 KB | -| GetHelp | 5,990,237.6 ns | 60,012.37 ns | 56,135.61 ns | 5,983,071.9 ns | 85.9375 | 7.8125 | 366 KB | -| Within | 90,532,090.0 ns | 978,130.29 ns | 914,943.68 ns | 90,418,050.0 ns | 8250.0000 | 1250.0000 | 34,134 KB | -| WithinBulk | 136,200,131.0 ns | 2,643,670.56 ns | 3,875,058.02 ns | 135,265,500.0 ns | 14000.0000 | 1000.0000 | 61,162 KB | -| WithinLike | 117,553,698.8 ns | 2,282,752.98 ns | 3,200,100.87 ns | 116,213,366.7 ns | 11666.6667 | 1666.6667 | 48,289 KB | -| DefaultTargetNameBinding | 701,108.6 ns | 10,557.17 ns | 9,875.18 ns | 701,832.1 ns | 38.0859 | - | 156 KB | -| CustomTargetNameBinding | 852,397.7 ns | 6,473.52 ns | 5,405.68 ns | 850,940.1 ns | 85.9375 | - | 352 KB | -| NestedTargetNameBinding | 839,413.8 ns | 6,618.42 ns | 5,867.06 ns | 839,922.0 ns | 85.9375 | - | 352 KB | -| AssertHasFieldValue | 3,125,266.4 ns | 32,987.28 ns | 29,242.37 ns | 3,125,243.2 ns | 234.3750 | - | 962 KB | -| PathTokenize | 828.7 ns | 7.28 ns | 6.81 ns | 829.1 ns | 0.2632 | - | 1 KB | -| PathExpressionBuild | 529.4 ns | 5.93 ns | 5.54 ns | 529.7 ns | 0.3500 | - | 1 KB | -| PathExpressionGet | 349,019.9 ns | 3,132.67 ns | 2,930.30 ns | 348,427.6 ns | 17.0898 | - | 70 KB | diff --git a/pipeline.build.ps1 b/pipeline.build.ps1 index e7197694cb..29c3fd596a 100644 --- a/pipeline.build.ps1 +++ b/pipeline.build.ps1 @@ -114,14 +114,16 @@ task TestDotNet { if ($CodeCoverage) { exec { # Test library - dotnet test --collect:"Code Coverage" --logger trx -r (Join-Path $PWD -ChildPath reports/) tests/PSRule.Tests + # dotnet test --collect:"Code Coverage" --logger trx -r ../../../../../reports/ tests/PSRule.Tests + dotnet test } } else { exec { # Test library # dotnet test --logger "console;verbosity=detailed" tests/PSRule.Tests - dotnet test --logger trx -r (Join-Path $PWD -ChildPath reports/) tests/PSRule.Tests + # dotnet test --logger trx -r ../../../../../reports/ tests/PSRule.Tests + dotnet test } } } diff --git a/schemas/PSRule-options.schema.json b/schemas/PSRule-options.schema.json index 9f7367e709..a707bc0a70 100644 --- a/schemas/PSRule-options.schema.json +++ b/schemas/PSRule-options.schema.json @@ -1,907 +1,913 @@ { - "$schema": "https://json-schema.org/draft-07/schema#", - "title": "PSRule options", - "description": "A schema for PSRule YAML options files.", - "$ref": "#/definitions/options", - "definitions": { - "configuration": { - "type": "object", - "title": "Configuration values", - "description": "A set of key/ value configuration options for rules.", - "markdownDescription": "A set of key/ value configuration options for rules. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#configuration)", + "$schema": "https://json-schema.org/draft-07/schema#", + "title": "PSRule options", + "description": "A schema for PSRule YAML options files.", + "$ref": "#/definitions/options", + "definitions": { + "configuration": { + "type": "object", + "title": "Configuration values", + "description": "A set of key/ value configuration options for rules.", + "markdownDescription": "A set of key/ value configuration options for rules. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#configuration)", + "properties": { + "AZURE_PARAMETER_FILE_EXPANSION": { + "type": "boolean", + "title": "Parameter file expansion", + "description": "Determines if Azure template parameter files will automatically be expanded. By default, parameter files will not be automatically expanded. When enabled, PSRule will discover and expand JSON parameter files for Azure templates or Bicep modules.", + "markdownDescription": "Determines if Azure template parameter files will automatically be expanded. By default, parameter files will not be automatically expanded. When enabled, PSRule will discover and expand JSON parameter files for Azure templates or Bicep modules. [See help](https://azure.github.io/PSRule.Rules.Azure/setup/configuring-expansion/#parameterfileexpansion)\r\rApplies to: [PSRule for Azure](https://aka.ms/ps-rule-azure)", + "default": false + }, + "AZURE_BICEP_FILE_EXPANSION": { + "type": "boolean", + "title": "Bicep source expansion", + "description": "This configuration option determines if Azure Bicep source files will automatically be expanded. By default, Bicep files will not be automatically expanded.", + "markdownDescription": "This configuration option determines if Azure Bicep source files will automatically be expanded. By default, Bicep files will not be automatically expanded. [See help](https://azure.github.io/PSRule.Rules.Azure/setup/configuring-expansion/#bicepsourceexpansion)\r\rApplies to: [PSRule for Azure](https://aka.ms/ps-rule-azure)", + "default": false + }, + "AZURE_BICEP_FILE_EXPANSION_TIMEOUT": { + "type": "integer", + "title": "Bicep compilation timeout", + "description": "This configuration option determines the maximum time to spend building a single Bicep source file. The timeout is configured in seconds.", + "markdownDescription": "This configuration option determines the maximum time to spend building a single Bicep source file. The timeout is configured in seconds. [See help](https://azure.github.io/PSRule.Rules.Azure/setup/configuring-expansion/#bicepcompilationtimeout)\r\rApplies to: [PSRule for Azure](https://aka.ms/ps-rule-azure)", + "default": 5, + "minimum": 1, + "maximum": 120 + }, + "AZURE_PARAMETER_FILE_METADATA_LINK": { + "type": "boolean", + "title": "Bicep source expansion", + "description": "This configuration option determines if Azure Bicep source files will automatically be expanded. By default, Bicep files will not be automatically expanded.", + "markdownDescription": "This configuration option determines if Azure Bicep source files will automatically be expanded. By default, Bicep files will not be automatically expanded. [See help](https://azure.github.io/PSRule.Rules.Azure/setup/configuring-expansion/#bicepsourceexpansion)\r\rApplies to: [PSRule for Azure](https://aka.ms/ps-rule-azure)", + "default": false + }, + "AZURE_DEPLOYMENT": { + "type": "object", + "title": "Deployment properties", + "description": "This configuration option sets the deployment object use by the deployment() function. Configure this option to change the details of the deployment when exporting templates for analysis. Provided properties will override the default.", + "markdownDescription": "This configuration option sets the deployment object use by the `deployment()` function. Configure this option to change the details of the deployment when exporting templates for analysis. Provided properties will override the default. [See help](https://azure.github.io/PSRule.Rules.Azure/setup/configuring-expansion/#deploymentproperties)\r\rApplies to: [PSRule for Azure](https://aka.ms/ps-rule-azure)", + "properties": { + "name": { + "type": "string", + "default": "ps-rule-test-deployment" + } + }, + "additionalProperties": false + }, + "AZURE_RESOURCE_GROUP": { + "type": "object", + "title": "Deployment resource group", + "description": "This configuration option sets the resource group object used by the resourceGroup() function. Configure this option to change the resource group object when using exporting templates for analysis. Provided properties will override the default.", + "markdownDescription": "This configuration option sets the resource group object used by the `resourceGroup()` function. Configure this option to change the resource group object when using exporting templates for analysis. Provided properties will override the default. [See help](https://azure.github.io/PSRule.Rules.Azure/setup/configuring-expansion/#deploymentresourcegroup)\r\rApplies to: [PSRule for Azure](https://aka.ms/ps-rule-azure)", + "properties": { + "name": { + "type": "string", + "default": "ps-rule-test-rg" + }, + "location": { + "type": "string", + "default": "eastus" + }, + "tags": { + "type": "object", + "default": {} + }, "properties": { - "AZURE_PARAMETER_FILE_EXPANSION": { - "type": "boolean", - "title": "Parameter file expansion", - "description": "Determines if Azure template parameter files will automatically be expanded. By default, parameter files will not be automatically expanded. When enabled, PSRule will discover and expand JSON parameter files for Azure templates or Bicep modules.", - "markdownDescription": "Determines if Azure template parameter files will automatically be expanded. By default, parameter files will not be automatically expanded. When enabled, PSRule will discover and expand JSON parameter files for Azure templates or Bicep modules. [See help](https://azure.github.io/PSRule.Rules.Azure/setup/configuring-expansion/#parameterfileexpansion)\r\rApplies to: [PSRule for Azure](https://aka.ms/ps-rule-azure)", - "default": false - }, - "AZURE_BICEP_FILE_EXPANSION": { - "type": "boolean", - "title": "Bicep source expansion", - "description": "This configuration option determines if Azure Bicep source files will automatically be expanded. By default, Bicep files will not be automatically expanded.", - "markdownDescription": "This configuration option determines if Azure Bicep source files will automatically be expanded. By default, Bicep files will not be automatically expanded. [See help](https://azure.github.io/PSRule.Rules.Azure/setup/configuring-expansion/#bicepsourceexpansion)\r\rApplies to: [PSRule for Azure](https://aka.ms/ps-rule-azure)", - "default": false - }, - "AZURE_BICEP_FILE_EXPANSION_TIMEOUT": { - "type": "integer", - "title": "Bicep compilation timeout", - "description": "This configuration option determines the maximum time to spend building a single Bicep source file. The timeout is configured in seconds.", - "markdownDescription": "This configuration option determines the maximum time to spend building a single Bicep source file. The timeout is configured in seconds. [See help](https://azure.github.io/PSRule.Rules.Azure/setup/configuring-expansion/#bicepcompilationtimeout)\r\rApplies to: [PSRule for Azure](https://aka.ms/ps-rule-azure)", - "default": 5, - "minimum": 1, - "maximum": 120 - }, - "AZURE_PARAMETER_FILE_METADATA_LINK": { - "type": "boolean", - "title": "Bicep source expansion", - "description": "This configuration option determines if Azure Bicep source files will automatically be expanded. By default, Bicep files will not be automatically expanded.", - "markdownDescription": "This configuration option determines if Azure Bicep source files will automatically be expanded. By default, Bicep files will not be automatically expanded. [See help](https://azure.github.io/PSRule.Rules.Azure/setup/configuring-expansion/#bicepsourceexpansion)\r\rApplies to: [PSRule for Azure](https://aka.ms/ps-rule-azure)", - "default": false - }, - "AZURE_DEPLOYMENT": { - "type": "object", - "title": "Deployment properties", - "description": "This configuration option sets the deployment object use by the deployment() function. Configure this option to change the details of the deployment when exporting templates for analysis. Provided properties will override the default.", - "markdownDescription": "This configuration option sets the deployment object use by the `deployment()` function. Configure this option to change the details of the deployment when exporting templates for analysis. Provided properties will override the default. [See help](https://azure.github.io/PSRule.Rules.Azure/setup/configuring-expansion/#deploymentproperties)\r\rApplies to: [PSRule for Azure](https://aka.ms/ps-rule-azure)", - "properties": { - "name": { - "type": "string", - "default": "ps-rule-test-deployment" - } - }, - "additionalProperties": false - }, - "AZURE_RESOURCE_GROUP": { - "type": "object", - "title": "Deployment resource group", - "description": "This configuration option sets the resource group object used by the resourceGroup() function. Configure this option to change the resource group object when using exporting templates for analysis. Provided properties will override the default.", - "markdownDescription": "This configuration option sets the resource group object used by the `resourceGroup()` function. Configure this option to change the resource group object when using exporting templates for analysis. Provided properties will override the default. [See help](https://azure.github.io/PSRule.Rules.Azure/setup/configuring-expansion/#deploymentresourcegroup)\r\rApplies to: [PSRule for Azure](https://aka.ms/ps-rule-azure)", - "properties": { - "name": { - "type": "string", - "default": "ps-rule-test-rg" - }, - "location": { - "type": "string", - "default": "eastus" - }, - "tags": { - "type": "object", - "default": {} - }, - "properties": { - "type": "object", - "properties": { - "provisioningState": { - "type": "string", - "default": "Succeeded" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - "AZURE_SUBSCRIPTION": { - "type": "object", - "title": "Deployment subscription", - "description": "This configuration option sets the subscription object used by the subscription() function. Configure this option to change the subscription object when using exporting templates for analysis. Provided properties will override the default.", - "markdownDescription": "This configuration option sets the subscription object used by the `subscription()` function. Configure this option to change the subscription object when using exporting templates for analysis. Provided properties will override the default. [See help](https://azure.github.io/PSRule.Rules.Azure/setup/configuring-expansion/#deploymentsubscription)\r\rApplies to: [PSRule for Azure](https://aka.ms/ps-rule-azure)", - "properties": { - "subscriptionId": { - "type": "string", - "default": "ffffffff-ffff-ffff-ffff-ffffffffffff" - }, - "displayName": { - "type": "string", - "default": "PSRule Test Subscription" - }, - "state": { - "type": "string", - "default": "NotDefined" - } - }, - "additionalProperties": false - }, - "AZURE_TENANT": { - "type": "object", - "title": "Deployment tenant", - "description": "This configuration option sets the tenant object used by the tenant() function. Configure this option to change the tenant object when using exporting templates for analysis. Provided properties will override the default.", - "markdownDescription": "This configuration option sets the tenant object used by the `tenant()` function. Configure this option to change the tenant object when using exporting templates for analysis. Provided properties will override the default. [See help](https://azure.github.io/PSRule.Rules.Azure/setup/configuring-expansion/#deploymenttenant)\r\rApplies to: [PSRule for Azure](https://aka.ms/ps-rule-azure)", - "properties": { - "countryCode": { - "type": "string", - "default": "US" - }, - "tenantId": { - "type": "string", - "default": "ffffffff-ffff-ffff-ffff-ffffffffffff" - }, - "displayName": { - "type": "string", - "default": "PSRule" - } - }, - "additionalProperties": false - }, - "AZURE_MANAGEMENT_GROUP": { - "type": "object", - "title": "Deployment management group", - "description": "This configuration option sets the management group object used by the managementGroup() function. Configure this option to change the management group object when using exporting templates for analysis. Provided properties will override the default.", - "markdownDescription": "This configuration option sets the management group object used by the `managementGroup()` function. Configure this option to change the management group object when using exporting templates for analysis. Provided properties will override the default. [See help](https://azure.github.io/PSRule.Rules.Azure/setup/configuring-expansion/#deploymentmanagementgroup)\r\rApplies to: [PSRule for Azure](https://aka.ms/ps-rule-azure)", - "properties": { - "name": { - "type": "string", - "default": "psrule-test" - }, - "properties": { - "type": "object", - "properties": { - "displayName": { - "type": "string", - "default": "PSRule Test Management Group" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - "AZURE_PARAMETER_DEFAULTS": { - "type": "object", - "title": "Required parameter defaults", - "description": "This configuration option allows a fallback value to be configured for required parameters. When a parameter value is not provided and a default is not set, the fallback value will be used.", - "markdownDescription": "This configuration option allows a fallback value to be configured for required parameters. When a parameter value is not provided and a default is not set, the fallback value will be used. [See help](https://azure.github.io/PSRule.Rules.Azure/setup/configuring-expansion/#requiredparameterdefaults)\r\rApplies to: [PSRule for Azure](https://aka.ms/ps-rule-azure)" + "type": "object", + "properties": { + "provisioningState": { + "type": "string", + "default": "Succeeded" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "AZURE_SUBSCRIPTION": { + "type": "object", + "title": "Deployment subscription", + "description": "This configuration option sets the subscription object used by the subscription() function. Configure this option to change the subscription object when using exporting templates for analysis. Provided properties will override the default.", + "markdownDescription": "This configuration option sets the subscription object used by the `subscription()` function. Configure this option to change the subscription object when using exporting templates for analysis. Provided properties will override the default. [See help](https://azure.github.io/PSRule.Rules.Azure/setup/configuring-expansion/#deploymentsubscription)\r\rApplies to: [PSRule for Azure](https://aka.ms/ps-rule-azure)", + "properties": { + "subscriptionId": { + "type": "string", + "default": "ffffffff-ffff-ffff-ffff-ffffffffffff" }, - "additionalProperties": true, - "defaultSnippets": [ - { - "label": "Custom configuration value", - "body": { - "${1:Key}": "${2:Value}" - } - } - ] + "displayName": { + "type": "string", + "default": "PSRule Test Subscription" + }, + "state": { + "type": "string", + "default": "NotDefined" + } + }, + "additionalProperties": false }, - "convention-option": { - "type": "object", - "title": "Convention options", - "description": "Options that configure conventions.", - "properties": { - "include": { - "type": "array", - "title": "Include conventions", - "description": "An ordered list of conventions to include.", - "items": { - "type": "string" - }, - "uniqueItems": true - } + "AZURE_TENANT": { + "type": "object", + "title": "Deployment tenant", + "description": "This configuration option sets the tenant object used by the tenant() function. Configure this option to change the tenant object when using exporting templates for analysis. Provided properties will override the default.", + "markdownDescription": "This configuration option sets the tenant object used by the `tenant()` function. Configure this option to change the tenant object when using exporting templates for analysis. Provided properties will override the default. [See help](https://azure.github.io/PSRule.Rules.Azure/setup/configuring-expansion/#deploymenttenant)\r\rApplies to: [PSRule for Azure](https://aka.ms/ps-rule-azure)", + "properties": { + "countryCode": { + "type": "string", + "default": "US" }, - "additionalProperties": false + "tenantId": { + "type": "string", + "default": "ffffffff-ffff-ffff-ffff-ffffffffffff" + }, + "displayName": { + "type": "string", + "default": "PSRule" + } + }, + "additionalProperties": false }, - "binding-option": { - "type": "object", - "title": "Object binding", - "description": "Configure property/ object binding options.", + "AZURE_MANAGEMENT_GROUP": { + "type": "object", + "title": "Deployment management group", + "description": "This configuration option sets the management group object used by the managementGroup() function. Configure this option to change the management group object when using exporting templates for analysis. Provided properties will override the default.", + "markdownDescription": "This configuration option sets the management group object used by the `managementGroup()` function. Configure this option to change the management group object when using exporting templates for analysis. Provided properties will override the default. [See help](https://azure.github.io/PSRule.Rules.Azure/setup/configuring-expansion/#deploymentmanagementgroup)\r\rApplies to: [PSRule for Azure](https://aka.ms/ps-rule-azure)", + "properties": { + "name": { + "type": "string", + "default": "psrule-test" + }, "properties": { - "field": { - "type": "object", - "title": "Field", - "description": "Custom fields to bind.", - "markdownDescription": "Custom fields to bind. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingfield)", - "additionalProperties": { - "type": "array", - "description": "A custom field to bind.", - "markdownDescription": "Custom field to bind. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingfield)", - "items": { - "type": "string" - }, - "uniqueItems": true - } - }, - "ignoreCase": { - "type": "boolean", - "title": "Ignore case", - "description": "Determines if custom binding uses ignores case when matching properties. The default is true.", - "markdownDescription": "Determines if custom binding uses ignores case when matching properties. The default is `true`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingignorecase)", - "default": true - }, - "nameSeparator": { - "type": "string", - "title": "Name separator", - "description": "Configures the separator to use for building a qualified name. The default is '/'.", - "markdownDescription": "Configures the separator to use for building a qualified name. The default is `/`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingnameseparator)", - "default": "/" - }, - "preferTargetInfo": { - "type": "boolean", - "title": "Prefer target info", - "description": "Determines if binding prefers target info provided by the object over custom configuration. The default is false.", - "markdownDescription": "Determines if binding prefers target info provided by the object over custom configuration. The default is `false`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingprefertargetinfo)", - "default": false - }, - "targetName": { - "type": "array", - "title": "Bind TargetName", - "description": "Specifies one or more property names to bind TargetName to.", - "markdownDescription": "Specifies one or more property names to bind TargetName to. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingtargetname)", - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "targetType": { - "type": "array", - "title": "Bind TargetType", - "description": "Specifies one or more property names to bind TargetType to.", - "markdownDescription": "Specifies one or more property names to bind TargetType to. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingtargettype)", - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "useQualifiedName": { - "type": "boolean", - "title": "Use qualified name", - "description": "Determines if a qualified TargetName is used. The default is false.", - "markdownDescription": "Determines if a qualified TargetName is used. The default is `false`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingusequalifiedname)", - "default": false + "type": "object", + "properties": { + "displayName": { + "type": "string", + "default": "PSRule Test Management Group" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "AZURE_PARAMETER_DEFAULTS": { + "type": "object", + "title": "Required parameter defaults", + "description": "This configuration option allows a fallback value to be configured for required parameters. When a parameter value is not provided and a default is not set, the fallback value will be used.", + "markdownDescription": "This configuration option allows a fallback value to be configured for required parameters. When a parameter value is not provided and a default is not set, the fallback value will be used. [See help](https://azure.github.io/PSRule.Rules.Azure/setup/configuring-expansion/#requiredparameterdefaults)\r\rApplies to: [PSRule for Azure](https://aka.ms/ps-rule-azure)" + } + }, + "additionalProperties": true, + "defaultSnippets": [ + { + "label": "Custom configuration value", + "body": { + "${1:Key}": "${2:Value}" + } + } + ] + }, + "convention-option": { + "type": "object", + "title": "Convention options", + "description": "Options that configure conventions.", + "properties": { + "include": { + "type": "array", + "title": "Include conventions", + "description": "An ordered list of conventions to include.", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + "additionalProperties": false + }, + "binding-option": { + "type": "object", + "title": "Object binding", + "description": "Configure property/ object binding options.", + "properties": { + "field": { + "type": "object", + "title": "Field", + "description": "Custom fields to bind.", + "markdownDescription": "Custom fields to bind. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingfield)", + "additionalProperties": { + "type": "array", + "description": "A custom field to bind.", + "markdownDescription": "Custom field to bind. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingfield)", + "items": { + "type": "string" }, - "additionalProperties": false + "uniqueItems": true + } + }, + "ignoreCase": { + "type": "boolean", + "title": "Ignore case", + "description": "Determines if custom binding uses ignores case when matching properties. The default is true.", + "markdownDescription": "Determines if custom binding uses ignores case when matching properties. The default is `true`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingignorecase)", + "default": true + }, + "nameSeparator": { + "type": "string", + "title": "Name separator", + "description": "Configures the separator to use for building a qualified name. The default is '/'.", + "markdownDescription": "Configures the separator to use for building a qualified name. The default is `/`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingnameseparator)", + "default": "/" }, - "execution-option": { + "preferTargetInfo": { + "type": "boolean", + "title": "Prefer target info", + "description": "Determines if binding prefers target info provided by the object over custom configuration. The default is false.", + "markdownDescription": "Determines if binding prefers target info provided by the object over custom configuration. The default is `false`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingprefertargetinfo)", + "default": false + }, + "targetName": { + "type": "array", + "title": "Bind TargetName", + "description": "Specifies one or more property names to bind TargetName to.", + "markdownDescription": "Specifies one or more property names to bind TargetName to. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingtargetname)", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "targetType": { + "type": "array", + "title": "Bind TargetType", + "description": "Specifies one or more property names to bind TargetType to.", + "markdownDescription": "Specifies one or more property names to bind TargetType to. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingtargettype)", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "useQualifiedName": { + "type": "boolean", + "title": "Use qualified name", + "description": "Determines if a qualified TargetName is used. The default is false.", + "markdownDescription": "Determines if a qualified TargetName is used. The default is `false`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingusequalifiedname)", + "default": false + } + }, + "additionalProperties": false + }, + "execution-option": { + "type": "object", + "title": "Execution options", + "description": "Options that affect execution.", + "markdownDescription": "Options that affect execution. [See help](https://aka.ms/ps-rule/options)", + "properties": { + "aliasReferenceWarning": { + "type": "boolean", + "title": "Warn on resource aliases", + "description": "Enable or disable warnings when an alias to a resource is used. The default is true.", + "markdownDescription": "Enable or disable warnings when an alias to a resource is used. The default is `true`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executionaliasreferencewarning)", + "default": true + }, + "duplicateResourceId": { + "type": "string", + "title": "Duplicate resource identifiers", + "description": "Determines how to handle duplicate resources identifiers during execution. Regardless of the value, only the first resource will be used. By defaut, an error is thrown. When set to Warn, a warning is generated. When set to Ignore, no output will be displayed.", + "markdownDescription": "Determines how to handle duplicate resources identifiers during execution.\n\nRegardless of the value, only the first resource will be used. By defaut, an error is thrown. When set to `Warn`, a warning is generated. When set to `Ignore`, no output will be displayed. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executionduplicateresourceid)", + "enum": [ + "Ignore", + "Warn", + "Error" + ], + "default": "Error" + }, + "languageMode": { + "type": "string", + "title": "Language mode", + "description": "The PowerShell language mode to use for rule execution. The default is FullLanguage.", + "markdownDescription": "The PowerShell language mode to use for rule execution. The default is `FullLanguage`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executionlanguagemode)", + "enum": [ + "FullLanguage", + "ConstrainedLanguage" + ], + "default": "FullLanguage" + }, + "inconclusiveWarning": { + "type": "boolean", + "title": "Warn on inconclusive rules", + "description": "Enable or disable warnings for inconclusive rules. The default is true.", + "markdownDescription": "Enable or disable warnings for inconclusive rules. The default is `true`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executioninconclusivewarning)", + "default": true + }, + "notProcessedWarning": { + "type": "boolean", + "title": "Warn on unprocessed objects", + "description": "Enable or disable warnings for objects that are not processed by any rule. The default is true.", + "markdownDescription": "Enable or disable warnings for objects that are not processed by any rule. The default is `true`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executionnotprocessedwarning)", + "default": true + }, + "suppressedRuleWarning": { + "type": "boolean", + "title": "Warn on suppressed rules", + "description": "Enable or disable warnings for suppressed rules. The default is true.", + "markdownDescription": "Enable or disable warnings for suppressed rules. The default is `true`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executionsuppressedrulewarning)", + "default": true + }, + "invariantCultureWarning": { + "type": "boolean", + "title": "Warn on invariant culture", + "description": "Enable or disable warning when invariant culture is used. The default is true.", + "markdownDescription": "Enable or disable warning when invariant culture is used. The default is `true`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executioninvariantculturewarning)", + "default": true + }, + "initialSessionState": { + "type": "string", + "title": "Initial Session State", + "description": "Determines how the initial session state for executing PowerShell code is created. The default is BuiltIn.", + "markdownDescription": "Determines how the initial session state for executing PowerShell code is created. The default is `BuiltIn`.\n- When set to `BuiltIn` all built-in cmdlets are loaded.\n- When set to `Minimal` only cmdlets for hosting PowerShell will be loaded.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executioninitialsessionstate)", + "enum": [ + "BuiltIn", + "Minimal" + ], + "default": "BuiltIn" + } + }, + "additionalProperties": false + }, + "include-option": { + "type": "object", + "title": "Include options", + "description": "Options that affect source locations imported for execution.", + "properties": { + "module": { + "type": "array", + "title": "Include module", + "description": "Automatically include rules and resources from the specified modules.", + "markdownDescription": "Automatically include rules and resources from the specified modules. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#includemodule)", + "items": { + "type": "string", + "title": "Include module", + "description": "Automatically include rules and resources from the specified modules.", + "markdownDescription": "Automatically include rules and resources from the specified modules. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#includemodule)" + }, + "uniqueItems": true + }, + "path": { + "type": "array", + "title": "Include path", + "description": "Automatically include rules and resources from the specified paths.", + "markdownDescription": "Automatically include rules and resources from the specified paths. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#includepath)", + "items": { + "type": "string", + "title": "Include path", + "description": "Automatically include rules and resources from the specified paths.", + "markdownDescription": "Automatically include rules and resources from the specified paths. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#includepath)" + }, + "uniqueItems": true + } + }, + "additionalProperties": false + }, + "input-option": { + "type": "object", + "title": "Input options", + "description": "Options that affect how input types are processed.", + "markdownDescription": "Options that affect how input types are processed.", + "properties": { + "format": { + "type": "string", + "title": "Input format", + "description": "The input string format. The default is Detect, which will try to detect the format when the -InputPath parameter is used.", + "markdownDescription": "The input string format. The default is `Detect`, which will try to detect the format when the `-InputPath` parameter is used. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#inputformat)", + "enum": [ + "None", + "Yaml", + "Json", + "Markdown", + "PowerShellData", + "File", + "Detect" + ], + "default": "Detect" + }, + "ignoreGitPath": { + "type": "boolean", + "title": "Ignore .git path", + "description": "Determine if files within the .git path are ignored when the -InputPath parameter is used. This is enabled by default.", + "markdownDescription": "Determine if files within the `.git` path are ignored when the `-InputPath` parameter is used. This is enabled by default. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#inputignoregitpath)", + "default": true + }, + "ignoreRepositoryCommon": { + "type": "boolean", + "title": "Ignore common files", + "description": "Determine if common repository files are ignored when the -InputPath parameter is used. This is enabled by default.", + "markdownDescription": "Determine if common repository files are ignored when the `-InputPath` parameter is used. This is enabled by default. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#inputignorerepositorycommon)", + "default": true + }, + "ignoreObjectSource": { + "type": "boolean", + "title": "Ignore object source", + "description": "Determines if objects are ignore based on their file source path. When set, objects from the pipeline or read from files will be exclude based on their source path and the configuration of pathIgnore, ignoreGitPath, and ignoreRepositoryCommon options.", + "markdownDescription": "Determines if objects are ignore based on their file source path. When set, objects from the pipeline or read from files will be exclude based on their source path and the configuration of `pathIgnore`, `ignoreGitPath`, and `ignoreRepositoryCommon` options. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#inputignoreobjectsource)", + "default": false + }, + "ignoreUnchangedPath": { + "type": "boolean", + "title": "Ignore unchanged path", + "description": "Determine if unchanged files are ignored. By default, PSRule will process all files within an input path. For large repositories, this can result in a large number of files being processed. Additionally, for a pull request you may only be interested in files that have changed.", + "markdownDescription": "Determine if unchanged files are ignored. By default, PSRule will process all files within an input path. For large repositories, this can result in a large number of files being processed. Additionally, for a pull request you may only be interested in files that have changed. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#inputignoreunchangedpath)", + "default": false + }, + "objectPath": { + "type": "string", + "title": "Object path", + "description": "The object path to a property to use instead of the pipeline object.", + "markdownDescription": "The object path to a property to use instead of the pipeline object. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#inputobjectpath)" + }, + "pathIgnore": { + "type": "array", + "title": "Path ignore", + "description": "Exclude input files that match the path spec. To exclude a file specify the path prefix or file name. To re-include previously excluded paths use ! at the start of the entry. Wildcards are supported.", + "markdownDescription": "Exclude input files that match the path spec. To exclude a file specify the path prefix or file name. To re-include previously excluded paths use `!` at the start of the entry. Wildcards are supported. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#inputpathignore)", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "targetType": { + "type": "array", + "title": "Target type", + "description": "Only process objects that match one of the included types.", + "markdownDescription": "Only process objects that match one of the included types. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#inputtargettype)", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + "additionalProperties": false + }, + "logging-option": { + "type": "object", + "title": "Logging options", + "description": "Options for configuring information logging.", + "properties": { + "limitDebug": { + "type": "array", + "title": "Scopes for debug messages", + "description": "Limits debug messages to a list of named debug scopes. No scopes are set by default.", + "markdownDescription": "Limits debug messages to a list of named debug scopes. No scopes are set by default. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#logginglimitdebug)", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "limitVerbose": { + "type": "array", + "title": "Scopes for verbose messages", + "description": "Limits verbose messages to a list of named verbose scopes. No scopes are set by default.", + "markdownDescription": "Limits verbose messages to a list of named verbose scopes. No scopes are set by default. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#logginglimitverbose)", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "ruleFail": { + "type": "string", + "title": "Report fail to stream", + "description": "Log fail outcomes for each rule to a specific informational stream. The default is None.", + "markdownDescription": "Log fail outcomes for each rule to a specific informational stream. The default is `None`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#loggingrulefail)", + "enum": [ + "None", + "Error", + "Warning", + "Information" + ], + "default": "None" + }, + "rulePass": { + "type": "string", + "title": "Report pass to stream", + "description": "Log pass outcomes for each rule to a specific informational stream. The default is None.", + "markdownDescription": "Log pass outcomes for each rule to a specific informational stream. The default is `None`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#loggingrulepass)", + "enum": [ + "None", + "Error", + "Warning", + "Information" + ], + "default": "None" + } + }, + "additionalProperties": false + }, + "suppression-option": { + "type": "object", + "title": "Suppress rules", + "description": "Specifies suppression rules.", + "markdownDescription": "Specifies suppression rules. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#suppression)", + "uniqueItems": true, + "additionalProperties": { + "oneOf": [ + { + "type": "array", + "title": "Suppressed rule", + "description": "The name of the rule to suppress.", + "markdownDescription": "The name of the rule to suppress. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#suppression)", + "items": { + "type": "string", + "title": "Suppress when", + "description": "Suppress the rule on TargetNames to suppress.", + "markdownDescription": "Suppress the rule on TargetNames to suppress. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#suppression)" + }, + "uniqueItems": true + }, + { "type": "object", - "title": "Execution options", - "description": "Options that affect execution.", - "markdownDescription": "Options that affect execution. [See help](https://aka.ms/ps-rule/options)", + "title": "Suppressed rule", + "description": "The name of the rule to suppress.", + "markdownDescription": "The name of the rule to suppress. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#suppression)", "properties": { - "aliasReferenceWarning": { - "type": "boolean", - "title": "Warn on resource aliases", - "description": "Enable or disable warnings when an alias to a resource is used. The default is true.", - "markdownDescription": "Enable or disable warnings when an alias to a resource is used. The default is `true`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executionaliasreferencewarning)", - "default": true - }, - "duplicateResourceId": { - "type": "string", - "title": "Duplicate resource identifiers", - "description": "Determines how to handle duplicate resources identifiers during execution. Regardless of the value, only the first resource will be used. By defaut, an error is thrown. When set to Warn, a warning is generated. When set to Ignore, no output will be displayed.", - "markdownDescription": "Determines how to handle duplicate resources identifiers during execution.\n\nRegardless of the value, only the first resource will be used. By defaut, an error is thrown. When set to `Warn`, a warning is generated. When set to `Ignore`, no output will be displayed. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executionduplicateresourceid)", - "enum": [ - "Ignore", - "Warn", - "Error" - ], - "default": "Error" - }, - "languageMode": { - "type": "string", - "title": "Language mode", - "description": "The PowerShell language mode to use for rule execution. The default is FullLanguage.", - "markdownDescription": "The PowerShell language mode to use for rule execution. The default is `FullLanguage`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executionlanguagemode)", - "enum": [ - "FullLanguage", - "ConstrainedLanguage" - ], - "default": "FullLanguage" - }, - "inconclusiveWarning": { - "type": "boolean", - "title": "Warn on inconclusive rules", - "description": "Enable or disable warnings for inconclusive rules. The default is true.", - "markdownDescription": "Enable or disable warnings for inconclusive rules. The default is `true`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executioninconclusivewarning)", - "default": true - }, - "notProcessedWarning": { - "type": "boolean", - "title": "Warn on unprocessed objects", - "description": "Enable or disable warnings for objects that are not processed by any rule. The default is true.", - "markdownDescription": "Enable or disable warnings for objects that are not processed by any rule. The default is `true`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executionnotprocessedwarning)", - "default": true - }, - "suppressedRuleWarning": { - "type": "boolean", - "title": "Warn on suppressed rules", - "description": "Enable or disable warnings for suppressed rules. The default is true.", - "markdownDescription": "Enable or disable warnings for suppressed rules. The default is `true`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executionsuppressedrulewarning)", - "default": true - }, - "invariantCultureWarning": { - "type": "boolean", - "title": "Warn on invariant culture", - "description": "Enable or disable warning when invariant culture is used. The default is true.", - "markdownDescription": "Enable or disable warning when invariant culture is used. The default is `true`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executioninvariantculturewarning)", - "default": true - }, - "initialSessionState": { - "type": "string", - "title": "Initial Session State", - "description": "Determines how the initial session state for executing PowerShell code is created. The default is BuiltIn.", - "markdownDescription": "Determines how the initial session state for executing PowerShell code is created. The default is `BuiltIn`.\n- When set to `BuiltIn` all built-in cmdlets are loaded.\n- When set to `Minimal` only cmdlets for hosting PowerShell will be loaded.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executioninitialsessionstate)", - "enum": [ - "BuiltIn", - "Minimal" - ], - "default": "BuiltIn" - } + "targetName": { + "type": "array", + "title": "Suppress when", + "description": "One or more TargetNames to suppress.", + "markdownDescription": "One or more TargetNames to suppress. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#suppression)", + "items": { + "type": "string" + }, + "uniqueItems": true + } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "targetName" + ] + } + ] + } + }, + "output-option": { + "type": "object", + "title": "Output options", + "description": "Options that affect how output is generated.", + "properties": { + "as": { + "type": "string", + "title": "Result type", + "description": "Determine if detailed or summary results are generated. The default is Detail.", + "markdownDescription": "Determine if detailed or summary results are generated. The default is `Detail`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputas)", + "enum": [ + "Detail", + "Summary" + ], + "default": "Detail" }, - "include-option": { - "type": "object", - "title": "Include options", - "description": "Options that affect source locations imported for execution.", - "properties": { - "module": { - "type": "array", - "title": "Include module", - "description": "Automatically include rules and resources from the specified modules.", - "markdownDescription": "Automatically include rules and resources from the specified modules. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#includemodule)", - "items": { - "type": "string", - "title": "Include module", - "description": "Automatically include rules and resources from the specified modules.", - "markdownDescription": "Automatically include rules and resources from the specified modules. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#includemodule)" - }, - "uniqueItems": true - }, - "path": { - "type": "array", - "title": "Include path", - "description": "Automatically include rules and resources from the specified paths.", - "markdownDescription": "Automatically include rules and resources from the specified paths. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#includepath)", - "items": { - "type": "string", - "title": "Include path", - "description": "Automatically include rules and resources from the specified paths.", - "markdownDescription": "Automatically include rules and resources from the specified paths. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#includepath)" - }, - "uniqueItems": true - } + "banner": { + "title": "Banner format", + "description": "The information displayed for Assert-PSRule banner. The default is Default which includes Title, Source, SupportLinks, and RepositoryInfo.", + "markdownDescription": "The information displayed for Assert-PSRule banner. The default is `Default` which includes `Title`, `Source`, `SupportLinks`, and `RepositoryInfo`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputbanner)", + "oneOf": [ + { + "type": "string", + "enum": [ + "None", + "Title", + "Source", + "SupportLinks", + "RepositoryInfo", + "Default", + "Minimal" + ] }, - "additionalProperties": false + { + "type": "integer" + } + ], + "default": "Default" }, - "input-option": { - "type": "object", - "title": "Input options", - "description": "Options that affect how input types are processed.", - "markdownDescription": "Options that affect how input types are processed.", - "properties": { - "format": { - "type": "string", - "title": "Input format", - "description": "The input string format. The default is Detect, which will try to detect the format when the -InputPath parameter is used.", - "markdownDescription": "The input string format. The default is `Detect`, which will try to detect the format when the `-InputPath` parameter is used. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#inputformat)", - "enum": [ - "None", - "Yaml", - "Json", - "Markdown", - "PowerShellData", - "File", - "Detect" - ], - "default": "Detect" - }, - "ignoreGitPath": { - "type": "boolean", - "title": "Ignore .git path", - "description": "Determine if files within the .git path are ignored when the -InputPath parameter is used. This is enabled by default.", - "markdownDescription": "Determine if files within the `.git` path are ignored when the `-InputPath` parameter is used. This is enabled by default. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#inputignoregitpath)", - "default": true - }, - "ignoreRepositoryCommon": { - "type": "boolean", - "title": "Ignore common files", - "description": "Determine if common repository files are ignored when the -InputPath parameter is used. This is enabled by default.", - "markdownDescription": "Determine if common repository files are ignored when the `-InputPath` parameter is used. This is enabled by default. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#inputignorerepositorycommon)", - "default": true - }, - "ignoreObjectSource": { - "type": "boolean", - "title": "Ignore object source", - "description": "Determines if objects are ignore based on their file source path. When set, objects from the pipeline or read from files will be exclude based on their source path and the configuration of pathIgnore, ignoreGitPath, and ignoreRepositoryCommon options.", - "markdownDescription": "Determines if objects are ignore based on their file source path. When set, objects from the pipeline or read from files will be exclude based on their source path and the configuration of `pathIgnore`, `ignoreGitPath`, and `ignoreRepositoryCommon` options. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#inputignoreobjectsource)", - "default": false - }, - "ignoreUnchangedPath": { - "type": "boolean", - "title": "Ignore unchanged path", - "description": "Determine if unchanged files are ignored. By default, PSRule will process all files within an input path. For large repositories, this can result in a large number of files being processed. Additionally, for a pull request you may only be interested in files that have changed.", - "markdownDescription": "Determine if unchanged files are ignored. By default, PSRule will process all files within an input path. For large repositories, this can result in a large number of files being processed. Additionally, for a pull request you may only be interested in files that have changed. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#inputignoreunchangedpath)", - "default": false - }, - "objectPath": { - "type": "string", - "title": "Object path", - "description": "The object path to a property to use instead of the pipeline object.", - "markdownDescription": "The object path to a property to use instead of the pipeline object. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#inputobjectpath)" - }, - "pathIgnore": { - "type": "array", - "title": "Path ignore", - "description": "Exclude input files that match the path spec. To exclude a file specify the path prefix or file name. To re-include previously excluded paths use ! at the start of the entry. Wildcards are supported.", - "markdownDescription": "Exclude input files that match the path spec. To exclude a file specify the path prefix or file name. To re-include previously excluded paths use `!` at the start of the entry. Wildcards are supported. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#inputpathignore)", - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "targetType": { - "type": "array", - "title": "Target type", - "description": "Only process objects that match one of the included types.", - "markdownDescription": "Only process objects that match one of the included types. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#inputtargettype)", - "items": { - "type": "string" - }, - "uniqueItems": true - } + "culture": { + "type": "array", + "title": "Culture", + "description": "One or more cultures to use for generating output. When multiple cultures are specified, the first matching culture will be used. By default, the current PowerShell culture is used.", + "markdownDescription": "One or more cultures to use for generating output. When multiple cultures are specified, the first matching culture will be used. By default, the current PowerShell culture is used. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputculture)", + "items": { + "type": "string", + "description": "A culture for generating output.", + "minLength": 2 + }, + "uniqueItems": true, + "defaultSnippets": [ + { + "label": "en-AU", + "bodyText": [ + "en-AU" + ] + }, + { + "label": "en-US", + "bodyText": [ + "en-US" + ] }, - "additionalProperties": false + { + "label": "en-GB", + "bodyText": [ + "en-GB" + ] + } + ] }, - "logging-option": { - "type": "object", - "title": "Logging options", - "description": "Options for configuring information logging.", - "properties": { - "limitDebug": { - "type": "array", - "title": "Scopes for debug messages", - "description": "Limits debug messages to a list of named debug scopes. No scopes are set by default.", - "markdownDescription": "Limits debug messages to a list of named debug scopes. No scopes are set by default. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#logginglimitdebug)", - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "limitVerbose": { - "type": "array", - "title": "Scopes for verbose messages", - "description": "Limits verbose messages to a list of named verbose scopes. No scopes are set by default.", - "markdownDescription": "Limits verbose messages to a list of named verbose scopes. No scopes are set by default. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#logginglimitverbose)", - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "ruleFail": { - "type": "string", - "title": "Report fail to stream", - "description": "Log fail outcomes for each rule to a specific informational stream. The default is None.", - "markdownDescription": "Log fail outcomes for each rule to a specific informational stream. The default is `None`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#loggingrulefail)", - "enum": [ - "None", - "Error", - "Warning", - "Information" - ], - "default": "None" - }, - "rulePass": { - "type": "string", - "title": "Report pass to stream", - "description": "Log pass outcomes for each rule to a specific informational stream. The default is None.", - "markdownDescription": "Log pass outcomes for each rule to a specific informational stream. The default is `None`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#loggingrulepass)", - "enum": [ - "None", - "Error", - "Warning", - "Information" - ], - "default": "None" - } + "encoding": { + "type": "string", + "title": "Encoding", + "description": "The encoding to use when writing results to file. The default is Default, UTF-8 without BOM.", + "markdownDescription": "The encoding to use when writing results to file. The default is `Default`, UTF-8 without BOM. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputencoding)", + "enum": [ + "Default", + "UTF8", + "UTF7", + "Unicode", + "UTF32", + "ASCII" + ], + "default": "Default" + }, + "footer": { + "title": "Footer format", + "description": "The information displayed for Assert-PSRule footer. The default is Default which includes RuleCount, RunInfo, and OutputFile.", + "markdownDescription": "The information displayed for Assert-PSRule footer. The default is `Default` which includes `RuleCount`, `RunInfo`, and `OutputFile`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputfooter)", + "oneOf": [ + { + "type": "string", + "enum": [ + "None", + "RuleCount", + "RunInfo", + "OutputFile", + "Default" + ] }, - "additionalProperties": false + { + "type": "integer", + "minimum": 0 + } + ], + "default": "Default" }, - "suppression-option": { - "type": "object", - "title": "Suppress rules", - "description": "Specifies suppression rules.", - "markdownDescription": "Specifies suppression rules. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#suppression)", - "uniqueItems": true, - "additionalProperties": { - "oneOf": [ - { - "type": "array", - "title": "Suppressed rule", - "description": "The name of the rule to suppress.", - "markdownDescription": "The name of the rule to suppress. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#suppression)", - "items": { - "type": "string", - "title": "Suppress when", - "description": "Suppress the rule on TargetNames to suppress.", - "markdownDescription": "Suppress the rule on TargetNames to suppress. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#suppression)" - }, - "uniqueItems": true - }, - { - "type": "object", - "title": "Suppressed rule", - "description": "The name of the rule to suppress.", - "markdownDescription": "The name of the rule to suppress. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#suppression)", - "properties": { - "targetName": { - "type": "array", - "title": "Suppress when", - "description": "One or more TargetNames to suppress.", - "markdownDescription": "One or more TargetNames to suppress. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#suppression)", - "items": { - "type": "string" - }, - "uniqueItems": true - } - }, - "additionalProperties": false, - "required": [ - "targetName" - ] - } - ] + "format": { + "type": "string", + "title": "Output format", + "description": "The output format to use when returning results. The default is None.", + "markdownDescription": "The output format to use when returning results. The default is `None`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputformat)", + "enum": [ + "None", + "Yaml", + "Json", + "Markdown", + "NUnit3", + "Csv", + "Wide", + "Sarif" + ], + "default": "None" + }, + "jobSummaryPath": { + "type": "string", + "title": "Job Summary Path", + "description": "The path to a job summary output file.", + "markdownDescription": "The path to a job summary output file. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputjobsummarypath)" + }, + "jsonIndent": { + "type": "integer", + "title": "Output Json Indent", + "description": "The indentation level for JSON output. The default is 0.", + "markdownDescription": "The indentation level for JSON output. The default is `0`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputjsonindent)", + "minimum": 0, + "maximum": 4, + "default": 0 + }, + "outcome": { + "type": "string", + "title": "Output outcome", + "description": "The outcome of rule results to return. The default is Processed.", + "markdownDescription": "The outcome of rule results to return. The default is `Processed`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputoutcome)", + "enum": [ + "None", + "Fail", + "Pass", + "Error", + "Processed", + "All" + ], + "default": "Processed" + }, + "path": { + "type": "string", + "title": "Output path", + "description": "The file path location to save results.", + "markdownDescription": "The file path location to save results. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputpath)" + }, + "style": { + "type": "string", + "title": "Output Style", + "description": "The style that results will be presented in. The default is Detect.", + "markdownDescription": "The style that results will be presented in. The default is `Detect`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputstyle)", + "enum": [ + "Client", + "Plain", + "AzurePipelines", + "GitHubActions", + "VisualStudioCode", + "Detect" + ], + "default": "Detect" + }, + "sarifProblemsOnly": { + "type": "boolean", + "title": "SARIF Problems Only", + "description": "Determines if SARIF output only includes rules with fail or error outcomes. The default is true.", + "markdownDescription": "Determines if SARIF output only includes rules with fail or error outcomes. The default is `true`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputsarifproblemsonly)", + "default": true + } + }, + "additionalProperties": false + }, + "options": { + "properties": { + "binding": { + "type": "object", + "oneOf": [ + { + "$ref": "#/definitions/binding-option" } + ] }, - "output-option": { - "type": "object", - "title": "Output options", - "description": "Options that affect how output is generated.", - "properties": { - "as": { - "type": "string", - "title": "Result type", - "description": "Determine if detailed or summary results are generated. The default is Detail.", - "markdownDescription": "Determine if detailed or summary results are generated. The default is `Detail`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputas)", - "enum": [ - "Detail", - "Summary" - ], - "default": "Detail" - }, - "banner": { - "title": "Banner format", - "description": "The information displayed for Assert-PSRule banner. The default is Default which includes Title, Source, SupportLinks, and RepositoryInfo.", - "markdownDescription": "The information displayed for Assert-PSRule banner. The default is `Default` which includes `Title`, `Source`, `SupportLinks`, and `RepositoryInfo`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputbanner)", - "oneOf": [ - { - "type": "string", - "enum": [ - "None", - "Title", - "Source", - "SupportLinks", - "RepositoryInfo", - "Default", - "Minimal" - ] - }, - { - "type": "integer" - } - ], - "default": "Default" - }, - "culture": { - "type": "array", - "title": "Culture", - "description": "One or more cultures to use for generating output. When multiple cultures are specified, the first matching culture will be used. By default, the current PowerShell culture is used.", - "markdownDescription": "One or more cultures to use for generating output. When multiple cultures are specified, the first matching culture will be used. By default, the current PowerShell culture is used. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputculture)", - "items": { - "type": "string", - "description": "A culture for generating output.", - "minLength": 2 - }, - "uniqueItems": true, - "defaultSnippets": [ - { - "label": "en-AU", - "bodyText": [ - "en-AU" - ] - }, - { - "label": "en-US", - "bodyText": [ - "en-US" - ] - }, - { - "label": "en-GB", - "bodyText": [ - "en-GB" - ] - } - ] - }, - "encoding": { - "type": "string", - "title": "Encoding", - "description": "The encoding to use when writing results to file. The default is Default, UTF-8 without BOM.", - "markdownDescription": "The encoding to use when writing results to file. The default is `Default`, UTF-8 without BOM. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputencoding)", - "enum": [ - "Default", - "UTF8", - "UTF7", - "Unicode", - "UTF32", - "ASCII" - ], - "default": "Default" - }, - "footer": { - "title": "Footer format", - "description": "The information displayed for Assert-PSRule footer. The default is Default which includes RuleCount, RunInfo, and OutputFile.", - "markdownDescription": "The information displayed for Assert-PSRule footer. The default is `Default` which includes `RuleCount`, `RunInfo`, and `OutputFile`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputfooter)", - "oneOf": [ - { - "type": "string", - "enum": [ - "None", - "RuleCount", - "RunInfo", - "OutputFile", - "Default" - ] - }, - { - "type": "integer", - "minimum": 0 - } - ], - "default": "Default" - }, - "format": { - "type": "string", - "title": "Output format", - "description": "The output format to use when returning results. The default is None.", - "markdownDescription": "The output format to use when returning results. The default is `None`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputformat)", - "enum": [ - "None", - "Yaml", - "Json", - "Markdown", - "NUnit3", - "Csv", - "Wide", - "Sarif" - ], - "default": "None" - }, - "outcome": { - "type": "string", - "title": "Output outcome", - "description": "The outcome of rule results to return. The default is Processed.", - "markdownDescription": "The outcome of rule results to return. The default is `Processed`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputoutcome)", - "enum": [ - "None", - "Fail", - "Pass", - "Error", - "Processed", - "All" - ], - "default": "Processed" - }, - "path": { - "type": "string", - "title": "Output path", - "description": "The file path location to save results.", - "markdownDescription": "The file path location to save results. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputpath)" - }, - "style": { - "type": "string", - "title": "Output Style", - "description": "The style that results will be presented in. The default is Detect.", - "markdownDescription": "The style that results will be presented in. The default is `Detect`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputstyle)", - "enum": [ - "Client", - "Plain", - "AzurePipelines", - "GitHubActions", - "VisualStudioCode", - "Detect" - ], - "default": "Detect" - }, - "jsonIndent": { - "type": "integer", - "title": "Output Json Indent", - "description": "The indentation level for JSON output. The default is 0.", - "markdownDescription": "The indentation level for JSON output. The default is `0`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputjsonindent)", - "minimum": 0, - "maximum": 4, - "default": 0 - }, - "sarifProblemsOnly": { - "type": "boolean", - "title": "SARIF Problems Only", - "description": "Determines if SARIF output only includes rules with fail or error outcomes. The default is true.", - "markdownDescription": "Determines if SARIF output only includes rules with fail or error outcomes. The default is `true`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputsarifproblemsonly)", - "default": true - } - }, - "additionalProperties": false + "configuration": { + "type": "object", + "oneOf": [ + { + "$ref": "#/definitions/configuration" + } + ] }, - "options": { - "properties": { - "binding": { - "type": "object", - "oneOf": [ - { - "$ref": "#/definitions/binding-option" - } - ] - }, - "configuration": { - "type": "object", - "oneOf": [ - { - "$ref": "#/definitions/configuration" - } - ] - }, - "convention": { - "type": "object", - "$ref": "#/definitions/convention-option" - }, - "execution": { - "type": "object", - "$ref": "#/definitions/execution-option" - }, - "include": { - "type": "object", - "$ref": "#/definitions/include-option" - }, - "input": { - "type": "object", - "$ref": "#/definitions/input-option" - }, - "logging": { - "type": "object", - "oneOf": [ - { - "$ref": "#/definitions/logging-option" - } - ] - }, - "output": { - "type": "object", - "$ref": "#/definitions/output-option" - }, - "repository": { - "type": "object", - "$ref": "#/definitions/repository-option" - }, - "requires": { - "type": "object", - "$ref": "#/definitions/requires" - }, - "rule": { - "type": "object", - "oneOf": [ - { - "$ref": "#/definitions/rule-option" - } - ] - }, - "suppression": { - "type": "object", - "oneOf": [ - { - "$ref": "#/definitions/suppression-option" - } - ] - } - }, - "additionalProperties": false + "convention": { + "type": "object", + "$ref": "#/definitions/convention-option" }, - "repository-option": { - "type": "object", - "title": "Repository", - "description": "Configures repository options.", - "markdownDescription": "Configures repository options.", - "properties": { - "baseRef": { - "type": "string", - "title": "Base Reference", - "description": "Sets the repository base ref used for comparisons of changed files. By default, the base ref is detected from environment vairables set by the build system.", - "markdownDescription": "Sets the repository base ref used for comparisons of changed files. By default, the base ref is detected from environment vairables set by the build system. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#repositorybaseref)" - }, - "url": { - "type": "string", - "title": "Repository URL", - "description": "Sets the repository URL reported in output. By default, the repository URL is detected from environment variables set by the build system.", - "markdownDescription": "Sets the repository URL reported in output. By default, the repository URL is detected from environment variables set by the build system. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#repositoryurl)" - } - }, - "additionalProperties": false + "execution": { + "type": "object", + "$ref": "#/definitions/execution-option" + }, + "include": { + "type": "object", + "$ref": "#/definitions/include-option" + }, + "input": { + "type": "object", + "$ref": "#/definitions/input-option" + }, + "logging": { + "type": "object", + "oneOf": [ + { + "$ref": "#/definitions/logging-option" + } + ] + }, + "output": { + "type": "object", + "$ref": "#/definitions/output-option" + }, + "repository": { + "type": "object", + "$ref": "#/definitions/repository-option" }, "requires": { - "type": "object", - "title": "Required modules", - "description": "Specifies the required version of a module to use.", - "markdownDescription": "Specifies the required version of a module to use. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#requires)", - "additionalProperties": { - "type": "string", - "title": "Version constraint", - "description": "Specifies a module to constrain to a specific version.", - "markdownDescription": "Specifies a module to constrain to a specific version. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#requires)", - "pattern": "^(@pre |@prerelease )?(((?:^|~|\\>=|\\>|=|\\<=|\\<)?v?(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)(?:\\s|\\s?\\|\\|\\s?)?){1,}$" - }, - "defaultSnippets": [ - { - "label": "Version constraint", - "body": { - "${1:Module}": "'>=${2:1.0.0}'" - } - } - ] + "type": "object", + "$ref": "#/definitions/requires" }, - "rule-option": { - "type": "object", - "title": "Baseline options", - "description": "Options that include/ exclude and configure rules.", - "properties": { - "include": { - "type": "array", - "title": "Include rules", - "description": "Optionally filter to rules by name.", - "markdownDescription": "Optionally filter to rules by name. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#ruleinclude)", - "items": { - "type": "string", - "$ref": "#/definitions/resourceName" - }, - "uniqueItems": true - }, - "includeLocal": { - "type": "boolean", - "title": "Include local", - "description": "Automatically include all local rules in the search path unless they have been explicitly excluded.", - "markdownDescription": "Automatically include all local rules in the search path unless they have been explicitly excluded. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#ruleincludelocal)", - "default": false - }, - "exclude": { - "type": "array", - "title": "Exclude rules", - "description": "Specifies rules to exclude by name.", - "markdownDescription": "Specifies rules to exclude by name. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#ruleexclude)", - "items": { - "type": "string", - "$ref": "#/definitions/resourceName" - }, - "uniqueItems": true - }, - "tag": { - "type": "object", - "title": "Tags", - "description": "Require rules to have the following tags.", - "markdownDescription": "Require rules to have the following tags. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#ruletag)", - "additionalProperties": { - "oneOf": [ - { - "type": "string", - "description": "A required tag." - }, - { - "type": "array", - "description": "A required tag.", - "items": { - "type": "string" - }, - "uniqueItems": true - } - ] - } - } - }, - "additionalProperties": false + "rule": { + "type": "object", + "oneOf": [ + { + "$ref": "#/definitions/rule-option" + } + ] }, - "resourceName": { + "suppression": { + "type": "object", + "oneOf": [ + { + "$ref": "#/definitions/suppression-option" + } + ] + } + }, + "additionalProperties": false + }, + "repository-option": { + "type": "object", + "title": "Repository", + "description": "Configures repository options.", + "markdownDescription": "Configures repository options.", + "properties": { + "baseRef": { + "type": "string", + "title": "Base Reference", + "description": "Sets the repository base ref used for comparisons of changed files. By default, the base ref is detected from environment vairables set by the build system.", + "markdownDescription": "Sets the repository base ref used for comparisons of changed files. By default, the base ref is detected from environment vairables set by the build system. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#repositorybaseref)" + }, + "url": { + "type": "string", + "title": "Repository URL", + "description": "Sets the repository URL reported in output. By default, the repository URL is detected from environment variables set by the build system.", + "markdownDescription": "Sets the repository URL reported in output. By default, the repository URL is detected from environment variables set by the build system. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#repositoryurl)" + } + }, + "additionalProperties": false + }, + "requires": { + "type": "object", + "title": "Required modules", + "description": "Specifies the required version of a module to use.", + "markdownDescription": "Specifies the required version of a module to use. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#requires)", + "additionalProperties": { + "type": "string", + "title": "Version constraint", + "description": "Specifies a module to constrain to a specific version.", + "markdownDescription": "Specifies a module to constrain to a specific version. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#requires)", + "pattern": "^(@pre |@prerelease )?(((?:^|~|\\>=|\\>|=|\\<=|\\<)?v?(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)(?:\\s|\\s?\\|\\|\\s?)?){1,}$" + }, + "defaultSnippets": [ + { + "label": "Version constraint", + "body": { + "${1:Module}": "'>=${2:1.0.0}'" + } + } + ] + }, + "rule-option": { + "type": "object", + "title": "Baseline options", + "description": "Options that include/ exclude and configure rules.", + "properties": { + "include": { + "type": "array", + "title": "Include rules", + "description": "Optionally filter to rules by name.", + "markdownDescription": "Optionally filter to rules by name. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#ruleinclude)", + "items": { "type": "string", - "minLength": 3 + "$ref": "#/definitions/resourceName" + }, + "uniqueItems": true + }, + "includeLocal": { + "type": "boolean", + "title": "Include local", + "description": "Automatically include all local rules in the search path unless they have been explicitly excluded.", + "markdownDescription": "Automatically include all local rules in the search path unless they have been explicitly excluded. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#ruleincludelocal)", + "default": false + }, + "exclude": { + "type": "array", + "title": "Exclude rules", + "description": "Specifies rules to exclude by name.", + "markdownDescription": "Specifies rules to exclude by name. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#ruleexclude)", + "items": { + "type": "string", + "$ref": "#/definitions/resourceName" + }, + "uniqueItems": true + }, + "tag": { + "type": "object", + "title": "Tags", + "description": "Require rules to have the following tags.", + "markdownDescription": "Require rules to have the following tags. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#ruletag)", + "additionalProperties": { + "oneOf": [ + { + "type": "string", + "description": "A required tag." + }, + { + "type": "array", + "description": "A required tag.", + "items": { + "type": "string" + }, + "uniqueItems": true + } + ] + } } + }, + "additionalProperties": false + }, + "resourceName": { + "type": "string", + "minLength": 3 } + } } diff --git a/src/PSRule.Types/Data/ModuleConstraint.cs b/src/PSRule.Types/Data/ModuleConstraint.cs index 1e4dc4bab5..4e572dc458 100644 --- a/src/PSRule.Types/Data/ModuleConstraint.cs +++ b/src/PSRule.Types/Data/ModuleConstraint.cs @@ -3,16 +3,30 @@ namespace PSRule.Data { + /// + /// A version constraint for a PSRule module. + /// public sealed class ModuleConstraint { + /// + /// Create a version constraint for a PSRule module. + /// + /// The name of the module. + /// The version constraint of the module. public ModuleConstraint(string module, IVersionConstraint constraint) { Module = module; Constraint = constraint; } + /// + /// The name of the module. + /// public string Module { get; } + /// + /// The version constraint of the module. + /// public IVersionConstraint Constraint { get; } } } diff --git a/src/PSRule/Configuration/JobSummaryFormat.cs b/src/PSRule/Configuration/JobSummaryFormat.cs new file mode 100644 index 0000000000..e6edb4830b --- /dev/null +++ b/src/PSRule/Configuration/JobSummaryFormat.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace PSRule.Configuration +{ + /// + /// The information displayed for job summaries. + /// Currently this is not exposed as a configuration option. + /// + [Flags] + [JsonConverter(typeof(StringEnumConverter))] + internal enum JobSummaryFormat + { + /// + /// No job summary is outputted. + /// + None = 0, + + /// + /// Include rule analysis within job summary. + /// + Analysis = 1, + + /// + /// The rules module versions used in this run are shown. + /// + Source = 2, + + /// + /// The default information shown in job summaries. + /// + Default = Analysis | Source, + } +} diff --git a/src/PSRule/Configuration/OutputOption.cs b/src/PSRule/Configuration/OutputOption.cs index 4b3cd1e7f4..5cb94a05b6 100644 --- a/src/PSRule/Configuration/OutputOption.cs +++ b/src/PSRule/Configuration/OutputOption.cs @@ -47,6 +47,7 @@ public OutputOption() Encoding = null; Footer = null; Format = null; + JobSummaryPath = null; JsonIndent = null; Path = null; SarifProblemsOnly = null; @@ -68,6 +69,7 @@ public OutputOption(OutputOption option) Encoding = option.Encoding; Footer = option.Footer; Format = option.Format; + JobSummaryPath = option.JobSummaryPath; JsonIndent = option.JsonIndent; Outcome = option.Outcome; Path = option.Path; @@ -91,6 +93,7 @@ public bool Equals(OutputOption other) Encoding == other.Encoding && Footer == other.Footer && Format == other.Format && + JobSummaryPath == other.JobSummaryPath && JsonIndent == other.JsonIndent && Outcome == other.Outcome && Path == other.Path && @@ -110,6 +113,7 @@ public override int GetHashCode() hash = hash * 23 + (Encoding.HasValue ? Encoding.Value.GetHashCode() : 0); hash = hash * 23 + (Footer.HasValue ? Footer.Value.GetHashCode() : 0); hash = hash * 23 + (Format.HasValue ? Format.Value.GetHashCode() : 0); + hash = hash * 23 + (JobSummaryPath != null ? JobSummaryPath.GetHashCode() : 0); hash = hash * 23 + (JsonIndent.HasValue ? JsonIndent.Value.GetHashCode() : 0); hash = hash * 23 + (Outcome.HasValue ? Outcome.Value.GetHashCode() : 0); hash = hash * 23 + (Path != null ? Path.GetHashCode() : 0); @@ -129,6 +133,7 @@ internal static OutputOption Combine(OutputOption o1, OutputOption o2) Encoding = o1.Encoding ?? o2.Encoding, Footer = o1.Footer ?? o2.Footer, Format = o1.Format ?? o2.Format, + JobSummaryPath = o1.JobSummaryPath ?? o2.JobSummaryPath, JsonIndent = o1.JsonIndent ?? o2.JsonIndent, Outcome = o1.Outcome ?? o2.Outcome, Path = o1.Path ?? o2.Path, @@ -174,6 +179,18 @@ internal static OutputOption Combine(OutputOption o1, OutputOption o2) [DefaultValue(null)] public OutputFormat? Format { get; set; } + /// + /// The path to a job summary output file. + /// + [DefaultValue(null)] + public string JobSummaryPath { get; set; } + + /// + /// The indentation for JSON output. + /// + [DefaultValue(null)] + public int? JsonIndent { get; set; } + /// /// The outcome of rule results to return. /// @@ -192,12 +209,6 @@ internal static OutputOption Combine(OutputOption o1, OutputOption o2) [DefaultValue(null)] public OutputStyle? Style { get; set; } - /// - /// The indentation for JSON output - /// - [DefaultValue(null)] - public int? JsonIndent { get; set; } - /// /// Determines if SARIF output only includes rules with fail or error outcomes. /// @@ -224,6 +235,12 @@ internal void Load(EnvironmentHelper env) if (env.TryEnum("PSRULE_OUTPUT_FORMAT", out OutputFormat format)) Format = format; + if (env.TryString("PSRULE_OUTPUT_JOBSUMMARYPATH", out var jobSummaryPath)) + JobSummaryPath = jobSummaryPath; + + if (env.TryInt("PSRULE_OUTPUT_JSONINDENT", out var jsonIndent)) + JsonIndent = jsonIndent; + if (env.TryEnum("PSRULE_OUTPUT_OUTCOME", out RuleOutcome outcome)) Outcome = outcome; @@ -233,9 +250,6 @@ internal void Load(EnvironmentHelper env) if (env.TryEnum("PSRULE_OUTPUT_STYLE", out OutputStyle style)) Style = style; - if (env.TryInt("PSRULE_OUTPUT_JSONINDENT", out var jsonIndent)) - JsonIndent = jsonIndent; - if (env.TryBool("PSRULE_OUTPUT_SARIFPROBLEMSONLY", out var sarifProblemsOnly)) SarifProblemsOnly = sarifProblemsOnly; } @@ -260,6 +274,12 @@ internal void Load(Dictionary index) if (index.TryPopEnum("Output.Format", out OutputFormat format)) Format = format; + if (index.TryPopString("Output.JobSummaryPath", out var jobSummaryPath)) + JobSummaryPath = jobSummaryPath; + + if (index.TryPopValue("Output.JsonIndent", out var jsonIndent)) + JsonIndent = jsonIndent; + if (index.TryPopEnum("Output.Outcome", out RuleOutcome outcome)) Outcome = outcome; @@ -269,9 +289,6 @@ internal void Load(Dictionary index) if (index.TryPopEnum("Output.Style", out OutputStyle style)) Style = style; - if (index.TryPopValue("Output.JsonIndent", out var jsonIndent)) - JsonIndent = jsonIndent; - if (index.TryPopBool("Output.SarifProblemsOnly", out var sarifProblemsOnly)) SarifProblemsOnly = sarifProblemsOnly; } diff --git a/src/PSRule/Data/ITargetSourceCollection.cs b/src/PSRule/Data/ITargetSourceCollection.cs index 7f86b69037..1a6082d7a5 100644 --- a/src/PSRule/Data/ITargetSourceCollection.cs +++ b/src/PSRule/Data/ITargetSourceCollection.cs @@ -41,18 +41,14 @@ internal void AddRange(TargetSourceInfo[] sourceInfo) internal void Add(TargetSourceInfo sourceInfo) { - if (sourceInfo == null) + if (sourceInfo == null || string.IsNullOrEmpty(sourceInfo.Type)) return; - if (_Index == null && !string.IsNullOrEmpty(sourceInfo.Type)) - _Index = new Dictionary(StringComparer.OrdinalIgnoreCase); - - if (string.IsNullOrEmpty(sourceInfo.Type) || _Index.ContainsKey(sourceInfo.Type)) + _Index ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + if (_Index.ContainsKey(sourceInfo.Type)) return; - if (_Items == null) - _Items = new List(); - + _Items ??= new List(); _Items.Add(sourceInfo); _Index.Add(sourceInfo.Type, sourceInfo); } diff --git a/src/PSRule/Definitions/Expressions/Exceptions.cs b/src/PSRule/Definitions/Expressions/Exceptions.cs index eda4c4e5c1..da85f400bb 100644 --- a/src/PSRule/Definitions/Expressions/Exceptions.cs +++ b/src/PSRule/Definitions/Expressions/Exceptions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; @@ -13,38 +13,66 @@ namespace PSRule.Definitions.Expressions /// public abstract class SelectorException : PipelineException { + /// + /// Create an empty selector exception. + /// protected SelectorException() : base() { } + /// + /// Create an selector exception. + /// protected SelectorException(string message) : base(message) { } + /// + /// Create an selector exception. + /// protected SelectorException(string message, Exception innerException) : base(message, innerException) { } + /// + /// Create an selector exception. + /// protected SelectorException(SerializationInfo info, StreamingContext context) : base(info, context) { } } + /// + /// An expression parser exception. + /// [Serializable] public sealed class ExpressionParseException : SelectorException { - public ExpressionParseException() - { - } - + /// + /// Create an empty expression parse exception. + /// + public ExpressionParseException() { } + + /// + /// Create an expression parse exception. + /// public ExpressionParseException(string message) : base(message) { } + /// + /// Create an expression parse exception. + /// public ExpressionParseException(string message, Exception innerException) : base(message, innerException) { } + /// + /// Create an expression parse exception. + /// internal ExpressionParseException(string expression, string message) : base(message) { Expression = expression; } + /// + /// Create an expression parse exception. + /// internal ExpressionParseException(string expression, string message, Exception innerException) : base(message, innerException) { @@ -54,8 +82,12 @@ internal ExpressionParseException(string expression, string message, Exception i private ExpressionParseException(SerializationInfo info, StreamingContext context) : base(info, context) { } + /// + /// The related expression. + /// public string Expression { get; } + /// [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] public override void GetObjectData(SerializationInfo info, StreamingContext context) { @@ -64,35 +96,58 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont } } + /// + /// A base class for an expression exception. + /// public abstract class ExpressionException : SelectorException { - protected ExpressionException() - { - } - + /// + /// Create an empty expression exception. + /// + protected ExpressionException() { } + + /// + /// Create an expression exception. + /// protected ExpressionException(string message) : base(message) { } + /// + /// Create an expression exception. + /// protected ExpressionException(string message, Exception innerException) : base(message, innerException) { } + /// + /// Create an expression exception. + /// protected ExpressionException(string expression, string message) : base(message) { Expression = expression; } + /// + /// Create an expression exception. + /// protected ExpressionException(string expression, string message, Exception innerException) : base(message, innerException) { Expression = expression; } + /// + /// Create an expression exception. + /// protected ExpressionException(SerializationInfo info, StreamingContext context) : base(info, context) { } + /// + /// The related expression. + /// public string Expression { get; } + /// [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] public override void GetObjectData(SerializationInfo info, StreamingContext context) { @@ -101,25 +156,41 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont } } + /// + /// An expression reference exception. + /// [Serializable] public sealed class ExpressionReferenceException : SelectorException { - public ExpressionReferenceException() - { - } - + /// + /// Create an empty expression reference exception. + /// + public ExpressionReferenceException() { } + + /// + /// Create an expression reference exception. + /// public ExpressionReferenceException(string message) : base(message) { } + /// + /// Create an expression reference exception. + /// public ExpressionReferenceException(string message, Exception innerException) : base(message, innerException) { } + /// + /// Create an expression reference exception. + /// internal ExpressionReferenceException(string expression, string message) : base(message) { Expression = expression; } + /// + /// Create an expression reference exception. + /// internal ExpressionReferenceException(string expression, string message, Exception innerException) : base(message, innerException) { @@ -129,8 +200,12 @@ internal ExpressionReferenceException(string expression, string message, Excepti private ExpressionReferenceException(SerializationInfo info, StreamingContext context) : base(info, context) { } + /// + /// The related expression. + /// public string Expression { get; } + /// [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] public override void GetObjectData(SerializationInfo info, StreamingContext context) { @@ -139,26 +214,39 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont } } - + /// + /// An expression argument exception. + /// [Serializable] public sealed class ExpressionArgumentException : ExpressionException { - public ExpressionArgumentException() - { - } - + /// + /// Create an empty expression argument exception. + /// + public ExpressionArgumentException() { } + + /// + /// Create an expression argument exception. + /// public ExpressionArgumentException(string message) : base(message) { } + /// + /// Create an expression argument exception. + /// public ExpressionArgumentException(string message, Exception innerException) : base(message, innerException) { } + /// + /// Create an expression argument exception. + /// internal ExpressionArgumentException(string expression, string message) : base(expression, message) { } private ExpressionArgumentException(SerializationInfo info, StreamingContext context) : base(info, context) { } + /// [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] public override void GetObjectData(SerializationInfo info, StreamingContext context) { diff --git a/src/PSRule/Definitions/ICondition.cs b/src/PSRule/Definitions/ICondition.cs index 1209e1dd8a..47b8167cb0 100644 --- a/src/PSRule/Definitions/ICondition.cs +++ b/src/PSRule/Definitions/ICondition.cs @@ -6,19 +6,41 @@ namespace PSRule.Definitions { + /// + /// A result from an language condition. + /// public interface IConditionResult { + /// + /// Determine if the condition had errors. + /// bool HadErrors { get; } + /// + /// The number of sub-conditions that were evaluated. + /// int Count { get; } + /// + /// The number of sub-conditions that passed. + /// int Pass { get; } } + /// + /// A language condition. + /// public interface ICondition : ILanguageBlock, IDisposable { + /// + /// Invoke the condition to get a result. + /// + /// Returns the result of the condition. IConditionResult If(); + /// + /// The action of error to take when execution the condition. + /// ActionPreference ErrorAction { get; } } } diff --git a/src/PSRule/Host/HostHelper.cs b/src/PSRule/Host/HostHelper.cs index 1a7309fb06..53b926fdc6 100644 --- a/src/PSRule/Host/HostHelper.cs +++ b/src/PSRule/Host/HostHelper.cs @@ -108,9 +108,6 @@ internal static void ImportResource(Source[] source, RunspaceContext context) /// /// Called from PowerShell to get additional metdata from a language block, such as comment help. /// - /// - /// - /// internal static CommentMetadata GetCommentMeta(string path, int lineNumber, int offset) { var context = RunspaceContext.CurrentThread; diff --git a/src/PSRule/PSRule.csproj b/src/PSRule/PSRule.csproj index e28d594891..8c47e91749 100644 --- a/src/PSRule/PSRule.csproj +++ b/src/PSRule/PSRule.csproj @@ -64,6 +64,11 @@ True ReportStrings.resx + + True + True + Summaries.resx + True True @@ -98,6 +103,10 @@ ResXFileCodeGenerator ReportStrings.Designer.cs + + ResXFileCodeGenerator + Summaries.Designer.cs + ResXFileCodeGenerator ViewStrings.Designer.cs diff --git a/src/PSRule/PSRule.psm1 b/src/PSRule/PSRule.psm1 index a7ac5b2ba4..d96b38f181 100644 --- a/src/PSRule/PSRule.psm1 +++ b/src/PSRule/PSRule.psm1 @@ -1288,6 +1288,16 @@ function New-PSRuleOption { [ValidateSet('None', 'Yaml', 'Json', 'Markdown', 'NUnit3', 'Csv', 'Wide', 'Sarif')] [PSRule.Configuration.OutputFormat]$OutputFormat = 'None', + # Sets the Output.JobSummaryPath option + [Parameter(Mandatory = $False)] + [String]$OutputJobSummaryPath = '', + + # Sets the Output.JsonIndent option + [Parameter(Mandatory = $False)] + [ValidateRange(0, 4)] + [Alias('JsonIndent')] + [int]$OutputJsonIndent = 0, + # Sets the Output.Outcome option [Parameter(Mandatory = $False)] [ValidateSet('None', 'Fail', 'Pass', 'Error', 'Processed', 'All')] @@ -1307,12 +1317,6 @@ function New-PSRuleOption { [ValidateSet('Client', 'Plain', 'AzurePipelines', 'GitHubActions', 'VisualStudioCode', 'Detect')] [PSRule.Configuration.OutputStyle]$OutputStyle = [PSRule.Configuration.OutputStyle]::Detect, - # Sets the Output.JsonIndent option - [Parameter(Mandatory = $False)] - [ValidateRange(0, 4)] - [Alias('JsonIndent')] - [int]$OutputJsonIndent = 0, - # Sets the Repository.BaseRef option [Parameter(Mandatory = $False)] [String]$RepositoryBaseRef, @@ -1580,6 +1584,16 @@ function Set-PSRuleOption { [ValidateSet('None', 'Yaml', 'Json', 'Markdown', 'NUnit3', 'Csv', 'Wide', 'Sarif')] [PSRule.Configuration.OutputFormat]$OutputFormat = 'None', + # Sets the Output.JobSummaryPath option + [Parameter(Mandatory = $False)] + [String]$OutputJobSummaryPath = '', + + # Sets the Output.JsonIndent option + [Parameter(Mandatory = $False)] + [ValidateRange(0, 4)] + [Alias('JsonIndent')] + [int]$OutputJsonIndent = 0, + # Sets the Output.Outcome option [Parameter(Mandatory = $False)] [ValidateSet('None', 'Fail', 'Pass', 'Error', 'Processed', 'All')] @@ -1599,12 +1613,6 @@ function Set-PSRuleOption { [ValidateSet('Client', 'Plain', 'AzurePipelines', 'GitHubActions', 'VisualStudioCode', 'Detect')] [PSRule.Configuration.OutputStyle]$OutputStyle = [PSRule.Configuration.OutputStyle]::Detect, - # Sets the Output.JsonIndent option - [Parameter(Mandatory = $False)] - [ValidateRange(0, 4)] - [Alias('JsonIndent')] - [Int]$OutputJsonIndent = 0, - # Sets the Repository.BaseRef option [Parameter(Mandatory = $False)] [String]$RepositoryBaseRef, @@ -2319,6 +2327,16 @@ function SetOptions { [ValidateSet('None', 'Yaml', 'Json', 'Markdown', 'NUnit3', 'Csv', 'Wide', 'Sarif')] [PSRule.Configuration.OutputFormat]$OutputFormat = 'None', + # Sets the Output.JobSummaryPath option + [Parameter(Mandatory = $False)] + [String]$OutputJobSummaryPath = '', + + # Sets the Output.JsonIndent option + [Parameter(Mandatory = $False)] + [ValidateRange(0, 4)] + [Alias('JsonIndent')] + [int]$OutputJsonIndent = 0, + # Sets the Output.Outcome option [Parameter(Mandatory = $False)] [ValidateSet('None', 'Fail', 'Pass', 'Error', 'Processed', 'All')] @@ -2338,12 +2356,6 @@ function SetOptions { [ValidateSet('Client', 'Plain', 'AzurePipelines', 'GitHubActions', 'VisualStudioCode', 'Detect')] [PSRule.Configuration.OutputStyle]$OutputStyle = [PSRule.Configuration.OutputStyle]::Detect, - # Sets the Output.JsonIndent option - [Parameter(Mandatory = $False)] - [ValidateRange(0, 4)] - [Alias('JsonIndent')] - [Int]$OutputJsonIndent = 0, - # Sets the Repository.BaseRef option [Parameter(Mandatory = $False)] [String]$RepositoryBaseRef, @@ -2534,6 +2546,11 @@ function SetOptions { $Option.Output.Format = $OutputFormat; } + # Sets option Output.OutputJobSummaryPath + if ($PSBoundParameters.ContainsKey('OutputJobSummaryPath')) { + $Option.Output.JobSummaryPath = $OutputJobSummaryPath; + } + # Sets option Output.JsonIndent if ($PSBoundParameters.ContainsKey('OutputJsonIndent')) { $Option.Output.JsonIndent = $OutputJsonIndent; diff --git a/src/PSRule/Pipeline/AssertPipeline.cs b/src/PSRule/Pipeline/AssertPipeline.cs index 49692fb1eb..7341c41232 100644 --- a/src/PSRule/Pipeline/AssertPipeline.cs +++ b/src/PSRule/Pipeline/AssertPipeline.cs @@ -6,6 +6,7 @@ using PSRule.Configuration; using PSRule.Definitions.Rules; using PSRule.Pipeline.Formatters; +using PSRule.Pipeline.Output; using PSRule.Resources; using PSRule.Rules; @@ -38,7 +39,7 @@ private sealed class AssertWriter : PipelineWriter private SeverityLevel _Level; internal AssertWriter(PSRuleOption option, Source[] source, PipelineWriter inner, PipelineWriter next, OutputStyle style, string resultVariableName, IHostContext hostContext) - : base(inner, option) + : base(inner, option, hostContext.ShouldProcess) { _InnerWriter = next; _ResultVariableName = resultVariableName; @@ -46,7 +47,7 @@ internal AssertWriter(PSRuleOption option, Source[] source, PipelineWriter inner if (!string.IsNullOrEmpty(resultVariableName)) _Results = new List(); - _Formatter = GetFormatter(GetStyle(style), source, inner, option); + _Formatter = GetFormatter(style, source, inner, option); } private static IAssertFormatter GetFormatter(OutputStyle style, Source[] source, PipelineWriter inner, PSRuleOption option) @@ -65,25 +66,9 @@ private static IAssertFormatter GetFormatter(OutputStyle style, Source[] source, new ClientFormatter(source, inner, option); } - private static OutputStyle GetStyle(OutputStyle style) - { - if (style != OutputStyle.Detect) - return style; - - if (EnvironmentHelper.Default.IsAzurePipelines()) - return OutputStyle.AzurePipelines; - - if (EnvironmentHelper.Default.IsGitHubActions()) - return OutputStyle.GitHubActions; - - return EnvironmentHelper.Default.IsVisualStudioCode() ? - OutputStyle.VisualStudioCode : - OutputStyle.Client; - } - public override void WriteObject(object sendToPipeline, bool enumerateCollection) { - if (!(sendToPipeline is InvokeResult result)) + if (sendToPipeline is not InvokeResult result) return; ProcessResult(result); @@ -221,8 +206,22 @@ public sealed override IPipeline Build(IPipelineWriter writer = null) bindTargetType: BindTargetTypeHook, bindField: BindFieldHook), source: Source, - writer: writer ?? PrepareWriter(), + writer: HandleJobSummary(writer ?? PrepareWriter()), outcome: RuleOutcome.Processed); } + + private IPipelineWriter HandleJobSummary(IPipelineWriter writer) + { + if (string.IsNullOrEmpty(Option.Output.JobSummaryPath)) + return writer; + + return new JobSummaryWriter + ( + inner: writer, + option: Option, + shouldProcess: ShouldProcess, + source: Source + ); + } } } diff --git a/src/PSRule/Pipeline/Formatters/AssertFormatter.cs b/src/PSRule/Pipeline/Formatters/AssertFormatter.cs index 3269a403a8..5f515e08ba 100644 --- a/src/PSRule/Pipeline/Formatters/AssertFormatter.cs +++ b/src/PSRule/Pipeline/Formatters/AssertFormatter.cs @@ -421,7 +421,7 @@ private void RepositoryInfo() if (GitHelper.TryRevision(out var revision)) WriteLineFormat(FormatterStrings.Repository_Revision, revision); - if (!string.IsNullOrEmpty(repository) || !string.IsNullOrEmpty(branch) || !string.IsNullOrEmpty(revision)) + if (!string.IsNullOrEmpty(branch) || !string.IsNullOrEmpty(revision)) LineBreak(); } diff --git a/src/PSRule/Pipeline/GetRuleHelpPipeline.cs b/src/PSRule/Pipeline/GetRuleHelpPipeline.cs index 0bb3be50ee..6a34882bec 100644 --- a/src/PSRule/Pipeline/GetRuleHelpPipeline.cs +++ b/src/PSRule/Pipeline/GetRuleHelpPipeline.cs @@ -72,8 +72,8 @@ private sealed class HelpWriter : PipelineWriter private readonly bool _ShouldOutput; private readonly string _TypeName; - internal HelpWriter(PipelineWriter inner, PSRuleOption option, LanguageMode languageMode, bool inSession, bool online, bool full) - : base(inner, option) + internal HelpWriter(PipelineWriter inner, PSRuleOption option, ShouldProcess shouldProcess, LanguageMode languageMode, bool inSession, bool online, bool full) + : base(inner, option, shouldProcess) { _LanguageMode = languageMode; _InSession = inSession; @@ -147,6 +147,7 @@ protected override PipelineWriter PrepareWriter() return new HelpWriter( inner: GetOutput(), option: Option, + shouldProcess: HostContext.ShouldProcess, languageMode: Option.Execution.LanguageMode.GetValueOrDefault(ExecutionOption.Default.LanguageMode.Value), inSession: HostContext.InSession, online: _Online, diff --git a/src/PSRule/Pipeline/InvokeResult.cs b/src/PSRule/Pipeline/InvokeResult.cs index e2b0bd3890..36be3d9ac7 100644 --- a/src/PSRule/Pipeline/InvokeResult.cs +++ b/src/PSRule/Pipeline/InvokeResult.cs @@ -18,6 +18,7 @@ public sealed class InvokeResult private long _Time; private int _Total; private int _Error; + private int _Pass; private int _Fail; internal InvokeResult() @@ -26,6 +27,7 @@ internal InvokeResult() _Time = 0; _Total = 0; _Error = 0; + _Pass = 0; _Fail = 0; } @@ -34,16 +36,34 @@ internal InvokeResult() /// internal long Time => _Time; + /// + /// The total number of rule records. + /// internal int Total => _Total; + /// + /// The number of rule records with a error result. + /// internal int Error => _Error; + /// + /// The number of rule records with a fail result. + /// internal int Fail => _Fail; - internal int Pass => _Total - _Error - _Fail; + /// + /// The number of rules records with a pass result. + /// + internal int Pass => _Pass; + /// + /// The worst outcome of all rule records. + /// public RuleOutcome Outcome => _Outcome; + /// + /// The highest severity level of all rule records. + /// public SeverityLevel Level => _Level; internal string TargetName @@ -85,6 +105,9 @@ public bool IsSuccess() return _Outcome == RuleOutcome.Pass || _Outcome == RuleOutcome.None; } + /// + /// Determines of the target object was processed. + /// public bool IsProcessed() { return _Outcome == RuleOutcome.Pass || _Outcome == RuleOutcome.Fail || _Outcome == RuleOutcome.Error; @@ -96,10 +119,13 @@ public bool IsProcessed() /// The record after processing a rule. internal void Add(RuleRecord ruleRecord) { - _Outcome = _Outcome.GetWorstCase(ruleRecord.Outcome); + _Outcome = ruleRecord.Outcome.GetWorstCase(_Outcome); _Time += ruleRecord.Time; _Total++; + if (ruleRecord.Outcome == RuleOutcome.Pass) + _Pass++; + if (ruleRecord.Outcome == RuleOutcome.Error) _Error++; diff --git a/src/PSRule/Pipeline/Output/CsvOutputWriter.cs b/src/PSRule/Pipeline/Output/CsvOutputWriter.cs index 8821ad1efb..c04b49c03a 100644 --- a/src/PSRule/Pipeline/Output/CsvOutputWriter.cs +++ b/src/PSRule/Pipeline/Output/CsvOutputWriter.cs @@ -17,8 +17,8 @@ internal sealed class CsvOutputWriter : SerializationOutputWriter private readonly StringBuilder _Builder; - internal CsvOutputWriter(PipelineWriter inner, PSRuleOption option) - : base(inner, option) + internal CsvOutputWriter(PipelineWriter inner, PSRuleOption option, ShouldProcess shouldProcess) + : base(inner, option, shouldProcess) { _Builder = new StringBuilder(); } diff --git a/src/PSRule/Pipeline/Output/FileOutputWriter.cs b/src/PSRule/Pipeline/Output/FileOutputWriter.cs index ac9e1da0c4..c7ffcdd91f 100644 --- a/src/PSRule/Pipeline/Output/FileOutputWriter.cs +++ b/src/PSRule/Pipeline/Output/FileOutputWriter.cs @@ -19,15 +19,13 @@ internal sealed class FileOutputWriter : PipelineWriter private readonly Encoding _Encoding; private readonly string _Path; - private readonly ShouldProcess _ShouldProcess; private readonly bool _WriteHost; internal FileOutputWriter(PipelineWriter inner, PSRuleOption option, Encoding encoding, string path, ShouldProcess shouldProcess, bool writeHost) - : base(inner, option) + : base(inner, option, shouldProcess) { _Encoding = encoding; _Path = path; - _ShouldProcess = shouldProcess; _WriteHost = writeHost; } @@ -39,11 +37,7 @@ public override void WriteObject(object sendToPipeline, bool enumerateCollection private void WriteToFile(object o) { var rootedPath = PSRuleOption.GetRootedPath(_Path); - var parentPath = Directory.GetParent(rootedPath); - if (!parentPath.Exists && _ShouldProcess(target: parentPath.FullName, action: PSRuleResources.ShouldCreatePath)) - Directory.CreateDirectory(path: parentPath.FullName); - - if (_ShouldProcess(target: rootedPath, action: PSRuleResources.ShouldWriteFile)) + if (CreateFile(rootedPath)) { File.WriteAllText(path: rootedPath, contents: o.ToString(), encoding: _Encoding); InfoOutputPath(rootedPath); diff --git a/src/PSRule/Pipeline/Output/HostPipelineWriter.cs b/src/PSRule/Pipeline/Output/HostPipelineWriter.cs index 9f89d6c8da..eecbab3e4d 100644 --- a/src/PSRule/Pipeline/Output/HostPipelineWriter.cs +++ b/src/PSRule/Pipeline/Output/HostPipelineWriter.cs @@ -18,11 +18,11 @@ internal sealed class HostPipelineWriter : PipelineWriter private const string Source = "PSRule"; private const string HostTag = "PSHOST"; - private Action OnWriteWarning; - private Action OnWriteVerbose; - private Action OnWriteError; - private Action OnWriteInformation; - private Action OnWriteDebug; + private Action _OnWriteWarning; + private Action _OnWriteVerbose; + private Action _OnWriteError; + private Action _OnWriteInformation; + private Action _OnWriteDebug; internal Action OnWriteObject; private bool _LogError; @@ -36,8 +36,8 @@ internal sealed class HostPipelineWriter : PipelineWriter private string _ScopeName; - internal HostPipelineWriter(IHostContext hostContext, PSRuleOption option) - : base(null, option) + internal HostPipelineWriter(IHostContext hostContext, PSRuleOption option, ShouldProcess shouldProcess) + : base(null, option, shouldProcess) { if (hostContext != null) { @@ -60,11 +60,11 @@ private void UseCommandRuntime(IHostContext hostContext) if (hostContext == null) return; - OnWriteVerbose = hostContext.Verbose; - OnWriteWarning = hostContext.Warning; - OnWriteError = hostContext.Error; - OnWriteInformation = hostContext.Information; - OnWriteDebug = hostContext.Debug; + _OnWriteVerbose = hostContext.Verbose; + _OnWriteWarning = hostContext.Warning; + _OnWriteError = hostContext.Error; + _OnWriteInformation = hostContext.Information; + _OnWriteDebug = hostContext.Debug; OnWriteObject = hostContext.Object; } @@ -97,10 +97,10 @@ private static bool GetPreferenceVariable(IHostContext hostContext, string varia /// A valid PowerShell error record. public override void WriteError(ErrorRecord errorRecord) { - if (OnWriteError == null || !ShouldWriteError()) + if (_OnWriteError == null || !ShouldWriteError()) return; - OnWriteError(errorRecord); + _OnWriteError(errorRecord); } /// @@ -109,10 +109,10 @@ public override void WriteError(ErrorRecord errorRecord) /// A message to log. public override void WriteVerbose(string message) { - if (OnWriteVerbose == null || !ShouldWriteVerbose()) + if (_OnWriteVerbose == null || !ShouldWriteVerbose()) return; - OnWriteVerbose(message); + _OnWriteVerbose(message); } /// @@ -121,10 +121,10 @@ public override void WriteVerbose(string message) /// A message to log public override void WriteWarning(string message) { - if (OnWriteWarning == null || !ShouldWriteWarning()) + if (_OnWriteWarning == null || !ShouldWriteWarning()) return; - OnWriteWarning(message); + _OnWriteWarning(message); } /// @@ -132,10 +132,10 @@ public override void WriteWarning(string message) /// public override void WriteInformation(InformationRecord informationRecord) { - if (OnWriteInformation == null || !ShouldWriteInformation()) + if (_OnWriteInformation == null || !ShouldWriteInformation()) return; - OnWriteInformation(informationRecord); + _OnWriteInformation(informationRecord); } /// @@ -143,11 +143,11 @@ public override void WriteInformation(InformationRecord informationRecord) /// public override void WriteDebug(string text, params object[] args) { - if (OnWriteDebug == null || string.IsNullOrEmpty(text) || !ShouldWriteDebug()) + if (_OnWriteDebug == null || string.IsNullOrEmpty(text) || !ShouldWriteDebug()) return; text = args == null || args.Length == 0 ? text : string.Format(Thread.CurrentThread.CurrentCulture, text, args); - OnWriteDebug(text); + _OnWriteDebug(text); } public override void WriteObject(object sendToPipeline, bool enumerateCollection) @@ -163,12 +163,12 @@ public override void WriteObject(object sendToPipeline, bool enumerateCollection public override void WriteHost(HostInformationMessage info) { - if (OnWriteInformation == null) + if (_OnWriteInformation == null) return; var record = new InformationRecord(info, Source); record.Tags.Add(HostTag); - OnWriteInformation(record); + _OnWriteInformation(record); } public override bool ShouldWriteVerbose() diff --git a/src/PSRule/Pipeline/Output/JobSummaryWriter.cs b/src/PSRule/Pipeline/Output/JobSummaryWriter.cs new file mode 100644 index 0000000000..58f481aee5 --- /dev/null +++ b/src/PSRule/Pipeline/Output/JobSummaryWriter.cs @@ -0,0 +1,242 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using PSRule.Configuration; +using PSRule.Resources; +using PSRule.Rules; + +namespace PSRule.Pipeline.Output +{ + /// + /// Define an pipeline writer to write a job summary to disk. + /// + internal sealed class JobSummaryWriter : ResultOutputWriter + { + private const string TICK = "✔️"; + private const string CROSS = "❌"; + private const string QUESTION = "❔"; + private const string HEADER_H1 = "# "; + private const string HEADER_H2 = "## "; + + private readonly string _OutputPath; + private readonly Encoding _Encoding; + private readonly JobSummaryFormat _JobSummary; + private readonly Source[] _Source; + + private Stream _Stream; + private StreamWriter _Writer; + private bool _IsDisposed; + + public JobSummaryWriter(IPipelineWriter inner, PSRuleOption option, ShouldProcess shouldProcess, string outputPath = null, Stream stream = null, Source[] source = null) + : base(inner, option, shouldProcess) + { + _OutputPath = outputPath ?? PSRuleOption.GetRootedPath(Option.Output.JobSummaryPath); + _Encoding = option.Output.GetEncoding(); + _JobSummary = JobSummaryFormat.Default; + _Stream = stream; + _Source = source; + } + + public override void Begin() + { + Open(); + base.Begin(); + } + + public sealed override void End() + { + Flush(); + base.End(); + } + + #region Helper methods + + private void Open() + { + if (_OutputPath == null || _IsDisposed) + return; + + _Stream ??= new FileStream(_OutputPath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read); + _Writer = new StreamWriter(_Stream, _Encoding, 2048, false); + } + + private void Source() + { + if (!_JobSummary.HasFlag(JobSummaryFormat.Source) || _Source == null || _Source.Length == 0) + return; + + H2(Summaries.JobSummary_Source); + WriteLine(Summaries.JobSummary_IncludedModules); + WriteLine(); + + WriteLine($"{Summaries.JobSummary_Module} | {Summaries.JobSummary_Version}"); + WriteLine("------ | --------"); + + var version = Engine.GetVersion(); + if (!string.IsNullOrEmpty(version)) + WriteLine($"PSRule | v{version}"); + + var list = new HashSet(StringComparer.OrdinalIgnoreCase); + for (var i = 0; i < _Source.Length; i++) + { + if (_Source[i].Module != null && !list.Contains(_Source[i].Module.Name)) + { + var projectLink = string.IsNullOrEmpty(_Source[i].Module.ProjectUri) ? _Source[i].Module.Name : $"[{_Source[i].Module.Name}]({_Source[i].Module.ProjectUri})"; + WriteLine($"{projectLink} | v{_Source[i].Module.Version}"); + list.Add(_Source[i].Module.Name); + } + } + WriteLine(); + } + + private void H1(string text) + { + if (string.IsNullOrEmpty(text)) + return; + + WriteLine(string.Concat(HEADER_H1, text)); + WriteLine(); + } + + private void H2(string text) + { + if (string.IsNullOrEmpty(text)) + return; + + WriteLine(string.Concat(HEADER_H2, text)); + WriteLine(); + } + + private void WriteLine(string text = null) + { + if (_Writer == null || _IsDisposed) + return; + + _Writer.WriteLine(text ?? string.Empty); + } + + private void WriteLine(string text, params object[] args) + { + if (_Writer == null || _IsDisposed) + return; + + _Writer.WriteLine(text ?? string.Empty, args); + } + + private void Complete() + { + var results = GetResults(); + H1(Summaries.JobSummary_Title); + FinalResult(results); + Source(); + Analysis(results); + } + + private void Analysis(InvokeResult[] o) + { + if (o == null || o.Length == 0) + return; + + H2(Summaries.JobSummary_Analysis); + var rows = o.SelectMany(r => r.AsRecord()).Where(r => r.Outcome == RuleOutcome.Fail || r.Outcome == RuleOutcome.Error).ToArray(); + if (rows.Length > 0) + { + WriteLine(Summaries.JobSummary_AnalysisResults); + WriteLine(); + WriteLine($"{Summaries.JobSummary_Name} | {Summaries.JobSummary_TargetName} | {Summaries.JobSummary_Synopsis}"); + WriteLine("---- | ----------- | --------"); + } + else + { + WriteLine(Summaries.JobSummary_AnalysisEmpty); + } + for (var i = 0; i < rows.Length; i++) + WriteAnalysisRow(rows[i]); + } + + private void WriteAnalysisRow(RuleRecord record) + { + var link = record.Info.GetOnlineHelpUrl(); + var name = link != null ? $"[{record.RuleName}]({link})" : record.RuleName; + WriteLine($"{name} | {record.TargetName} | {record.Info.Synopsis.Markdown}"); + } + + private void FinalResult(InvokeResult[] o) + { + var count = o?.Length; + var overall = RuleOutcome.None; + long time = 0; + var pass = 0; + var fail = 0; + var total = 0; + var error = 0; + for (var i = 0; o != null && i < o.Length; i++) + { + overall = o[i].Outcome.GetWorstCase(overall); + time += o[i].Time; + pass += o[i].Pass; + fail += o[i].Fail; + error += o[i].Error; + total += o[i].Total; + } + WriteLine(Summaries.JobSummary_FinalResultMessage, + OutcomeEmoji(overall), + Enum.GetName(typeof(RuleOutcome), overall), + pass + fail + error, + count, + TimeSpan.FromMilliseconds(time) + ); + WriteLine(); + } + + private static string OutcomeEmoji(RuleOutcome outcome) + { + switch (outcome) + { + case RuleOutcome.Pass: + return TICK; + + case RuleOutcome.Error: + case RuleOutcome.Fail: + return CROSS; + + default: + return QUESTION; + } + } + + protected override void Flush() + { + if (_Writer == null || _IsDisposed) + return; + + Complete(); + _Writer.Flush(); + } + + #endregion Helper methods + + #region IDisposable + + protected override void Dispose(bool disposing) + { + if (!_IsDisposed) + { + if (disposing) + { + _Writer?.Dispose(); + _Stream?.Dispose(); + } + _IsDisposed = true; + } + base.Dispose(disposing); + } + + #endregion IDisposable + } +} diff --git a/src/PSRule/Pipeline/Output/JsonOutputWriter.cs b/src/PSRule/Pipeline/Output/JsonOutputWriter.cs index 1ee79d1c66..744378267b 100644 --- a/src/PSRule/Pipeline/Output/JsonOutputWriter.cs +++ b/src/PSRule/Pipeline/Output/JsonOutputWriter.cs @@ -12,8 +12,8 @@ namespace PSRule.Pipeline.Output { internal sealed class JsonOutputWriter : SerializationOutputWriter { - internal JsonOutputWriter(PipelineWriter inner, PSRuleOption option) - : base(inner, option) { } + internal JsonOutputWriter(PipelineWriter inner, PSRuleOption option, ShouldProcess shouldProcess) + : base(inner, option, shouldProcess) { } protected override string Serialize(object[] o) { diff --git a/src/PSRule/Pipeline/Output/MarkdownOutputWriter.cs b/src/PSRule/Pipeline/Output/MarkdownOutputWriter.cs index 7d1587b9d2..e40e769caa 100644 --- a/src/PSRule/Pipeline/Output/MarkdownOutputWriter.cs +++ b/src/PSRule/Pipeline/Output/MarkdownOutputWriter.cs @@ -19,8 +19,8 @@ internal sealed class MarkdownOutputWriter : SerializationOutputWriter { - internal NUnit3OutputWriter(PipelineWriter inner, PSRuleOption option) - : base(inner, option) { } + internal NUnit3OutputWriter(PipelineWriter inner, PSRuleOption option, ShouldProcess shouldProcess) + : base(inner, option, shouldProcess) { } public override void WriteObject(object sendToPipeline, bool enumerateCollection) { diff --git a/src/PSRule/Pipeline/Output/SarifOutputWriter.cs b/src/PSRule/Pipeline/Output/SarifOutputWriter.cs index 7ac5e6c127..fc325ab5a6 100644 --- a/src/PSRule/Pipeline/Output/SarifOutputWriter.cs +++ b/src/PSRule/Pipeline/Output/SarifOutputWriter.cs @@ -339,8 +339,8 @@ internal sealed class SarifOutputWriter : SerializationOutputWriter { - internal YamlOutputWriter(PipelineWriter inner, PSRuleOption option) - : base(inner, option) { } + internal YamlOutputWriter(PipelineWriter inner, PSRuleOption option, ShouldProcess shouldProcess) + : base(inner, option, shouldProcess) { } protected override string Serialize(object[] o) { diff --git a/src/PSRule/Pipeline/PipelineBuilder.cs b/src/PSRule/Pipeline/PipelineBuilder.cs index 4d93143fab..e6d57ff74c 100644 --- a/src/PSRule/Pipeline/PipelineBuilder.cs +++ b/src/PSRule/Pipeline/PipelineBuilder.cs @@ -242,7 +242,7 @@ protected PipelineBuilderBase(Source[] source, IHostContext hostContext) { Option = new PSRuleOption(); Source = source; - _Output = new HostPipelineWriter(hostContext, Option); + _Output = new HostPipelineWriter(hostContext, Option, ShouldProcess); HostContext = hostContext; BindTargetNameHook = PipelineHookActions.BindTargetName; BindTargetTypeHook = PipelineHookActions.BindTargetType; @@ -291,6 +291,7 @@ public virtual IPipelineBuilder Configure(PSRuleOption option) Option.Output = new OutputOption(option.Output); Option.Output.Outcome ??= OutputOption.Default.Outcome; Option.Output.Banner ??= OutputOption.Default.Banner; + Option.Output.Style = GetStyle(option.Output.Style ?? OutputOption.Default.Style.Value); Option.Repository = GetRepository(option.Repository); return this; } @@ -408,13 +409,13 @@ protected virtual PipelineWriter PrepareWriter() var output = GetOutput(); return Option.Output.Format switch { - OutputFormat.Csv => new CsvOutputWriter(output, Option), - OutputFormat.Json => new JsonOutputWriter(output, Option), - OutputFormat.NUnit3 => new NUnit3OutputWriter(output, Option), - OutputFormat.Yaml => new YamlOutputWriter(output, Option), - OutputFormat.Markdown => new MarkdownOutputWriter(output, Option), - OutputFormat.Wide => new WideOutputWriter(output, Option), - OutputFormat.Sarif => new SarifOutputWriter(Source, output, Option), + OutputFormat.Csv => new CsvOutputWriter(output, Option, ShouldProcess), + OutputFormat.Json => new JsonOutputWriter(output, Option, ShouldProcess), + OutputFormat.NUnit3 => new NUnit3OutputWriter(output, Option, ShouldProcess), + OutputFormat.Yaml => new YamlOutputWriter(output, Option, ShouldProcess), + OutputFormat.Markdown => new MarkdownOutputWriter(output, Option, ShouldProcess), + OutputFormat.Wide => new WideOutputWriter(output, Option, ShouldProcess), + OutputFormat.Sarif => new SarifOutputWriter(Source, output, Option, ShouldProcess), _ => output, }; } @@ -594,5 +595,26 @@ protected bool TryChangedFiles(out string[] files) return true; } + + protected bool ShouldProcess(string target, string action) + { + return HostContext == null || HostContext.ShouldProcess(target, action); + } + + protected static OutputStyle GetStyle(OutputStyle style) + { + if (style != OutputStyle.Detect) + return style; + + if (EnvironmentHelper.Default.IsAzurePipelines()) + return OutputStyle.AzurePipelines; + + if (EnvironmentHelper.Default.IsGitHubActions()) + return OutputStyle.GitHubActions; + + return EnvironmentHelper.Default.IsVisualStudioCode() ? + OutputStyle.VisualStudioCode : + OutputStyle.Client; + } } } diff --git a/src/PSRule/Pipeline/PipelineLogger.cs b/src/PSRule/Pipeline/PipelineLogger.cs index 87b51665e6..8be2b1a8c2 100644 --- a/src/PSRule/Pipeline/PipelineLogger.cs +++ b/src/PSRule/Pipeline/PipelineLogger.cs @@ -126,6 +126,22 @@ public virtual void End() } + #region IDisposable + + protected virtual void Dispose(bool disposing) + { + // Do nothing, but allow override. + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion IDisposable + protected abstract void DoWriteError(ErrorRecord errorRecord); protected abstract void DoWriteVerbose(string message); @@ -150,11 +166,11 @@ internal sealed class PipelineLogger : PipelineLoggerBase private HashSet _VerboseFilter; private HashSet _DebugFilter; - private Action OnWriteWarning; - private Action OnWriteVerbose; - private Action OnWriteError; - private Action OnWriteInformation; - private Action OnWriteDebug; + private Action _OnWriteWarning; + private Action _OnWriteVerbose; + private Action _OnWriteError; + private Action _OnWriteInformation; + private Action _OnWriteDebug; internal Action OnWriteObject; private bool _LogError; @@ -165,11 +181,11 @@ internal sealed class PipelineLogger : PipelineLoggerBase internal void UseCommandRuntime(PSCmdlet commandRuntime) { - OnWriteVerbose = commandRuntime.WriteVerbose; - OnWriteWarning = commandRuntime.WriteWarning; - OnWriteError = commandRuntime.WriteError; - OnWriteInformation = commandRuntime.WriteInformation; - OnWriteDebug = commandRuntime.WriteDebug; + _OnWriteVerbose = commandRuntime.WriteVerbose; + _OnWriteWarning = commandRuntime.WriteWarning; + _OnWriteError = commandRuntime.WriteError; + _OnWriteInformation = commandRuntime.WriteInformation; + _OnWriteDebug = commandRuntime.WriteDebug; OnWriteObject = commandRuntime.WriteObject; } @@ -207,10 +223,10 @@ private static bool GetPreferenceVariable(EngineIntrinsics executionContext, str /// A valid PowerShell error record. protected override void DoWriteError(ErrorRecord errorRecord) { - if (OnWriteError == null) + if (_OnWriteError == null) return; - OnWriteError(errorRecord); + _OnWriteError(errorRecord); } /// @@ -219,10 +235,10 @@ protected override void DoWriteError(ErrorRecord errorRecord) /// A message to log. protected override void DoWriteVerbose(string message) { - if (OnWriteVerbose == null) + if (_OnWriteVerbose == null) return; - OnWriteVerbose(message); + _OnWriteVerbose(message); } /// @@ -231,10 +247,10 @@ protected override void DoWriteVerbose(string message) /// A message to log protected override void DoWriteWarning(string message) { - if (OnWriteWarning == null) + if (_OnWriteWarning == null) return; - OnWriteWarning(message); + _OnWriteWarning(message); } /// @@ -242,10 +258,10 @@ protected override void DoWriteWarning(string message) /// protected override void DoWriteInformation(InformationRecord informationRecord) { - if (OnWriteInformation == null) + if (_OnWriteInformation == null) return; - OnWriteInformation(informationRecord); + _OnWriteInformation(informationRecord); } /// @@ -253,10 +269,10 @@ protected override void DoWriteInformation(InformationRecord informationRecord) /// protected override void DoWriteDebug(DebugRecord debugRecord) { - if (OnWriteDebug == null) + if (_OnWriteDebug == null) return; - OnWriteDebug(debugRecord.Message); + _OnWriteDebug(debugRecord.Message); } protected override void DoWriteObject(object sendToPipeline, bool enumerateCollection) diff --git a/src/PSRule/Pipeline/PipelineWriter.cs b/src/PSRule/Pipeline/PipelineWriter.cs index d92f93e7b4..974119e9dc 100644 --- a/src/PSRule/Pipeline/PipelineWriter.cs +++ b/src/PSRule/Pipeline/PipelineWriter.cs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Collections.Generic; +using System.IO; using System.Management.Automation; using System.Threading; using PSRule.Configuration; @@ -13,7 +15,7 @@ namespace PSRule.Pipeline /// /// An writer which recieves output from PSRule. /// - public interface IPipelineWriter + public interface IPipelineWriter : IDisposable { /// /// Write a verbose message. @@ -110,12 +112,16 @@ internal abstract class PipelineWriter : IPipelineWriter protected const string DebugPreference = "DebugPreference"; private readonly IPipelineWriter _Writer; + private readonly ShouldProcess _ShouldProcess; protected readonly PSRuleOption Option; - protected PipelineWriter(IPipelineWriter inner, PSRuleOption option) + private bool _IsDisposed; + + protected PipelineWriter(IPipelineWriter inner, PSRuleOption option, ShouldProcess shouldProcess) { _Writer = inner; + _ShouldProcess = shouldProcess; Option = option; } @@ -249,6 +255,28 @@ public virtual void ExitScope() _Writer.ExitScope(); } + #region IDisposable + + protected virtual void Dispose(bool disposing) + { + if (!_IsDisposed) + { + if (disposing && _Writer != null) + _Writer.Dispose(); + + _IsDisposed = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion IDisposable + protected void WriteErrorInfo(RuleRecord record) { if (record == null || record.Error == null) @@ -273,6 +301,27 @@ protected void WriteErrorInfo(RuleRecord record) WriteError(errorRecord); } + private bool ShouldProcess(string target, string action) + { + return _ShouldProcess == null || _ShouldProcess(target, action); + } + + private bool CreatePath(string path) + { + var parentPath = Directory.GetParent(path); + if (!parentPath.Exists && ShouldProcess(target: parentPath.FullName, action: PSRuleResources.ShouldCreatePath)) + { + Directory.CreateDirectory(path: parentPath.FullName); + return true; + } + return parentPath.Exists; + } + + protected bool CreateFile(string path) + { + return CreatePath(path) && ShouldProcess(target: path, action: PSRuleResources.ShouldWriteFile); + } + /// /// Get the value of a preference variable. /// @@ -282,12 +331,12 @@ protected static ActionPreference GetPreferenceVariable(System.Management.Automa } } - internal abstract class SerializationOutputWriter : PipelineWriter + internal abstract class ResultOutputWriter : PipelineWriter { private readonly List _Result; - protected SerializationOutputWriter(PipelineWriter inner, PSRuleOption option) - : base(inner, option) + protected ResultOutputWriter(IPipelineWriter inner, PSRuleOption option, ShouldProcess shouldProcess) + : base(inner, option, shouldProcess) { _Result = new List(); } @@ -299,10 +348,13 @@ public override void WriteObject(object sendToPipeline, bool enumerateCollection if (sendToPipeline is InvokeResult result) { - Add(result.AsRecord()); - return; + Add(typeof(T) == typeof(RuleRecord) ? result.AsRecord() : result); } - Add(sendToPipeline); + else + { + Add(sendToPipeline); + } + base.WriteObject(sendToPipeline, enumerateCollection); } protected void Add(object o) @@ -313,11 +365,42 @@ protected void Add(object o) _Result.Add(item); } + /// + /// Clear any buffers from the writer. + /// + protected virtual void Flush() { } + + protected T[] GetResults() + { + return _Result.ToArray(); + } + } + + internal abstract class SerializationOutputWriter : ResultOutputWriter + { + protected SerializationOutputWriter(IPipelineWriter inner, PSRuleOption option, ShouldProcess shouldProcess) + : base(inner, option, shouldProcess) { } + public sealed override void End() { - var results = _Result.ToArray(); + var results = GetResults(); base.WriteObject(Serialize(results), false); ProcessError(results); + Flush(); + base.End(); + } + + public override void WriteObject(object sendToPipeline, bool enumerateCollection) + { + if (sendToPipeline is InvokeResult && Option.Output.As == ResultFormat.Summary) + return; + + if (sendToPipeline is InvokeResult result) + { + Add(result.AsRecord()); + return; + } + Add(sendToPipeline); } protected abstract string Serialize(T[] o); diff --git a/src/PSRule/Pipeline/PipelineWriterExtensions.cs b/src/PSRule/Pipeline/PipelineWriterExtensions.cs index f4470e3461..dcae9df625 100644 --- a/src/PSRule/Pipeline/PipelineWriterExtensions.cs +++ b/src/PSRule/Pipeline/PipelineWriterExtensions.cs @@ -13,6 +13,9 @@ namespace PSRule.Pipeline /// public static class PipelineWriterExtensions { + /// + /// Write a debug message. + /// public static void WriteDebug(this IPipelineWriter writer, DebugRecord debugRecord) { if (debugRecord == null) diff --git a/src/PSRule/Pipeline/RulePipeline.cs b/src/PSRule/Pipeline/RulePipeline.cs index b78da6cd95..3c71fb30dd 100644 --- a/src/PSRule/Pipeline/RulePipeline.cs +++ b/src/PSRule/Pipeline/RulePipeline.cs @@ -68,6 +68,7 @@ protected virtual void Dispose(bool disposing) { if (disposing) { + Writer.Dispose(); Context.Dispose(); Pipeline.Dispose(); } diff --git a/src/PSRule/Pipeline/SourcePipeline.cs b/src/PSRule/Pipeline/SourcePipeline.cs index ee6447223a..aa33f8a9fc 100644 --- a/src/PSRule/Pipeline/SourcePipeline.cs +++ b/src/PSRule/Pipeline/SourcePipeline.cs @@ -123,7 +123,7 @@ internal SourcePipelineBuilder(IHostContext hostContext, PSRuleOption option) { _Source = new Dictionary(StringComparer.OrdinalIgnoreCase); _HostContext = hostContext; - _Writer = new HostPipelineWriter(hostContext, option); + _Writer = new HostPipelineWriter(hostContext, option, ShouldProcess); _Writer.EnterScope("[Discovery.Source]"); _UseDefaultPath = option == null || option.Include == null || option.Include.Path == null; _LocalPath = Engine.GetLocalPath(); @@ -297,11 +297,11 @@ private static Source.ModuleInfo LoadManifest(string basePath) if (!File.Exists(path)) return null; - var reader = new StreamReader(path); + using var reader = new StreamReader(path); var data = reader.ReadToEnd(); var ast = System.Management.Automation.Language.Parser.ParseInput(data, out _, out _); var hashtable = ast.FindAll(item => item is System.Management.Automation.Language.HashtableAst, false).FirstOrDefault(); - if (hashtable.SafeGetValue() is not Hashtable manifest) + if (hashtable == null || hashtable.SafeGetValue() is not Hashtable manifest) return null; var version = manifest["ModuleVersion"] as string; @@ -492,5 +492,10 @@ private static bool IsJsonFile(string extension) { return extension == SOURCE_FILE_EXTENSION_JSON || extension == SOURCE_FILE_EXTENSION_JSONC; } + + private bool ShouldProcess(string target, string action) + { + return _HostContext == null || _HostContext.ShouldProcess(target, action); + } } } diff --git a/src/PSRule/Pipeline/TestPipeline.cs b/src/PSRule/Pipeline/TestPipeline.cs index e30f803e80..0309f25876 100644 --- a/src/PSRule/Pipeline/TestPipeline.cs +++ b/src/PSRule/Pipeline/TestPipeline.cs @@ -14,8 +14,8 @@ private sealed class BooleanWriter : PipelineWriter { private readonly RuleOutcome _Outcome; - internal BooleanWriter(PipelineWriter output, RuleOutcome outcome) - : base(output, null) + internal BooleanWriter(PipelineWriter output, RuleOutcome outcome, ShouldProcess shouldProcess) + : base(output, null, shouldProcess) { _Outcome = outcome; } @@ -38,7 +38,12 @@ private bool ShouldOutput(RuleOutcome outcome) protected override PipelineWriter PrepareWriter() { - return new BooleanWriter(GetOutput(), Option.Output.Outcome.Value); + return new BooleanWriter(GetOutput(), Option.Output.Outcome.Value, ShouldProcess); + } + + private new static bool ShouldProcess(string target, string action) + { + return true; } } } diff --git a/src/PSRule/Resources/Summaries.Designer.cs b/src/PSRule/Resources/Summaries.Designer.cs new file mode 100644 index 0000000000..3a2222974c --- /dev/null +++ b/src/PSRule/Resources/Summaries.Designer.cs @@ -0,0 +1,171 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace PSRule.Resources { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Summaries { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Summaries() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PSRule.Resources.Summaries", typeof(Summaries).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Analysis. + /// + internal static string JobSummary_Analysis { + get { + return ResourceManager.GetString("JobSummary_Analysis", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Analysis did not return any failing or erroring results.. + /// + internal static string JobSummary_AnalysisEmpty { + get { + return ResourceManager.GetString("JobSummary_AnalysisEmpty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The following results were reported with fail or error results.. + /// + internal static string JobSummary_AnalysisResults { + get { + return ResourceManager.GetString("JobSummary_AnalysisResults", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} PSRule completed with an overall result of '{1}' with {2} rule(s) and {3} target(s) in {4}.. + /// + internal static string JobSummary_FinalResultMessage { + get { + return ResourceManager.GetString("JobSummary_FinalResultMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The following modules where included in analysis.. + /// + internal static string JobSummary_IncludedModules { + get { + return ResourceManager.GetString("JobSummary_IncludedModules", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Module. + /// + internal static string JobSummary_Module { + get { + return ResourceManager.GetString("JobSummary_Module", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Name. + /// + internal static string JobSummary_Name { + get { + return ResourceManager.GetString("JobSummary_Name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Source. + /// + internal static string JobSummary_Source { + get { + return ResourceManager.GetString("JobSummary_Source", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Synopsis. + /// + internal static string JobSummary_Synopsis { + get { + return ResourceManager.GetString("JobSummary_Synopsis", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Target name. + /// + internal static string JobSummary_TargetName { + get { + return ResourceManager.GetString("JobSummary_TargetName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PSRule result summary. + /// + internal static string JobSummary_Title { + get { + return ResourceManager.GetString("JobSummary_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Version. + /// + internal static string JobSummary_Version { + get { + return ResourceManager.GetString("JobSummary_Version", resourceCulture); + } + } + } +} diff --git a/src/PSRule/Resources/Summaries.resx b/src/PSRule/Resources/Summaries.resx new file mode 100644 index 0000000000..5d1255f9ed --- /dev/null +++ b/src/PSRule/Resources/Summaries.resx @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Analysis + + + Analysis did not return any failing or erroring results. + + + The following results were reported with fail or error results. + + + {0} PSRule completed with an overall result of '{1}' with {2} rule(s) and {3} target(s) in {4}. + + + The following modules where included in analysis. + + + Module + + + Name + + + Source + + + Synopsis + + + Target name + + + PSRule result summary + + + Version + + \ No newline at end of file diff --git a/src/PSRule/Runtime/RuleConditionResult.cs b/src/PSRule/Runtime/RuleConditionResult.cs index a0a42ca072..55ada4c170 100644 --- a/src/PSRule/Runtime/RuleConditionResult.cs +++ b/src/PSRule/Runtime/RuleConditionResult.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Collections.Generic; @@ -73,10 +73,13 @@ internal RuleConditionResult(int pass, int count, bool hadErrors) HadErrors = hadErrors; } + /// public int Pass { get; } + /// public int Count { get; } + /// public bool HadErrors { get; } } } diff --git a/tests/PSRule.Tests/AssertTests.cs b/tests/PSRule.Tests/AssertTests.cs index bef6fa01a4..42e57c1930 100644 --- a/tests/PSRule.Tests/AssertTests.cs +++ b/tests/PSRule.Tests/AssertTests.cs @@ -21,11 +21,11 @@ public sealed class AssertTests private const string LANGUAGE = "Language"; private const string LANGUAGEELEMENT = "Variable"; - private readonly ITestOutputHelper Output; + private readonly ITestOutputHelper _Output; public AssertTests(ITestOutputHelper output) { - Output = output; + _Output = output; } [Fact] @@ -162,7 +162,7 @@ public void HasField() ); Assert.False(assert.HasField(null, null).Result); - Assert.False(assert.HasField(null, new string[] { }).Result); + Assert.False(assert.HasField(null, Array.Empty()).Result); Assert.True(assert.HasField(value, new string[] { "value" }).Result); Assert.True(assert.HasField(value, new string[] { "notValue", "Value" }).Result); Assert.True(assert.HasField(value, new string[] { "value2" }).Result); @@ -193,7 +193,7 @@ public void NotHasField() ); Assert.False(assert.NotHasField(null, null).Result); - Assert.False(assert.NotHasField(null, new string[] { }).Result); + Assert.False(assert.NotHasField(null, Array.Empty()).Result); Assert.False(assert.NotHasField(value, new string[] { "value" }).Result); Assert.False(assert.NotHasField(value, new string[] { "notValue", "Value" }).Result); Assert.True(assert.NotHasField(value, new string[] { "notValue", "Value" }, true).Result); @@ -1054,7 +1054,7 @@ public void NotIn() Assert.Equal("values", assert.NotIn(value, "values", new string[] { "Value2" }).ToResultReason().FirstOrDefault().Path); // Empty - value = GetObject((name: "null", value: null), (name: "empty", value: new string[] { })); + value = GetObject((name: "null", value: null), (name: "empty", value: Array.Empty())); Assert.True(assert.NotIn(value, "null", new string[] { "Value1", "Value3" }).Result); Assert.True(assert.NotIn(value, "empty", new string[] { "Value1", "Value3" }).Result); Assert.True(assert.NotIn(value, "notValue", new string[] { "Value1", "Value3" }).Result); @@ -1257,7 +1257,7 @@ public void NullOrEmpty() (name: "value2", value: null), (name: "value3", value: ""), (name: "value4", value: 0), - (name: "value5", value: new string[] { }) + (name: "value5", value: Array.Empty()) ); Assert.False(assert.NullOrEmpty(value, "value").Result); Assert.True(assert.NullOrEmpty(value, "value2").Result); @@ -1438,7 +1438,7 @@ private static Runtime.Assert GetAssertionHelper() return new Runtime.Assert(); } - private string GetSourcePath(string fileName) + private static string GetSourcePath(string fileName) { return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName); } @@ -1449,7 +1449,7 @@ private bool AssertionResult(AssertResult result) { var reasons = result.GetReason(); for (var i = 0; reasons != null && i < reasons.Length; i++) - Output.WriteLine(reasons[i]); + _Output.WriteLine(reasons[i]); } return result.Result; } diff --git a/tests/PSRule.Tests/OutputWriterTests.cs b/tests/PSRule.Tests/OutputWriterTests.cs index ce48b060d9..ff5dc7bfc1 100644 --- a/tests/PSRule.Tests/OutputWriterTests.cs +++ b/tests/PSRule.Tests/OutputWriterTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections; +using System.IO; using System.Linq; using System.Management.Automation; using System.Xml; @@ -32,7 +33,7 @@ public void Sarif() result.Add(GetFail()); result.Add(GetFail("rid-003", SeverityLevel.Warning)); result.Add(GetFail("rid-004", SeverityLevel.Information)); - var writer = new SarifOutputWriter(null, output, option); + var writer = new SarifOutputWriter(null, output, option, null); writer.Begin(); writer.WriteObject(result, false); writer.End(); @@ -70,7 +71,7 @@ public void SarifProblemsOnly() result.Add(GetFail()); result.Add(GetFail("rid-003", SeverityLevel.Warning)); result.Add(GetFail("rid-004", SeverityLevel.Information)); - var writer = new SarifOutputWriter(null, output, option); + var writer = new SarifOutputWriter(null, output, option, null); writer.Begin(); writer.WriteObject(result, false); writer.End(); @@ -104,7 +105,7 @@ public void Yaml() result.Add(GetFail()); result.Add(GetFail("rid-003", SeverityLevel.Warning)); result.Add(GetFail("rid-004", SeverityLevel.Information)); - var writer = new YamlOutputWriter(output, option); + var writer = new YamlOutputWriter(output, option, null); writer.Begin(); writer.WriteObject(result, false); writer.End(); @@ -187,7 +188,7 @@ public void Json() result.Add(GetFail()); result.Add(GetFail("rid-003", SeverityLevel.Warning)); result.Add(GetFail("rid-004", SeverityLevel.Information)); - var writer = new JsonOutputWriter(output, option); + var writer = new JsonOutputWriter(output, option, null); writer.Begin(); writer.WriteObject(result, false); writer.End(); @@ -290,7 +291,7 @@ public void NUnit3() result.Add(GetFail()); result.Add(GetFail("rid-003", SeverityLevel.Warning)); result.Add(GetFail("rid-004", SeverityLevel.Information, "Synopsis \"with quotes\".")); - var writer = new NUnit3OutputWriter(output, option); + var writer = new NUnit3OutputWriter(output, option, null); writer.Begin(); writer.WriteObject(result, false); writer.End(); @@ -305,6 +306,27 @@ public void NUnit3() Assert.Equal("", xml); } + [Fact] + public void JobSummary() + { + using var stream = new MemoryStream(); + var option = GetOption(); + var output = new TestWriter(option); + var result = new InvokeResult(); + result.Add(GetPass()); + result.Add(GetFail()); + result.Add(GetFail("rid-003", SeverityLevel.Warning, ruleId: "TestModule\\Rule-003")); + var writer = new JobSummaryWriter(output, option, null, outputPath: "", stream: stream); + writer.Begin(); + writer.WriteObject(result, false); + writer.End(); + + stream.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(stream); + var s = reader.ReadToEnd().Replace(Environment.NewLine, "\r\n"); + Assert.Equal("# PSRule result summary\r\n\r\n❌ PSRule completed with an overall result of 'Fail' with 3 rule(s) and 1 target(s) in 00:00:00.\r\n\r\n## Analysis\r\n\r\nThe following results were reported with fail or error results.\r\n\r\nName | Target name | Synopsis\r\n---- | ----------- | --------\r\nrule-002 | TestObject1 | This is rule 002.\r\nRule-003 | TestObject1 | This is rule 002.\r\n", s); + } + #region Helper methods private static RuleRecord GetPass() @@ -332,11 +354,11 @@ private static RuleRecord GetPass() ); } - private static RuleRecord GetFail(string ruleRef = "rid-002", SeverityLevel level = SeverityLevel.Error, string synopsis = "This is rule 002.") + private static RuleRecord GetFail(string ruleRef = "rid-002", SeverityLevel level = SeverityLevel.Error, string synopsis = "This is rule 002.", string ruleId = "TestModule\\rule-002") { return new RuleRecord( runId: "run-001", - ruleId: ResourceId.Parse("TestModule\\rule-002"), + ruleId: ResourceId.Parse(ruleId), @ref: ruleRef, targetObject: new TargetObject(new PSObject()), targetName: "TestObject1", diff --git a/tests/PSRule.Tests/PSRule.Options.Tests.ps1 b/tests/PSRule.Tests/PSRule.Options.Tests.ps1 index a03f1685ae..e88d85d41f 100644 --- a/tests/PSRule.Tests/PSRule.Options.Tests.ps1 +++ b/tests/PSRule.Tests/PSRule.Options.Tests.ps1 @@ -1618,6 +1618,39 @@ Describe 'New-PSRuleOption' -Tag 'Option','New-PSRuleOption' { } } + Context 'Output.JobSummaryPath' { + It 'from default' { + $option = New-PSRuleOption -Default; + $option.Output.JobSummaryPath | Should -BeNullOrEmpty; + } + + It 'from Hashtable' { + $option = New-PSRuleOption -Option @{ 'Output.JobSummaryPath' = './summary.md' }; + $option.Output.JobSummaryPath | Should -Be './summary.md'; + } + + It 'from YAML' { + $option = New-PSRuleOption -Option (Join-Path -Path $here -ChildPath 'PSRule.Tests.yml'); + $option.Output.JobSummaryPath | Should -Be 'summary.md'; + } + + It 'from Environment' { + try { + $env:PSRULE_OUTPUT_JOBSUMMARYPATH = './summary.md'; + $option = New-PSRuleOption; + $option.Output.JobSummaryPath | Should -Be './summary.md'; + } + finally { + Remove-Item 'env:PSRULE_OUTPUT_JOBSUMMARYPATH' -Force; + } + } + + It 'from parameter' { + $option = New-PSRuleOption -OutputJobSummaryPath './summary.md' -Path $emptyOptionsFilePath; + $option.Output.JobSummaryPath | Should -Be './summary.md'; + } + } + Context 'Output.JsonIndent' { It 'from default' { $option = New-PSRuleOption -Default; @@ -2179,6 +2212,13 @@ Describe 'Set-PSRuleOption' -Tag 'Option','Set-PSRuleOption' { } } + Context 'Read Output.JobSummaryPath' { + It 'from parameter' { + $option = Set-PSRuleOption -OutputJobSummaryPath './summary.md' @optionParams; + $option.Output.JobSummaryPath | Should -Be './summary.md'; + } + } + Context 'Read Output.JsonIndent' { It 'from parameter' { $option = Set-PSRuleOption -OutputJsonIndent 4 @optionParams; diff --git a/tests/PSRule.Tests/PSRule.Tests.csproj b/tests/PSRule.Tests/PSRule.Tests.csproj index 41d688eacf..fe74773ed5 100644 --- a/tests/PSRule.Tests/PSRule.Tests.csproj +++ b/tests/PSRule.Tests/PSRule.Tests.csproj @@ -7,6 +7,8 @@ false PSRule portable + false + false diff --git a/tests/PSRule.Tests/PSRule.Tests.yml b/tests/PSRule.Tests/PSRule.Tests.yml index 36ad3679b8..f109197e7a 100644 --- a/tests/PSRule.Tests/PSRule.Tests.yml +++ b/tests/PSRule.Tests/PSRule.Tests.yml @@ -88,6 +88,7 @@ output: encoding: UTF7 footer: RuleCount format: Json + jobSummaryPath: summary.md outcome: Pass path: 'out/OutputPath.txt' sarifProblemsOnly: false diff --git a/tests/PSRule.Tests/TestAssertWriter.cs b/tests/PSRule.Tests/TestAssertWriter.cs index b1b6ee677c..7c342a2a88 100644 --- a/tests/PSRule.Tests/TestAssertWriter.cs +++ b/tests/PSRule.Tests/TestAssertWriter.cs @@ -13,7 +13,7 @@ internal sealed class TestAssertWriter : PipelineWriter private readonly StringBuilder _Output; public TestAssertWriter(PSRuleOption option) - : base(null, option) + : base(null, option, null) { _Output = new StringBuilder(); } diff --git a/tests/PSRule.Tests/TestWriter.cs b/tests/PSRule.Tests/TestWriter.cs index d94b22e8ce..59226a2ecf 100644 --- a/tests/PSRule.Tests/TestWriter.cs +++ b/tests/PSRule.Tests/TestWriter.cs @@ -10,12 +10,12 @@ namespace PSRule { internal sealed class TestWriter : PipelineWriter { - internal List Errors = new List(); - internal List Warnings = new List(); - internal List Output = new List(); + internal List Errors = new(); + internal List Warnings = new(); + internal List Output = new(); public TestWriter(PSRuleOption option) - : base(null, option) { } + : base(null, option, null) { } public override void WriteError(ErrorRecord errorRecord) { From 0913ee926656553be34d59e11e3bba30e573e249 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 12 Nov 2022 18:49:48 +1000 Subject: [PATCH 105/156] Bump Microsoft.NET.Test.Sdk from 17.3.2 to 17.4.0 (#1331) * Bump Microsoft.NET.Test.Sdk from 17.3.2 to 17.4.0 Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 17.3.2 to 17.4.0. - [Release notes](https://github.com/microsoft/vstest/releases) - [Commits](https://github.com/microsoft/vstest/compare/v17.3.2...v17.4.0) --- updated-dependencies: - dependency-name: Microsoft.NET.Test.Sdk dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Bump change log Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bernie White --- docs/CHANGELOG-v2.md | 3 +++ tests/PSRule.Tests/PSRule.Tests.csproj | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index c03c6d0edc..1eef666eef 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -37,6 +37,9 @@ What's changed since pre-release v2.6.0-B0013: [#1264](https://github.com/microsoft/PSRule/issues/1264) - Job summaries provide a markdown output for pipelines in addition to other supported output formats. - To use, configure the `Output.JobSummaryPath` option. +- Engineering: + - Bump Microsoft.NET.Test.Sdk to v17.4.0. + [#1331](https://github.com/microsoft/PSRule/pull/1331) ## v2.6.0-B0013 (pre-release) diff --git a/tests/PSRule.Tests/PSRule.Tests.csproj b/tests/PSRule.Tests/PSRule.Tests.csproj index fe74773ed5..7274b1f51b 100644 --- a/tests/PSRule.Tests/PSRule.Tests.csproj +++ b/tests/PSRule.Tests/PSRule.Tests.csproj @@ -12,7 +12,7 @@ - + From 8bd9dcac014ea4f418266d4d98cdf2cc6413b018 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sun, 13 Nov 2022 20:06:48 +1000 Subject: [PATCH 106/156] Added time bound suppression groups #1335 (#1338) --- .vscode/launch.json | 136 +- .vscode/tasks.json | 600 +- README.md | 5 +- docs/CHANGELOG-v2.md | 5 + .../PSRule/en-US/about_PSRule_Options.md | 189 +- .../en-US/about_PSRule_SuppressionGroups.md | 10 +- pipeline.build.ps1 | 2 +- schemas/PSRule-language.schema.json | 6097 +++++++++-------- schemas/PSRule-options.schema.json | 46 +- schemas/PSRule-resources.schema.json | 46 +- .../RunspaceContextDiagnosticExtensions.cs | 30 +- .../ExecutionActionPreference.cs | 7 +- src/PSRule/Configuration/ExecutionOption.cs | 82 +- .../SuppressionGroups/SuppressionGroup.cs | 20 + src/PSRule/Host/HostHelper.cs | 5 +- src/PSRule/PSRule.psm1 | 95 +- src/PSRule/Pipeline/GetRuleHelpPipeline.cs | 18 +- src/PSRule/Pipeline/PipelineBuilder.cs | 6 + src/PSRule/Pipeline/PipelineContext.cs | 21 +- .../Resources/PSRuleResources.Designer.cs | 9 + src/PSRule/Resources/PSRuleResources.resx | 3 + tests/PSRule.Tests/PSRule.Common.Tests.ps1 | 24 +- tests/PSRule.Tests/PSRule.Options.Tests.ps1 | 54 + tests/PSRule.Tests/PSRule.Tests.yml | 5 +- tests/PSRule.Tests/Selectors.Rule.jsonc | 3468 +++++----- tests/PSRule.Tests/SuppressionFilterTests.cs | 2 +- tests/PSRule.Tests/SuppressionGroupTests.cs | 31 +- .../PSRule.Tests/SuppressionGroups.Rule.jsonc | 138 +- .../PSRule.Tests/SuppressionGroups.Rule.yaml | 17 + 29 files changed, 5770 insertions(+), 5401 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index eb9e5cebd0..251413c5d1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,61 +1,79 @@ { - "version": "0.2.0", - "configurations": [ - { - "type": "PowerShell", - "request": "launch", - "name": "PowerShell Launch Current File", - "script": "${file}", - "args": [], - "cwd": "${file}" - }, - { - "type": "PowerShell", - "request": "launch", - "name": "PowerShell Launch Current File in Temporary Console", - "script": "${file}", - "args": [], - "cwd": "${file}", - "createTemporaryIntegratedConsole": true - }, - { - "type": "PowerShell", - "request": "launch", - "name": "PowerShell Launch Current File w/Args Prompt", - "script": "${file}", - "args": [ - "${command:SpecifyScriptArgs}" - ], - "cwd": "${file}" - }, - { - "type": "PowerShell", - "request": "attach", - "name": "PowerShell Attach to Host Process", - "processId": "${command:PickPSHostProcess}", - "runspaceId": 1 - }, - { - "type": "PowerShell", - "request": "launch", - "name": "PowerShell Interactive Session", - "cwd": "${workspaceRoot}" - }, - { - "name": "Debug PSRule Cmdlets", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "Build", - "program": "pwsh", - "args": [ - "-NoExit", - "-NoProfile", - "-Command", - "Import-Module ${workspaceFolder}/out/modules/PSRule/PSRule.psd1", - ], - "cwd": "${workspaceFolder}", - "stopAtEntry": false, - "console": "integratedTerminal" - } - ] + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Test Launch", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "Build .NET", + "program": "dotnet", + "args": [ + "test" + ], + "cwd": "${workspaceFolder}/tests/PSRule.Tests", + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Attach", + "type": "coreclr", + "request": "attach" + }, + { + "type": "PowerShell", + "request": "launch", + "name": "PowerShell Launch Current File", + "script": "${file}", + "args": [], + "cwd": "${file}" + }, + { + "type": "PowerShell", + "request": "launch", + "name": "PowerShell Launch Current File in Temporary Console", + "script": "${file}", + "args": [], + "cwd": "${file}", + "createTemporaryIntegratedConsole": true + }, + { + "type": "PowerShell", + "request": "launch", + "name": "PowerShell Launch Current File w/Args Prompt", + "script": "${file}", + "args": [ + "${command:SpecifyScriptArgs}" + ], + "cwd": "${file}" + }, + { + "type": "PowerShell", + "request": "attach", + "name": "PowerShell Attach to Host Process", + "processId": "${command:PickPSHostProcess}", + "runspaceId": 1 + }, + { + "type": "PowerShell", + "request": "launch", + "name": "PowerShell Interactive Session", + "cwd": "${workspaceRoot}" + }, + { + "name": "Debug PSRule Cmdlets", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "Build", + "program": "pwsh", + "args": [ + "-NoExit", + "-NoProfile", + "-Command", + "Import-Module ${workspaceFolder}/out/modules/PSRule/PSRule.psd1", + ], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "console": "integratedTerminal" + } + ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8f1aab1ccc..d9dfbc9dfe 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,298 +1,310 @@ { - // See https://go.microsoft.com/fwlink/?LinkId=733558 - // for the documentation about the tasks.json format - "version": "2.0.0", - "tasks": [ - { - "label": "Test", - "detail": "Build and run unit tests.", - "type": "shell", - "command": "Invoke-Build Test -AssertStyle Detect", - "group": { - "kind": "test", - "isDefault": true - }, - "problemMatcher": [ - "$pester" - ], - "presentation": { - "clear": true, - "panel": "dedicated" - }, - "linux": { - "options": { - "shell": { - "executable": "pwsh", - "args": [ - "-c" - ] - } - } - } - }, - { - "label": "Run Pester test group", - "detail": "Runs a specific group for Pester tests.", - "type": "shell", - "command": "Invoke-Build Test -AssertStyle Detect -TestGroup '${input:pesterTestGroup}'", - "group": "test", - "problemMatcher": [ - "$pester" - ], - "presentation": { - "clear": true, - "panel": "dedicated" - }, - "linux": { - "options": { - "shell": { - "executable": "pwsh", - "args": [ - "-c" - ] - } - } - } - }, - { - "label": "Analyze repository", - "detail": "Run repository analysis.", - "type": "shell", - "command": "Invoke-Build AnalyzeRepository -AssertStyle Detect", - "problemMatcher": [], - "presentation": { - "clear": true, - "panel": "dedicated" - }, - "linux": { - "options": { - "shell": { - "executable": "pwsh", - "args": [ - "-c" - ] - } - } - } - }, - { - "label": "Build module", - "detail": "Build PSRule PowerShell module.", - "type": "shell", - "command": "Invoke-Build Build", - "group": { - "kind": "build", - "isDefault": true - }, - "problemMatcher": [], - "linux": { - "options": { - "shell": { - "executable": "pwsh", - "args": [ - "-c" - ] - } - } - } - }, - { - "label": "coverage", - "type": "shell", - "command": "Invoke-Build Test -CodeCoverage", - "problemMatcher": [ - "$pester" - ], - "presentation": { - "clear": true, - "panel": "dedicated" - }, - "linux": { - "options": { - "shell": { - "executable": "pwsh", - "args": [ - "-c" - ] - } - } - } - }, - { - "label": "build-docs", - "type": "shell", - "command": "Invoke-Build BuildHelp", - "problemMatcher": [], - "linux": { - "options": { - "shell": { - "executable": "pwsh", - "args": [ - "-c" - ] - } - } - } - }, - { - "label": "scaffold-docs", - "detail": "Generate cmdlet markdown docs.", - "type": "shell", - "command": "Invoke-Build ScaffoldHelp", - "problemMatcher": [], - "linux": { - "options": { - "shell": { - "executable": "pwsh", - "args": [ - "-c" - ] - } - } - } - }, - { - "label": "clean", - "detail": "Clean up temporary working paths.", - "type": "shell", - "command": "Invoke-Build Clean", - "problemMatcher": [], - "linux": { - "options": { - "shell": { - "executable": "pwsh", - "args": [ - "-c" - ] - } - } - } - }, - { - "label": "script-analyzer", - "type": "shell", - "command": "Invoke-Build Analyze", - "problemMatcher": [], - "linux": { - "options": { - "shell": { - "executable": "pwsh", - "args": [ - "-c" - ] - } - } - } - }, - { - "label": "benchmark", - "type": "shell", - "command": "Invoke-Build Benchmark", - "problemMatcher": [], - "presentation": { - "clear": true, - "panel": "dedicated" - }, - "linux": { - "options": { - "shell": { - "executable": "pwsh", - "args": [ - "-c" - ] - } - } - } - }, - { - "label": "build-site", - "type": "shell", - "command": "Invoke-Build BuildSite", - "problemMatcher": [], - "presentation": { - "clear": true, - "panel": "dedicated" - }, - "linux": { - "options": { - "shell": { - "executable": "pwsh", - "args": [ - "-c" - ] - } - } - } - }, - { - "label": "Serve docs", - "detail": "Build and run documentation site locally.", - "type": "shell", - "command": "mkdocs serve", - "problemMatcher": [], - "presentation": { - "clear": true, - "panel": "dedicated" - } - }, - { - "label": "install python dependencies", - "detail": "Install or upgrade dependencies to build and debug mkdocs documentation locally.", - "type": "shell", - "command": "python3 -m pip install -r requirements-docs.txt", - "problemMatcher": [], - "presentation": { - "clear": true, - "panel": "dedicated" - } - }, - { - "type": "PSRule", - "problemMatcher": [ - "$PSRule" - ], - "modules": [ - "PSRule.Rules.MSFT.OSS" - ], - "label": "PSRule: Run analysis for repository", - "options": { - "env": { - "PSRULE_OUTPUT_FORMAT": "Sarif", - "PSRULE_OUTPUT_PATH": "reports/ps-rule-results.sarif" - } - } - }, - { - "label": "Build CLI", - "detail": "Builds PSRule CLI.", - "type": "shell", - "command": "Invoke-Build BuildCLI", - "group": { - "kind": "build", - "isDefault": true - }, - "problemMatcher": [], - "presentation": { - "clear": true, - "panel": "dedicated" - }, - "linux": { - "options": { - "shell": { - "executable": "pwsh", - "args": [ - "-c" - ] - } - } - } + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Test", + "detail": "Build and run unit tests.", + "type": "shell", + "command": "Invoke-Build Test -AssertStyle Detect", + "group": { + "kind": "test", + "isDefault": true + }, + "problemMatcher": [ + "$pester" + ], + "presentation": { + "clear": true, + "panel": "dedicated" + }, + "linux": { + "options": { + "shell": { + "executable": "pwsh", + "args": [ + "-c" + ] + } } - ], - "inputs": [ - { - "id": "pesterTestGroup", - "type": "promptString", - "description": "A group to use for Pester tests." + } + }, + { + "label": "Run Pester test group", + "detail": "Runs a specific group for Pester tests.", + "type": "shell", + "command": "Invoke-Build Test -AssertStyle Detect -TestGroup '${input:pesterTestGroup}'", + "group": "test", + "problemMatcher": [ + "$pester" + ], + "presentation": { + "clear": true, + "panel": "dedicated" + }, + "linux": { + "options": { + "shell": { + "executable": "pwsh", + "args": [ + "-c" + ] + } } - ] + } + }, + { + "label": "Analyze repository", + "detail": "Run repository analysis.", + "type": "shell", + "command": "Invoke-Build AnalyzeRepository -AssertStyle Detect", + "problemMatcher": [], + "presentation": { + "clear": true, + "panel": "dedicated" + }, + "linux": { + "options": { + "shell": { + "executable": "pwsh", + "args": [ + "-c" + ] + } + } + } + }, + { + "label": "Build module", + "detail": "Build PSRule PowerShell module.", + "type": "shell", + "command": "Invoke-Build Build", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [], + "linux": { + "options": { + "shell": { + "executable": "pwsh", + "args": [ + "-c" + ] + } + } + } + }, + { + "label": "coverage", + "type": "shell", + "command": "Invoke-Build Test -CodeCoverage", + "problemMatcher": [ + "$pester" + ], + "presentation": { + "clear": true, + "panel": "dedicated" + }, + "linux": { + "options": { + "shell": { + "executable": "pwsh", + "args": [ + "-c" + ] + } + } + } + }, + { + "label": "build-docs", + "type": "shell", + "command": "Invoke-Build BuildHelp", + "problemMatcher": [], + "linux": { + "options": { + "shell": { + "executable": "pwsh", + "args": [ + "-c" + ] + } + } + } + }, + { + "label": "scaffold-docs", + "detail": "Generate cmdlet markdown docs.", + "type": "shell", + "command": "Invoke-Build ScaffoldHelp", + "problemMatcher": [], + "linux": { + "options": { + "shell": { + "executable": "pwsh", + "args": [ + "-c" + ] + } + } + } + }, + { + "label": "clean", + "detail": "Clean up temporary working paths.", + "type": "shell", + "command": "Invoke-Build Clean", + "problemMatcher": [], + "linux": { + "options": { + "shell": { + "executable": "pwsh", + "args": [ + "-c" + ] + } + } + } + }, + { + "label": "script-analyzer", + "type": "shell", + "command": "Invoke-Build Analyze", + "problemMatcher": [], + "linux": { + "options": { + "shell": { + "executable": "pwsh", + "args": [ + "-c" + ] + } + } + } + }, + { + "label": "benchmark", + "type": "shell", + "command": "Invoke-Build Benchmark", + "problemMatcher": [], + "presentation": { + "clear": true, + "panel": "dedicated" + }, + "linux": { + "options": { + "shell": { + "executable": "pwsh", + "args": [ + "-c" + ] + } + } + } + }, + { + "label": "build-site", + "type": "shell", + "command": "Invoke-Build BuildSite", + "problemMatcher": [], + "presentation": { + "clear": true, + "panel": "dedicated" + }, + "linux": { + "options": { + "shell": { + "executable": "pwsh", + "args": [ + "-c" + ] + } + } + } + }, + { + "label": "Serve docs", + "detail": "Build and run documentation site locally.", + "type": "shell", + "command": "mkdocs serve", + "problemMatcher": [], + "presentation": { + "clear": true, + "panel": "dedicated" + } + }, + { + "label": "install python dependencies", + "detail": "Install or upgrade dependencies to build and debug mkdocs documentation locally.", + "type": "shell", + "command": "python3 -m pip install -r requirements-docs.txt", + "problemMatcher": [], + "presentation": { + "clear": true, + "panel": "dedicated" + } + }, + { + "type": "PSRule", + "problemMatcher": [ + "$PSRule" + ], + "modules": [ + "PSRule.Rules.MSFT.OSS" + ], + "label": "PSRule: Run analysis for repository", + "options": { + "env": { + "PSRULE_OUTPUT_FORMAT": "Sarif", + "PSRULE_OUTPUT_PATH": "reports/ps-rule-results.sarif" + } + } + }, + { + "label": "Build CLI", + "detail": "Builds PSRule CLI.", + "type": "shell", + "command": "Invoke-Build BuildCLI", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [], + "presentation": { + "clear": true, + "panel": "dedicated" + }, + "linux": { + "options": { + "shell": { + "executable": "pwsh", + "args": [ + "-c" + ] + } + } + } + }, + { + "label": "Build .NET", + "detail": "Build .NET projects for debugging.", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + } + ], + "inputs": [ + { + "id": "pesterTestGroup", + "type": "promptString", + "description": "A group to use for Pester tests." + } + ] } diff --git a/README.md b/README.md index a26984e422..e143678622 100644 --- a/README.md +++ b/README.md @@ -291,10 +291,11 @@ The following conceptual topics exist in the `PSRule` module: - [Execution.DuplicateResourceId](https://aka.ms/ps-rule/options#executionduplicateresourceid) - [Execution.LanguageMode](https://aka.ms/ps-rule/options#executionlanguagemode) - [Execution.InconclusiveWarning](https://aka.ms/ps-rule/options#executioninconclusivewarning) - - [Execution.NotProcessedWarning](https://aka.ms/ps-rule/options#executionnotprocessedwarning) - - [Execution.SuppressedRuleWarning](https://aka.ms/ps-rule/options#executionsuppressedrulewarning) - [Execution.InvariantCultureWarning](https://aka.ms/ps-rule/options#executioninvariantculturewarning) - [Execution.InitialSessionState](https://aka.ms/ps-rule/options#executioninitialsessionstate) + - [Execution.NotProcessedWarning](https://aka.ms/ps-rule/options#executionnotprocessedwarning) + - [Execution.SuppressedRuleWarning](https://aka.ms/ps-rule/options#executionsuppressedrulewarning) + - [Execution.SuppressionGroupExpired](https://aka.ms/ps-rule/options#executionsuppressiongroupexpired) - [Include.Module](https://aka.ms/ps-rule/options#includemodule) - [Include.Path](https://aka.ms/ps-rule/options#includepath) - [Input.Format](https://aka.ms/ps-rule/options#inputformat) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 1eef666eef..cfe1e01485 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -37,6 +37,11 @@ What's changed since pre-release v2.6.0-B0013: [#1264](https://github.com/microsoft/PSRule/issues/1264) - Job summaries provide a markdown output for pipelines in addition to other supported output formats. - To use, configure the `Output.JobSummaryPath` option. + - Added support for time bound suppression groups by @BernieWhite. + [#1335](https://github.com/microsoft/PSRule/issues/1335) + - Suppression groups can be configured to expire after a specified time by setting the `spec.expiresOn` property. + - When a suppression group expires, the suppression group will generate a warning by default. + - Configure the `Execution.SuppressionGroupExpired` option to ignore or error on expired suppression groups. - Engineering: - Bump Microsoft.NET.Test.Sdk to v17.4.0. [#1331](https://github.com/microsoft/PSRule/pull/1331) diff --git a/docs/concepts/PSRule/en-US/about_PSRule_Options.md b/docs/concepts/PSRule/en-US/about_PSRule_Options.md index 0c06d36890..426a47adfb 100644 --- a/docs/concepts/PSRule/en-US/about_PSRule_Options.md +++ b/docs/concepts/PSRule/en-US/about_PSRule_Options.md @@ -18,10 +18,11 @@ The following workspace options are available for use: - [Execution.DuplicateResourceId](#executionduplicateresourceid) - [Execution.LanguageMode](#executionlanguagemode) - [Execution.InconclusiveWarning](#executioninconclusivewarning) -- [Execution.NotProcessedWarning](#executionnotprocessedwarning) -- [Execution.SuppressedRuleWarning](#executionsuppressedrulewarning) - [Execution.InvariantCultureWarning](#executioninvariantculturewarning) - [Execution.InitialSessionState](#executioninitialsessionstate) +- [Execution.NotProcessedWarning](#executionnotprocessedwarning) +- [Execution.SuppressedRuleWarning](#executionsuppressedrulewarning) +- [Execution.SuppressionGroupExpired](#executionsuppressiongroupexpired) - [Include.Module](#includemodule) - [Include.Path](#includepath) - [Input.Format](#inputformat) @@ -741,6 +742,7 @@ The following preferences are available: - `Warn` (2) - Continue to execute but log a warning. - `Error` (3) - Abort and throw an error. This is the default. +- `Debug` (4) - Continue to execute but log a debug message. ```powershell # PowerShell: Using the DuplicateResourceId parameter @@ -880,6 +882,101 @@ variables: value: false ``` +### Execution.InvariantCultureWarning + +When evaluating rules inside a CI host, if invariant culture is used, a warning is shown by default. +You can suppress this warning if you set the culture with `-Culture` or the `Output.Culture` option. + +This warning can also be suppressed by using: + +```powershell +# PowerShell: Using the InvariantCultureWarning parameter +$option = New-PSRuleOption -InvariantCultureWarning $False; +``` + +```powershell +# PowerShell: Using the Execution.InvariantCultureWarning hashtable key +$option = New-PSRuleOption -Option @{ 'Execution.InvariantCultureWarning' = $False }; +``` + +```powershell +# PowerShell: Using the InvariantCultureWarning parameter to set YAML +Set-PSRuleOption -InvariantCultureWarning $False; +``` + +```yaml +# YAML: Using the execution/invariantCultureWarning property +execution: + invariantCultureWarning: false +``` + +```bash +# Bash: Using environment variable +export PSRULE_EXECUTION_INVARIANTCULTUREWARNING=false +``` + +```yaml +# GitHub Actions: Using environment variable +env: + PSRULE_EXECUTION_INVARIANTCULTUREWARNING: false +``` + +```yaml +# Azure Pipelines: Using environment variable +variables: +- name: PSRULE_EXECUTION_INVARIANTCULTUREWARNING + value: false +``` + +### Execution.InitialSessionState + +Determines how the initial session state for executing PowerShell code is created. + +The following preferences are available: + +- `BuiltIn` (0) - Create the initial session state with all built-in cmdlets loaded. + This is the default. +- `Minimal` (1) - Create the initial session state with only a minimum set of cmdlets loaded. + +```powershell +# PowerShell: Using the InitialSessionState parameter +$option = New-PSRuleOption -InitialSessionState 'Minimal'; +``` + +```powershell +# PowerShell: Using the Execution.InitialSessionState hashtable key +$option = New-PSRuleOption -Option @{ 'Execution.InitialSessionState' = 'Minimal' }; +``` + +```powershell +# PowerShell: Using the InitialSessionState parameter to set YAML +Set-PSRuleOption -InitialSessionState 'Minimal'; +``` + +```yaml +# YAML: Using the execution/initialSessionState property +execution: + initialSessionState: Minimal +``` + +```bash +# Bash: Using environment variable +export PSRULE_EXECUTION_INITIALSESSIONSTATE=Minimal +``` + +```yaml +# GitHub Actions: Using environment variable +env: + PSRULE_EXECUTION_INITIALSESSIONSTATE: Minimal +``` + +```yaml +# Azure Pipelines: Using environment variable +variables: +- name: PSRULE_EXECUTION_INITIALSESSIONSTATE + value: Minimal +``` + ### Execution.NotProcessedWarning When evaluating rules, it is possible to incorrectly select a path with rules that use pre-conditions that do not accept the pipeline object. @@ -982,99 +1079,59 @@ variables: value: false ``` -### Execution.InvariantCultureWarning - -When evaluating rules inside a CI host, if invariant culture is used, a warning is shown by default. -You can suppress this warning if you set the culture with `-Culture` or the `Output.Culture` option. - -This warning can also be suppressed by using: - -```powershell -# PowerShell: Using the InvariantCultureWarning parameter -$option = New-PSRuleOption -InvariantCultureWarning $False; -``` - -```powershell -# PowerShell: Using the Execution.InvariantCultureWarning hashtable key -$option = New-PSRuleOption -Option @{ 'Execution.InvariantCultureWarning' = $False }; -``` +### Execution.SuppressionGroupExpired -```powershell -# PowerShell: Using the InvariantCultureWarning parameter to set YAML -Set-PSRuleOption -InvariantCultureWarning $False; -``` - -```yaml -# YAML: Using the execution/invariantCultureWarning property -execution: - invariantCultureWarning: false -``` - -```bash -# Bash: Using environment variable -export PSRULE_EXECUTION_INVARIANTCULTUREWARNING=false -``` - -```yaml -# GitHub Actions: Using environment variable -env: - PSRULE_EXECUTION_INVARIANTCULTUREWARNING: false -``` - -```yaml -# Azure Pipelines: Using environment variable -variables: -- name: PSRULE_EXECUTION_INVARIANTCULTUREWARNING - value: false -``` - -### Execution.InitialSessionState - -Determines how the initial session state for executing PowerShell code is created. +Determines how to handle expired suppression groups. +Regardless of the value, an expired suppression group will be ignored. +By defaut, a warning is generated, however this behaviour can be modified by this option. The following preferences are available: -- `BuiltIn` (0) - Create the initial session state with all built-in cmdlets loaded. +- `None` (0) - No preference. + Inherits the default of `Warn`. +- `Ignore` (1) - Continue to execute silently. +- `Warn` (2) - Continue to execute but log a warning. This is the default. -- `Minimal` (1) - Create the initial session state with only a minimum set of cmdlets loaded. +- `Error` (3) - Abort and throw an error. +- `Debug` (4) - Continue to execute but log a debug message. ```powershell -# PowerShell: Using the InitialSessionState parameter -$option = New-PSRuleOption -InitialSessionState 'Minimal'; +# PowerShell: Using the SuppressionGroupExpired parameter +$option = New-PSRuleOption -SuppressionGroupExpired 'Error'; ``` ```powershell -# PowerShell: Using the Execution.InitialSessionState hashtable key -$option = New-PSRuleOption -Option @{ 'Execution.InitialSessionState' = 'Minimal' }; +# PowerShell: Using the Execution.SuppressionGroupExpired hashtable key +$option = New-PSRuleOption -Option @{ 'Execution.SuppressionGroupExpired' = 'Error' }; ``` ```powershell -# PowerShell: Using the InitialSessionState parameter to set YAML -Set-PSRuleOption -InitialSessionState 'Minimal'; +# PowerShell: Using the SuppressionGroupExpired parameter to set YAML +Set-PSRuleOption -SuppressionGroupExpired 'Error'; ``` ```yaml -# YAML: Using the execution/initialSessionState property +# YAML: Using the execution/suppressionGroupExpired property execution: - initialSessionState: Minimal + suppressionGroupExpired: Error ``` ```bash # Bash: Using environment variable -export PSRULE_EXECUTION_INITIALSESSIONSTATE=Minimal +export PSRULE_EXECUTION_SUPPRESSIONGROUPEXPIRED=Error ``` ```yaml # GitHub Actions: Using environment variable env: - PSRULE_EXECUTION_INITIALSESSIONSTATE: Minimal + PSRULE_EXECUTION_SUPPRESSIONGROUPEXPIRED: Error ``` ```yaml # Azure Pipelines: Using environment variable variables: -- name: PSRULE_EXECUTION_INITIALSESSIONSTATE - value: Minimal +- name: PSRULE_EXECUTION_SUPPRESSIONGROUPEXPIRED + value: Error ``` ### Include.Module @@ -2949,10 +3006,12 @@ convention: # Configure execution options execution: aliasReferenceWarning: false + duplicateResourceId: Warn languageMode: ConstrainedLanguage inconclusiveWarning: false notProcessedWarning: false suppressedRuleWarning: false + suppressionGroupExpired: Error # Configure include options include: @@ -3058,10 +3117,12 @@ convention: # Configure execution options execution: aliasReferenceWarning: true + duplicateResourceId: Error languageMode: FullLanguage inconclusiveWarning: true notProcessedWarning: true suppressedRuleWarning: true + suppressionGroupExpired: Warn # Configure include options include: diff --git a/docs/concepts/PSRule/en-US/about_PSRule_SuppressionGroups.md b/docs/concepts/PSRule/en-US/about_PSRule_SuppressionGroups.md index 8ea7405ffe..e35eaf339a 100644 --- a/docs/concepts/PSRule/en-US/about_PSRule_SuppressionGroups.md +++ b/docs/concepts/PSRule/en-US/about_PSRule_SuppressionGroups.md @@ -32,6 +32,7 @@ kind: SuppressionGroup metadata: name: '{{ Name }}' spec: + expiresOn: null rule: [] if: { } ``` @@ -46,6 +47,7 @@ spec: "name": "{{ Name }}" }, "spec": { + "expiresOn": null, "rule": [], "if": {} } @@ -59,6 +61,10 @@ If no rules are specified, suppression will occur for all rules. Within the `if` object, one or more conditions or logical operators can be used. When the `if` condition is `true` the object will be suppressed for the current rule. +Optionally, an expiry can be set using the `expiresOn` property. +When the expiry date is reached, the suppression will no longer be applied. +To configure an expiry, set a RFC3339 (ISO 8601) formatted date time using the format `yyyy-MM-ddTHH:mm:ssZ`. + ### Documentation Suppression groups can be configured with a synopsis. @@ -105,6 +111,7 @@ kind: SuppressionGroup metadata: name: SuppressWithTestType spec: + expiresOn: '2030-01-01T00:00:00Z' rule: - 'FromFile3' - 'FromFile5' @@ -147,6 +154,7 @@ spec: "name": "SuppressWithTestType" }, "spec": { + "expiresOn": "2030-01-01T00:00:00Z", "rule": [ "FromFile3", "FromFile5" @@ -162,7 +170,7 @@ spec: ## NOTE -An online version of this document is available at https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_SuppressionGroups/. +An online version of this document is available at . ## SEE ALSO diff --git a/pipeline.build.ps1 b/pipeline.build.ps1 index 29c3fd596a..a39b07e4b6 100644 --- a/pipeline.build.ps1 +++ b/pipeline.build.ps1 @@ -327,7 +327,7 @@ task Rules { task Benchmark { if ($Benchmark -or $BuildTask -eq 'Benchmark') { - dotnet run -p src/PSRule.Benchmark -f net6.0 -c Release -- benchmark --output $PWD; + dotnet run --project src/PSRule.Benchmark -f net6.0 -c Release -- benchmark --output $PWD; } } diff --git a/schemas/PSRule-language.schema.json b/schemas/PSRule-language.schema.json index 4bba0a6b6e..58d530ee71 100644 --- a/schemas/PSRule-language.schema.json +++ b/schemas/PSRule-language.schema.json @@ -1,3173 +1,3182 @@ { - "$schema": "https://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/resource-v1", - "definitions": { - "resource-v1": { - "oneOf": [ - { - "$ref": "#/definitions/rule-v1" - }, - { - "$ref": "#/definitions/baseline-v1" - }, - { - "$ref": "#/definitions/moduleConfig-v1" - }, - { - "$ref": "#/definitions/selector-v1" - }, - { - "$ref": "#/definitions/suppressionGroup-v1" - } - ] + "$schema": "https://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/resource-v1", + "definitions": { + "resource-v1": { + "oneOf": [ + { + "$ref": "#/definitions/rule-v1" }, - "resource-metadata": { - "type": "object", - "title": "Metadata", - "description": "Additional information to identify the resource.", - "properties": { - "name": { - "type": "string", - "title": "Name", - "description": "The name of the resource. This must be unique.", - "$ref": "#/definitions/resourceName" - }, - "annotations": { - "type": "object", - "title": "Annotations", - "description": "Additional annotations for the resource.", - "properties": { - "obsolete": { - "type": "boolean", - "title": "Obsolete", - "description": "A common annotation that flags the resource as obsolete.", - "default": false - } - }, - "additionalProperties": true, - "defaultSnippets": [ - { - "label": "Annotation key/ value", - "body": { - "${1:Key}": "${2:Value}" - } - } - ] - }, - "tags": { - "$ref": "#/definitions/resourceTags" - } - }, - "required": [ - "name" - ] + { + "$ref": "#/definitions/baseline-v1" }, - "baseline-v1": { - "type": "object", - "title": "Baseline", - "description": "A PSRule Baseline.", - "markdownDescription": "A PSRule Baseline. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Baseline/)", - "properties": { - "apiVersion": { - "type": "string", - "title": "API Version", - "description": "The API Version for the PSRule resources.", - "enum": [ - "github.com/microsoft/PSRule/v1" - ] - }, - "kind": { - "type": "string", - "title": "Kind", - "description": "A PSRule Baseline resource.", - "markdownDescription": "A PSRule Baseline resource. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Baseline/)", - "enum": [ - "Baseline" - ] - }, - "metadata": { - "type": "object", - "$ref": "#/definitions/resource-metadata" - }, - "spec": { - "type": "object", - "$ref": "#/definitions/baselineSpec" - } - }, - "required": [ - "apiVersion", - "kind", - "metadata", - "spec" - ], - "additionalProperties": false + { + "$ref": "#/definitions/moduleConfig-v1" }, - "baselineSpec": { - "type": "object", - "title": "Spec", - "description": "A specification for a baseline.", - "markdownDescription": "A specification for a baseline. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Baseline/)", - "properties": { - "binding": { - "$ref": "#/definitions/binding-option" - }, - "configuration": { - "$ref": "#/definitions/configuration" - }, - "rule": { - "type": "object", - "title": "Rule", - "description": "A filter for included or excluded rules.", - "properties": { - "include": { - "type": "array", - "title": "Include rules", - "description": "Rules to include by name in the baseline.", - "markdownDescription": "Rules to include by name in the baseline. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#ruleinclude)", - "items": { - "type": "string", - "$ref": "#/definitions/resourceNameReference" - }, - "uniqueItems": true - }, - "exclude": { - "type": "array", - "title": "Exclude rules", - "description": "Rules to exclude by name from the baseline.", - "markdownDescription": "Rules to exclude by name from the baseline. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#ruleexclude)", - "items": { - "type": "string", - "$ref": "#/definitions/resourceNameReference" - }, - "uniqueItems": true - }, - "tag": { - "type": "object", - "title": "Tags", - "description": "Require rules to have the following tags.", - "markdownDescription": "Require rules to have the following tags. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#ruletag)", - "additionalProperties": { - "oneOf": [ - { - "type": "string", - "description": "A required tag." - }, - { - "type": "array", - "description": "A required tag.", - "items": { - "type": "string" - }, - "uniqueItems": true - } - ] - } - }, - "labels": { - "type": "object", - "title": "Labels", - "description": "Require rules to have the following associated labels.", - "markdownDescription": "Require rules to have the following associated labels.", - "additionalProperties": { - "oneOf": [ - { - "type": "string", - "description": "A required reference." - }, - { - "type": "array", - "description": "A required reference.", - "items": { - "type": "string" - }, - "uniqueItems": true - } - ] - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false + { + "$ref": "#/definitions/selector-v1" }, - "moduleConfig-v1": { - "type": "object", - "title": "ModuleConfig", - "description": "A PSRule ModuleConfig.", - "properties": { - "apiVersion": { - "type": "string", - "title": "API Version", - "description": "The API Version for the PSRule resources.", - "enum": [ - "github.com/microsoft/PSRule/v1" - ] - }, - "kind": { - "type": "string", - "title": "Kind", - "description": "A PSRule ModuleConfig resource.", - "enum": [ - "ModuleConfig" - ] - }, - "metadata": { - "type": "object", - "$ref": "#/definitions/resource-metadata" - }, - "spec": { - "type": "object", - "$ref": "#/definitions/moduleConfigSpec" - } - }, - "required": [ - "apiVersion", - "kind", - "metadata", - "spec" - ], - "additionalProperties": false + { + "$ref": "#/definitions/suppressionGroup-v1" + } + ] + }, + "resource-metadata": { + "type": "object", + "title": "Metadata", + "description": "Additional information to identify the resource.", + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "The name of the resource. This must be unique.", + "$ref": "#/definitions/resourceName" }, - "moduleConfigSpec": { - "type": "object", - "title": "Spec", - "description": "A specification for a ModuleConfig.", - "properties": { - "binding": { - "$ref": "#/definitions/binding-option" - }, - "configuration": { - "$ref": "#/definitions/configuration" - }, - "convention": { - "$ref": "#/definitions/convention-option" - }, - "output": { - "type": "object", - "title": "Output options", - "description": "Options that affect how output is generated.", - "properties": { - "culture": { - "type": "array", - "title": "Culture", - "description": "One or more cultures to use for generating output. When multiple cultures are specified, the first matching culture will be used. By default, the current PowerShell culture is used.", - "markdownDescription": "One or more cultures to use for generating output. When multiple cultures are specified, the first matching culture will be used. By default, the current PowerShell culture is used. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputculture)", - "items": { - "type": "string", - "description": "A culture for generating output.", - "minLength": 2 - }, - "uniqueItems": true, - "defaultSnippets": [ - { - "label": "en-AU", - "bodyText": [ - "en-AU" - ] - }, - { - "label": "en-US", - "bodyText": [ - "en-US" - ] - }, - { - "label": "en-GB", - "bodyText": [ - "en-GB" - ] - } - ] - } - }, - "additionalProperties": false - }, - "rule": { - "type": "object", - "title": "Rule", - "description": "A filter for included or excluded rules.", - "properties": { - "baseline": { - "type": "string", - "title": "Baseline", - "description": "The name of a baseline to use.", - "markdownDescription": "The name of a baseline to use. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#rulebaseline)", - "$ref": "#/definitions/resourceNameReference" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false + "annotations": { + "type": "object", + "title": "Annotations", + "description": "Additional annotations for the resource.", + "properties": { + "obsolete": { + "type": "boolean", + "title": "Obsolete", + "description": "A common annotation that flags the resource as obsolete.", + "default": false + } + }, + "additionalProperties": true, + "defaultSnippets": [ + { + "label": "Annotation key/ value", + "body": { + "${1:Key}": "${2:Value}" + } + } + ] }, - "resourceName": { - "type": "string", - "minLength": 3, - "maxLength": 128, - "pattern": "^[^<>:/\\\\|?*\"'`+@._\\-\\x00-\\x1F][^<>:/\\\\|?*\"'`+@\\x00-\\x1F]{1,126}[^<>:/\\\\|?*\"'`+@._\\-\\x00-\\x1F]$" + "tags": { + "$ref": "#/definitions/resourceTags" + } + }, + "required": [ + "name" + ] + }, + "baseline-v1": { + "type": "object", + "title": "Baseline", + "description": "A PSRule Baseline.", + "markdownDescription": "A PSRule Baseline. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Baseline/)", + "properties": { + "apiVersion": { + "type": "string", + "title": "API Version", + "description": "The API Version for the PSRule resources.", + "enum": [ + "github.com/microsoft/PSRule/v1" + ] }, - "resourceNameReference": { - "type": "string", - "minLength": 3, - "pattern": "^.*\\[^<>:/\\\\|?*\"'`+@._\\-\\x00-\\x1F][^<>:/\\\\|?*\"'`+@\\x00-\\x1F]{1,126}[^<>:/\\\\|?*\"'`+@._\\-\\x00-\\x1F]$" - }, - "resourceTags": { - "type": "object", - "title": "Tags", - "description": "Additional indexed key/ value pairs for the resource.", - "additionalProperties": { - "type": "string" - }, - "defaultSnippets": [ - { - "label": "Tag key/ value", - "body": { - "${1:Key}": "${2:Value}" - } - } - ] + "kind": { + "type": "string", + "title": "Kind", + "description": "A PSRule Baseline resource.", + "markdownDescription": "A PSRule Baseline resource. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Baseline/)", + "enum": [ + "Baseline" + ] }, - "binding-option": { - "type": "object", - "title": "Object binding", - "description": "Configure property/ object binding options.", - "properties": { - "field": { - "type": "object", - "title": "Field", - "description": "Custom fields to bind.", - "markdownDescription": "Custom fields to bind. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingfield)", - "additionalProperties": { - "type": "array", - "description": "A custom field to bind.", - "markdownDescription": "Custom field to bind. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingfield)", - "items": { - "type": "string" - }, - "uniqueItems": true - } - }, - "ignoreCase": { - "type": "boolean", - "title": "Ignore case", - "description": "Determines if custom binding uses ignores case when matching properties. The default is true.", - "markdownDescription": "Determines if custom binding uses ignores case when matching properties. The default is `true`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingignorecase)", - "default": true - }, - "nameSeparator": { + "metadata": { + "type": "object", + "$ref": "#/definitions/resource-metadata" + }, + "spec": { + "type": "object", + "$ref": "#/definitions/baselineSpec" + } + }, + "required": [ + "apiVersion", + "kind", + "metadata", + "spec" + ], + "additionalProperties": false + }, + "baselineSpec": { + "type": "object", + "title": "Spec", + "description": "A specification for a baseline.", + "markdownDescription": "A specification for a baseline. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Baseline/)", + "properties": { + "binding": { + "$ref": "#/definitions/binding-option" + }, + "configuration": { + "$ref": "#/definitions/configuration" + }, + "rule": { + "type": "object", + "title": "Rule", + "description": "A filter for included or excluded rules.", + "properties": { + "include": { + "type": "array", + "title": "Include rules", + "description": "Rules to include by name in the baseline.", + "markdownDescription": "Rules to include by name in the baseline. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#ruleinclude)", + "items": { + "type": "string", + "$ref": "#/definitions/resourceNameReference" + }, + "uniqueItems": true + }, + "exclude": { + "type": "array", + "title": "Exclude rules", + "description": "Rules to exclude by name from the baseline.", + "markdownDescription": "Rules to exclude by name from the baseline. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#ruleexclude)", + "items": { + "type": "string", + "$ref": "#/definitions/resourceNameReference" + }, + "uniqueItems": true + }, + "tag": { + "type": "object", + "title": "Tags", + "description": "Require rules to have the following tags.", + "markdownDescription": "Require rules to have the following tags. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#ruletag)", + "additionalProperties": { + "oneOf": [ + { "type": "string", - "title": "Name separator", - "description": "Configures the separator to use for building a qualified name. The default is '/'.", - "markdownDescription": "Configures the separator to use for building a qualified name. The default is `/`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingnameseparator)", - "default": "/" - }, - "preferTargetInfo": { - "type": "boolean", - "title": "Prefer target info", - "description": "Determines if binding prefers target info provided by the object over custom configuration. The default is false.", - "markdownDescription": "Determines if binding prefers target info provided by the object over custom configuration. The default is `false`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingprefertargetinfo)", - "default": false - }, - "targetName": { + "description": "A required tag." + }, + { "type": "array", - "title": "Bind TargetName", - "description": "Specifies one or more property names to bind TargetName to.", - "markdownDescription": "Specifies one or more property names to bind TargetName to. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingtargetname)", + "description": "A required tag.", "items": { - "type": "string" + "type": "string" }, "uniqueItems": true - }, - "targetType": { + } + ] + } + }, + "labels": { + "type": "object", + "title": "Labels", + "description": "Require rules to have the following associated labels.", + "markdownDescription": "Require rules to have the following associated labels.", + "additionalProperties": { + "oneOf": [ + { + "type": "string", + "description": "A required reference." + }, + { "type": "array", - "title": "Bind TargetType", - "description": "Specifies one or more property names to bind TargetType to.", - "markdownDescription": "Specifies one or more property names to bind TargetType to. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingtargettype)", + "description": "A required reference.", "items": { - "type": "string" + "type": "string" }, "uniqueItems": true - }, - "useQualifiedName": { - "type": "boolean", - "title": "Use qualified name", - "description": "Determines if a qualified TargetName is used. The default is false.", - "markdownDescription": "Determines if a qualified TargetName is used. The default is `false`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingusequalifiedname)", - "default": false - } - }, - "additionalProperties": false + } + ] + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "moduleConfig-v1": { + "type": "object", + "title": "ModuleConfig", + "description": "A PSRule ModuleConfig.", + "properties": { + "apiVersion": { + "type": "string", + "title": "API Version", + "description": "The API Version for the PSRule resources.", + "enum": [ + "github.com/microsoft/PSRule/v1" + ] + }, + "kind": { + "type": "string", + "title": "Kind", + "description": "A PSRule ModuleConfig resource.", + "enum": [ + "ModuleConfig" + ] + }, + "metadata": { + "type": "object", + "$ref": "#/definitions/resource-metadata" + }, + "spec": { + "type": "object", + "$ref": "#/definitions/moduleConfigSpec" + } + }, + "required": [ + "apiVersion", + "kind", + "metadata", + "spec" + ], + "additionalProperties": false + }, + "moduleConfigSpec": { + "type": "object", + "title": "Spec", + "description": "A specification for a ModuleConfig.", + "properties": { + "binding": { + "$ref": "#/definitions/binding-option" }, "configuration": { - "type": "object", - "title": "Configuration values", - "description": "A set of key/ value configuration options for rules.", - "markdownDescription": "A set of key/ value configuration options for rules. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#configuration)", - "defaultSnippets": [ - { - "label": "Configuration value", - "body": { - "${1:Key}": "${2:Value}" - } - } - ] + "$ref": "#/definitions/configuration" }, - "convention-option": { - "type": "object", - "title": "Convention options", - "description": "Options that configure conventions.", - "properties": { - "include": { - "type": "array", - "title": "Include conventions", - "description": "An ordered list of conventions to include.", - "markdownDescription": "An ordered list of conventions to include. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#conventioninclude)", - "items": { - "type": "string" - }, - "uniqueItems": true - } - }, - "additionalProperties": false + "convention": { + "$ref": "#/definitions/convention-option" }, - "selector-v1": { - "type": "object", - "title": "Selector", - "description": "A selector resource.", - "markdownDescription": "A selector resource. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Selectors/)", - "properties": { - "apiVersion": { - "type": "string", - "title": "API Version", - "description": "The API Version for the PSRule resources.", - "enum": [ - "github.com/microsoft/PSRule/v1" - ] - }, - "kind": { - "type": "string", - "title": "Kind", - "description": "A PSRule Selector resource.", - "enum": [ - "Selector" - ] + "output": { + "type": "object", + "title": "Output options", + "description": "Options that affect how output is generated.", + "properties": { + "culture": { + "type": "array", + "title": "Culture", + "description": "One or more cultures to use for generating output. When multiple cultures are specified, the first matching culture will be used. By default, the current PowerShell culture is used.", + "markdownDescription": "One or more cultures to use for generating output. When multiple cultures are specified, the first matching culture will be used. By default, the current PowerShell culture is used. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#outputculture)", + "items": { + "type": "string", + "description": "A culture for generating output.", + "minLength": 2 + }, + "uniqueItems": true, + "defaultSnippets": [ + { + "label": "en-AU", + "bodyText": [ + "en-AU" + ] }, - "metadata": { - "type": "object", - "$ref": "#/definitions/resource-metadata" + { + "label": "en-US", + "bodyText": [ + "en-US" + ] }, - "spec": { - "type": "object", - "$ref": "#/definitions/selectorSpec" + { + "label": "en-GB", + "bodyText": [ + "en-GB" + ] } - }, - "required": [ - "apiVersion", - "kind", - "metadata", - "spec" - ] + ] + } + }, + "additionalProperties": false }, - "selectorSpec": { - "type": "object", - "title": "Spec", - "description": "The specification for a selector.", - "markdownDescription": "The specification for a selector. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Selectors/)", - "properties": { - "if": { - "type": "object", - "title": "If", - "description": "A condition is made up of one or more expressions that will determine if an object is selected by the selector.", - "markdownDescription": "A condition is made up of one or more [expressions](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/) that will determine if an object is selected by the selector. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Selectors/)", - "$ref": "#/definitions/expressions" - } + "rule": { + "type": "object", + "title": "Rule", + "description": "A filter for included or excluded rules.", + "properties": { + "baseline": { + "type": "string", + "title": "Baseline", + "description": "The name of a baseline to use.", + "markdownDescription": "The name of a baseline to use. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#rulebaseline)", + "$ref": "#/definitions/resourceNameReference" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "resourceName": { + "type": "string", + "minLength": 3, + "maxLength": 128, + "pattern": "^[^<>:/\\\\|?*\"'`+@._\\-\\x00-\\x1F][^<>:/\\\\|?*\"'`+@\\x00-\\x1F]{1,126}[^<>:/\\\\|?*\"'`+@._\\-\\x00-\\x1F]$" + }, + "resourceNameReference": { + "type": "string", + "minLength": 3, + "pattern": "^.*\\[^<>:/\\\\|?*\"'`+@._\\-\\x00-\\x1F][^<>:/\\\\|?*\"'`+@\\x00-\\x1F]{1,126}[^<>:/\\\\|?*\"'`+@._\\-\\x00-\\x1F]$" + }, + "resourceTags": { + "type": "object", + "title": "Tags", + "description": "Additional indexed key/ value pairs for the resource.", + "additionalProperties": { + "type": "string" + }, + "defaultSnippets": [ + { + "label": "Tag key/ value", + "body": { + "${1:Key}": "${2:Value}" + } + } + ] + }, + "binding-option": { + "type": "object", + "title": "Object binding", + "description": "Configure property/ object binding options.", + "properties": { + "field": { + "type": "object", + "title": "Field", + "description": "Custom fields to bind.", + "markdownDescription": "Custom fields to bind. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingfield)", + "additionalProperties": { + "type": "array", + "description": "A custom field to bind.", + "markdownDescription": "Custom field to bind. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingfield)", + "items": { + "type": "string" }, - "required": [ - "if" - ], - "additionalProperties": false + "uniqueItems": true + } }, - "suppressionGroup-v1": { - "type": "object", - "title": "SuppressionGroup", - "description": "A PSRule Suppression Group", - "markdownDescription": "A PSRule Suppression Group. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_SuppressionGroups/)", - "properties": { - "apiVersion": { - "type": "string", - "title": "API Version", - "description": "The API Version for the PSRule resources.", - "enum": [ - "github.com/microsoft/PSRule/v1" - ] - }, - "kind": { - "type": "string", - "title": "Kind", - "description": "A PSRule SuppressionGroup resource.", - "enum": [ - "SuppressionGroup" - ] - }, - "metadata": { - "type": "object", - "$ref": "#/definitions/resource-metadata" - }, - "spec": { - "type": "object", - "$ref": "#/definitions/suppressionGroupSpec" - } - }, - "required": [ - "apiVersion", - "kind", - "metadata", - "spec" - ] + "ignoreCase": { + "type": "boolean", + "title": "Ignore case", + "description": "Determines if custom binding uses ignores case when matching properties. The default is true.", + "markdownDescription": "Determines if custom binding uses ignores case when matching properties. The default is `true`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingignorecase)", + "default": true }, - "suppressionGroupSpec": { - "type": "object", - "title": "Spec", - "description": "PSRule Suppression Group specification.", - "properties": { - "rule": { - "type": "array", - "title": "Rule pre-condition", - "description": "This only applies to rules that match the specified rule names", - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "if": { - "type": "object", - "$ref": "#/definitions/expressions" - } - }, - "required": [ - "if" - ], - "additionalProperties": false + "nameSeparator": { + "type": "string", + "title": "Name separator", + "description": "Configures the separator to use for building a qualified name. The default is '/'.", + "markdownDescription": "Configures the separator to use for building a qualified name. The default is `/`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingnameseparator)", + "default": "/" }, - "rule-v1": { - "type": "object", - "title": "Rule", - "description": "A rule resource.", - "markdownDescription": "A rule resource. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Rules/)", - "properties": { - "apiVersion": { - "type": "string", - "title": "API Version", - "description": "The API Version for the PSRule resources.", - "enum": [ - "github.com/microsoft/PSRule/v1" - ] - }, - "kind": { - "type": "string", - "title": "Kind", - "description": "A rule resource.", - "markdownDescription": "A rule resource. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Rules/)", - "enum": [ - "Rule" - ] - }, - "metadata": { - "type": "object", - "$ref": "#/definitions/ruleMetadata" - }, - "spec": { - "type": "object", - "$ref": "#/definitions/ruleSpec" - } - }, - "required": [ - "apiVersion", - "kind", - "metadata", - "spec" - ] + "preferTargetInfo": { + "type": "boolean", + "title": "Prefer target info", + "description": "Determines if binding prefers target info provided by the object over custom configuration. The default is false.", + "markdownDescription": "Determines if binding prefers target info provided by the object over custom configuration. The default is `false`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingprefertargetinfo)", + "default": false }, - "ruleSpec": { - "type": "object", - "title": "Spec", - "description": "The specification for the rule.", - "markdownDescription": "The specification for the rule. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Rules/)", - "properties": { - "condition": { - "type": "object", - "title": "Condition", - "description": "A condition is made up of one or more expressions that will determine if the rule passes or fails.", - "markdownDescription": "A condition is made up of one or more [expressions](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/) that will determine if the rule passes or fails.\n\n[See help](https://microsoft.github.io/PSRule/v2/authoring/writing-rules/)", - "oneOf": [ - { - "$ref": "#/definitions/expressions" - } - ] - }, - "level": { - "type": "string", - "title": "Level", - "description": "If the rule fails, how serious is the result. By default this is set to Error.", - "markdownDescription": "If the rule fails, how serious is the result. By default this is set to `Error`. [See help](https://microsoft.github.io/PSRule/v2/authoring/writing-rules/)", - "enum": [ - "Error", - "Warning", - "Information" - ], - "default": "Error" - }, - "type": { - "type": "array", - "title": "Type pre-condition", - "description": "This rule only applies to objects that match the specifies types.", - "items": { - "type": "string", - "default": "" - }, - "uniqueItems": true - }, - "with": { - "type": "array", - "title": "Selector pre-condition", - "description": "This rule only applies to objects that match the specified selectors.", - "items": { - "type": "string", - "default": "" - }, - "uniqueItems": true - }, - "where": { - "type": "object", - "title": "Sub-selector pre-condition", - "description": "The rule only applies to objects that match the sub-selector condition.", - "markdownDescription": "The rule only applies to objects that match the sub-selector condition.\n\n[See help](https://microsoft.github.io/PSRule/v2/expressions/sub-selectors/)", - "oneOf": [ - { - "$ref": "#/definitions/expressions" - } - ] - } - }, - "required": [ - "condition" - ], - "additionalProperties": false, - "default": { - "condition": {} + "targetName": { + "type": "array", + "title": "Bind TargetName", + "description": "Specifies one or more property names to bind TargetName to.", + "markdownDescription": "Specifies one or more property names to bind TargetName to. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingtargetname)", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "targetType": { + "type": "array", + "title": "Bind TargetType", + "description": "Specifies one or more property names to bind TargetType to.", + "markdownDescription": "Specifies one or more property names to bind TargetType to. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingtargettype)", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "useQualifiedName": { + "type": "boolean", + "title": "Use qualified name", + "description": "Determines if a qualified TargetName is used. The default is false.", + "markdownDescription": "Determines if a qualified TargetName is used. The default is `false`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#bindingusequalifiedname)", + "default": false + } + }, + "additionalProperties": false + }, + "configuration": { + "type": "object", + "title": "Configuration values", + "description": "A set of key/ value configuration options for rules.", + "markdownDescription": "A set of key/ value configuration options for rules. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#configuration)", + "defaultSnippets": [ + { + "label": "Configuration value", + "body": { + "${1:Key}": "${2:Value}" + } + } + ] + }, + "convention-option": { + "type": "object", + "title": "Convention options", + "description": "Options that configure conventions.", + "properties": { + "include": { + "type": "array", + "title": "Include conventions", + "description": "An ordered list of conventions to include.", + "markdownDescription": "An ordered list of conventions to include. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#conventioninclude)", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + "additionalProperties": false + }, + "selector-v1": { + "type": "object", + "title": "Selector", + "description": "A selector resource.", + "markdownDescription": "A selector resource. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Selectors/)", + "properties": { + "apiVersion": { + "type": "string", + "title": "API Version", + "description": "The API Version for the PSRule resources.", + "enum": [ + "github.com/microsoft/PSRule/v1" + ] + }, + "kind": { + "type": "string", + "title": "Kind", + "description": "A PSRule Selector resource.", + "enum": [ + "Selector" + ] + }, + "metadata": { + "type": "object", + "$ref": "#/definitions/resource-metadata" + }, + "spec": { + "type": "object", + "$ref": "#/definitions/selectorSpec" + } + }, + "required": [ + "apiVersion", + "kind", + "metadata", + "spec" + ] + }, + "selectorSpec": { + "type": "object", + "title": "Spec", + "description": "The specification for a selector.", + "markdownDescription": "The specification for a selector. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Selectors/)", + "properties": { + "if": { + "type": "object", + "title": "If", + "description": "A condition is made up of one or more expressions that will determine if an object is selected by the selector.", + "markdownDescription": "A condition is made up of one or more [expressions](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/) that will determine if an object is selected by the selector. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Selectors/)", + "$ref": "#/definitions/expressions" + } + }, + "required": [ + "if" + ], + "additionalProperties": false + }, + "suppressionGroup-v1": { + "type": "object", + "title": "SuppressionGroup", + "description": "A PSRule Suppression Group", + "markdownDescription": "A PSRule Suppression Group. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_SuppressionGroups/)", + "properties": { + "apiVersion": { + "type": "string", + "title": "API Version", + "description": "The API Version for the PSRule resources.", + "enum": [ + "github.com/microsoft/PSRule/v1" + ] + }, + "kind": { + "type": "string", + "title": "Kind", + "description": "A PSRule SuppressionGroup resource.", + "enum": [ + "SuppressionGroup" + ] + }, + "metadata": { + "type": "object", + "$ref": "#/definitions/resource-metadata" + }, + "spec": { + "type": "object", + "$ref": "#/definitions/suppressionGroupSpec" + } + }, + "required": [ + "apiVersion", + "kind", + "metadata", + "spec" + ] + }, + "suppressionGroupSpec": { + "type": "object", + "title": "Spec", + "description": "PSRule Suppression Group specification.", + "markdownDescription": "PSRule Suppression Group specification.", + "properties": { + "expiresOn": { + "type": "string", + "title": "Expires On", + "description": "The date time that the suppression is valid until. After this date time, the suppression is ignored. When not set, the suppression does not expire. This RFC3339 (ISO 8601) formatted date time using the format yyyy-MM-ddTHH:mm:ssZ.", + "markdownDescription": "The date time that the suppression is valid until. After this date time, the suppression is ignored. When not set, the suppression does not expire. This RFC3339 (ISO 8601) formatted date time using the format `yyyy-MM-ddTHH:mm:ssZ`.", + "format": "date-time" + }, + "rule": { + "type": "array", + "title": "Rule pre-condition", + "description": "This only applies to rules that match the specified rule names.", + "markdownDescription": "This only applies to rules that match the specified rule names.", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "if": { + "type": "object", + "$ref": "#/definitions/expressions" + } + }, + "required": [ + "if" + ], + "additionalProperties": false + }, + "rule-v1": { + "type": "object", + "title": "Rule", + "description": "A rule resource.", + "markdownDescription": "A rule resource. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Rules/)", + "properties": { + "apiVersion": { + "type": "string", + "title": "API Version", + "description": "The API Version for the PSRule resources.", + "enum": [ + "github.com/microsoft/PSRule/v1" + ] + }, + "kind": { + "type": "string", + "title": "Kind", + "description": "A rule resource.", + "markdownDescription": "A rule resource. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Rules/)", + "enum": [ + "Rule" + ] + }, + "metadata": { + "type": "object", + "$ref": "#/definitions/ruleMetadata" + }, + "spec": { + "type": "object", + "$ref": "#/definitions/ruleSpec" + } + }, + "required": [ + "apiVersion", + "kind", + "metadata", + "spec" + ] + }, + "ruleSpec": { + "type": "object", + "title": "Spec", + "description": "The specification for the rule.", + "markdownDescription": "The specification for the rule. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Rules/)", + "properties": { + "condition": { + "type": "object", + "title": "Condition", + "description": "A condition is made up of one or more expressions that will determine if the rule passes or fails.", + "markdownDescription": "A condition is made up of one or more [expressions](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/) that will determine if the rule passes or fails.\n\n[See help](https://microsoft.github.io/PSRule/v2/authoring/writing-rules/)", + "oneOf": [ + { + "$ref": "#/definitions/expressions" } + ] }, - "ruleMetadata": { - "type": "object", - "title": "Metadata", - "description": "Additional information to identify the resource.", - "properties": { - "name": { - "type": "string", - "title": "Name", - "description": "The name of the resource. This must be unique.", - "$ref": "#/definitions/resourceName" - }, - "ref": { - "title": "Reference", - "description": "An optional stable opaque identifier of this resource for lookup. This must be unique if set.", - "$ref": "#/definitions/resourceName" - }, - "annotations": { - "type": "object", - "title": "Annotations", - "description": "Additional annotations for the resource.", - "additionalProperties": true, - "defaultSnippets": [ - { - "label": "Annotation key/ value", - "body": { - "${1:Key}": "${2:Value}" - } - } - ] - }, - "alias": { - "type": "array", - "title": "Aliases", - "description": "Alternative names this resource is known by.", - "items": { - "type": "string", - "title": "Alias", - "description": "An alternative name.", - "$ref": "#/definitions/resourceName" - }, - "uniqueItems": true - }, - "tags": { - "$ref": "#/definitions/resourceTags" - }, - "labels": { - "type": "object", - "title": "Labels", - "description": "Any taxonomy references.", - "additionalProperties": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ] - }, - "defaultSnippets": [ - { - "label": "Reference key/ value", - "body": { - "${1:Key}": "${2:Value}" - } - }, - { - "label": "Reference key/ multi-value", - "body": { - "${1:Key}": [ - "${2:Value}" - ] - } - } - ] - } - }, - "required": [ - "name" - ] + "level": { + "type": "string", + "title": "Level", + "description": "If the rule fails, how serious is the result. By default this is set to Error.", + "markdownDescription": "If the rule fails, how serious is the result. By default this is set to `Error`. [See help](https://microsoft.github.io/PSRule/v2/authoring/writing-rules/)", + "enum": [ + "Error", + "Warning", + "Information" + ], + "default": "Error" + }, + "type": { + "type": "array", + "title": "Type pre-condition", + "description": "This rule only applies to objects that match the specifies types.", + "items": { + "type": "string", + "default": "" + }, + "uniqueItems": true + }, + "with": { + "type": "array", + "title": "Selector pre-condition", + "description": "This rule only applies to objects that match the specified selectors.", + "items": { + "type": "string", + "default": "" + }, + "uniqueItems": true }, - "selectorExpressionValueMultiString": { + "where": { + "type": "object", + "title": "Sub-selector pre-condition", + "description": "The rule only applies to objects that match the sub-selector condition.", + "markdownDescription": "The rule only applies to objects that match the sub-selector condition.\n\n[See help](https://microsoft.github.io/PSRule/v2/expressions/sub-selectors/)", + "oneOf": [ + { + "$ref": "#/definitions/expressions" + } + ] + } + }, + "required": [ + "condition" + ], + "additionalProperties": false, + "default": { + "condition": {} + } + }, + "ruleMetadata": { + "type": "object", + "title": "Metadata", + "description": "Additional information to identify the resource.", + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "The name of the resource. This must be unique.", + "$ref": "#/definitions/resourceName" + }, + "ref": { + "title": "Reference", + "description": "An optional stable opaque identifier of this resource for lookup. This must be unique if set.", + "$ref": "#/definitions/resourceName" + }, + "annotations": { + "type": "object", + "title": "Annotations", + "description": "Additional annotations for the resource.", + "additionalProperties": true, + "defaultSnippets": [ + { + "label": "Annotation key/ value", + "body": { + "${1:Key}": "${2:Value}" + } + } + ] + }, + "alias": { + "type": "array", + "title": "Aliases", + "description": "Alternative names this resource is known by.", + "items": { + "type": "string", + "title": "Alias", + "description": "An alternative name.", + "$ref": "#/definitions/resourceName" + }, + "uniqueItems": true + }, + "tags": { + "$ref": "#/definitions/resourceTags" + }, + "labels": { + "type": "object", + "title": "Labels", + "description": "Any taxonomy references.", + "additionalProperties": { "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" } + } ] + }, + "defaultSnippets": [ + { + "label": "Reference key/ value", + "body": { + "${1:Key}": "${2:Value}" + } + }, + { + "label": "Reference key/ multi-value", + "body": { + "${1:Key}": [ + "${2:Value}" + ] + } + } + ] + } + }, + "required": [ + "name" + ] + }, + "selectorExpressionValueMultiString": { + "oneOf": [ + { + "type": "string" }, - "selectorExpressionValue": { - "oneOf": [ - { - "type": "string", - "ztitle": "Value from string", - "zdescription": "A value to compare.", - "default": "" - }, - { - "type": "boolean", - "ztitle": "Value from boolean", - "zdescription": "A value to compare.", - "default": true - }, - { - "type": "integer", - "ztitle": "Value from integer", - "zdescription": "A value to compare.", - "default": 0 - }, - { - "type": "object", - "ztitle": "Value for object", - "zdescription": "A value to compare.", - "not": { - "propertyNames": { - "enum": [ - "$" - ] - } - } - }, - { - "$ref": "#/definitions/fn" - } + { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + ] + }, + "selectorExpressionValue": { + "oneOf": [ + { + "type": "string", + "ztitle": "Value from string", + "zdescription": "A value to compare.", + "default": "" + }, + { + "type": "boolean", + "ztitle": "Value from boolean", + "zdescription": "A value to compare.", + "default": true + }, + { + "type": "integer", + "ztitle": "Value from integer", + "zdescription": "A value to compare.", + "default": 0 + }, + { + "type": "object", + "ztitle": "Value for object", + "zdescription": "A value to compare.", + "not": { + "propertyNames": { + "enum": [ + "$" + ] + } + } + }, + { + "$ref": "#/definitions/fn" + } + ] + }, + "expressions": { + "type": "object", + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operators/definitions/allOf" + }, + { + "$ref": "#/definitions/expressions/definitions/operators/definitions/anyOf" + }, + { + "$ref": "#/definitions/expressions/definitions/operators/definitions/not" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/exists" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/equals" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notEquals" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/hasValue" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/match" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notMatch" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/in" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notIn" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/setOf" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/subset" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/count" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notCount" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/less" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/lessOrEquals" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/greater" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/greaterOrEquals" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/startsWith" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notStartsWith" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/endsWith" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notEndsWith" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/contains" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notContains" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isString" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isLower" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isArray" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isBoolean" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isDateTime" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isInteger" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isNumeric" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isUpper" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/hasSchema" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/version" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/hasDefault" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/withinPath" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notWithinPath" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/like" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notLike" + } + ], + "defaultSnippets": [ + { + "label": "field", + "description": "The object path of a field to compare.", + "markdownDescription": "The object path of a field to compare.", + "body": { + "field": "${1}" + } + }, + { + "label": "value", + "body": { + "value": "${1}" + } + }, + { + "label": "name", + "body": { + "name": "." + } + }, + { + "label": "type", + "body": { + "type": "." + } + }, + { + "label": "source", + "body": { + "source": "${1}" + } + }, + { + "label": "allOf", + "body": { + "allOf": [ + "${1}" ] + } }, - "expressions": { - "type": "object", - "oneOf": [ + { + "label": "anyOf", + "body": { + "anyOf": [ + "${1}" + ] + } + }, + { + "label": "not", + "body": { + "not": {} + } + } + ], + "definitions": { + "operands": { + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands/definitions/field" + }, + { + "$ref": "#/definitions/expressions/definitions/operands/definitions/value" + }, + { + "$ref": "#/definitions/expressions/definitions/operands/definitions/type" + }, + { + "$ref": "#/definitions/expressions/definitions/operands/definitions/name" + }, + { + "$ref": "#/definitions/expressions/definitions/operands/definitions/source" + } + ], + "definitions": { + "string-only": { + "oneOf": [ { - "$ref": "#/definitions/expressions/definitions/operators/definitions/allOf" + "$ref": "#/definitions/expressions/definitions/operands/definitions/field" }, { - "$ref": "#/definitions/expressions/definitions/operators/definitions/anyOf" + "$ref": "#/definitions/expressions/definitions/operands/definitions/value" }, { - "$ref": "#/definitions/expressions/definitions/operators/definitions/not" + "$ref": "#/definitions/expressions/definitions/operands/definitions/type" }, { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/exists" + "$ref": "#/definitions/expressions/definitions/operands/definitions/name" }, { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/equals" + "$ref": "#/definitions/expressions/definitions/operands/definitions/source" + } + ] + }, + "field": { + "type": "object", + "properties": { + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" }, + "where": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/where" + } + }, + "required": [ + "field" + ] + }, + "value": { + "type": "object", + "properties": { + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "value" + ] + }, + "type": { + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + } + }, + "required": [ + "type" + ] + }, + "name": { + "type": "object", + "properties": { + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + } + }, + "required": [ + "name" + ] + }, + "source": { + "type": "object", + "properties": { + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "source" + ] + } + } + }, + "properties": { + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands/definitions/field" + }, + { + "$ref": "#/definitions/expressions/definitions/operands/definitions/value" + }, + { + "$ref": "#/definitions/expressions/definitions/operands/definitions/name" + }, + { + "$ref": "#/definitions/expressions/definitions/operands/definitions/type" + }, + { + "$ref": "#/definitions/expressions/definitions/operands/definitions/source" + } + ], + "definitions": { + "field": { + "type": "string", + "title": "Field", + "description": "The object path of a field to compare.", + "markdownDescription": "The object path of a field to compare.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#field)", + "minLength": 1 + }, + "where": { + "type": "object", + "title": "Sub-selector filter", + "description": "Limits the condition to matching items.", + "markdownDescription": "Limits the condition to matching items.\n\n[See help](https://microsoft.github.io/PSRule/v2/expressions/sub-selectors/)", + "allOf": [ { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/notEquals" - }, + "$ref": "#/definitions/expressions" + } + ] + }, + "value": { + "allOf": [ { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/hasValue" + "$ref": "#/definitions/selectorExpressionValue" + } + ] + }, + "name": { + "type": "string", + "title": "Name", + "description": "The target name of the object.", + "markdownDescription": "The target name of the object. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#name)", + "default": ".", + "enum": [ + "." + ] + }, + "type": { + "type": "string", + "title": "Type", + "description": "The target type of the object.", + "markdownDescription": "The target type of the object. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#type)", + "default": ".", + "enum": [ + "." + ] + }, + "source": { + "type": "string", + "title": "Source", + "description": "The source of the object currently being processed by the pipeline.", + "markdownDescription": "The source of the object currently being processed by the pipeline. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#source)", + "default": "" + } + } + }, + "conditions": { + "type": "object", + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/exists" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/equals" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notEquals" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/hasValue" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/match" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notMatch" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/in" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notIn" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/setOf" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/subset" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/count" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notCount" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/less" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/lessOrEquals" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/greater" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/greaterOrEquals" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/startsWith" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notStartsWith" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/endsWith" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notEndsWith" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/contains" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notContains" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isString" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isLower" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isArray" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isBoolean" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isDateTime" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isInteger" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isNumeric" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/isUpper" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/hasSchema" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/version" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/hasDefault" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/withinPath" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notWithinPath" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/like" + }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/notLike" + } + ], + "definitions": { + "equals": { + "type": "object", + "title": "equals", + "description": "Must have the specified value.", + "markdownDescription": "Must have the specified value.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#equals)", + "properties": { + "equals": { + "title": "Equals", + "description": "Must have the specified value.", + "markdownDescription": "Must have the specified value.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#equals)", + "default": "", + "oneOf": [ + { + "$ref": "#/definitions/selectorExpressionValue" + } + ] + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type of compared operand.", + "markdownDescription": "Convert type of compared operand.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#equals)", + "default": false + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive. Only applies to string values.", + "markdownDescription": "Determines if comparing values is case-sensitive. Only applies to string values.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#equals)", + "default": false }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/match" + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/notMatch" + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/in" + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/notIn" + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "equals" + ], + "oneOf": [ { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/setOf" - }, + "$ref": "#/definitions/expressions/definitions/operands" + } + ] + }, + "count": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "title": "Count", + "description": "Must include the specified number of values.", + "markdownDescription": "Must include the specified number of values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#count)", + "minimum": 0 + } + }, + "required": [ + "count" + ], + "oneOf": [ { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/subset" + "$ref": "#/definitions/expressions/definitions/operands" + } + ] + }, + "exists": { + "type": "object", + "properties": { + "exists": { + "type": "boolean", + "title": "Exists", + "description": "Must have the named field.", + "markdownDescription": "Must have the named field. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#exists)", + "default": true }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/count" + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + } + }, + "required": [ + "exists", + "field" + ] + }, + "notEquals": { + "type": "object", + "properties": { + "notEquals": { + "title": "Not Equals", + "description": "Must not have the specified value.", + "markdownDescription": "Must not have the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notequals)", + "default": "", + "oneOf": [ + { + "$ref": "#/definitions/selectorExpressionValue" + } + ] + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type of compared operand.", + "markdownDescription": "Convert type of compared operand. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notequals)", + "default": false + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive. Only applies to string values.", + "markdownDescription": "Determines if comparing values is case-sensitive. Only applies to string values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notequals)", + "default": false }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/notCount" + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/less" + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/lessOrEquals" + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/greater" + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "notEquals" + ], + "oneOf": [ { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/greaterOrEquals" + "$ref": "#/definitions/expressions/definitions/operands" + } + ] + }, + "hasValue": { + "type": "object", + "properties": { + "hasValue": { + "type": "boolean", + "title": "Has Value", + "description": "Must have a non-empty value.", + "markdownDescription": "Must have a non-empty value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasvalue)", + "default": true }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + } + }, + "required": [ + "hasValue" + ], + "oneOf": [ { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/startsWith" + "$ref": "#/definitions/expressions/definitions/operands" + } + ] + }, + "match": { + "type": "object", + "properties": { + "match": { + "type": "string", + "title": "Match", + "description": "Must match the regular expression.", + "markdownDescription": "Must match the regular expression. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#match)", + "default": "" }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/notStartsWith" + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/endsWith" + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/notEndsWith" + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/contains" + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "match" + ], + "oneOf": [ { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/notContains" + "$ref": "#/definitions/expressions/definitions/operands" + } + ] + }, + "notMatch": { + "type": "object", + "properties": { + "notMatch": { + "type": "string", + "title": "Not Match", + "description": "Must not match the regular expression.", + "markdownDescription": "Must not match the regular expression. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notmatch)", + "default": "" }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/isString" + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/isLower" + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/isArray" + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/isBoolean" + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "notMatch" + ], + "oneOf": [ { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/isDateTime" + "$ref": "#/definitions/expressions/definitions/operands" + } + ] + }, + "in": { + "type": "object", + "properties": { + "in": { + "type": "array", + "title": "In", + "description": "Must equal one of the specified values.", + "markdownDescription": "Must equal one of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#in)", + "default": [ + "" + ], + "uniqueItems": true }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/isInteger" + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/isNumeric" + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/isUpper" + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/hasSchema" + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "in" + ], + "oneOf": [ { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/version" + "$ref": "#/definitions/expressions/definitions/operands" + } + ] + }, + "notIn": { + "type": "object", + "properties": { + "notIn": { + "type": "array", + "title": "Not In", + "description": "Must not equal any of the specified values.", + "markdownDescription": "Must not equal any of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notin)", + "default": [ + "" + ], + "uniqueItems": true }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/hasDefault" + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/withinPath" + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/notWithinPath" + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/like" + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/notLike" + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" } - ], - "defaultSnippets": [ + }, + "required": [ + "notIn" + ], + "oneOf": [ { - "label": "field", - "description": "The object path of a field to compare.", - "markdownDescription": "The object path of a field to compare.", - "body": { - "field": "${1}" - } + "$ref": "#/definitions/expressions/definitions/operands" + } + ] + }, + "setOf": { + "type": "object", + "properties": { + "setOf": { + "type": "array", + "title": "SetOf", + "description": "Must include all of but only specified values.", + "markdownDescription": "Must include all of but only values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#setof)", + "default": [ + "" + ], + "uniqueItems": true + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive.", + "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#setof)", + "default": false }, - { - "label": "value", - "body": { - "value": "${1}" - } + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" }, - { - "label": "name", - "body": { - "name": "." - } + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" }, - { - "label": "type", - "body": { - "type": "." - } + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "setOf" + ], + "oneOf": [ { - "label": "source", - "body": { - "source": "${1}" - } + "$ref": "#/definitions/expressions/definitions/properties" + } + ] + }, + "subset": { + "type": "object", + "properties": { + "subset": { + "type": "array", + "title": "Subset", + "description": "Must include all of the specified values.", + "markdownDescription": "Must include all of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#subset)", + "default": [ + "" + ], + "uniqueItems": true + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive.", + "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#subset)", + "default": false + }, + "unique": { + "type": "boolean", + "title": "Unique", + "description": "Determines if each of the field values must be unique.", + "markdownDescription": "Determines if each of the field values must be unique. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#subset)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "subset" + ], + "oneOf": [ { - "label": "allOf", - "body": { - "allOf": [ - "${1}" - ] - } + "$ref": "#/definitions/expressions/definitions/properties" + } + ] + }, + "notCount": { + "type": "object", + "properties": { + "notCount": { + "type": "integer", + "title": "NotCount", + "description": "Determines if operand does not have number of items.", + "markdownDescription": "Determines if operand does not have number of items. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notcount)", + "minimum": 0, + "default": 0 }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "notCount" + ], + "oneOf": [ { - "label": "anyOf", - "body": { - "anyOf": [ - "${1}" - ] + "$ref": "#/definitions/expressions/definitions/properties" + } + ], + "additionalProperties": false + }, + "less": { + "type": "object", + "properties": { + "less": { + "title": "Less", + "description": "Must be less then the specified value.", + "markdownDescription": "Must be less then the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", + "default": 0, + "oneOf": [ + { + "type": "integer" + }, + { + "type": "object", + "$ref": "#/definitions/fn" } + ] + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type of compared operand.", + "markdownDescription": "Convert type of compared operand. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "less" + ], + "oneOf": [ { - "label": "not", - "body": { - "not": {} - } + "$ref": "#/definitions/expressions/definitions/operands" } - ], - "definitions": { - "operands": { - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands/definitions/field" - }, - { - "$ref": "#/definitions/expressions/definitions/operands/definitions/value" - }, - { - "$ref": "#/definitions/expressions/definitions/operands/definitions/type" - }, - { - "$ref": "#/definitions/expressions/definitions/operands/definitions/name" - }, - { - "$ref": "#/definitions/expressions/definitions/operands/definitions/source" - } - ], - "definitions": { - "string-only": { - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands/definitions/field" - }, - { - "$ref": "#/definitions/expressions/definitions/operands/definitions/value" - }, - { - "$ref": "#/definitions/expressions/definitions/operands/definitions/type" - }, - { - "$ref": "#/definitions/expressions/definitions/operands/definitions/name" - }, - { - "$ref": "#/definitions/expressions/definitions/operands/definitions/source" - } - ] - }, - "field": { - "type": "object", - "properties": { - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "where": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/where" - } - }, - "required": [ - "field" - ] - }, - "value": { - "type": "object", - "properties": { - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - } - }, - "required": [ - "value" - ] - }, - "type": { - "type": "object", - "properties": { - "type": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/type" - } - }, - "required": [ - "type" - ] - }, - "name": { - "type": "object", - "properties": { - "name": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/name" - } - }, - "required": [ - "name" - ] - }, - "source": { - "type": "object", - "properties": { - "source": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/source" - } - }, - "required": [ - "source" - ] - } + ], + "additionalProperties": false + }, + "lessOrEquals": { + "type": "object", + "properties": { + "lessOrEquals": { + "title": "Less or Equal to", + "description": "Must be less or equal to the specified value.", + "markdownDescription": "Must be less or equal to the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#lessorequals)", + "default": 0, + "oneOf": [ + { + "type": "integer" + }, + { + "type": "object", + "$ref": "#/definitions/fn" } + ] }, - "properties": { - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands/definitions/field" - }, - { - "$ref": "#/definitions/expressions/definitions/operands/definitions/value" - }, - { - "$ref": "#/definitions/expressions/definitions/operands/definitions/name" - }, - { - "$ref": "#/definitions/expressions/definitions/operands/definitions/type" - }, - { - "$ref": "#/definitions/expressions/definitions/operands/definitions/source" - } - ], - "definitions": { - "field": { - "type": "string", - "title": "Field", - "description": "The object path of a field to compare.", - "markdownDescription": "The object path of a field to compare.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#field)", - "minLength": 1 - }, - "where": { - "type": "object", - "title": "Sub-selector filter", - "description": "Limits the condition to matching items.", - "markdownDescription": "Limits the condition to matching items.\n\n[See help](https://microsoft.github.io/PSRule/v2/expressions/sub-selectors/)", - "allOf": [ - { - "$ref": "#/definitions/expressions" - } - ] - }, - "value": { - "allOf": [ - { - "$ref": "#/definitions/selectorExpressionValue" - } - ] - }, - "name": { - "type": "string", - "title": "Name", - "description": "The target name of the object.", - "markdownDescription": "The target name of the object. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#name)", - "default": ".", - "enum": [ - "." - ] - }, - "type": { - "type": "string", - "title": "Type", - "description": "The target type of the object.", - "markdownDescription": "The target type of the object. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#type)", - "default": ".", - "enum": [ - "." - ] - }, - "source": { - "type": "string", - "title": "Source", - "description": "The source of the object currently being processed by the pipeline.", - "markdownDescription": "The source of the object currently being processed by the pipeline. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#source)", - "default": "" - } + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type of compared operand.", + "markdownDescription": "Convert type of compared operand. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "lessOrEquals" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "greater": { + "type": "object", + "properties": { + "greater": { + "title": "Greater", + "description": "Must be greater then the specified value.", + "markdownDescription": "Must be greater then the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#greater)", + "default": 0, + "oneOf": [ + { + "type": "integer" + }, + { + "type": "object", + "$ref": "#/definitions/fn" } + ] }, - "conditions": { - "type": "object", - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/exists" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/equals" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/notEquals" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/hasValue" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/match" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/notMatch" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/in" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/notIn" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/setOf" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/subset" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/count" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/notCount" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/less" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/lessOrEquals" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/greater" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/greaterOrEquals" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/startsWith" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/notStartsWith" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/endsWith" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/notEndsWith" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/contains" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/notContains" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/isString" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/isLower" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/isArray" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/isBoolean" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/isDateTime" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/isInteger" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/isNumeric" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/isUpper" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/hasSchema" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/version" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/hasDefault" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/withinPath" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/notWithinPath" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/like" - }, - { - "$ref": "#/definitions/expressions/definitions/conditions/definitions/notLike" - } - ], - "definitions": { - "equals": { - "type": "object", - "title": "equals", - "description": "Must have the specified value.", - "markdownDescription": "Must have the specified value.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#equals)", - "properties": { - "equals": { - "title": "Equals", - "description": "Must have the specified value.", - "markdownDescription": "Must have the specified value.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#equals)", - "default": "", - "oneOf": [ - { - "$ref": "#/definitions/selectorExpressionValue" - } - ] - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type of compared operand.", - "markdownDescription": "Convert type of compared operand.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#equals)", - "default": false - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive. Only applies to string values.", - "markdownDescription": "Determines if comparing values is case-sensitive. Only applies to string values.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#equals)", - "default": false - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - }, - "type": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/type" - }, - "name": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/name" - }, - "source": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/source" - } - }, - "required": [ - "equals" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ] - }, - "count": { - "type": "object", - "properties": { - "count": { - "type": "integer", - "title": "Count", - "description": "Must include the specified number of values.", - "markdownDescription": "Must include the specified number of values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#count)", - "minimum": 0 - } - }, - "required": [ - "count" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ] - }, - "exists": { - "type": "object", - "properties": { - "exists": { - "type": "boolean", - "title": "Exists", - "description": "Must have the named field.", - "markdownDescription": "Must have the named field. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#exists)", - "default": true - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - } - }, - "required": [ - "exists", - "field" - ] - }, - "notEquals": { - "type": "object", - "properties": { - "notEquals": { - "title": "Not Equals", - "description": "Must not have the specified value.", - "markdownDescription": "Must not have the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notequals)", - "default": "", - "oneOf": [ - { - "$ref": "#/definitions/selectorExpressionValue" - } - ] - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type of compared operand.", - "markdownDescription": "Convert type of compared operand. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notequals)", - "default": false - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive. Only applies to string values.", - "markdownDescription": "Determines if comparing values is case-sensitive. Only applies to string values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notequals)", - "default": false - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - }, - "type": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/type" - }, - "name": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/name" - }, - "source": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/source" - } - }, - "required": [ - "notEquals" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ] - }, - "hasValue": { - "type": "object", - "properties": { - "hasValue": { - "type": "boolean", - "title": "Has Value", - "description": "Must have a non-empty value.", - "markdownDescription": "Must have a non-empty value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasvalue)", - "default": true - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - } - }, - "required": [ - "hasValue" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ] - }, - "match": { - "type": "object", - "properties": { - "match": { - "type": "string", - "title": "Match", - "description": "Must match the regular expression.", - "markdownDescription": "Must match the regular expression. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#match)", - "default": "" - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - }, - "type": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/type" - }, - "name": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/name" - }, - "source": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/source" - } - }, - "required": [ - "match" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ] - }, - "notMatch": { - "type": "object", - "properties": { - "notMatch": { - "type": "string", - "title": "Not Match", - "description": "Must not match the regular expression.", - "markdownDescription": "Must not match the regular expression. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notmatch)", - "default": "" - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - }, - "type": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/type" - }, - "name": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/name" - }, - "source": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/source" - } - }, - "required": [ - "notMatch" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ] - }, - "in": { - "type": "object", - "properties": { - "in": { - "type": "array", - "title": "In", - "description": "Must equal one of the specified values.", - "markdownDescription": "Must equal one of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#in)", - "default": [ - "" - ], - "uniqueItems": true - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - }, - "type": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/type" - }, - "name": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/name" - }, - "source": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/source" - } - }, - "required": [ - "in" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ] - }, - "notIn": { - "type": "object", - "properties": { - "notIn": { - "type": "array", - "title": "Not In", - "description": "Must not equal any of the specified values.", - "markdownDescription": "Must not equal any of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notin)", - "default": [ - "" - ], - "uniqueItems": true - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - }, - "type": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/type" - }, - "name": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/name" - }, - "source": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/source" - } - }, - "required": [ - "notIn" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ] - }, - "setOf": { - "type": "object", - "properties": { - "setOf": { - "type": "array", - "title": "SetOf", - "description": "Must include all of but only specified values.", - "markdownDescription": "Must include all of but only values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#setof)", - "default": [ - "" - ], - "uniqueItems": true - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#setof)", - "default": false - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - }, - "type": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/type" - }, - "name": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/name" - }, - "source": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/source" - } - }, - "required": [ - "setOf" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/properties" - } - ] - }, - "subset": { - "type": "object", - "properties": { - "subset": { - "type": "array", - "title": "Subset", - "description": "Must include all of the specified values.", - "markdownDescription": "Must include all of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#subset)", - "default": [ - "" - ], - "uniqueItems": true - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#subset)", - "default": false - }, - "unique": { - "type": "boolean", - "title": "Unique", - "description": "Determines if each of the field values must be unique.", - "markdownDescription": "Determines if each of the field values must be unique. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#subset)", - "default": false - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - }, - "type": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/type" - }, - "name": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/name" - }, - "source": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/source" - } - }, - "required": [ - "subset" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/properties" - } - ] - }, - "notCount": { - "type": "object", - "properties": { - "notCount": { - "type": "integer", - "title": "NotCount", - "description": "Determines if operand does not have number of items.", - "markdownDescription": "Determines if operand does not have number of items. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notcount)", - "minimum": 0, - "default": 0 - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - } - }, - "required": [ - "notCount" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/properties" - } - ], - "additionalProperties": false - }, - "less": { - "type": "object", - "properties": { - "less": { - "title": "Less", - "description": "Must be less then the specified value.", - "markdownDescription": "Must be less then the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", - "default": 0, - "oneOf": [ - { - "type": "integer" - }, - { - "type": "object", - "$ref": "#/definitions/fn" - } - ] - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type of compared operand.", - "markdownDescription": "Convert type of compared operand. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", - "default": false - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - }, - "type": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/type" - }, - "name": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/name" - }, - "source": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/source" - } - }, - "required": [ - "less" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ], - "additionalProperties": false - }, - "lessOrEquals": { - "type": "object", - "properties": { - "lessOrEquals": { - "title": "Less or Equal to", - "description": "Must be less or equal to the specified value.", - "markdownDescription": "Must be less or equal to the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#lessorequals)", - "default": 0, - "oneOf": [ - { - "type": "integer" - }, - { - "type": "object", - "$ref": "#/definitions/fn" - } - ] - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type of compared operand.", - "markdownDescription": "Convert type of compared operand. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", - "default": false - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - }, - "type": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/type" - }, - "name": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/name" - }, - "source": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/source" - } - }, - "required": [ - "lessOrEquals" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ], - "additionalProperties": false - }, - "greater": { - "type": "object", - "properties": { - "greater": { - "title": "Greater", - "description": "Must be greater then the specified value.", - "markdownDescription": "Must be greater then the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#greater)", - "default": 0, - "oneOf": [ - { - "type": "integer" - }, - { - "type": "object", - "$ref": "#/definitions/fn" - } - ] - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type of compared operand.", - "markdownDescription": "Convert type of compared operand. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", - "default": false - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - }, - "type": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/type" - }, - "name": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/name" - }, - "source": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/source" - } - }, - "required": [ - "greater" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ], - "additionalProperties": false - }, - "greaterOrEquals": { - "type": "object", - "properties": { - "greaterOrEquals": { - "title": "Greater or Equal to", - "description": "Must be greater or equal to the specified value.", - "markdownDescription": "Must be greater or equal to the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#greaterorequals)", - "default": 0, - "oneOf": [ - { - "type": "integer" - }, - { - "type": "object", - "$ref": "#/definitions/fn" - } - ] - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type of compared operand.", - "markdownDescription": "Convert type of compared operand. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", - "default": false - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - }, - "type": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/type" - }, - "name": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/name" - }, - "source": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/source" - } - }, - "required": [ - "greaterOrEquals" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ], - "additionalProperties": false - }, - "startsWith": { - "type": "object", - "properties": { - "startsWith": { - "title": "Starts with", - "description": "Must start with one of the specified values.", - "markdownDescription": "Must start with one of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#startswith)", - "$ref": "#/definitions/selectorExpressionValueMultiString", - "default": "" - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type to string.", - "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#startswith)", - "default": false - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#startswith)", - "default": false - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - }, - "type": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/type" - }, - "name": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/name" - }, - "source": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/source" - } - }, - "required": [ - "startsWith" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ], - "additionalProperties": false - }, - "notStartsWith": { - "type": "object", - "properties": { - "notStartsWith": { - "title": "Not starts with", - "description": "Must not start with any of the specified values.", - "markdownDescription": "Must not start with any of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notstartswith)", - "$ref": "#/definitions/selectorExpressionValueMultiString" - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type to string.", - "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notstartswith)", - "default": false - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notstartswith)", - "default": false - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - }, - "type": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/type" - }, - "name": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/name" - }, - "source": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/source" - } - }, - "required": [ - "notStartsWith" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ], - "additionalProperties": false - }, - "endsWith": { - "type": "object", - "properties": { - "endsWith": { - "title": "Ends with", - "description": "Must end with one of the specified values.", - "markdownDescription": "Must end with one of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#endswith)", - "$ref": "#/definitions/selectorExpressionValueMultiString", - "default": "" - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type to string.", - "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#endswith)", - "default": false - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#endswith)", - "default": false - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - }, - "type": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/type" - }, - "name": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/name" - }, - "source": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/source" - } - }, - "required": [ - "endsWith" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ], - "additionalProperties": false - }, - "notEndsWith": { - "type": "object", - "properties": { - "notEndsWith": { - "title": "Not Ends with", - "description": "Must not end with any of the specified values.", - "markdownDescription": "Must not end with any of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notendswith)", - "$ref": "#/definitions/selectorExpressionValueMultiString" - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type to string.", - "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notendswith)", - "default": false - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notendswith)", - "default": false - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - }, - "type": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/type" - }, - "name": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/name" - }, - "source": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/source" - } - }, - "required": [ - "notEndsWith" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ], - "additionalProperties": false - }, - "contains": { - "type": "object", - "title": "contains", - "description": "Must contain one of the specified values.", - "markdownDescription": "Must contain one of the specified values.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#contains)", - "properties": { - "contains": { - "title": "Contains", - "description": "Must contain one of the specified values.", - "markdownDescription": "Must contain one of the specified values.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#contains)", - "default": "", - "allOf": [ - { - "$ref": "#/definitions/selectorExpressionValueMultiString" - } - ] - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type to string.", - "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#contains)", - "default": false - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#contains)", - "default": false - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - }, - "type": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/type" - }, - "name": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/name" - }, - "source": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/source" - } - }, - "required": [ - "contains" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ], - "additionalProperties": false, - "minProperties": 2 - }, - "notContains": { - "type": "object", - "properties": { - "notContains": { - "title": "Not Contains", - "description": "Must not contain any of the specified values.", - "markdownDescription": "Must not contain any of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notcontains)", - "$ref": "#/definitions/selectorExpressionValueMultiString" - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type to string.", - "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notcontains)", - "default": false - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notcontains)", - "default": false - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - }, - "type": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/type" - }, - "name": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/name" - }, - "source": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/source" - } - }, - "required": [ - "notContains" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ], - "additionalProperties": false - }, - "isString": { - "type": "object", - "properties": { - "isString": { - "type": "boolean", - "title": "Is string", - "description": "Must be a string type.", - "markdownDescription": "Must be a string type. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isstring)", - "default": true - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - } - }, - "required": [ - "isString" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ], - "additionalProperties": false - }, - "isArray": { - "type": "object", - "properties": { - "isArray": { - "type": "boolean", - "title": "Is array", - "description": "Must be an array type.", - "markdownDescription": "Must be an array type. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isarray)", - "default": true - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - } - }, - "required": [ - "isArray" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ], - "additionalProperties": false - }, - "isBoolean": { - "type": "object", - "properties": { - "isBoolean": { - "type": "boolean", - "title": "Is boolean", - "description": "Must be a boolean type.", - "markdownDescription": "Must be a boolean type. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isboolean)", - "default": true - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type to boolean.", - "markdownDescription": "Convert type to boolean. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isboolean)", - "default": false - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - } - }, - "required": [ - "isBoolean" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ], - "additionalProperties": false - }, - "isDateTime": { - "type": "object", - "properties": { - "isDateTime": { - "type": "boolean", - "title": "Is datetime", - "description": "Must be a datetime type.", - "markdownDescription": "Must be a datetime type. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isdatetime)", - "default": true - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type to datetime.", - "markdownDescription": "Convert type to datetime. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isdatetime)", - "default": false - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - } - }, - "required": [ - "isDateTime" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ], - "additionalProperties": false - }, - "isInteger": { - "type": "object", - "properties": { - "isInteger": { - "type": "boolean", - "title": "Is integer", - "description": "Must be an integer type.", - "markdownDescription": "Must be an integer type. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isinteger)", - "default": true - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type to integer.", - "markdownDescription": "Convert type to integer. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isinteger)", - "default": false - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - } - }, - "required": [ - "isInteger" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ], - "additionalProperties": false - }, - "isNumeric": { - "type": "object", - "properties": { - "isNumeric": { - "type": "boolean", - "title": "Is numeric", - "description": "Must be a numeric type.", - "markdownDescription": "Must be a numeric type. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isnumeric)", - "default": true - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type to numeric.", - "markdownDescription": "Convert type to numeric. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isnumeric)", - "default": false - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - } - }, - "required": [ - "isNumeric" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ], - "additionalProperties": false, - "minProperties": 2 - }, - "isLower": { - "type": "object", - "properties": { - "isLower": { - "type": "boolean", - "title": "Is Lowercase", - "description": "Must be a lowercase string.", - "markdownDescription": "Must be a lowercase string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#islower)", - "default": true - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - } - }, - "required": [ - "isLower" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ], - "additionalProperties": false, - "minProperties": 2 - }, - "isUpper": { - "type": "object", - "properties": { - "isUpper": { - "type": "boolean", - "title": "Is Uppercase", - "description": "Must be an uppercase string.", - "markdownDescription": "Must be an uppercase string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isupper)", - "default": true - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - } - }, - "required": [ - "isUpper" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ], - "additionalProperties": false, - "minProperties": 2 - }, - "hasSchema": { - "type": "object", - "properties": { - "hasSchema": { - "type": "array", - "title": "Has schema", - "description": "Must use one of the specified schemas of the value of $schema. If an empty array is specified any non-empty $schema can be specified.", - "markdownDescription": "Must use one of the specified schemas of the value of `$schema`. If an empty array is specified any non-empty `$schema` can be specified [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasschema)", - "default": [], - "items": { - "type": "string", - "title": "Has schema", - "description": "Must use one of the specified schemas of the value of $schema. If an empty array is specified any non-empty $schema can be specified.", - "markdownDescription": "Must use one of the specified schemas of the value of `$schema`. If an empty array is specified any non-empty `$schema` can be specified [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasschema)", - "minLength": 1 - }, - "uniqueItems": true - }, - "ignoreScheme": { - "type": "boolean", - "title": "Ignore scheme", - "description": "Determines comparing values is case-sensitive.", - "markdownDescription": "Determines comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasschema)", - "default": false - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing schemas is case-sensitive.", - "markdownDescription": "Determines if comparing schemas is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasschema)", - "default": false - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - } - }, - "required": [ - "hasSchema" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/properties" - } - ], - "additionalProperties": false, - "minProperties": 2 - }, - "version": { - "type": "object", - "properties": { - "version": { - "type": "string", - "title": "Version", - "description": "Must be a valid semantic version. A constraint can optionally be provided to require the semantic version to be within a range.", - "markdownDescription": "Must be a valid semantic version. A constraint can optionally be provided to require the semantic version to be within a range. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#version)", - "default": "" - }, - "includePrerelease": { - "type": "boolean", - "title": "Include pre-release", - "description": "Determines if pre-release versions are included. By default, pre-release versions are not included.", - "markdownDescription": "Determines if pre-release versions are included. By default, pre-release versions are not included. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#version)", - "default": false - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - } - }, - "required": [ - "version" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/properties" - } - ], - "additionalProperties": false, - "minProperties": 2 - }, - "hasDefault": { - "type": "object", - "properties": { - "hasDefault": { - "title": "Has Default", - "description": "The field must either not exist or be set to the configured value.", - "markdownDescription": "The field must either not exist or be set to the configured value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasdefault)", - "default": "", - "oneOf": [ - { - "$ref": "#/definitions/selectorExpressionValue" - } - ] - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasdefault)", - "default": false - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - } - }, - "required": [ - "hasDefault" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/properties" - } - ], - "additionalProperties": false, - "minProperties": 2 - }, - "withinPath": { - "type": "object", - "properties": { - "withinPath": { - "type": "array", - "title": "Within Path", - "description": "The file path must exist within the required paths.", - "markdownDescription": "The file path must exist within the required paths. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#withinpath)", - "default": [], - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#withinpath)", - "default": false - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - } - }, - "required": [ - "withinPath" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ], - "additionalProperties": false, - "maxProperties": 2 - }, - "notWithinPath": { - "type": "object", - "properties": { - "notWithinPath": { - "type": "array", - "title": "Not Within Path", - "description": "The file path must not exist within the required paths.", - "markdownDescription": "The file path must not exist within the required paths. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notwithinpath)", - "default": [], - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notwithinpath)", - "default": false - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - } - }, - "required": [ - "notWithinPath" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ], - "additionalProperties": false, - "minProperties": 2 - }, - "like": { - "type": "object", - "properties": { - "like": { - "title": "Like", - "description": "Must match any of the specified wildcard patterns.", - "markdownDescription": "Must match any of the specified wildcard patterns. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#like)", - "$ref": "#/definitions/selectorExpressionValueMultiString" - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type to string.", - "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#like)", - "default": false - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#like)", - "default": false - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - } - }, - "required": [ - "like" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ], - "additionalProperties": false, - "minProperties": 2 - }, - "notLike": { - "type": "object", - "properties": { - "notLike": { - "title": "Not like", - "description": "Must not match any of the specified wildcard patterns.", - "markdownDescription": "Must not match any of the specified wildcard patterns. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notlike)", - "$ref": "#/definitions/selectorExpressionValueMultiString" - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type to string.", - "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notlike)", - "default": false - }, - "caseSensitive": { - "type": "boolean", - "title": "Case sensitive", - "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notlike)", - "default": false - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" - } - }, - "required": [ - "notLike" - ], - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operands" - } - ], - "additionalProperties": false, - "minProperties": 2 - } + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type of compared operand.", + "markdownDescription": "Convert type of compared operand. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "greater" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "greaterOrEquals": { + "type": "object", + "properties": { + "greaterOrEquals": { + "title": "Greater or Equal to", + "description": "Must be greater or equal to the specified value.", + "markdownDescription": "Must be greater or equal to the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#greaterorequals)", + "default": 0, + "oneOf": [ + { + "type": "integer" + }, + { + "type": "object", + "$ref": "#/definitions/fn" } + ] }, - "operators": { - "type": "object", - "oneOf": [ - { - "$ref": "#/definitions/expressions/definitions/operators/definitions/allOf" - }, - { - "$ref": "#/definitions/expressions/definitions/operators/definitions/anyOf" - }, - { - "$ref": "#/definitions/expressions/definitions/operators/definitions/not" - } - ], - "definitions": { - "allOf": { - "type": "object", - "title": "allOf", - "description": "All of the expressions must be true.", - "markdownDescription": "All of the expressions must be true.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#allof)", - "properties": { - "allOf": { - "type": "array", - "title": "AllOf", - "description": "All of the expressions must be true.", - "markdownDescription": "All of the expressions must be true.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#allof)", - "items": { - "$ref": "#/definitions/expressions" - } - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "where": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/where" - } - }, - "required": [ - "allOf" - ], - "oneOf": [ - { - "properties": { - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "where": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/where" - } - } - } - ], - "additionalProperties": false - }, - "anyOf": { - "type": "object", - "properties": { - "anyOf": { - "type": "array", - "title": "AnyOf", - "description": "One of the expressions must be true.", - "markdownDescription": "All of the expressions must be true.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#anyof)", - "items": { - "$ref": "#/definitions/expressions" - } - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "where": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/where" - } - }, - "required": [ - "anyOf" - ], - "oneOf": [ - { - "properties": { - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "where": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/where" - } - } - } - ], - "additionalProperties": false - }, - "not": { - "type": "object", - "properties": { - "not": { - "type": "object", - "title": "Not", - "description": "The nested expression must not be true.", - "markdownDescription": "The nested expression must not be true.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#not)", - "$ref": "#/definitions/expressions" - } - }, - "required": [ - "not" - ], - "additionalProperties": false - } + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type of compared operand.", + "markdownDescription": "Convert type of compared operand. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "greaterOrEquals" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "startsWith": { + "type": "object", + "properties": { + "startsWith": { + "title": "Starts with", + "description": "Must start with one of the specified values.", + "markdownDescription": "Must start with one of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#startswith)", + "$ref": "#/definitions/selectorExpressionValueMultiString", + "default": "" + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type to string.", + "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#startswith)", + "default": false + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive.", + "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#startswith)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "startsWith" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "notStartsWith": { + "type": "object", + "properties": { + "notStartsWith": { + "title": "Not starts with", + "description": "Must not start with any of the specified values.", + "markdownDescription": "Must not start with any of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notstartswith)", + "$ref": "#/definitions/selectorExpressionValueMultiString" + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type to string.", + "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notstartswith)", + "default": false + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive.", + "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notstartswith)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "notStartsWith" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "endsWith": { + "type": "object", + "properties": { + "endsWith": { + "title": "Ends with", + "description": "Must end with one of the specified values.", + "markdownDescription": "Must end with one of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#endswith)", + "$ref": "#/definitions/selectorExpressionValueMultiString", + "default": "" + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type to string.", + "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#endswith)", + "default": false + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive.", + "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#endswith)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "endsWith" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "notEndsWith": { + "type": "object", + "properties": { + "notEndsWith": { + "title": "Not Ends with", + "description": "Must not end with any of the specified values.", + "markdownDescription": "Must not end with any of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notendswith)", + "$ref": "#/definitions/selectorExpressionValueMultiString" + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type to string.", + "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notendswith)", + "default": false + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive.", + "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notendswith)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "notEndsWith" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "contains": { + "type": "object", + "title": "contains", + "description": "Must contain one of the specified values.", + "markdownDescription": "Must contain one of the specified values.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#contains)", + "properties": { + "contains": { + "title": "Contains", + "description": "Must contain one of the specified values.", + "markdownDescription": "Must contain one of the specified values.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#contains)", + "default": "", + "allOf": [ + { + "$ref": "#/definitions/selectorExpressionValueMultiString" } + ] + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type to string.", + "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#contains)", + "default": false + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive.", + "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#contains)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" } + }, + "required": [ + "contains" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false, + "minProperties": 2 + }, + "notContains": { + "type": "object", + "properties": { + "notContains": { + "title": "Not Contains", + "description": "Must not contain any of the specified values.", + "markdownDescription": "Must not contain any of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notcontains)", + "$ref": "#/definitions/selectorExpressionValueMultiString" + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type to string.", + "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notcontains)", + "default": false + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive.", + "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notcontains)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "notContains" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "isString": { + "type": "object", + "properties": { + "isString": { + "type": "boolean", + "title": "Is string", + "description": "Must be a string type.", + "markdownDescription": "Must be a string type. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isstring)", + "default": true + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "isString" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "isArray": { + "type": "object", + "properties": { + "isArray": { + "type": "boolean", + "title": "Is array", + "description": "Must be an array type.", + "markdownDescription": "Must be an array type. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isarray)", + "default": true + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "isArray" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "isBoolean": { + "type": "object", + "properties": { + "isBoolean": { + "type": "boolean", + "title": "Is boolean", + "description": "Must be a boolean type.", + "markdownDescription": "Must be a boolean type. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isboolean)", + "default": true + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type to boolean.", + "markdownDescription": "Convert type to boolean. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isboolean)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "isBoolean" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "isDateTime": { + "type": "object", + "properties": { + "isDateTime": { + "type": "boolean", + "title": "Is datetime", + "description": "Must be a datetime type.", + "markdownDescription": "Must be a datetime type. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isdatetime)", + "default": true + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type to datetime.", + "markdownDescription": "Convert type to datetime. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isdatetime)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "isDateTime" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "isInteger": { + "type": "object", + "properties": { + "isInteger": { + "type": "boolean", + "title": "Is integer", + "description": "Must be an integer type.", + "markdownDescription": "Must be an integer type. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isinteger)", + "default": true + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type to integer.", + "markdownDescription": "Convert type to integer. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isinteger)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "isInteger" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false + }, + "isNumeric": { + "type": "object", + "properties": { + "isNumeric": { + "type": "boolean", + "title": "Is numeric", + "description": "Must be a numeric type.", + "markdownDescription": "Must be a numeric type. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isnumeric)", + "default": true + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type to numeric.", + "markdownDescription": "Convert type to numeric. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isnumeric)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "isNumeric" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false, + "minProperties": 2 + }, + "isLower": { + "type": "object", + "properties": { + "isLower": { + "type": "boolean", + "title": "Is Lowercase", + "description": "Must be a lowercase string.", + "markdownDescription": "Must be a lowercase string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#islower)", + "default": true + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "isLower" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false, + "minProperties": 2 + }, + "isUpper": { + "type": "object", + "properties": { + "isUpper": { + "type": "boolean", + "title": "Is Uppercase", + "description": "Must be an uppercase string.", + "markdownDescription": "Must be an uppercase string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#isupper)", + "default": true + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "isUpper" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false, + "minProperties": 2 + }, + "hasSchema": { + "type": "object", + "properties": { + "hasSchema": { + "type": "array", + "title": "Has schema", + "description": "Must use one of the specified schemas of the value of $schema. If an empty array is specified any non-empty $schema can be specified.", + "markdownDescription": "Must use one of the specified schemas of the value of `$schema`. If an empty array is specified any non-empty `$schema` can be specified [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasschema)", + "default": [], + "items": { + "type": "string", + "title": "Has schema", + "description": "Must use one of the specified schemas of the value of $schema. If an empty array is specified any non-empty $schema can be specified.", + "markdownDescription": "Must use one of the specified schemas of the value of `$schema`. If an empty array is specified any non-empty `$schema` can be specified [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasschema)", + "minLength": 1 + }, + "uniqueItems": true + }, + "ignoreScheme": { + "type": "boolean", + "title": "Ignore scheme", + "description": "Determines comparing values is case-sensitive.", + "markdownDescription": "Determines comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasschema)", + "default": false + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing schemas is case-sensitive.", + "markdownDescription": "Determines if comparing schemas is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasschema)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "hasSchema" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/properties" + } + ], + "additionalProperties": false, + "minProperties": 2 + }, + "version": { + "type": "object", + "properties": { + "version": { + "type": "string", + "title": "Version", + "description": "Must be a valid semantic version. A constraint can optionally be provided to require the semantic version to be within a range.", + "markdownDescription": "Must be a valid semantic version. A constraint can optionally be provided to require the semantic version to be within a range. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#version)", + "default": "" + }, + "includePrerelease": { + "type": "boolean", + "title": "Include pre-release", + "description": "Determines if pre-release versions are included. By default, pre-release versions are not included.", + "markdownDescription": "Determines if pre-release versions are included. By default, pre-release versions are not included. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#version)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "version" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/properties" + } + ], + "additionalProperties": false, + "minProperties": 2 + }, + "hasDefault": { + "type": "object", + "properties": { + "hasDefault": { + "title": "Has Default", + "description": "The field must either not exist or be set to the configured value.", + "markdownDescription": "The field must either not exist or be set to the configured value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasdefault)", + "default": "", + "oneOf": [ + { + "$ref": "#/definitions/selectorExpressionValue" + } + ] + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive.", + "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasdefault)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "hasDefault" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/properties" + } + ], + "additionalProperties": false, + "minProperties": 2 + }, + "withinPath": { + "type": "object", + "properties": { + "withinPath": { + "type": "array", + "title": "Within Path", + "description": "The file path must exist within the required paths.", + "markdownDescription": "The file path must exist within the required paths. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#withinpath)", + "default": [], + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive.", + "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#withinpath)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "withinPath" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false, + "maxProperties": 2 + }, + "notWithinPath": { + "type": "object", + "properties": { + "notWithinPath": { + "type": "array", + "title": "Not Within Path", + "description": "The file path must not exist within the required paths.", + "markdownDescription": "The file path must not exist within the required paths. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notwithinpath)", + "default": [], + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive.", + "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notwithinpath)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "notWithinPath" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false, + "minProperties": 2 + }, + "like": { + "type": "object", + "properties": { + "like": { + "title": "Like", + "description": "Must match any of the specified wildcard patterns.", + "markdownDescription": "Must match any of the specified wildcard patterns. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#like)", + "$ref": "#/definitions/selectorExpressionValueMultiString" + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type to string.", + "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#like)", + "default": false + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive.", + "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#like)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "like" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false, + "minProperties": 2 + }, + "notLike": { + "type": "object", + "properties": { + "notLike": { + "title": "Not like", + "description": "Must not match any of the specified wildcard patterns.", + "markdownDescription": "Must not match any of the specified wildcard patterns. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notlike)", + "$ref": "#/definitions/selectorExpressionValueMultiString" + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type to string.", + "markdownDescription": "Convert type to string. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notlike)", + "default": false + }, + "caseSensitive": { + "type": "boolean", + "title": "Case sensitive", + "description": "Determines if comparing values is case-sensitive.", + "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notlike)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "notLike" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operands" + } + ], + "additionalProperties": false, + "minProperties": 2 } + } }, - "fn": { - "ztitle": "Value from function", - "zdescription": "A function expression that once evaluated specifies the value.", - "zmarkdownDescription": "A function expression that once evaluated specifies the value.", - "properties": { - "$": { - "type": "object", - "title": "Value from function", - "description": "A function expression that once evaluated specifies the value.", - "markdownDescription": "A function expression that once evaluated specifies the value.", - "$ref": "#/definitions/fn/definitions/function" + "operators": { + "type": "object", + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/operators/definitions/allOf" + }, + { + "$ref": "#/definitions/expressions/definitions/operators/definitions/anyOf" + }, + { + "$ref": "#/definitions/expressions/definitions/operators/definitions/not" + } + ], + "definitions": { + "allOf": { + "type": "object", + "title": "allOf", + "description": "All of the expressions must be true.", + "markdownDescription": "All of the expressions must be true.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#allof)", + "properties": { + "allOf": { + "type": "array", + "title": "AllOf", + "description": "All of the expressions must be true.", + "markdownDescription": "All of the expressions must be true.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#allof)", + "items": { + "$ref": "#/definitions/expressions" + } + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "where": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/where" + } + }, + "required": [ + "allOf" + ], + "oneOf": [ + { + "properties": { + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "where": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/where" + } + } } + ], + "additionalProperties": false }, - "required": [ - "$" - ], - "definitions": { - "function": { + "anyOf": { + "type": "object", + "properties": { + "anyOf": { + "type": "array", + "title": "AnyOf", + "description": "One of the expressions must be true.", + "markdownDescription": "All of the expressions must be true.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#anyof)", + "items": { + "$ref": "#/definitions/expressions" + } + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "where": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/where" + } + }, + "required": [ + "anyOf" + ], + "oneOf": [ + { + "properties": { + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "where": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/where" + } + } + } + ], + "additionalProperties": false + }, + "not": { + "type": "object", + "properties": { + "not": { + "type": "object", + "title": "Not", + "description": "The nested expression must not be true.", + "markdownDescription": "The nested expression must not be true.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#not)", + "$ref": "#/definitions/expressions" + } + }, + "required": [ + "not" + ], + "additionalProperties": false + } + } + } + } + }, + "fn": { + "ztitle": "Value from function", + "zdescription": "A function expression that once evaluated specifies the value.", + "zmarkdownDescription": "A function expression that once evaluated specifies the value.", + "properties": { + "$": { + "type": "object", + "title": "Value from function", + "description": "A function expression that once evaluated specifies the value.", + "markdownDescription": "A function expression that once evaluated specifies the value.", + "$ref": "#/definitions/fn/definitions/function" + } + }, + "required": [ + "$" + ], + "definitions": { + "function": { + "oneOf": [ + { + "$ref": "#/definitions/fn/definitions/function/definitions/substring" + }, + { + "$ref": "#/definitions/fn/definitions/function/definitions/string" + }, + { + "$ref": "#/definitions/fn/definitions/function/definitions/boolean" + }, + { + "$ref": "#/definitions/fn/definitions/function/definitions/integer" + }, + { + "$ref": "#/definitions/fn/definitions/function/definitions/concat" + }, + { + "$ref": "#/definitions/fn/definitions/function/definitions/path" + }, + { + "$ref": "#/definitions/fn/definitions/function/definitions/configuration" + } + ], + "definitions": { + "equal": { + "type": "object", + "properties": { + "equal": { + "type": "array", + "title": "Equal", + "description": "The equal operator checks for equity between two operands.", + "markdownDescription": "The `equal` operator checks for equity two operands.", + "items": { "oneOf": [ + { + "$ref": "#/definitions/fn/definitions/function" + }, + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "boolean" + } + ] + }, + "additionalItems": false, + "minItems": 2, + "maxItems": 2 + } + }, + "additionalProperties": false, + "required": [ + "equal" + ] + }, + "substring": { + "type": "object", + "properties": { + "substring": { + "title": "Substring", + "description": "The substring function, copies a number of characters from input starting from a zero based position.", + "markdownDescription": "The `substring` function, copies a number of characters from input starting from a zero based position.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "$ref": "#/definitions/fn/definitions/function", + "defaultSnippets": [ { - "$ref": "#/definitions/fn/definitions/function/definitions/substring" - }, - { - "$ref": "#/definitions/fn/definitions/function/definitions/string" - }, - { - "$ref": "#/definitions/fn/definitions/function/definitions/boolean" - }, - { - "$ref": "#/definitions/fn/definitions/function/definitions/integer" - }, - { - "$ref": "#/definitions/fn/definitions/function/definitions/concat" - }, - { - "$ref": "#/definitions/fn/definitions/function/definitions/path" + "label": "From string", + "description": "Set value to a specific value.", + "body": "" }, { - "$ref": "#/definitions/fn/definitions/function/definitions/configuration" - } - ], - "definitions": { - "equal": { - "type": "object", - "properties": { - "equal": { - "type": "array", - "title": "Equal", - "description": "The equal operator checks for equity between two operands.", - "markdownDescription": "The `equal` operator checks for equity two operands.", - "items": { - "oneOf": [ - { - "$ref": "#/definitions/fn/definitions/function" - }, - { - "type": "string" - }, - { - "type": "integer" - }, - { - "type": "boolean" - } - ] - }, - "additionalItems": false, - "minItems": 2, - "maxItems": 2 - } - }, - "additionalProperties": false, - "required": [ - "equal" - ] - }, - "substring": { - "type": "object", - "properties": { - "substring": { - "title": "Substring", - "description": "The substring function, copies a number of characters from input starting from a zero based position.", - "markdownDescription": "The `substring` function, copies a number of characters from input starting from a zero based position.", - "oneOf": [ - { - "type": "string" - }, - { - "type": "object", - "$ref": "#/definitions/fn/definitions/function", - "defaultSnippets": [ - { - "label": "From string", - "description": "Set value to a specific value.", - "body": "" - }, - { - "label": "From configuration", - "description": "Configure length from configuration.", - "body": { - "configuration": "${1}" - } - } - ] - } - ] - }, - "start": { - "title": "Start", - "description": "The zero based position to start copying characters from.", - "default": 0, - "oneOf": [ - { - "type": "integer", - "minimum": 0 - }, - { - "type": "object", - "$ref": "#/definitions/fn/definitions/function" - } - ], - "defaultSnippets": [ - { - "label": "From integer", - "description": "Set start to a specific value.", - "body": 0 - }, - { - "label": "From configuration", - "description": "Configure start from configuration.", - "body": { - "configuration": "${1}" - } - } - ] - }, - "length": { - "title": "Length", - "description": "The number of character to copy from the source string.", - "oneOf": [ - { - "type": "integer", - "minimum": 0 - }, - { - "type": "object", - "$ref": "#/definitions/fn/definitions/function" - } - ], - "defaultSnippets": [ - { - "label": "From integer", - "description": "Set length to a specific value.", - "body": 0 - }, - { - "label": "From configuration", - "description": "Configure length from configuration.", - "body": { - "configuration": "${1}" - } - } - ] - } - }, - "additionalProperties": false, - "required": [ - "substring" - ], - "defaultSnippets": [ - { - "label": "Substring from string", - "description": "Set value to a specific value.", - "body": { - "substring": "${1}", - "length": "${2}" - } - }, - { - "label": "Substring from configuration", - "description": "Configure length from configuration.", - "body": { - "substring": { - "configuration": "${1}" - }, - "length": "${2}" - } - } - ] - }, - "string": { - "type": "object", - "properties": { - "string": { - "title": "String", - "oneOf": [ - { - "type": "string", - "description": "A literal string value." - }, - { - "type": "object", - "description": "Converts the operand in to a string value.", - "$ref": "#/definitions/fn/definitions/function" - } - ] - } - }, - "additionalProperties": false, - "required": [ - "string" - ] - }, - "integer": { - "type": "object", - "properties": { - "integer": { - "title": "Integer", - "oneOf": [ - { - "type": "integer", - "description": "A literal integer value." - }, - { - "type": "object", - "description": "Converts the operand in to an integer.", - "$ref": "#/definitions/fn/definitions/function" - } - ] - } - }, - "additionalProperties": false, - "required": [ - "integer" - ] - }, - "boolean": { - "type": "object", - "properties": { - "boolean": { - "title": "Boolean", - "oneOf": [ - { - "type": "boolean", - "description": "A literal boolean value.", - "markdownDescription": "A literal boolean value." - }, - { - "type": "object", - "description": "Converts the operand to a boolean value.", - "markdownDescription": "Converts the operand to a boolean value.", - "$ref": "#/definitions/fn/definitions/function" - } - ] - } - }, - "additionalProperties": false, - "required": [ - "boolean" - ] - }, - "concat": { - "type": "object", - "properties": { - "concat": { - "type": "array", - "description": "The concat function combines two or more operands.", - "markdownDescription": "The `concat` function combines two or more operands.", - "items": { - "$ref": "#/definitions/fn/definitions/function" - }, - "additionalItems": false, - "minItems": 2 - } - }, - "additionalProperties": false, - "required": [ - "concat" - ] - }, - "path": { - "type": "object", - "properties": { - "path": { - "type": "string", - "title": "Path", - "description": "The path function returns a value from an object path.", - "markdownDescription": "The `path` function returns a value from an object path." - } - }, - "additionalProperties": false, - "required": [ - "path" - ] - }, - "configuration": { - "type": "object", - "properties": { - "configuration": { - "type": "string", - "title": "Configuration", - "description": "The configuration function returns a value retrieved from configuration by name.", - "markdownDescription": "The `configuration` function returns a value retrieved from configuration by name.", - "minLength": 1 - } - }, - "additionalProperties": false, - "required": [ - "configuration" - ] + "label": "From configuration", + "description": "Configure length from configuration.", + "body": { + "configuration": "${1}" + } } + ] + } + ] + }, + "start": { + "title": "Start", + "description": "The zero based position to start copying characters from.", + "default": 0, + "oneOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "object", + "$ref": "#/definitions/fn/definitions/function" + } + ], + "defaultSnippets": [ + { + "label": "From integer", + "description": "Set start to a specific value.", + "body": 0 + }, + { + "label": "From configuration", + "description": "Configure start from configuration.", + "body": { + "configuration": "${1}" + } } + ] + }, + "length": { + "title": "Length", + "description": "The number of character to copy from the source string.", + "oneOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "object", + "$ref": "#/definitions/fn/definitions/function" + } + ], + "defaultSnippets": [ + { + "label": "From integer", + "description": "Set length to a specific value.", + "body": 0 + }, + { + "label": "From configuration", + "description": "Configure length from configuration.", + "body": { + "configuration": "${1}" + } + } + ] + } + }, + "additionalProperties": false, + "required": [ + "substring" + ], + "defaultSnippets": [ + { + "label": "Substring from string", + "description": "Set value to a specific value.", + "body": { + "substring": "${1}", + "length": "${2}" + } + }, + { + "label": "Substring from configuration", + "description": "Configure length from configuration.", + "body": { + "substring": { + "configuration": "${1}" + }, + "length": "${2}" + } + } + ] + }, + "string": { + "type": "object", + "properties": { + "string": { + "title": "String", + "oneOf": [ + { + "type": "string", + "description": "A literal string value." + }, + { + "type": "object", + "description": "Converts the operand in to a string value.", + "$ref": "#/definitions/fn/definitions/function" + } + ] + } + }, + "additionalProperties": false, + "required": [ + "string" + ] + }, + "integer": { + "type": "object", + "properties": { + "integer": { + "title": "Integer", + "oneOf": [ + { + "type": "integer", + "description": "A literal integer value." + }, + { + "type": "object", + "description": "Converts the operand in to an integer.", + "$ref": "#/definitions/fn/definitions/function" + } + ] + } + }, + "additionalProperties": false, + "required": [ + "integer" + ] + }, + "boolean": { + "type": "object", + "properties": { + "boolean": { + "title": "Boolean", + "oneOf": [ + { + "type": "boolean", + "description": "A literal boolean value.", + "markdownDescription": "A literal boolean value." + }, + { + "type": "object", + "description": "Converts the operand to a boolean value.", + "markdownDescription": "Converts the operand to a boolean value.", + "$ref": "#/definitions/fn/definitions/function" + } + ] + } + }, + "additionalProperties": false, + "required": [ + "boolean" + ] + }, + "concat": { + "type": "object", + "properties": { + "concat": { + "type": "array", + "description": "The concat function combines two or more operands.", + "markdownDescription": "The `concat` function combines two or more operands.", + "items": { + "$ref": "#/definitions/fn/definitions/function" + }, + "additionalItems": false, + "minItems": 2 + } + }, + "additionalProperties": false, + "required": [ + "concat" + ] + }, + "path": { + "type": "object", + "properties": { + "path": { + "type": "string", + "title": "Path", + "description": "The path function returns a value from an object path.", + "markdownDescription": "The `path` function returns a value from an object path." + } + }, + "additionalProperties": false, + "required": [ + "path" + ] + }, + "configuration": { + "type": "object", + "properties": { + "configuration": { + "type": "string", + "title": "Configuration", + "description": "The configuration function returns a value retrieved from configuration by name.", + "markdownDescription": "The `configuration` function returns a value retrieved from configuration by name.", + "minLength": 1 } + }, + "additionalProperties": false, + "required": [ + "configuration" + ] } + } } + } } + } } diff --git a/schemas/PSRule-options.schema.json b/schemas/PSRule-options.schema.json index a707bc0a70..7c28fc65c4 100644 --- a/schemas/PSRule-options.schema.json +++ b/schemas/PSRule-options.schema.json @@ -271,11 +271,12 @@ "type": "string", "title": "Duplicate resource identifiers", "description": "Determines how to handle duplicate resources identifiers during execution. Regardless of the value, only the first resource will be used. By defaut, an error is thrown. When set to Warn, a warning is generated. When set to Ignore, no output will be displayed.", - "markdownDescription": "Determines how to handle duplicate resources identifiers during execution.\n\nRegardless of the value, only the first resource will be used. By defaut, an error is thrown. When set to `Warn`, a warning is generated. When set to `Ignore`, no output will be displayed. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executionduplicateresourceid)", + "markdownDescription": "Determines how to handle duplicate resources identifiers during execution.\n\nRegardless of the value, only the first resource will be used. By defaut, an error is thrown.\n\n- When set to `Warn`, a warning is generated.\n- When set to `Debug`, a message is written to the debug log.\n- When set to `Ignore`, no output will be displayed.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executionduplicateresourceid)", "enum": [ "Ignore", "Warn", - "Error" + "Error", + "Debug" ], "default": "Error" }, @@ -297,20 +298,6 @@ "markdownDescription": "Enable or disable warnings for inconclusive rules. The default is `true`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executioninconclusivewarning)", "default": true }, - "notProcessedWarning": { - "type": "boolean", - "title": "Warn on unprocessed objects", - "description": "Enable or disable warnings for objects that are not processed by any rule. The default is true.", - "markdownDescription": "Enable or disable warnings for objects that are not processed by any rule. The default is `true`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executionnotprocessedwarning)", - "default": true - }, - "suppressedRuleWarning": { - "type": "boolean", - "title": "Warn on suppressed rules", - "description": "Enable or disable warnings for suppressed rules. The default is true.", - "markdownDescription": "Enable or disable warnings for suppressed rules. The default is `true`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executionsuppressedrulewarning)", - "default": true - }, "invariantCultureWarning": { "type": "boolean", "title": "Warn on invariant culture", @@ -328,6 +315,33 @@ "Minimal" ], "default": "BuiltIn" + }, + "notProcessedWarning": { + "type": "boolean", + "title": "Warn on unprocessed objects", + "description": "Enable or disable warnings for objects that are not processed by any rule. The default is true.", + "markdownDescription": "Enable or disable warnings for objects that are not processed by any rule. The default is `true`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executionnotprocessedwarning)", + "default": true + }, + "suppressedRuleWarning": { + "type": "boolean", + "title": "Warn on suppressed rules", + "description": "Enable or disable warnings for suppressed rules. The default is true.", + "markdownDescription": "Enable or disable warnings for suppressed rules. The default is `true`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executionsuppressedrulewarning)", + "default": true + }, + "suppressionGroupExpired": { + "type": "string", + "title": "Expired suppression groups", + "description": "Determines how to handle expired suppression groups. Regardless of the value, an expired suppression group will be ignored. By default, a warning is generated. When set to Error, an error is thrown. When set to Debug, a message is written to the debug log. When set to Ignore, no output will be displayed.", + "markdownDescription": "Determines how to handle expired suppression groups.\n\nRegardless of the value, an expired suppression group will be ignored. By default, a warning is generated.\n\n- When set to `Error`, an error is thrown.\n- When set to `Debug`, a message is written to the debug log.\n- When set to `Ignore`, no output will be displayed.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#executionsuppressiongroupexpired)", + "enum": [ + "Ignore", + "Warn", + "Error", + "Debug" + ], + "default": "Warn" } }, "additionalProperties": false diff --git a/schemas/PSRule-resources.schema.json b/schemas/PSRule-resources.schema.json index cfdf5cc96d..2b1503e16e 100644 --- a/schemas/PSRule-resources.schema.json +++ b/schemas/PSRule-resources.schema.json @@ -1,25 +1,25 @@ { - "$schema": "https://json-schema.org/draft-07/schema#", - "title": "PSRule resources", - "description": "A schema for PSRule resources.", - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "https://raw.githubusercontent.com/microsoft/PSRule/main/schemas/PSRule-language.schema.json#/definitions/rule-v1" - }, - { - "$ref": "https://raw.githubusercontent.com/microsoft/PSRule/main/schemas/PSRule-language.schema.json#/definitions/baseline-v1" - }, - { - "$ref": "https://raw.githubusercontent.com/microsoft/PSRule/main/schemas/PSRule-language.schema.json#/definitions/moduleConfig-v1" - }, - { - "$ref": "https://raw.githubusercontent.com/microsoft/PSRule/main/schemas/PSRule-language.schema.json#/definitions/selector-v1" - }, - { - "$ref": "https://raw.githubusercontent.com/microsoft/PSRule/main/schemas/PSRule-language.schema.json#/definitions/suppressionGroup-v1" - } - ] - } + "$schema": "https://json-schema.org/draft-07/schema#", + "title": "PSRule resources", + "description": "A schema for PSRule resources.", + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "https://raw.githubusercontent.com/microsoft/PSRule/main/schemas/PSRule-language.schema.json#/definitions/rule-v1" + }, + { + "$ref": "https://raw.githubusercontent.com/microsoft/PSRule/main/schemas/PSRule-language.schema.json#/definitions/baseline-v1" + }, + { + "$ref": "https://raw.githubusercontent.com/microsoft/PSRule/main/schemas/PSRule-language.schema.json#/definitions/moduleConfig-v1" + }, + { + "$ref": "https://raw.githubusercontent.com/microsoft/PSRule/main/schemas/PSRule-language.schema.json#/definitions/selector-v1" + }, + { + "$ref": "https://raw.githubusercontent.com/microsoft/PSRule/main/schemas/PSRule-language.schema.json#/definitions/suppressionGroup-v1" + } + ] + } } diff --git a/src/PSRule/Common/RunspaceContextDiagnosticExtensions.cs b/src/PSRule/Common/RunspaceContextDiagnosticExtensions.cs index d3d0082783..67ec869614 100644 --- a/src/PSRule/Common/RunspaceContextDiagnosticExtensions.cs +++ b/src/PSRule/Common/RunspaceContextDiagnosticExtensions.cs @@ -42,7 +42,7 @@ internal static void WarnRuleNotFound(this RunspaceContext context) internal static void WarnDuplicateRuleName(this RunspaceContext context, string ruleName) { - if (context.Writer == null || !context.Writer.ShouldWriteWarning()) + if (context == null || context.Writer == null || !context.Writer.ShouldWriteWarning()) return; context.Writer.WriteWarning(PSRuleResources.DuplicateRuleName, ruleName); @@ -50,20 +50,40 @@ internal static void WarnDuplicateRuleName(this RunspaceContext context, string internal static void DuplicateResourceId(this RunspaceContext context, ResourceId id, ResourceId duplicateId) { - if (context == null) + if (context == null || context.Pipeline == null) return; var action = context.Pipeline.Option.Execution.DuplicateResourceId.GetValueOrDefault(ExecutionOption.Default.DuplicateResourceId.Value); + context.Throw(action, PSRuleResources.DuplicateResourceId, id.Value, duplicateId.Value); + } + + internal static void SuppressionGroupExpired(this RunspaceContext context, ResourceId suppressionGroupId) + { + if (context == null || context.Pipeline == null) + return; + + var action = context.Pipeline.Option.Execution.SuppressionGroupExpired.GetValueOrDefault(ExecutionOption.Default.SuppressionGroupExpired.Value); + context.Throw(action, PSRuleResources.SuppressionGroupExpired, suppressionGroupId.Value); + } + + internal static void Throw(this RunspaceContext context, ExecutionActionPreference action, string message, params object[] args) + { + if (context == null || action == ExecutionActionPreference.Ignore) + return; + if (action == ExecutionActionPreference.Error) - throw new RuleException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.DuplicateResourceId, id.Value, duplicateId.Value)); + throw new RuleException(string.Format(Thread.CurrentThread.CurrentCulture, message, args)); else if (action == ExecutionActionPreference.Warn && context.Writer != null && context.Writer.ShouldWriteWarning()) - context.Writer.WriteWarning(PSRuleResources.DuplicateResourceId, id.Value, duplicateId.Value); + context.Writer.WriteWarning(message, args); + + else if (action == ExecutionActionPreference.Debug && context.Writer != null && context.Writer.ShouldWriteDebug()) + context.Writer.WriteDebug(message, args); } internal static void DebugPropertyObsolete(this RunspaceContext context, string variableName, string propertyName) { - if (context.Writer == null || !context.Writer.ShouldWriteDebug()) + if (context == null || context.Writer == null || !context.Writer.ShouldWriteDebug()) return; context.Writer.WriteDebug(PSRuleResources.DebugPropertyObsolete, context.RuleBlock.Name, variableName, propertyName); diff --git a/src/PSRule/Configuration/ExecutionActionPreference.cs b/src/PSRule/Configuration/ExecutionActionPreference.cs index 10a6cac518..487b3bd677 100644 --- a/src/PSRule/Configuration/ExecutionActionPreference.cs +++ b/src/PSRule/Configuration/ExecutionActionPreference.cs @@ -28,6 +28,11 @@ public enum ExecutionActionPreference /// /// Generate an error. /// - Error = 3 + Error = 3, + + /// + /// Continue to execute but write a debug log. + /// + Debug = 4 } } diff --git a/src/PSRule/Configuration/ExecutionOption.cs b/src/PSRule/Configuration/ExecutionOption.cs index dce24bce4c..e03238f246 100644 --- a/src/PSRule/Configuration/ExecutionOption.cs +++ b/src/PSRule/Configuration/ExecutionOption.cs @@ -23,6 +23,7 @@ public sealed class ExecutionOption : IEquatable private const bool DEFAULT_INVARIANTCULTUREWARNING = true; private const ExecutionActionPreference DEFAULT_DUPLICATERESOURCEID = ExecutionActionPreference.Error; private const SessionState DEFAULT_INITIALSESSIONSTATE = SessionState.BuiltIn; + private const ExecutionActionPreference DEFAULT_SUPPRESSIONGROUPEXPIRED = ExecutionActionPreference.Warn; internal static readonly ExecutionOption Default = new() { @@ -30,10 +31,11 @@ public sealed class ExecutionOption : IEquatable DuplicateResourceId = DEFAULT_DUPLICATERESOURCEID, LanguageMode = DEFAULT_LANGUAGEMODE, InconclusiveWarning = DEFAULT_INCONCLUSIVEWARNING, - NotProcessedWarning = DEFAULT_NOTPROCESSEDWARNING, - SuppressedRuleWarning = DEFAULT_SUPPRESSEDRULEWARNING, InvariantCultureWarning = DEFAULT_INVARIANTCULTUREWARNING, InitialSessionState = DEFAULT_INITIALSESSIONSTATE, + NotProcessedWarning = DEFAULT_NOTPROCESSEDWARNING, + SuppressedRuleWarning = DEFAULT_SUPPRESSEDRULEWARNING, + SuppressionGroupExpired = DEFAULT_SUPPRESSIONGROUPEXPIRED, }; /// @@ -45,10 +47,11 @@ public ExecutionOption() DuplicateResourceId = null; LanguageMode = null; InconclusiveWarning = null; - NotProcessedWarning = null; - SuppressedRuleWarning = null; InvariantCultureWarning = null; InitialSessionState = null; + NotProcessedWarning = null; + SuppressedRuleWarning = null; + SuppressionGroupExpired = null; } /// @@ -64,10 +67,11 @@ public ExecutionOption(ExecutionOption option) DuplicateResourceId = option.DuplicateResourceId; LanguageMode = option.LanguageMode; InconclusiveWarning = option.InconclusiveWarning; - NotProcessedWarning = option.NotProcessedWarning; - SuppressedRuleWarning = option.SuppressedRuleWarning; InvariantCultureWarning = option.InvariantCultureWarning; InitialSessionState = option.InitialSessionState; + NotProcessedWarning = option.NotProcessedWarning; + SuppressedRuleWarning = option.SuppressedRuleWarning; + SuppressionGroupExpired = option.SuppressionGroupExpired; } /// @@ -84,10 +88,11 @@ public bool Equals(ExecutionOption other) DuplicateResourceId == other.DuplicateResourceId && LanguageMode == other.LanguageMode && InconclusiveWarning == other.InconclusiveWarning && + InvariantCultureWarning == other.InvariantCultureWarning && + InitialSessionState == other.InitialSessionState && NotProcessedWarning == other.NotProcessedWarning && SuppressedRuleWarning == other.NotProcessedWarning && - InvariantCultureWarning == other.InvariantCultureWarning && - InitialSessionState == other.InitialSessionState; + SuppressionGroupExpired == other.SuppressionGroupExpired; } /// @@ -100,10 +105,11 @@ public override int GetHashCode() hash = hash * 23 + (DuplicateResourceId.HasValue ? DuplicateResourceId.Value.GetHashCode() : 0); hash = hash * 23 + (LanguageMode.HasValue ? LanguageMode.Value.GetHashCode() : 0); hash = hash * 23 + (InconclusiveWarning.HasValue ? InconclusiveWarning.Value.GetHashCode() : 0); - hash = hash * 23 + (NotProcessedWarning.HasValue ? NotProcessedWarning.Value.GetHashCode() : 0); - hash = hash * 23 + (SuppressedRuleWarning.HasValue ? SuppressedRuleWarning.Value.GetHashCode() : 0); hash = hash * 23 + (InvariantCultureWarning.HasValue ? InvariantCultureWarning.Value.GetHashCode() : 0); hash = hash * 23 + (InitialSessionState.HasValue ? InitialSessionState.Value.GetHashCode() : 0); + hash = hash * 23 + (NotProcessedWarning.HasValue ? NotProcessedWarning.Value.GetHashCode() : 0); + hash = hash * 23 + (SuppressedRuleWarning.HasValue ? SuppressedRuleWarning.Value.GetHashCode() : 0); + hash = hash * 23 + (SuppressionGroupExpired.HasValue ? SuppressionGroupExpired.Value.GetHashCode() : 0); return hash; } } @@ -139,6 +145,7 @@ internal static ExecutionOption Combine(ExecutionOption o1, ExecutionOption o2) /// Regardless of the value, only the first resource will be used. /// By defaut, an error is thrown. /// When set to Warn, a warning is generated. + /// When set to Debug, a message is written to the debug log. /// When set to Ignore, no output will be displayed. /// [DefaultValue(null)] @@ -157,29 +164,40 @@ internal static ExecutionOption Combine(ExecutionOption o1, ExecutionOption o2) public bool? InconclusiveWarning { get; set; } /// - /// Determines if a warning is raised when an object is not processed by any rule. + /// Determines if warning is raised when invariant culture is used. /// [DefaultValue(null)] - public bool? NotProcessedWarning { get; set; } + public bool? InvariantCultureWarning { get; set; } /// - /// Determines if a warning is raised when a rule is suppressed. + /// Determines how the initial session state for executing PowerShell code is created. + /// The default is . /// [DefaultValue(null)] - public bool? SuppressedRuleWarning { get; set; } + public SessionState? InitialSessionState { get; set; } /// - /// Determines if warning is raised when invariant culture is used. + /// Determines if a warning is raised when an object is not processed by any rule. /// [DefaultValue(null)] - public bool? InvariantCultureWarning { get; set; } + public bool? NotProcessedWarning { get; set; } /// - /// Determines how the initial session state for executing PowerShell code is created. - /// The default is . + /// Determines how to handle expired suppression groups. + /// Regardless of the value, an expired suppression group will be ignored. + /// By default, a warning is generated. + /// When set to Error, an error is thrown. + /// When set to Debug, a message is written to the debug log. + /// When set to Ignore, no output will be displayed. /// [DefaultValue(null)] - public SessionState? InitialSessionState { get; set; } + public ExecutionActionPreference? SuppressionGroupExpired { get; set; } + + /// + /// Determines if a warning is raised when a rule is suppressed. + /// + [DefaultValue(null)] + public bool? SuppressedRuleWarning { get; set; } internal void Load(EnvironmentHelper env) { @@ -195,17 +213,20 @@ internal void Load(EnvironmentHelper env) if (env.TryBool("PSRULE_EXECUTION_INCONCLUSIVEWARNING", out bvalue)) InconclusiveWarning = bvalue; + if (env.TryBool("PSRULE_EXECUTION_INVARIANTCULTUREWARNING", out bvalue)) + InvariantCultureWarning = bvalue; + + if (env.TryEnum("PSRULE_EXECUTION_INITIALSESSIONSTATE", out SessionState initialSessionState)) + InitialSessionState = initialSessionState; + if (env.TryBool("PSRULE_EXECUTION_NOTPROCESSEDWARNING", out bvalue)) NotProcessedWarning = bvalue; if (env.TryBool("PSRULE_EXECUTION_SUPPRESSEDRULEWARNING", out bvalue)) SuppressedRuleWarning = bvalue; - if (env.TryBool("PSRULE_EXECUTION_INVARIANTCULTUREWARNING", out bvalue)) - InvariantCultureWarning = bvalue; - - if (env.TryEnum("PSRULE_EXECUTION_INITIALSESSIONSTATE", out SessionState initialSessionState)) - InitialSessionState = initialSessionState; + if (env.TryEnum("PSRULE_EXECUTION_SUPPRESSIONGROUPEXPIRED", out ExecutionActionPreference suppressionGroupExpired)) + SuppressionGroupExpired = suppressionGroupExpired; } internal void Load(Dictionary index) @@ -222,17 +243,20 @@ internal void Load(Dictionary index) if (index.TryPopBool("Execution.InconclusiveWarning", out bvalue)) InconclusiveWarning = bvalue; + if (index.TryPopBool("Execution.InvariantCultureWarning", out bvalue)) + InvariantCultureWarning = bvalue; + + if (index.TryPopEnum("Execution.InitialSessionState", out SessionState initialSessionState)) + InitialSessionState = initialSessionState; + if (index.TryPopBool("Execution.NotProcessedWarning", out bvalue)) NotProcessedWarning = bvalue; if (index.TryPopBool("Execution.SuppressedRuleWarning", out bvalue)) SuppressedRuleWarning = bvalue; - if (index.TryPopBool("Execution.InvariantCultureWarning", out bvalue)) - InvariantCultureWarning = bvalue; - - if (index.TryPopEnum("Execution.InitialSessionState", out SessionState initialSessionState)) - InitialSessionState = initialSessionState; + if (index.TryPopEnum("Execution.SuppressionGroupExpired", out ExecutionActionPreference suppressionGroupExpired)) + SuppressionGroupExpired = suppressionGroupExpired; } } } diff --git a/src/PSRule/Definitions/SuppressionGroups/SuppressionGroup.cs b/src/PSRule/Definitions/SuppressionGroups/SuppressionGroup.cs index f0b59ce517..6c674ddaea 100644 --- a/src/PSRule/Definitions/SuppressionGroups/SuppressionGroup.cs +++ b/src/PSRule/Definitions/SuppressionGroups/SuppressionGroup.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using PSRule.Definitions.Expressions; using PSRule.Pipeline; @@ -11,8 +12,22 @@ namespace PSRule.Definitions.SuppressionGroups /// internal interface ISuppressionGroupV1Spec { + /// + /// The date time that the suppression is valid until. + /// After this date time, the suppression is ignored. + /// When not set, the suppression does not expire. + /// This RFC3339 (ISO 8601) formatted date time using the format yyyy-MM-ddTHH:mm:ssZ. + /// + DateTime? ExpiresOn { get; set; } + + /// + /// This only applies to rules that match the specified rule names. + /// string[] Rule { get; } + /// + /// An expression. If the expression evaluates as true and rules specified by are suppressed. + /// LanguageIf If { get; } } @@ -31,8 +46,13 @@ public SuppressionGroupV1(string apiVersion, SourceFile source, ResourceMetadata /// internal sealed class SuppressionGroupV1Spec : Spec, ISuppressionGroupV1Spec { + /// + public DateTime? ExpiresOn { get; set; } + + /// public string[] Rule { get; set; } + /// public LanguageIf If { get; set; } } } diff --git a/src/PSRule/Host/HostHelper.cs b/src/PSRule/Host/HostHelper.cs index 53b926fdc6..d397c91481 100644 --- a/src/PSRule/Host/HostHelper.cs +++ b/src/PSRule/Host/HostHelper.cs @@ -306,7 +306,10 @@ private static ILanguageBlock[] GetJsonLanguageBlocks(Source[] sources, Runspace { var result = new Collection(); var visitor = new ResourceValidator(context); - var deserializer = new JsonSerializer(); + var deserializer = new JsonSerializer + { + DateTimeZoneHandling = DateTimeZoneHandling.Utc + }; deserializer.Converters.Add(new ResourceObjectJsonConverter()); deserializer.Converters.Add(new FieldMapJsonConverter()); deserializer.Converters.Add(new StringArrayJsonConverter()); diff --git a/src/PSRule/PSRule.psm1 b/src/PSRule/PSRule.psm1 index d96b38f181..e16db570bd 100644 --- a/src/PSRule/PSRule.psm1 +++ b/src/PSRule/PSRule.psm1 @@ -1179,6 +1179,16 @@ function New-PSRuleOption { [Alias('ExecutionInconclusiveWarning')] [System.Boolean]$InconclusiveWarning = $True, + # Sets the Execution.InvariantCultureWarning option + [Parameter(Mandatory = $False)] + [Alias('ExecutionInvariantCultureWarning')] + [System.Boolean]$InvariantCultureWarning = $True, + + # Sets the Execution.InitialSessionState option + [Parameter(Mandatory = $False)] + [Alias('ExecutionInitialSessionState')] + [PSRule.Configuration.SessionState]$InitialSessionState = [PSRule.Configuration.SessionState]::BuiltIn, + # Sets the Execution.NotProcessedWarning option [Parameter(Mandatory = $False)] [Alias('ExecutionNotProcessedWarning')] @@ -1189,16 +1199,10 @@ function New-PSRuleOption { [Alias('ExecutionSuppressedRuleWarning')] [System.Boolean]$SuppressedRuleWarning = $True, - - # Sets the Execution.InvariantCultureWarning option - [Parameter(Mandatory = $False)] - [Alias('ExecutionInvariantCultureWarning')] - [System.Boolean]$InvariantCultureWarning = $True, - - # Sets the Execution.InitialSessionState option + # Sets the Execution.SuppressionGroupExpired option [Parameter(Mandatory = $False)] - [Alias('ExecutionInitialSessionState')] - [PSRule.Configuration.SessionState]$InitialSessionState = [PSRule.Configuration.SessionState]::BuiltIn, + [Alias('ExecutionSuppressionGroupExpired')] + [PSRule.Configuration.ExecutionActionPreference]$SuppressionGroupExpired = [PSRule.Configuration.ExecutionActionPreference]::Warn, # Sets the Include.Module option [Parameter(Mandatory = $False)] @@ -1476,6 +1480,16 @@ function Set-PSRuleOption { [Alias('ExecutionInconclusiveWarning')] [System.Boolean]$InconclusiveWarning = $True, + # Sets the Execution.InvariantCultureWarning option + [Parameter(Mandatory = $False)] + [Alias('ExecutionInvariantCultureWarning')] + [System.Boolean]$InvariantCultureWarning = $True, + + # Sets the Execution.InitialSessionState option + [Parameter(Mandatory = $False)] + [Alias('ExecutionInitialSessionState')] + [PSRule.Configuration.SessionState]$InitialSessionState = [PSRule.Configuration.SessionState]::BuiltIn, + # Sets the Execution.NotProcessedWarning option [Parameter(Mandatory = $False)] [Alias('ExecutionNotProcessedWarning')] @@ -1486,15 +1500,10 @@ function Set-PSRuleOption { [Alias('ExecutionSuppressedRuleWarning')] [System.Boolean]$SuppressedRuleWarning = $True, - # Sets the Execution.InvariantCultureWarning option + # Sets the Execution.SuppressionGroupExpired option [Parameter(Mandatory = $False)] - [Alias('ExecutionInvariantCultureWarning')] - [System.Boolean]$InvariantCultureWarning = $True, - - # Sets the Execution.InitialSessionState option - [Parameter(Mandatory = $False)] - [Alias('ExecutionInitialSessionState')] - [PSRule.Configuration.SessionState]$InitialSessionState = [PSRule.Configuration.SessionState]::BuiltIn, + [Alias('ExecutionSuppressionGroupExpired')] + [PSRule.Configuration.ExecutionActionPreference]$SuppressionGroupExpired = [PSRule.Configuration.ExecutionActionPreference]::Warn, # Sets the Include.Module option [Parameter(Mandatory = $False)] @@ -2219,6 +2228,16 @@ function SetOptions { [Alias('ExecutionInconclusiveWarning')] [System.Boolean]$InconclusiveWarning = $True, + # Sets the Execution.InvariantCultureWarning option + [Parameter(Mandatory = $False)] + [Alias('ExecutionInvariantCultureWarning')] + [System.Boolean]$InvariantCultureWarning = $True, + + # Sets the Execution.InitialSessionState option + [Parameter(Mandatory = $False)] + [Alias('ExecutionInitialSessionState')] + [PSRule.Configuration.SessionState]$InitialSessionState = [PSRule.Configuration.SessionState]::BuiltIn, + # Sets the Execution.NotProcessedWarning option [Parameter(Mandatory = $False)] [Alias('ExecutionNotProcessedWarning')] @@ -2229,15 +2248,10 @@ function SetOptions { [Alias('ExecutionSuppressedRuleWarning')] [System.Boolean]$SuppressedRuleWarning = $True, - # Sets the Execution.InvariantCultureWarning option + # Sets the Execution.SuppressionGroupExpired option [Parameter(Mandatory = $False)] - [Alias('ExecutionInvariantCultureWarning')] - [System.Boolean]$InvariantCultureWarning = $True, - - # Sets the Execution.InitialSessionState option - [Parameter(Mandatory = $False)] - [Alias('ExecutionInitialSessionState')] - [PSRule.Configuration.SessionState]$InitialSessionState = [PSRule.Configuration.SessionState]::BuiltIn, + [Alias('ExecutionSuppressionGroupExpired')] + [PSRule.Configuration.ExecutionActionPreference]$SuppressionGroupExpired = [PSRule.Configuration.ExecutionActionPreference]::Warn, # Sets the Include.Module option [Parameter(Mandatory = $False)] @@ -2411,11 +2425,26 @@ function SetOptions { $Option.Convention.Include = $Convention; } + # Sets option Execution.DuplicateResourceId + if ($PSBoundParameters.ContainsKey('DuplicateResourceId')) { + $Option.Execution.DuplicateResourceId = $DuplicateResourceId; + } + # Sets option Execution.InconclusiveWarning if ($PSBoundParameters.ContainsKey('InconclusiveWarning')) { $Option.Execution.InconclusiveWarning = $InconclusiveWarning; } + # Sets option Execution.InvariantCultureWarning + if ($PSBoundParameters.ContainsKey('InvariantCultureWarning')) { + $Option.Execution.InvariantCultureWarning = $InvariantCultureWarning; + } + + # Sets option Execution.InitialSessionState + if ($PSBoundParameters.ContainsKey('InitialSessionState')) { + $Option.Execution.InitialSessionState = $InitialSessionState; + } + # Sets option Execution.NotProcessedWarning if ($PSBoundParameters.ContainsKey('NotProcessedWarning')) { $Option.Execution.NotProcessedWarning = $NotProcessedWarning; @@ -2431,19 +2460,9 @@ function SetOptions { $Option.Execution.AliasReferenceWarning = $AliasReferenceWarning; } - # Sets option Execution.DuplicateResourceId - if ($PSBoundParameters.ContainsKey('DuplicateResourceId')) { - $Option.Execution.DuplicateResourceId = $DuplicateResourceId; - } - - # Sets option Execution.InvariantCultureWarning - if ($PSBoundParameters.ContainsKey('InvariantCultureWarning')) { - $Option.Execution.InvariantCultureWarning = $InvariantCultureWarning; - } - - # Sets option Execution.InitialSessionState - if ($PSBoundParameters.ContainsKey('InitialSessionState')) { - $Option.Execution.InitialSessionState = $InitialSessionState; + # Sets option Execution.SuppressionGroupExpired + if ($PSBoundParameters.ContainsKey('SuppressionGroupExpired')) { + $Option.Execution.SuppressionGroupExpired = $SuppressionGroupExpired; } # Sets option Include.Module diff --git a/src/PSRule/Pipeline/GetRuleHelpPipeline.cs b/src/PSRule/Pipeline/GetRuleHelpPipeline.cs index 6a34882bec..cc65346ed2 100644 --- a/src/PSRule/Pipeline/GetRuleHelpPipeline.cs +++ b/src/PSRule/Pipeline/GetRuleHelpPipeline.cs @@ -12,10 +12,19 @@ namespace PSRule.Pipeline { + /// + /// A helper to build a pipeline for getting help from rules. + /// public interface IHelpPipelineBuilder : IPipelineBuilder { + /// + /// Get the full help output for a rule. + /// void Full(); + /// + /// Open or show online help for a rule if it exists. + /// void Online(); } @@ -27,6 +36,7 @@ internal sealed class GetRuleHelpPipelineBuilder : PipelineBuilderBase, IHelpPip internal GetRuleHelpPipelineBuilder(Source[] source, IHostContext hostContext) : base(source, hostContext) { } + /// public override IPipelineBuilder Configure(PSRuleOption option) { if (option == null) @@ -40,16 +50,20 @@ public override IPipelineBuilder Configure(PSRuleOption option) return this; } + + /// public void Full() { _Full = true; } + /// public void Online() { _Online = true; } + /// public override IPipeline Build(IPipelineWriter writer = null) { return new GetRuleHelpPipeline( @@ -147,9 +161,9 @@ protected override PipelineWriter PrepareWriter() return new HelpWriter( inner: GetOutput(), option: Option, - shouldProcess: HostContext.ShouldProcess, + shouldProcess: ShouldProcess, languageMode: Option.Execution.LanguageMode.GetValueOrDefault(ExecutionOption.Default.LanguageMode.Value), - inSession: HostContext.InSession, + inSession: InSession, online: _Online, full: _Full ); diff --git a/src/PSRule/Pipeline/PipelineBuilder.cs b/src/PSRule/Pipeline/PipelineBuilder.cs index e6d57ff74c..7197490514 100644 --- a/src/PSRule/Pipeline/PipelineBuilder.cs +++ b/src/PSRule/Pipeline/PipelineBuilder.cs @@ -250,6 +250,11 @@ protected PipelineBuilderBase(Source[] source, IHostContext hostContext) VisitTargetObject = PipelineReceiverActions.PassThru; } + /// + /// Determines if the pipeline is executing in a remote PowerShell session. + /// + public bool InSession => HostContext != null && HostContext.InSession; + /// public void Name(string[] name) { @@ -482,6 +487,7 @@ protected static ExecutionOption GetExecutionOption(ExecutionOption option) { var result = ExecutionOption.Combine(option, ExecutionOption.Default); result.DuplicateResourceId = result.DuplicateResourceId == ExecutionActionPreference.None ? ExecutionOption.Default.DuplicateResourceId.Value : result.DuplicateResourceId; + result.SuppressionGroupExpired = result.SuppressionGroupExpired == ExecutionActionPreference.None ? ExecutionOption.Default.SuppressionGroupExpired.Value : result.SuppressionGroupExpired; return result; } diff --git a/src/PSRule/Pipeline/PipelineContext.cs b/src/PSRule/Pipeline/PipelineContext.cs index 8d1b2e6f11..2339cb22ec 100644 --- a/src/PSRule/Pipeline/PipelineContext.cs +++ b/src/PSRule/Pipeline/PipelineContext.cs @@ -184,13 +184,20 @@ internal void Import(RunspaceContext context, IResource resource) } else if (resource.Kind == ResourceKind.SuppressionGroup && resource is SuppressionGroupV1 suppressionGroup) { - SuppressionGroup.Add(new SuppressionGroupVisitor( - context: context, - id: suppressionGroup.Id, - source: suppressionGroup.Source, - spec: suppressionGroup.Spec, - info: suppressionGroup.Info - )); + if (!suppressionGroup.Spec.ExpiresOn.HasValue || suppressionGroup.Spec.ExpiresOn.Value > DateTime.UtcNow) + { + SuppressionGroup.Add(new SuppressionGroupVisitor( + context: context, + id: suppressionGroup.Id, + source: suppressionGroup.Source, + spec: suppressionGroup.Spec, + info: suppressionGroup.Info + )); + } + else + { + context.SuppressionGroupExpired(suppressionGroup.Id); + } } } diff --git a/src/PSRule/Resources/PSRuleResources.Designer.cs b/src/PSRule/Resources/PSRuleResources.Designer.cs index 63ad14b215..8c939522e4 100644 --- a/src/PSRule/Resources/PSRuleResources.Designer.cs +++ b/src/PSRule/Resources/PSRuleResources.Designer.cs @@ -798,6 +798,15 @@ internal static string SourceNotFound { } } + /// + /// Looks up a localized string similar to Suppression group '{0}' has expired and will be ignored.. + /// + internal static string SuppressionGroupExpired { + get { + return ResourceManager.GetString("SuppressionGroupExpired", resourceCulture); + } + } + /// /// Looks up a localized string similar to Using changed files: {0}. /// diff --git a/src/PSRule/Resources/PSRuleResources.resx b/src/PSRule/Resources/PSRuleResources.resx index d86264c7ad..a16cd27d4d 100644 --- a/src/PSRule/Resources/PSRuleResources.resx +++ b/src/PSRule/Resources/PSRuleResources.resx @@ -394,4 +394,7 @@ The object path expression reached the maximum depth of {0} evaluating the path '{1}'. + + Suppression group '{0}' has expired and will be ignored. + \ No newline at end of file diff --git a/tests/PSRule.Tests/PSRule.Common.Tests.ps1 b/tests/PSRule.Tests/PSRule.Common.Tests.ps1 index 81602d889a..4de8358a34 100644 --- a/tests/PSRule.Tests/PSRule.Common.Tests.ps1 +++ b/tests/PSRule.Tests/PSRule.Common.Tests.ps1 @@ -1298,24 +1298,26 @@ Describe 'Invoke-PSRule' -Tag 'Invoke-PSRule','Common' { $Null = $testObject | Invoke-PSRule @invokeParams -Option $option -Name 'FromFile1', 'FromFile2', 'WithTag2' -WarningVariable outWarnings -WarningAction SilentlyContinue; $warningMessages = $outwarnings.ToArray(); - $warningMessages.Length | Should -Be 6; + $warningMessages.Length | Should -Be 7; $warningMessages[0] | Should -BeOfType [System.Management.Automation.WarningRecord]; - $warningMessages[0].Message | Should -BeExactly "Rule '.\FromFile1' was suppressed by suppression group '.\SuppressWithTargetName' for 'TestObject1'. Ignore test objects by name."; + $warningMessages[0].Message | Should -BeExactly "Suppression group '.\SuppressWithExpiry' has expired and will be ignored."; $warningMessages[1] | Should -BeOfType [System.Management.Automation.WarningRecord]; - $warningMessages[1].Message | Should -BeExactly "Rule '.\FromFile2' was suppressed by suppression group '.\SuppressWithTargetName' for 'TestObject1'. Ignore test objects by name."; + $warningMessages[1].Message | Should -BeExactly "Rule '.\FromFile1' was suppressed by suppression group '.\SuppressWithTargetName' for 'TestObject1'. Ignore test objects by name."; $warningMessages[2] | Should -BeOfType [System.Management.Automation.WarningRecord]; - $warningMessages[2].Message | Should -BeExactly "Rule '.\WithTag2' was suppressed by suppression group '.\SuppressWithNonProdTag' for 'TestObject1'. Ignore objects with non-production tag."; + $warningMessages[2].Message | Should -BeExactly "Rule '.\FromFile2' was suppressed by suppression group '.\SuppressWithTargetName' for 'TestObject1'. Ignore test objects by name."; $warningMessages[3] | Should -BeOfType [System.Management.Automation.WarningRecord]; - $warningMessages[3].Message | Should -BeExactly "Rule '.\FromFile1' was suppressed by suppression group '.\SuppressWithTargetName' for 'TestObject2'. Ignore test objects by name."; + $warningMessages[3].Message | Should -BeExactly "Rule '.\WithTag2' was suppressed by suppression group '.\SuppressWithNonProdTag' for 'TestObject1'. Ignore objects with non-production tag."; $warningMessages[4] | Should -BeOfType [System.Management.Automation.WarningRecord]; - $warningMessages[4].Message | Should -BeExactly "Rule '.\FromFile2' was suppressed by suppression group '.\SuppressWithTargetName' for 'TestObject2'. Ignore test objects by name."; + $warningMessages[4].Message | Should -BeExactly "Rule '.\FromFile1' was suppressed by suppression group '.\SuppressWithTargetName' for 'TestObject2'. Ignore test objects by name."; $warningMessages[5] | Should -BeOfType [System.Management.Automation.WarningRecord]; - $warningMessages[5].Message | Should -BeExactly "Rule '.\WithTag2' was suppressed by suppression group '.\SuppressWithNonProdTag' for 'TestObject2'. Ignore objects with non-production tag."; + $warningMessages[5].Message | Should -BeExactly "Rule '.\FromFile2' was suppressed by suppression group '.\SuppressWithTargetName' for 'TestObject2'. Ignore test objects by name."; + $warningMessages[6] | Should -BeOfType [System.Management.Automation.WarningRecord]; + $warningMessages[6].Message | Should -BeExactly "Rule '.\WithTag2' was suppressed by suppression group '.\SuppressWithNonProdTag' for 'TestObject2'. Ignore objects with non-production tag."; } It 'Show warnings for all rules when rule property is null or empty' { - $option = New-PSRuleOption -SuppressedRuleWarning $True -OutputAs Detail -InvariantCultureWarning $False; + $option = New-PSRuleOption -SuppressedRuleWarning $True -OutputAs Detail -InvariantCultureWarning $False -SuppressionGroupExpired Ignore; $Null = $testObject | Invoke-PSRule @invokeParams2 -Option $option -WarningVariable outWarnings -WarningAction SilentlyContinue; @@ -1327,7 +1329,7 @@ Describe 'Invoke-PSRule' -Tag 'Invoke-PSRule','Common' { } It 'No warnings' { - $option = New-PSRuleOption -SuppressedRuleWarning $False -OutputAs Detail -InvariantCultureWarning $False; + $option = New-PSRuleOption -SuppressedRuleWarning $False -OutputAs Detail -InvariantCultureWarning $False -SuppressionGroupExpired Ignore; $Null = $testObject | Invoke-PSRule @invokeParams -Option $option -Name 'FromFile1', 'FromFile2', 'WithTag2' -WarningVariable outWarnings -WarningAction SilentlyContinue; @@ -1338,7 +1340,7 @@ Describe 'Invoke-PSRule' -Tag 'Invoke-PSRule','Common' { Context 'Summary' { It 'Show warnings' { - $option = New-PSRuleOption -SuppressedRuleWarning $True -OutputAs Summary -InvariantCultureWarning $False -OutputCulture 'en-US'; + $option = New-PSRuleOption -SuppressedRuleWarning $True -OutputAs Summary -InvariantCultureWarning $False -SuppressionGroupExpired Ignore -OutputCulture 'en-US'; $Null = $testObject | Invoke-PSRule @invokeParams -Option $option -Name 'FromFile3', 'FromFile5', 'WithTag3' -WarningVariable outWarnings -WarningAction SilentlyContinue; @@ -1370,7 +1372,7 @@ Describe 'Invoke-PSRule' -Tag 'Invoke-PSRule','Common' { } It 'No warnings' { - $option = New-PSRuleOption -SuppressedRuleWarning $False -OutputAs Summary -InvariantCultureWarning $False; + $option = New-PSRuleOption -SuppressedRuleWarning $False -OutputAs Summary -InvariantCultureWarning $False -SuppressionGroupExpired Ignore; $Null = $testObject | Invoke-PSRule @invokeParams -Option $option -Name 'FromFile3', 'FromFile5', 'WithTag3' -WarningVariable outWarnings -WarningAction SilentlyContinue; diff --git a/tests/PSRule.Tests/PSRule.Options.Tests.ps1 b/tests/PSRule.Tests/PSRule.Options.Tests.ps1 index e88d85d41f..8e66223353 100644 --- a/tests/PSRule.Tests/PSRule.Options.Tests.ps1 +++ b/tests/PSRule.Tests/PSRule.Options.Tests.ps1 @@ -739,6 +739,53 @@ Describe 'New-PSRuleOption' -Tag 'Option','New-PSRuleOption' { } } + Context 'Read Execution.SuppressionGroupExpired' { + It 'from default' { + $option = New-PSRuleOption -Default; + $option.Execution.SuppressionGroupExpired | Should -Be 'Warn' + } + + It 'from Hashtable' { + $option = New-PSRuleOption -Option @{ 'Execution.SuppressionGroupExpired' = 'error' }; + $option.Execution.SuppressionGroupExpired | Should -Be 'Error'; + + $option = New-PSRuleOption -Option @{ 'Execution.SuppressionGroupExpired' = 'Error' }; + $option.Execution.SuppressionGroupExpired | Should -Be 'Error'; + } + + It 'from YAML' { + $option = New-PSRuleOption -Option (Join-Path -Path $here -ChildPath 'PSRule.Tests.yml'); + $option.Execution.SuppressionGroupExpired | Should -Be 'Debug'; + } + + It 'from Environment' { + try { + # With enum + $Env:PSRULE_EXECUTION_SUPPRESSIONGROUPEXPIRED = 'error'; + $option = New-PSRuleOption; + $option.Execution.SuppressionGroupExpired | Should -Be 'Error'; + + # With enum + $Env:PSRULE_EXECUTION_SUPPRESSIONGROUPEXPIRED = 'Error'; + $option = New-PSRuleOption; + $option.Execution.SuppressionGroupExpired | Should -Be 'Error'; + + # With int + $Env:PSRULE_EXECUTION_SUPPRESSIONGROUPEXPIRED = '3'; + $option = New-PSRuleOption; + $option.Execution.SuppressionGroupExpired | Should -Be 'Error'; + } + finally { + Remove-Item 'Env:PSRULE_EXECUTION_SUPPRESSIONGROUPEXPIRED' -Force; + } + } + + It 'from parameter' { + $option = New-PSRuleOption -SuppressionGroupExpired 'Error' -Path $emptyOptionsFilePath; + $option.Execution.SuppressionGroupExpired | Should -Be 'Error'; + } + } + Context 'Read Execution.AliasReferenceWarning' { It 'from default' { $option = New-PSRuleOption -Default; @@ -2086,6 +2133,13 @@ Describe 'Set-PSRuleOption' -Tag 'Option','Set-PSRuleOption' { } } + Context 'Read Execution.SuppressionGroupExpired' { + It 'from parameter' { + $option = Set-PSRuleOption -SuppressionGroupExpired 'Error' @optionParams; + $option.Execution.SuppressionGroupExpired | Should -Be 'Error'; + } + } + Context 'Read Execution.AliasReferenceWarning' { It 'from parameter' { $option = Set-PSRuleOption -AliasReferenceWarning $False @optionParams; diff --git a/tests/PSRule.Tests/PSRule.Tests.yml b/tests/PSRule.Tests/PSRule.Tests.yml index f109197e7a..72bc70c663 100644 --- a/tests/PSRule.Tests/PSRule.Tests.yml +++ b/tests/PSRule.Tests/PSRule.Tests.yml @@ -53,10 +53,11 @@ execution: duplicateResourceId: Warn languageMode: ConstrainedLanguage inconclusiveWarning: false - notProcessedWarning: false - suppressedRuleWarning: false invariantCultureWarning: false initialSessionState: Minimal + notProcessedWarning: false + suppressedRuleWarning: false + suppressionGroupExpired: Debug # Configure input options input: diff --git a/tests/PSRule.Tests/Selectors.Rule.jsonc b/tests/PSRule.Tests/Selectors.Rule.jsonc index f3a29ce09c..4897db5812 100644 --- a/tests/PSRule.Tests/Selectors.Rule.jsonc +++ b/tests/PSRule.Tests/Selectors.Rule.jsonc @@ -2,1765 +2,1765 @@ // Licensed under the MIT License. // Selectors for unit testing [ - { - // Synopsis: A selector to match basic objects - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "BasicSelector" - }, - "spec": { - "if": { - "allOf": [ - { - "field": "Name", - "equals": "TargetObject1" - }, - { - "field": "Value", - "equals": "value1" - } - ] - } - } - }, - { - // Synopsis: A selector to match objects using a specific JSON schema - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "SelectJsonSchema" - }, - "spec": { - "if": { - "field": "$schema", - "match": "^(https{0,1}://schema\\.management\\.azure\\.com/schemas/.*/deploymentTemplate\\.json)$" - } - } - }, - { - // Synopsis: A selector to match object with a field - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "HasCustomValueField" - }, - "spec": { - "if": { - "field": "CustomValue", - "exists": true - } - } - }, - { - // Synopsis: Test AnyOf - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonAnyOf" - }, - "spec": { - "if": { - "anyOf": [ - { - "field": "AlternateName", - "exists": true - }, - { - "field": "Name", - "exists": true - } - ] - } - } - }, - { - // Synopsis: Test AllOf - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonAllOf" - }, - "spec": { - "if": { - "allOf": [ - { - "field": "AlternateName", - "exists": true - }, - { - "field": "Name", - "exists": true - } - ] - } - } - }, - { - //Synopsis: Test Not - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonNot" - }, - "spec": { - "if": { - "not": { - "anyOf": [ - { - "field": "AlternateName", - "exists": true - }, - { - "field": "Name", - "exists": true - } - ] - } - } - } - }, - { - // Synopsis: Test custom value is in - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonCustomValueIn" - }, - "spec": { - "if": { - "field": "CustomValue", - "in": [ - "Value1", - "Value2" - ] - } - } - }, - { - // Synopsis: Test exists true - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonExistsTrue" - }, - "spec": { - "if": { - "field": "Value", - "exists": true - } - } - }, - { - // Synopsis: Test exists false - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonExistsFalse" - }, - "spec": { - "if": { - "field": "Value", + { + // Synopsis: A selector to match basic objects + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "BasicSelector" + }, + "spec": { + "if": { + "allOf": [ + { + "field": "Name", + "equals": "TargetObject1" + }, + { + "field": "Value", + "equals": "value1" + } + ] + } + } + }, + { + // Synopsis: A selector to match objects using a specific JSON schema + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "SelectJsonSchema" + }, + "spec": { + "if": { + "field": "$schema", + "match": "^(https{0,1}://schema\\.management\\.azure\\.com/schemas/.*/deploymentTemplate\\.json)$" + } + } + }, + { + // Synopsis: A selector to match object with a field + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "HasCustomValueField" + }, + "spec": { + "if": { + "field": "CustomValue", + "exists": true + } + } + }, + { + // Synopsis: Test AnyOf + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonAnyOf" + }, + "spec": { + "if": { + "anyOf": [ + { + "field": "AlternateName", + "exists": true + }, + { + "field": "Name", + "exists": true + } + ] + } + } + }, + { + // Synopsis: Test AllOf + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonAllOf" + }, + "spec": { + "if": { + "allOf": [ + { + "field": "AlternateName", + "exists": true + }, + { + "field": "Name", + "exists": true + } + ] + } + } + }, + { + //Synopsis: Test Not + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonNot" + }, + "spec": { + "if": { + "not": { + "anyOf": [ + { + "field": "AlternateName", + "exists": true + }, + { + "field": "Name", + "exists": true + } + ] + } + } + } + }, + { + // Synopsis: Test custom value is in + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonCustomValueIn" + }, + "spec": { + "if": { + "field": "CustomValue", + "in": [ + "Value1", + "Value2" + ] + } + } + }, + { + // Synopsis: Test exists true + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonExistsTrue" + }, + "spec": { + "if": { + "field": "Value", + "exists": true + } + } + }, + { + // Synopsis: Test exists false + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonExistsFalse" + }, + "spec": { + "if": { + "field": "Value", + "exists": false + } + } + }, + { + // Synopsis: Test equals + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonEquals" + }, + "spec": { + "if": { + "allOf": [ + { + "field": "ValueString", + "equals": "abc", + "caseSensitive": true + }, + { + "field": "ValueInt", + "equals": 123 + }, + { + "field": "ValueBool", + "equals": true + }, + { + "anyOf": [ + { + "field": "ValueEnum", "exists": false - } - } - }, - { - // Synopsis: Test equals - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonEquals" - }, - "spec": { - "if": { - "allOf": [ - { - "field": "ValueString", - "equals": "abc", - "caseSensitive": true - }, - { - "field": "ValueInt", - "equals": 123 - }, - { - "field": "ValueBool", - "equals": true - }, - { - "anyOf": [ - { - "field": "ValueEnum", - "exists": false - }, - { - "field": "ValueEnum", - "equals": "All", - "convert": true - } - ] - } - ] - } - } - }, - { - // Synopsis: Test notEquals - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonNotEquals" - }, - "spec": { - "if": { - "allOf": [ - { - "field": "ValueString", - "notEquals": "abc", - "caseSensitive": true - }, - { - "field": "ValueInt", - "notEquals": 123 - }, - { - "field": "ValueBool", - "notEquals": true - }, - { - "anyOf": [ - { - "field": "ValueEnum", - "exists": false - }, - { - "field": "ValueEnum", - "notEquals": "All", - "convert": true - } - ] - } - ] - } - } - }, - { - // Synopsis: Test hasValue true - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonHasValueTrue" - }, - "spec": { - "if": { - "field": "Value", - "hasValue": true - } - } - }, - { - // Synopsis: Test hasValue false - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonHasValueFalse" - }, - "spec": { - "if": { - "field": "Value", - "hasValue": false - } - } - }, - { - // Synopsis: Test match - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonMatch" - }, - "spec": { - "if": { - "field": "Value", - "match": "^(abc|efg)$" - } - } - }, - { - // Synopsis: Test notMatch - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonNotMatch" - }, - "spec": { - "if": { - "field": "Value", - "notMatch": "^(abc|efg)$" - } - } - }, - { - // Synopsis: Test in - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonIn" - }, - "spec": { - "if": { - "field": "Value", - "in": [ - "Value1", - "Value2" - ] - } - } - }, - { - // Synopsis: Test notIn - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonNotIn" - }, - "spec": { - "if": { - "field": "Value", - "notIn": [ - "Value1", - "Value2" - ] - } - } - }, - { - // Synopsis: Test less - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonLess" - }, - "spec": { - "if": { - "anyOf": [ - { - "field": "Value", - "less": 3 - }, - { - "field": "ValueStr", - "less": 0, - "convert": true - } - ] - } - } - }, - { - //Synopsis: Test lessOrEquals - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonLessOrEquals" - }, - "spec": { - "if": { - "anyOf": [ - { - "field": "Value", - "lessOrEquals": 3 - }, - { - "field": "ValueStr", - "lessOrEquals": 0, - "convert": true - } - ] - } - } - }, - { - // Synopsis: Test greater - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonGreater" - }, - "spec": { - "if": { - "anyOf": [ - { - "field": "Value", - "greater": 3 - }, - { - "field": "ValueStr", - "greater": -1, - "convert": true - } - ] - } - } - }, - { - // Synopsis: Test greaterOrEquals - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonGreaterOrEquals" - }, - "spec": { - "if": { - "anyOf": [ - { - "field": "Value", - "greaterOrEquals": 3 - }, - { - "field": "ValueStr", - "greaterOrEquals": -1, - "convert": true - } - ] - } - } - }, - { - // Synopsis: Test startsWith - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonStartsWith" - }, - "spec": { - "if": { - "allOf": [ - { - "anyOf": [ - { - "field": "Value", - "startsWith": "a" - }, - { - "field": "Value", - "startsWith": "e", - "caseSensitive": true - } - ] - }, - { - "field": "Value", - "startsWith": [ - "a", - "e" - ] - }, - { - "anyOf": [ - { - "field": "OtherValue", - "exists": false - }, - { - "field": "OtherValue", - "startsWith": "a", - "convert": true - } - ] - } - ] - } - } - }, - { - // Synopsis: Test notStartsWith - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonNotStartsWith" - }, - "spec": { - "if": { - "allOf": [ - { - "anyOf": [ - { - "field": "Value", - "notStartsWith": "a" - }, - { - "field": "Value", - "notStartsWith": "e", - "caseSensitive": true - } - ] - }, - { - "field": "Value", - "notStartsWith": [ - "a", - "e" - ] - }, - { - "anyOf": [ - { - "field": "OtherValue", - "exists": false - }, - { - "field": "OtherValue", - "notStartsWith": "a", - "convert": true - } - ] - } - ] - } - } - }, - { - // Synopsis: Test endsWith - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonEndsWith" - }, - "spec": { - "if": { - "allOf": [ - { - "anyOf": [ - { - "field": "Value", - "endsWith": "c" - }, - { - "field": "Value", - "endsWith": "g", - "caseSensitive": true - } - ] - }, - { - "field": "Value", - "endsWith": [ - "c", - "g" - ] - }, - { - "anyOf": [ - { - "field": "OtherValue", - "exists": false - }, - { - "field": "OtherValue", - "endsWith": "l", - "convert": true - } - ] - } - ] - } - } - }, - { - // Synopsis: Test notEndsWith - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonNotEndsWith" - }, - "spec": { - "if": { - "allOf": [ - { - "anyOf": [ - { - "field": "Value", - "notEndsWith": "c" - }, - { - "field": "Value", - "notEndsWith": "g", - "caseSensitive": true - } - ] - }, - { - "field": "Value", - "notEndsWith": [ - "c", - "g" - ] - }, - { - "anyOf": [ - { - "field": "OtherValue", - "exists": false - }, - { - "field": "OtherValue", - "notEndsWith": "l", - "convert": true - } - ] - } - ] - } - } - }, - { - // Synopsis: Test contains - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonContains" - }, - "spec": { - "if": { - "allOf": [ - { - "anyOf": [ - { - "field": "Value", - "contains": "ab" - }, - { - "field": "Value", - "contains": "bc", - "caseSensitive": true - } - ] - }, - { - "field": "Value", - "contains": [ - "ab", - "bc" - ] - }, - { - "anyOf": [ - { - "field": "OtherValue", - "exists": false - }, - { - "field": "OtherValue", - "contains": "l", - "convert": true - } - ] - } - ] - } - } - }, - { - // Synopsis: Test notContains - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonNotContains" - }, - "spec": { - "if": { - "allOf": [ - { - "anyOf": [ - { - "field": "Value", - "notContains": "ab" - }, - { - "field": "Value", - "notContains": "bc", - "caseSensitive": true - } - ] - }, - { - "field": "Value", - "notContains": [ - "ab", - "bc" - ] - }, - { - "anyOf": [ - { - "field": "OtherValue", - "exists": false - }, - { - "field": "OtherValue", - "notContains": "l", - "convert": true - } - ] - } - ] - } - } - }, - { - // Synopsis: Test like - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonLike" - }, - "spec": { - "if": { - "allOf": [ - { - "anyOf": [ - { - "field": "Value", - "like": "ab*" - }, - { - "field": "Value", - "like": "*f*", - "caseSensitive": true - } - ] - }, - { - "field": "Value", - "like": [ - "ab*", - "*f*" - ] - }, - { - "anyOf": [ - { - "field": "OtherValue", - "exists": false - }, - { - "field": "OtherValue", - "like": "12*", - "convert": true - } - ] - } - ] - } - } - }, - { - // Synopsis: Test notLike - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonNotLike" - }, - "spec": { - "if": { - "allOf": [ - { - "anyOf": [ - { - "field": "Value", - "notLike": "ab*" - }, - { - "field": "Value", - "notLike": "*f*", - "caseSensitive": true - } - ] - }, - { - "field": "Value", - "notLike": [ - "ab*", - "*f*" - ] - }, - { - "anyOf": [ - { - "field": "OtherValue", - "exists": false - }, - { - "field": "OtherValue", - "notLike": "12*", - "convert": true - } - ] - } - ] - } - } - }, - { - // Synopsis: Test isString - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonIsStringTrue" - }, - "spec": { - "if": { - "field": "Value", - "isString": true - } - } - }, - { - // Synopsis: Test isString - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonIsStringFalse" - }, - "spec": { - "if": { - "field": "Value", - "isString": false - } - } - }, - { - // Synopsis: Test isArray - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonIsArrayTrue" - }, - "spec": { - "if": { - "field": "Value", - "isArray": true - } - } - }, - { - // Synopsis: Test isArray - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonIsArrayFalse" - }, - "spec": { - "if": { - "field": "Value", - "isArray": false - } - } - }, - { - // Synopsis: Test IsBoolean - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonIsBooleanTrue" - }, - "spec": { - "if": { - "field": "Value", - "isBoolean": true - } - } - }, - { - // Synopsis: Test IsBoolean with conversion - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonIsBooleanTrueWithConversion" - }, - "spec": { - "if": { - "field": "Value", - "isBoolean": true, + }, + { + "field": "ValueEnum", + "equals": "All", "convert": true - } - } - }, - { - // Synopsis: Test IsBoolean - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonIsBooleanFalse" - }, - "spec": { - "if": { - "field": "Value", - "isBoolean": false - } - } - }, - { - // Synopsis: Test IsBoolean with conversion - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonIsBooleanFalseWithConversion" - }, - "spec": { - "if": { - "field": "Value", - "isBoolean": false, + } + ] + } + ] + } + } + }, + { + // Synopsis: Test notEquals + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonNotEquals" + }, + "spec": { + "if": { + "allOf": [ + { + "field": "ValueString", + "notEquals": "abc", + "caseSensitive": true + }, + { + "field": "ValueInt", + "notEquals": 123 + }, + { + "field": "ValueBool", + "notEquals": true + }, + { + "anyOf": [ + { + "field": "ValueEnum", + "exists": false + }, + { + "field": "ValueEnum", + "notEquals": "All", "convert": true - } - } - }, - { - // Synopsis: Test IsDateTime - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonIsDateTimeTrue" - }, - "spec": { - "if": { + } + ] + } + ] + } + } + }, + { + // Synopsis: Test hasValue true + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonHasValueTrue" + }, + "spec": { + "if": { + "field": "Value", + "hasValue": true + } + } + }, + { + // Synopsis: Test hasValue false + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonHasValueFalse" + }, + "spec": { + "if": { + "field": "Value", + "hasValue": false + } + } + }, + { + // Synopsis: Test match + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonMatch" + }, + "spec": { + "if": { + "field": "Value", + "match": "^(abc|efg)$" + } + } + }, + { + // Synopsis: Test notMatch + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonNotMatch" + }, + "spec": { + "if": { + "field": "Value", + "notMatch": "^(abc|efg)$" + } + } + }, + { + // Synopsis: Test in + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonIn" + }, + "spec": { + "if": { + "field": "Value", + "in": [ + "Value1", + "Value2" + ] + } + } + }, + { + // Synopsis: Test notIn + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonNotIn" + }, + "spec": { + "if": { + "field": "Value", + "notIn": [ + "Value1", + "Value2" + ] + } + } + }, + { + // Synopsis: Test less + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonLess" + }, + "spec": { + "if": { + "anyOf": [ + { + "field": "Value", + "less": 3 + }, + { + "field": "ValueStr", + "less": 0, + "convert": true + } + ] + } + } + }, + { + //Synopsis: Test lessOrEquals + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonLessOrEquals" + }, + "spec": { + "if": { + "anyOf": [ + { + "field": "Value", + "lessOrEquals": 3 + }, + { + "field": "ValueStr", + "lessOrEquals": 0, + "convert": true + } + ] + } + } + }, + { + // Synopsis: Test greater + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonGreater" + }, + "spec": { + "if": { + "anyOf": [ + { + "field": "Value", + "greater": 3 + }, + { + "field": "ValueStr", + "greater": -1, + "convert": true + } + ] + } + } + }, + { + // Synopsis: Test greaterOrEquals + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonGreaterOrEquals" + }, + "spec": { + "if": { + "anyOf": [ + { + "field": "Value", + "greaterOrEquals": 3 + }, + { + "field": "ValueStr", + "greaterOrEquals": -1, + "convert": true + } + ] + } + } + }, + { + // Synopsis: Test startsWith + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonStartsWith" + }, + "spec": { + "if": { + "allOf": [ + { + "anyOf": [ + { "field": "Value", - "isDateTime": true - } - } - }, - { - // Synopsis: Test IsDateTime with conversion - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonIsDateTimeTrueWithConversion" - }, - "spec": { - "if": { + "startsWith": "a" + }, + { "field": "Value", - "isDateTime": true, + "startsWith": "e", + "caseSensitive": true + } + ] + }, + { + "field": "Value", + "startsWith": [ + "a", + "e" + ] + }, + { + "anyOf": [ + { + "field": "OtherValue", + "exists": false + }, + { + "field": "OtherValue", + "startsWith": "a", "convert": true - } - } - }, - { - // Synopsis: Test IsDateTime - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonIsDateTimeFalse" - }, - "spec": { - "if": { + } + ] + } + ] + } + } + }, + { + // Synopsis: Test notStartsWith + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonNotStartsWith" + }, + "spec": { + "if": { + "allOf": [ + { + "anyOf": [ + { "field": "Value", - "isDateTime": false - } - } - }, - { - // Synopsis: Test IsDateTime with conversion - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonIsDateTimeFalseWithConversion" - }, - "spec": { - "if": { + "notStartsWith": "a" + }, + { "field": "Value", - "isDateTime": false, + "notStartsWith": "e", + "caseSensitive": true + } + ] + }, + { + "field": "Value", + "notStartsWith": [ + "a", + "e" + ] + }, + { + "anyOf": [ + { + "field": "OtherValue", + "exists": false + }, + { + "field": "OtherValue", + "notStartsWith": "a", "convert": true - } - } - }, - { - // Synopsis: Test IsInteger - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonIsIntegerTrue" - }, - "spec": { - "if": { + } + ] + } + ] + } + } + }, + { + // Synopsis: Test endsWith + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonEndsWith" + }, + "spec": { + "if": { + "allOf": [ + { + "anyOf": [ + { "field": "Value", - "isInteger": true - } - } - }, - { - // Synopsis: Test IsInteger with conversion - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonIsIntegerTrueWithConversion" - }, - "spec": { - "if": { + "endsWith": "c" + }, + { "field": "Value", - "isInteger": true, + "endsWith": "g", + "caseSensitive": true + } + ] + }, + { + "field": "Value", + "endsWith": [ + "c", + "g" + ] + }, + { + "anyOf": [ + { + "field": "OtherValue", + "exists": false + }, + { + "field": "OtherValue", + "endsWith": "l", "convert": true - } - } - }, - { - // Synopsis: Test IsInteger - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonIsIntegerFalse" - }, - "spec": { - "if": { + } + ] + } + ] + } + } + }, + { + // Synopsis: Test notEndsWith + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonNotEndsWith" + }, + "spec": { + "if": { + "allOf": [ + { + "anyOf": [ + { "field": "Value", - "isInteger": false - } - } - }, - { - // Synopsis: Test IsInteger with conversion - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonIsIntegerFalseWithConversion" - }, - "spec": { - "if": { + "notEndsWith": "c" + }, + { "field": "Value", - "isInteger": false, + "notEndsWith": "g", + "caseSensitive": true + } + ] + }, + { + "field": "Value", + "notEndsWith": [ + "c", + "g" + ] + }, + { + "anyOf": [ + { + "field": "OtherValue", + "exists": false + }, + { + "field": "OtherValue", + "notEndsWith": "l", "convert": true - } - } - }, - { - // Synopsis: Test IsNumeric - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonIsNumericTrue" - }, - "spec": { - "if": { + } + ] + } + ] + } + } + }, + { + // Synopsis: Test contains + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonContains" + }, + "spec": { + "if": { + "allOf": [ + { + "anyOf": [ + { "field": "Value", - "isNumeric": true - } - } - }, - { - // Synopsis: Test IsNumeric - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonIsNumericTrueWithConversion" - }, - "spec": { - "if": { + "contains": "ab" + }, + { "field": "Value", - "isNumeric": true, + "contains": "bc", + "caseSensitive": true + } + ] + }, + { + "field": "Value", + "contains": [ + "ab", + "bc" + ] + }, + { + "anyOf": [ + { + "field": "OtherValue", + "exists": false + }, + { + "field": "OtherValue", + "contains": "l", "convert": true - } - } - }, - { - // Synopsis: Test IsNumeric - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonIsNumericFalse" - }, - "spec": { - "if": { + } + ] + } + ] + } + } + }, + { + // Synopsis: Test notContains + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonNotContains" + }, + "spec": { + "if": { + "allOf": [ + { + "anyOf": [ + { "field": "Value", - "isNumeric": false - } - } - }, - { - // Synopsis: Test IsNumeric - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonIsNumericFalseWithConversion" - }, - "spec": { - "if": { + "notContains": "ab" + }, + { "field": "Value", - "isNumeric": false, + "notContains": "bc", + "caseSensitive": true + } + ] + }, + { + "field": "Value", + "notContains": [ + "ab", + "bc" + ] + }, + { + "anyOf": [ + { + "field": "OtherValue", + "exists": false + }, + { + "field": "OtherValue", + "notContains": "l", "convert": true - } - } - }, - { - // Synopsis: Test isLower - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonIsLowerTrue" - }, - "spec": { - "if": { - "field": "Value", - "isLower": true - } - } - }, - { - // Synopsis: Test isLower - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonIsLowerFalse" - }, - "spec": { - "if": { - "field": "Value", - "isLower": false - } - } - }, - { - // Synopsis: Test isUpper - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonIsUpperTrue" - }, - "spec": { - "if": { - "field": "Value", - "isUpper": true - } - } - }, - { - // Synopsis: Test isUpper - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonIsUpperFalse" - }, - "spec": { - "if": { + } + ] + } + ] + } + } + }, + { + // Synopsis: Test like + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonLike" + }, + "spec": { + "if": { + "allOf": [ + { + "anyOf": [ + { "field": "Value", - "isUpper": false - } - } - }, - { - // Synopsis: Test setOf - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonSetOf" - }, - "spec": { - "if": { + "like": "ab*" + }, + { "field": "Value", - "setOf": [ - "cluster-autoscaler", - "kube-apiserver" - ], + "like": "*f*", "caseSensitive": true - } - } - }, - { - // Synopsis: Test subset - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonSubset" - }, - "spec": { - "if": { + } + ] + }, + { + "field": "Value", + "like": [ + "ab*", + "*f*" + ] + }, + { + "anyOf": [ + { + "field": "OtherValue", + "exists": false + }, + { + "field": "OtherValue", + "like": "12*", + "convert": true + } + ] + } + ] + } + } + }, + { + // Synopsis: Test notLike + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonNotLike" + }, + "spec": { + "if": { + "allOf": [ + { + "anyOf": [ + { "field": "Value", - "subset": [ - "cluster-autoscaler", - "kube-apiserver" - ], - "caseSensitive": true, - "unique": true - } - } - }, - { - // Synopsis: Test count - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonCount" - }, - "spec": { - "if": { + "notLike": "ab*" + }, + { "field": "Value", - "count": 2 - } - } - }, - { - // Synopsis: Test notCount - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonNotCount" - }, - "spec": { - "if": { - "field": "Value", - "notCount": 2 - } - } - }, - { - // Synopsis: Test hasSchema - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonHasSchema" - }, - "spec": { - "if": { - "field": ".", - "hasSchema": [ - "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", - "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#" - ] - } - } - }, - { - // Synopsis: Test hasSchema ignoring scheme - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonHasSchemaIgnoreScheme" - }, - "spec": { - "if": { - "field": ".", - "hasSchema": [ - "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", - "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#" - ], - "ignoreScheme": true - } - } - }, - { - // Synopsis: Test hasSchema with case sensitivity - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonHasSchemaCaseSensitive" - }, - "spec": { - "if": { - "field": ".", - "hasSchema": [ - "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", - "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#" - ], + "notLike": "*f*", "caseSensitive": true - } - } - }, - { - // Synopsis: Test hasSchema matching any schema - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonHasAnySchema" - }, - "spec": { - "if": { - "field": ".", - "hasSchema": [] - } - } - }, - { - // Synopsis: Test version - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonVersion" - }, - "spec": { - "if": { - "field": "version", - "version": "^1.2.3" - } - } - }, - { - // Synopsis: Test version with prerelease - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonVersionWithPrerelease" - }, - "spec": { - "if": { - "field": "version", - "version": "^1.2.3", - "includePrerelease": true - } - } - }, - { - // Synopsis: Test any valid version - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonVersionAnyVersion" - }, - "spec": { - "if": { - "field": "version", - "version": "" - } - } - }, - { - // Synopsis: Test hasDefault - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonHasDefault" - }, - "spec": { - "if": { - "allOf": [ - { - "field": "integerValue", - "hasDefault": 100 - }, - { - "field": "boolValue", - "hasDefault": true - }, - { - "field": "stringValue", - "hasDefault": "testValue", - "caseSensitive": true - } - ] - } - } - }, - { - // Synopsis: Test with type - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonTypeEquals" - }, - "spec": { - "if": { - "type": ".", - "equals": "CustomType1" - } - } - }, - { - // Synopsis: Test equals with name - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonNameEquals" - }, - "spec": { - "if": { - "name": ".", - "equals": "TargetObject1" - } - } - }, - { - // Synopsis: Test notEquals with name - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonNameNotEquals" - }, - "spec": { - "if": { - "name": ".", - "notEquals": "TargetObject1" - } - } - }, - { - // Synopsis: Test hasValue with name - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonNameHasValue" - }, - "spec": { - "if": { - "name": ".", - "hasValue": true - } - } - }, - { - // Synopsis: Test match with name - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonNameMatch" - }, - "spec": { - "if": { - "name": ".", - "match": ".*1$" - } - } - }, - { - // Synopsis: Test notMatch with name - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonNameNotMatch" - }, - "spec": { - "if": { - "name": ".", - "notMatch": ".*1$" - } - } - }, - { - // Synopsis: Test in with name - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonNameIn" - }, - "spec": { - "if": { - "name": ".", - "in": [ - "TargetObject1" - ] - } - } - }, - { - // Synopsis: Test notIn with name - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonNameNotIn" - }, - "spec": { - "if": { - "name": ".", - "notIn": [ - "TargetObject1" - ] - } - } - }, - { - // Synopsis: Test startsWith with name - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonNameStartsWith" - }, - "spec": { - "if": { - "name": ".", - "startsWith": [ - "1" - ] - } - } - }, - { - // Synopsis: Test endsWith with name - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonNameEndsWith" - }, - "spec": { - "if": { - "name": ".", - "endsWith": [ - "1" - ] - } - } - }, - { - // Synopsis: Test contains with name - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonNameContains" - }, - "spec": { - "if": { - "name": ".", - "contains": [ - ".1." - ] - } - } - }, - { - // Synopsis: Test isString with name - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonNameIsString" - }, - "spec": { - "if": { - "name": ".", - "isString": true - } - } - }, - { - // Synopsis: Test less with name - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonNameLess" - }, - "spec": { - "if": { - "name": ".", - "less": 8 - } - } - }, - { - // Synopsis: Test lessOrEquals with name - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonNameLessOrEquals" - }, - "spec": { - "if": { - "name": ".", - "lessOrEquals": 7 - } - } - }, - { - // Synopsis: Test greater with name - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonNameGreater" - }, - "spec": { - "if": { - "name": ".", - "greater": 8 - } - } - }, - { - // Synopsis: Test greaterOrEquals with name - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonNameGreaterOrEquals" - }, - "spec": { - "if": { - "name": ".", - "greaterOrEquals": 9 - } - } - }, - { - // Synopsis: Test isLower with name - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonNameIsLower" - }, - "spec": { - "if": { - "name": ".", - "isLower": true - } - } - }, - { - // Synopsis: Test isUpper with name - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonNameIsUpper" - }, - "spec": { - "if": { - "name": ".", - "isUpper": true - } - } - }, - { - // Synopsis: Test endsWith with source - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonEndsWithSource" - }, - "spec": { - "if": { - "source": "Template", - "endsWith": [ - "/template.json" - ] - } - } - }, - { - // Synopsis: Test withinPath with source - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonSourceWithinPath" - }, - "spec": { - "if": { - "source": "Template", - "withinPath": [ - "deployments/path/" - ] - } - } - }, - { - // Synopsis: Test withinPath with field - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonFieldWithinPath" - }, - "spec": { - "if": { - "field": "FullName", - "withinPath": [ - "policy/", - "security/" - ] - } - } - }, - { - // Synopsis: Test withinPath with source and case sensitive - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonSourceWithinPathCaseSensitive" - }, - "spec": { - "if": { - "source": "Template", - "withinPath": [ - "Deployments/Path/" - ], - "caseSensitive": true - } - } - }, - { - // Synopsis: Test withinPath with field and case sensitive - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonFieldWithinPathCaseSensitive" - }, - "spec": { - "if": { - "field": "FullName", - "withinPath": [ - "POLICY/", - "SECURITY/" - ], - "caseSensitive": true - } - } - }, - { - // Synopsis: Test notWithinPath with source - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonSourceNotWithinPath" - }, - "spec": { - "if": { - "source": "Template", - "notWithinPath": [ - "deployments/path/" - ] - } - } - }, - { - // Synopsis: Test notWithinPath with field - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonFieldNotWithinPath" - }, - "spec": { - "if": { - "field": "FullName", - "notWithinPath": [ - "policy/", - "security/" - ] - } - } - }, - { - // Synopsis: Test notWithinPath with source and case sensitive - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonSourceNotWithinPathCaseSensitive" - }, - "spec": { - "if": { - "source": "Template", - "notWithinPath": [ - "Deployments/Path/" - ], - "caseSensitive": true - } - } - }, - { - // Synopsis: Test notWithinPath with field and case sensitive - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "JsonFieldNotWithinPathCaseSensitive" - }, - "spec": { - "if": { - "field": "FullName", - "notWithinPath": [ - "POLICY/", - "SECURITY/" - ], - "caseSensitive": true - } - } + } + ] + }, + { + "field": "Value", + "notLike": [ + "ab*", + "*f*" + ] + }, + { + "anyOf": [ + { + "field": "OtherValue", + "exists": false + }, + { + "field": "OtherValue", + "notLike": "12*", + "convert": true + } + ] + } + ] + } + } + }, + { + // Synopsis: Test isString + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonIsStringTrue" + }, + "spec": { + "if": { + "field": "Value", + "isString": true + } + } + }, + { + // Synopsis: Test isString + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonIsStringFalse" + }, + "spec": { + "if": { + "field": "Value", + "isString": false + } + } + }, + { + // Synopsis: Test isArray + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonIsArrayTrue" + }, + "spec": { + "if": { + "field": "Value", + "isArray": true + } + } + }, + { + // Synopsis: Test isArray + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonIsArrayFalse" + }, + "spec": { + "if": { + "field": "Value", + "isArray": false + } + } + }, + { + // Synopsis: Test IsBoolean + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonIsBooleanTrue" + }, + "spec": { + "if": { + "field": "Value", + "isBoolean": true + } + } + }, + { + // Synopsis: Test IsBoolean with conversion + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonIsBooleanTrueWithConversion" + }, + "spec": { + "if": { + "field": "Value", + "isBoolean": true, + "convert": true + } + } + }, + { + // Synopsis: Test IsBoolean + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonIsBooleanFalse" + }, + "spec": { + "if": { + "field": "Value", + "isBoolean": false + } + } + }, + { + // Synopsis: Test IsBoolean with conversion + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonIsBooleanFalseWithConversion" + }, + "spec": { + "if": { + "field": "Value", + "isBoolean": false, + "convert": true + } + } + }, + { + // Synopsis: Test IsDateTime + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonIsDateTimeTrue" + }, + "spec": { + "if": { + "field": "Value", + "isDateTime": true + } + } + }, + { + // Synopsis: Test IsDateTime with conversion + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonIsDateTimeTrueWithConversion" + }, + "spec": { + "if": { + "field": "Value", + "isDateTime": true, + "convert": true + } + } + }, + { + // Synopsis: Test IsDateTime + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonIsDateTimeFalse" + }, + "spec": { + "if": { + "field": "Value", + "isDateTime": false + } + } + }, + { + // Synopsis: Test IsDateTime with conversion + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonIsDateTimeFalseWithConversion" + }, + "spec": { + "if": { + "field": "Value", + "isDateTime": false, + "convert": true + } + } + }, + { + // Synopsis: Test IsInteger + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonIsIntegerTrue" + }, + "spec": { + "if": { + "field": "Value", + "isInteger": true + } + } + }, + { + // Synopsis: Test IsInteger with conversion + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonIsIntegerTrueWithConversion" + }, + "spec": { + "if": { + "field": "Value", + "isInteger": true, + "convert": true + } + } + }, + { + // Synopsis: Test IsInteger + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonIsIntegerFalse" + }, + "spec": { + "if": { + "field": "Value", + "isInteger": false + } + } + }, + { + // Synopsis: Test IsInteger with conversion + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonIsIntegerFalseWithConversion" + }, + "spec": { + "if": { + "field": "Value", + "isInteger": false, + "convert": true + } + } + }, + { + // Synopsis: Test IsNumeric + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonIsNumericTrue" + }, + "spec": { + "if": { + "field": "Value", + "isNumeric": true + } + } + }, + { + // Synopsis: Test IsNumeric + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonIsNumericTrueWithConversion" + }, + "spec": { + "if": { + "field": "Value", + "isNumeric": true, + "convert": true + } + } + }, + { + // Synopsis: Test IsNumeric + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonIsNumericFalse" + }, + "spec": { + "if": { + "field": "Value", + "isNumeric": false + } + } + }, + { + // Synopsis: Test IsNumeric + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonIsNumericFalseWithConversion" + }, + "spec": { + "if": { + "field": "Value", + "isNumeric": false, + "convert": true + } + } + }, + { + // Synopsis: Test isLower + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonIsLowerTrue" + }, + "spec": { + "if": { + "field": "Value", + "isLower": true + } + } + }, + { + // Synopsis: Test isLower + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonIsLowerFalse" + }, + "spec": { + "if": { + "field": "Value", + "isLower": false + } + } + }, + { + // Synopsis: Test isUpper + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonIsUpperTrue" + }, + "spec": { + "if": { + "field": "Value", + "isUpper": true + } + } + }, + { + // Synopsis: Test isUpper + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonIsUpperFalse" + }, + "spec": { + "if": { + "field": "Value", + "isUpper": false + } + } + }, + { + // Synopsis: Test setOf + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonSetOf" + }, + "spec": { + "if": { + "field": "Value", + "setOf": [ + "cluster-autoscaler", + "kube-apiserver" + ], + "caseSensitive": true + } + } + }, + { + // Synopsis: Test subset + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonSubset" + }, + "spec": { + "if": { + "field": "Value", + "subset": [ + "cluster-autoscaler", + "kube-apiserver" + ], + "caseSensitive": true, + "unique": true + } + } + }, + { + // Synopsis: Test count + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonCount" + }, + "spec": { + "if": { + "field": "Value", + "count": 2 + } + } + }, + { + // Synopsis: Test notCount + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonNotCount" + }, + "spec": { + "if": { + "field": "Value", + "notCount": 2 + } + } + }, + { + // Synopsis: Test hasSchema + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonHasSchema" + }, + "spec": { + "if": { + "field": ".", + "hasSchema": [ + "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#" + ] + } + } + }, + { + // Synopsis: Test hasSchema ignoring scheme + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonHasSchemaIgnoreScheme" + }, + "spec": { + "if": { + "field": ".", + "hasSchema": [ + "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#" + ], + "ignoreScheme": true + } + } + }, + { + // Synopsis: Test hasSchema with case sensitivity + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonHasSchemaCaseSensitive" + }, + "spec": { + "if": { + "field": ".", + "hasSchema": [ + "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#" + ], + "caseSensitive": true + } + } + }, + { + // Synopsis: Test hasSchema matching any schema + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonHasAnySchema" + }, + "spec": { + "if": { + "field": ".", + "hasSchema": [] + } + } + }, + { + // Synopsis: Test version + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonVersion" + }, + "spec": { + "if": { + "field": "version", + "version": "^1.2.3" + } + } + }, + { + // Synopsis: Test version with prerelease + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonVersionWithPrerelease" + }, + "spec": { + "if": { + "field": "version", + "version": "^1.2.3", + "includePrerelease": true + } + } + }, + { + // Synopsis: Test any valid version + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonVersionAnyVersion" + }, + "spec": { + "if": { + "field": "version", + "version": "" + } + } + }, + { + // Synopsis: Test hasDefault + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonHasDefault" + }, + "spec": { + "if": { + "allOf": [ + { + "field": "integerValue", + "hasDefault": 100 + }, + { + "field": "boolValue", + "hasDefault": true + }, + { + "field": "stringValue", + "hasDefault": "testValue", + "caseSensitive": true + } + ] + } + } + }, + { + // Synopsis: Test with type + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonTypeEquals" + }, + "spec": { + "if": { + "type": ".", + "equals": "CustomType1" + } + } + }, + { + // Synopsis: Test equals with name + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonNameEquals" + }, + "spec": { + "if": { + "name": ".", + "equals": "TargetObject1" + } + } + }, + { + // Synopsis: Test notEquals with name + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonNameNotEquals" + }, + "spec": { + "if": { + "name": ".", + "notEquals": "TargetObject1" + } + } + }, + { + // Synopsis: Test hasValue with name + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonNameHasValue" + }, + "spec": { + "if": { + "name": ".", + "hasValue": true + } + } + }, + { + // Synopsis: Test match with name + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonNameMatch" + }, + "spec": { + "if": { + "name": ".", + "match": ".*1$" + } + } + }, + { + // Synopsis: Test notMatch with name + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonNameNotMatch" + }, + "spec": { + "if": { + "name": ".", + "notMatch": ".*1$" + } + } + }, + { + // Synopsis: Test in with name + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonNameIn" + }, + "spec": { + "if": { + "name": ".", + "in": [ + "TargetObject1" + ] + } + } + }, + { + // Synopsis: Test notIn with name + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonNameNotIn" + }, + "spec": { + "if": { + "name": ".", + "notIn": [ + "TargetObject1" + ] + } + } + }, + { + // Synopsis: Test startsWith with name + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonNameStartsWith" + }, + "spec": { + "if": { + "name": ".", + "startsWith": [ + "1" + ] + } + } + }, + { + // Synopsis: Test endsWith with name + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonNameEndsWith" + }, + "spec": { + "if": { + "name": ".", + "endsWith": [ + "1" + ] + } + } + }, + { + // Synopsis: Test contains with name + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonNameContains" + }, + "spec": { + "if": { + "name": ".", + "contains": [ + ".1." + ] + } + } + }, + { + // Synopsis: Test isString with name + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonNameIsString" + }, + "spec": { + "if": { + "name": ".", + "isString": true + } + } + }, + { + // Synopsis: Test less with name + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonNameLess" + }, + "spec": { + "if": { + "name": ".", + "less": 8 + } + } + }, + { + // Synopsis: Test lessOrEquals with name + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonNameLessOrEquals" + }, + "spec": { + "if": { + "name": ".", + "lessOrEquals": 7 + } + } + }, + { + // Synopsis: Test greater with name + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonNameGreater" + }, + "spec": { + "if": { + "name": ".", + "greater": 8 + } + } + }, + { + // Synopsis: Test greaterOrEquals with name + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonNameGreaterOrEquals" + }, + "spec": { + "if": { + "name": ".", + "greaterOrEquals": 9 + } + } + }, + { + // Synopsis: Test isLower with name + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonNameIsLower" + }, + "spec": { + "if": { + "name": ".", + "isLower": true + } + } + }, + { + // Synopsis: Test isUpper with name + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonNameIsUpper" + }, + "spec": { + "if": { + "name": ".", + "isUpper": true + } + } + }, + { + // Synopsis: Test endsWith with source + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonEndsWithSource" + }, + "spec": { + "if": { + "source": "Template", + "endsWith": [ + "/template.json" + ] + } + } + }, + { + // Synopsis: Test withinPath with source + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonSourceWithinPath" + }, + "spec": { + "if": { + "source": "Template", + "withinPath": [ + "deployments/path/" + ] + } + } + }, + { + // Synopsis: Test withinPath with field + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonFieldWithinPath" + }, + "spec": { + "if": { + "field": "FullName", + "withinPath": [ + "policy/", + "security/" + ] + } + } + }, + { + // Synopsis: Test withinPath with source and case sensitive + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonSourceWithinPathCaseSensitive" + }, + "spec": { + "if": { + "source": "Template", + "withinPath": [ + "Deployments/Path/" + ], + "caseSensitive": true + } + } + }, + { + // Synopsis: Test withinPath with field and case sensitive + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonFieldWithinPathCaseSensitive" + }, + "spec": { + "if": { + "field": "FullName", + "withinPath": [ + "POLICY/", + "SECURITY/" + ], + "caseSensitive": true + } + } + }, + { + // Synopsis: Test notWithinPath with source + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonSourceNotWithinPath" + }, + "spec": { + "if": { + "source": "Template", + "notWithinPath": [ + "deployments/path/" + ] + } + } + }, + { + // Synopsis: Test notWithinPath with field + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonFieldNotWithinPath" + }, + "spec": { + "if": { + "field": "FullName", + "notWithinPath": [ + "policy/", + "security/" + ] + } + } + }, + { + // Synopsis: Test notWithinPath with source and case sensitive + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonSourceNotWithinPathCaseSensitive" + }, + "spec": { + "if": { + "source": "Template", + "notWithinPath": [ + "Deployments/Path/" + ], + "caseSensitive": true + } + } + }, + { + // Synopsis: Test notWithinPath with field and case sensitive + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonFieldNotWithinPathCaseSensitive" + }, + "spec": { + "if": { + "field": "FullName", + "notWithinPath": [ + "POLICY/", + "SECURITY/" + ], + "caseSensitive": true + } } -] \ No newline at end of file + } +] diff --git a/tests/PSRule.Tests/SuppressionFilterTests.cs b/tests/PSRule.Tests/SuppressionFilterTests.cs index c0bcec904e..373d0b24e4 100644 --- a/tests/PSRule.Tests/SuppressionFilterTests.cs +++ b/tests/PSRule.Tests/SuppressionFilterTests.cs @@ -42,7 +42,7 @@ public void Match() #region Helper methods - private Source[] GetSource() + private static Source[] GetSource() { var builder = new SourcePipelineBuilder(null, null); builder.Directory(GetSourcePath("FromFileAlias.Rule.yaml")); diff --git a/tests/PSRule.Tests/SuppressionGroupTests.cs b/tests/PSRule.Tests/SuppressionGroupTests.cs index 5dbb2f52e4..05113f59b3 100644 --- a/tests/PSRule.Tests/SuppressionGroupTests.cs +++ b/tests/PSRule.Tests/SuppressionGroupTests.cs @@ -25,16 +25,31 @@ public void ReadSuppressionGroup(string path) context.Begin(); var suppressionGroup = HostHelper.GetSuppressionGroup(GetSource(path), context).ToArray(); Assert.NotNull(suppressionGroup); - Assert.Equal(3, suppressionGroup.Length); + Assert.Equal(4, suppressionGroup.Length); - Assert.Equal("SuppressWithTargetName", suppressionGroup[0].Name); - Assert.Equal("Ignore test objects by name.", suppressionGroup[0].Info.Synopsis.Text); + var actual = suppressionGroup[0]; + Assert.Equal("SuppressWithTargetName", actual.Name); + Assert.Equal("Ignore test objects by name.", actual.Info.Synopsis.Text); + Assert.Null(actual.Spec.ExpiresOn); + Assert.Contains(context.Pipeline.SuppressionGroup, g => g.Id.Equals(".\\SuppressWithTargetName")); - Assert.Equal("SuppressWithTestType", suppressionGroup[1].Name); - Assert.Equal("Ignore test objects by type.", suppressionGroup[1].Info.Synopsis.Text); + actual = suppressionGroup[1]; + Assert.Equal("SuppressWithTestType", actual.Name); + Assert.Equal("Ignore test objects by type.", actual.Info.Synopsis.Text); + Assert.Null(actual.Spec.ExpiresOn); + Assert.Contains(context.Pipeline.SuppressionGroup, g => g.Id.Equals(".\\SuppressWithTestType")); - Assert.Equal("SuppressWithNonProdTag", suppressionGroup[2].Name); - Assert.Equal("Ignore objects with non-production tag.", suppressionGroup[2].Info.Synopsis.Text); + actual = suppressionGroup[2]; + Assert.Equal("SuppressWithNonProdTag", actual.Name); + Assert.Equal("Ignore objects with non-production tag.", actual.Info.Synopsis.Text); + Assert.Null(actual.Spec.ExpiresOn); + Assert.Contains(context.Pipeline.SuppressionGroup, g => g.Id.Equals(".\\SuppressWithNonProdTag")); + + actual = suppressionGroup[3]; + Assert.Equal("SuppressWithExpiry", actual.Name); + Assert.Equal("Suppress with expiry.", actual.Info.Synopsis.Text); + Assert.Equal(DateTime.Parse("2022-01-01T00:00:00Z").ToUniversalTime(), actual.Spec.ExpiresOn); + Assert.DoesNotContain(context.Pipeline.SuppressionGroup, g => g.Id.Equals(".\\SuppressWithExpiry")); } #region Helper methods @@ -65,4 +80,4 @@ private static string GetSourcePath(string fileName) #endregion Helper methods } -} \ No newline at end of file +} diff --git a/tests/PSRule.Tests/SuppressionGroups.Rule.jsonc b/tests/PSRule.Tests/SuppressionGroups.Rule.jsonc index bb16c95888..37354fd188 100644 --- a/tests/PSRule.Tests/SuppressionGroups.Rule.jsonc +++ b/tests/PSRule.Tests/SuppressionGroups.Rule.jsonc @@ -2,64 +2,86 @@ // Licensed under the MIT License. // Suppression groups for unit testing [ - { - // Synopsis: Ignore test objects by name. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "SuppressionGroup", - "metadata": { - "name": "SuppressWithTargetName" - }, - "spec": { - "rule": [ - "FromFile1", - "FromFile2" - ], - "if": { - "name": ".", - "in": [ - "TestObject1", - "TestObject2" - ] - } - } + { + // Synopsis: Ignore test objects by name. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "SuppressionGroup", + "metadata": { + "name": "SuppressWithTargetName" }, - { - // Synopsis: Ignore test objects by type. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "SuppressionGroup", - "metadata": { - "name": "SuppressWithTestType" - }, - "spec": { - "rule": [ - "FromFile3", - "FromFile5" - ], - "if": { - "type": ".", - "equals": "TestType" - } - } + "spec": { + "rule": [ + "FromFile1", + "FromFile2" + ], + "if": { + "name": ".", + "in": [ + "TestObject1", + "TestObject2" + ] + } + } + }, + { + // Synopsis: Ignore test objects by type. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "SuppressionGroup", + "metadata": { + "name": "SuppressWithTestType" + }, + "spec": { + "rule": [ + "FromFile3", + "FromFile5" + ], + "if": { + "type": ".", + "equals": "TestType" + } + } + }, + { + // Synopsis: Suppress with non-production tag. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "SuppressionGroup", + "metadata": { + "name": "SuppressWithNonProdTag" + }, + "spec": { + "rule": [ + ".\\WithTag2", + ".\\WithTag3" + ], + "if": { + "field": "tags.env", + "in": [ + "dev", + "test" + ] + } + } + }, + { + // Synopsis: Suppress with expiry. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "SuppressionGroup", + "metadata": { + "name": "SuppressWithExpiry" }, - { - // Synopsis: Suppress with non-production tag - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "SuppressionGroup", - "metadata": { - "name": "SuppressWithNonProdTag" - }, - "spec": { - "rule": [ - ".\\WithTag2", - ".\\WithTag3" - ], - "if": { - "field": "tags.env", - "in": [ - "dev", - "test" - ] - } - } + "spec": { + "expiresOn": "2022-01-01T00:00:00Z", + "rule": [ + ".\\WithTag2", + ".\\WithTag3" + ], + "if": { + "field": "tags.env", + "in": [ + "dev", + "test" + ] + } } -] \ No newline at end of file + } +] diff --git a/tests/PSRule.Tests/SuppressionGroups.Rule.yaml b/tests/PSRule.Tests/SuppressionGroups.Rule.yaml index 272e3fef8b..21c6fafac1 100644 --- a/tests/PSRule.Tests/SuppressionGroups.Rule.yaml +++ b/tests/PSRule.Tests/SuppressionGroups.Rule.yaml @@ -48,3 +48,20 @@ spec: in: - 'dev' - 'test' + +--- +# Synopsis: Suppress with expiry. +apiVersion: github.com/microsoft/PSRule/v1 +kind: SuppressionGroup +metadata: + name: SuppressWithExpiry +spec: + expiresOn: '2022-01-01T00:00:00Z' + rule: + - '.\WithTag2' + - '.\WithTag3' + if: + field: 'tags.env' + in: + - 'dev' + - 'test' From d78c89225a9cf5526d7c373fe7740dfd12d1e760 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sun, 13 Nov 2022 21:28:20 +1000 Subject: [PATCH 107/156] Pre-release v2.6.0-B0034 (#1339) --- docs/CHANGELOG-v2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index cfe1e01485..7709ba7a19 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.6.0-B0034 (pre-release) + What's changed since pre-release v2.6.0-B0013: - New features: From 8e1039d17b592f01037bf6bc990c5d705f8fc254 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Nov 2022 18:58:50 +1000 Subject: [PATCH 108/156] Bump mkdocs-material from 8.5.9 to 8.5.10 (#1340) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.5.9 to 8.5.10. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.5.9...8.5.10) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 27d4392f3e..a0814cf7fa 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.4.2 -mkdocs-material==8.5.9 +mkdocs-material==8.5.10 pymdown-extensions==9.8 mike==1.1.2 mkdocs-simple-hooks==0.1.5 From 06df37e61400360152251fe65c4110c92b785c41 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Thu, 17 Nov 2022 02:30:21 +1000 Subject: [PATCH 109/156] Release v2.6.0 (#1342) --- docs/CHANGELOG-v2.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 7709ba7a19..9a4a213d5a 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,32 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.6.0 + +What's changed since v2.5.3: + +- New features: + - Added support for generating job summaries by @BernieWhite. + [#1264](https://github.com/microsoft/PSRule/issues/1264) + - Job summaries provide a markdown output for pipelines in addition to other supported output formats. + - To use, configure the `Output.JobSummaryPath` option. + - Added support for time bound suppression groups by @BernieWhite. + [#1335](https://github.com/microsoft/PSRule/issues/1335) + - Suppression groups can be configured to expire after a specified time by setting the `spec.expiresOn` property. + - When a suppression group expires, the suppression group will generate a warning by default. + - Configure the `Execution.SuppressionGroupExpired` option to ignore or error on expired suppression groups. +- Engineering: + - Bump Microsoft.NET.Test.Sdk to v17.4.0. + [#1331](https://github.com/microsoft/PSRule/pull/1331) + - Bump PSScriptAnalyzer to v1.21.0. + [#1318](https://github.com/microsoft/PSRule/pull/1318) + - Class clean up and documentation by @BernieWhite. + [#1186](https://github.com/microsoft/PSRule/issues/1186) + +What's changed since pre-release v2.6.0-B0034: + +- No additional changes. + ## v2.6.0-B0034 (pre-release) What's changed since pre-release v2.6.0-B0013: From ce44a88a2f437472d32eca1bb035c7b1369e3701 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sat, 19 Nov 2022 00:12:21 +1000 Subject: [PATCH 110/156] Fixes exception calling RuleSource value #1343 (#1344) --- docs/CHANGELOG-v2.md | 6 ++++++ src/PSRule/Common/Engine.cs | 9 --------- src/PSRule/Configuration/PSRuleOption.cs | 6 ++++++ src/PSRule/Help/MarkdownStream.cs | 15 ++++----------- src/PSRule/Pipeline/CommandLineBuilder.cs | 15 +++++++++++++-- src/PSRule/Pipeline/SourcePipeline.cs | 8 ++++++-- .../{ConfigTests.cs => ModuleConfigTests.cs} | 14 +++++++++----- ...igurationTests.cs => PSRuleOptionTests.cs} | 19 +++++++++++++++---- 8 files changed, 59 insertions(+), 33 deletions(-) rename tests/PSRule.Tests/{ConfigTests.cs => ModuleConfigTests.cs} (78%) rename tests/PSRule.Tests/{ConfigurationTests.cs => PSRuleOptionTests.cs} (85%) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 9a4a213d5a..4b3775fbd4 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,12 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since v2.6.0: + +- Bug fixes: + - Fixes exception calling `RuleSource` value cannot be null by @BernieWhite. + [#1343](https://github.com/microsoft/PSRule/issues/1343) + ## v2.6.0 What's changed since v2.5.3: diff --git a/src/PSRule/Common/Engine.cs b/src/PSRule/Common/Engine.cs index a8e497d549..d677432be6 100644 --- a/src/PSRule/Common/Engine.cs +++ b/src/PSRule/Common/Engine.cs @@ -1,19 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.IO; -using PSRule.Configuration; - namespace PSRule { internal static partial class Engine { - internal static string GetLocalPath() - { - return PSRuleOption.GetRootedBasePath(Path.GetDirectoryName(AppContext.BaseDirectory)); - } - internal static string GetVersion() { return _Version; diff --git a/src/PSRule/Configuration/PSRuleOption.cs b/src/PSRule/Configuration/PSRuleOption.cs index 6b9b4d3168..67cb370d8b 100644 --- a/src/PSRule/Configuration/PSRuleOption.cs +++ b/src/PSRule/Configuration/PSRuleOption.cs @@ -542,6 +542,9 @@ public static string GetFilePath(string path) /// A absolute path. internal static string GetRootedPath(string path, bool normalize = false, string basePath = null) { + if (string.IsNullOrEmpty(path)) + path = string.Empty; + basePath ??= GetWorkingPath(); var rootedPath = Path.IsPathRooted(path) ? Path.GetFullPath(path) : Path.GetFullPath(Path.Combine(basePath, path)); return normalize ? rootedPath.Replace(Backslash, Slash) : rootedPath; @@ -558,6 +561,9 @@ internal static string GetRootedPath(string path, bool normalize = false, string /// internal static string GetRootedBasePath(string path, bool normalize = false) { + if (string.IsNullOrEmpty(path)) + path = string.Empty; + var rootedPath = GetRootedPath(path); var basePath = rootedPath.Length > 0 && IsSeparator(rootedPath[rootedPath.Length - 1]) ? rootedPath diff --git a/src/PSRule/Help/MarkdownStream.cs b/src/PSRule/Help/MarkdownStream.cs index aaa0f2d570..b2f62f0813 100644 --- a/src/PSRule/Help/MarkdownStream.cs +++ b/src/PSRule/Help/MarkdownStream.cs @@ -158,11 +158,11 @@ public void SkipWhitespace() /// If the current character and sequential characters are line ending control characters, skip ahead. /// /// The number of line endings to skip. When max is 0, sequential line endings will be skipped. + /// Determines if escaped characters are skipped. /// The number of line endings skipped. public int SkipLineEnding(int max = 1, bool ignoreEscaping = false) { var skipped = 0; - while ((Current == CarrageReturn || Current == NewLine) && (max == 0 || skipped < max)) { if (Remaining == 0) @@ -171,7 +171,7 @@ public int SkipLineEnding(int max = 1, bool ignoreEscaping = false) if (Current == CarrageReturn && Peak() == NewLine) Next(); - Next(ignoreEscaping: ignoreEscaping); + Next(ignoreEscaping); skipped++; } return skipped; @@ -205,32 +205,27 @@ public bool Skip(char c) /// Skip ahead if the current character is expected. Keep skipping when the character is repeated. /// /// The character to skip. + /// The maximum number of characters to skip. /// The number of characters that where skipped. public int Skip(char c, int max) { var skipped = 0; - while (Current == c && (max == 0 || skipped < max)) { Next(); - skipped++; } - return skipped; } public int Skip(string sequence, int max = 0, bool ignoreEscaping = false) { var skipped = 0; - while (IsSequence(sequence) && (max == 0 || skipped < max)) { Skip(sequence.Length, ignoreEscaping); - skipped++; } - return skipped; } @@ -238,14 +233,12 @@ public int Skip(string sequence, int max = 0, bool ignoreEscaping = false) /// Skip ahead a number of characters. Use Next() in preference of Skip if the number to skip is 1. /// /// The number of characters to skip + /// Determines if escaped characters are skipped. public void Skip(int toSkip, bool ignoreEscaping = false) { toSkip = HasRemaining(toSkip) ? toSkip : Remaining; - for (var i = 0; i < toSkip; i++) - { Next(ignoreEscaping); - } } /// diff --git a/src/PSRule/Pipeline/CommandLineBuilder.cs b/src/PSRule/Pipeline/CommandLineBuilder.cs index d3366c9ff2..42d870ef7e 100644 --- a/src/PSRule/Pipeline/CommandLineBuilder.cs +++ b/src/PSRule/Pipeline/CommandLineBuilder.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; +using System.IO; using PSRule.Configuration; namespace PSRule.Pipeline @@ -22,7 +24,7 @@ public static class CommandLineBuilder /// A builder object to configure the pipeline. public static IInvokePipelineBuilder Invoke(string[] module, PSRuleOption option, IHostContext hostContext) { - var sourcePipeline = new SourcePipelineBuilder(hostContext, option); + var sourcePipeline = new SourcePipelineBuilder(hostContext, option, GetLocalPath()); for (var i = 0; i < module.Length; i++) sourcePipeline.ModuleByName(module[i]); @@ -44,7 +46,7 @@ public static IInvokePipelineBuilder Invoke(string[] module, PSRuleOption option /// A builder object to configure the pipeline. public static IInvokePipelineBuilder Assert(string[] module, PSRuleOption option, IHostContext hostContext) { - var sourcePipeline = new SourcePipelineBuilder(hostContext, option); + var sourcePipeline = new SourcePipelineBuilder(hostContext, option, GetLocalPath()); for (var i = 0; module != null && i < module.Length; i++) sourcePipeline.ModuleByName(module[i]); @@ -53,5 +55,14 @@ public static IInvokePipelineBuilder Assert(string[] module, PSRuleOption option pipeline.Configure(option); return pipeline; } + + internal static string GetLocalPath() + { + if (string.IsNullOrEmpty(AppContext.BaseDirectory) || + string.IsNullOrEmpty(Path.GetDirectoryName(AppContext.BaseDirectory))) + return null; + + return PSRuleOption.GetRootedBasePath(Path.GetDirectoryName(AppContext.BaseDirectory)); + } } } diff --git a/src/PSRule/Pipeline/SourcePipeline.cs b/src/PSRule/Pipeline/SourcePipeline.cs index aa33f8a9fc..0617afc0e6 100644 --- a/src/PSRule/Pipeline/SourcePipeline.cs +++ b/src/PSRule/Pipeline/SourcePipeline.cs @@ -119,14 +119,14 @@ public sealed class SourcePipelineBuilder : ISourcePipelineBuilder, ISourceComma private readonly bool _UseDefaultPath; private readonly string _LocalPath; - internal SourcePipelineBuilder(IHostContext hostContext, PSRuleOption option) + internal SourcePipelineBuilder(IHostContext hostContext, PSRuleOption option, string localPath = null) { _Source = new Dictionary(StringComparer.OrdinalIgnoreCase); _HostContext = hostContext; _Writer = new HostPipelineWriter(hostContext, option, ShouldProcess); _Writer.EnterScope("[Discovery.Source]"); _UseDefaultPath = option == null || option.Include == null || option.Include.Path == null; - _LocalPath = Engine.GetLocalPath(); + _LocalPath = localPath; // Include paths from options if (!_UseDefaultPath) @@ -243,6 +243,10 @@ private string FindModule(string name) /// private bool TryPackagedModule(string name, out string path) { + path = null; + if (_LocalPath == null) + return false; + Log($"Looking for modules in: {_LocalPath}"); path = PSRuleOption.GetRootedBasePath(Path.Combine(_LocalPath, "Modules", name)); return System.IO.Directory.Exists(path); diff --git a/tests/PSRule.Tests/ConfigTests.cs b/tests/PSRule.Tests/ModuleConfigTests.cs similarity index 78% rename from tests/PSRule.Tests/ConfigTests.cs rename to tests/PSRule.Tests/ModuleConfigTests.cs index 6c2c37155c..fdba1b4271 100644 --- a/tests/PSRule.Tests/ConfigTests.cs +++ b/tests/PSRule.Tests/ModuleConfigTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; @@ -13,7 +13,7 @@ namespace PSRule { - public sealed class ConfigTests + public sealed class ModuleConfigTests { [Theory] [InlineData("ModuleConfig.Rule.yaml")] @@ -26,21 +26,25 @@ public void ReadModuleConfig(string path) Assert.Equal("Configuration1", configuration[0].Name); } - private PSRuleOption GetOption() + #region Helper methods + + private static PSRuleOption GetOption() { return new PSRuleOption(); } - private Source[] GetSource(string path) + private static Source[] GetSource(string path) { var builder = new SourcePipelineBuilder(null, null); builder.Directory(GetSourcePath(path)); return builder.Build(); } - private string GetSourcePath(string fileName) + private static string GetSourcePath(string fileName) { return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName); } + + #endregion Helper methods } } diff --git a/tests/PSRule.Tests/ConfigurationTests.cs b/tests/PSRule.Tests/PSRuleOptionTests.cs similarity index 85% rename from tests/PSRule.Tests/ConfigurationTests.cs rename to tests/PSRule.Tests/PSRuleOptionTests.cs index 7ce4c62a34..ff91114af7 100644 --- a/tests/PSRule.Tests/ConfigurationTests.cs +++ b/tests/PSRule.Tests/PSRuleOptionTests.cs @@ -10,11 +10,18 @@ namespace PSRule { - [Trait(LANGUAGE, LANGUAGEELEMENT)] - public sealed class ConfigurationTests + public sealed class PSRuleOptionTests { - private const string LANGUAGE = "Language"; - private const string LANGUAGEELEMENT = "Variable"; + [Fact] + public void GetRootedBasePath() + { + var pwd = Directory.GetCurrentDirectory(); + var basePwd = $"{pwd}{Path.DirectorySeparatorChar}"; + Assert.Equal(basePwd, PSRuleOption.GetRootedBasePath(null)); + Assert.Equal(basePwd, PSRuleOption.GetRootedBasePath(pwd)); + Assert.Equal(pwd, PSRuleOption.GetRootedPath(null)); + Assert.Equal(pwd, PSRuleOption.GetRootedPath(pwd)); + } [Fact] public void Configuration() @@ -67,6 +74,8 @@ public void GetObjectArrayFromYaml() Assert.Equal("East US", pso.PropertyValue("location")); } + #region Helper methods + private static Runtime.Configuration GetConfigurationHelper(PSRuleOption option) { var builder = new OptionContextBuilder(option, null, null, null); @@ -97,5 +106,7 @@ private static string GetSourcePath(string fileName) { return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName); } + + #endregion Helper methods } } From 3d9589e13eaeca5fa9a22684cc91223f0f610bf8 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sat, 19 Nov 2022 00:29:14 +1000 Subject: [PATCH 111/156] Pre-release v2.7.0-B0001 (#1345) --- docs/CHANGELOG-v2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 4b3775fbd4..c53aa5da9e 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.7.0-B0001 (pre-release) + What's changed since v2.6.0: - Bug fixes: From ed5253d7363a41bdd3118d5e81e0871aab65305b Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sat, 19 Nov 2022 01:22:03 +1000 Subject: [PATCH 112/156] Additional clean up on #1343 (#1346) --- src/PSRule/Pipeline/CommandLineBuilder.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/PSRule/Pipeline/CommandLineBuilder.cs b/src/PSRule/Pipeline/CommandLineBuilder.cs index 42d870ef7e..eb8a071d6f 100644 --- a/src/PSRule/Pipeline/CommandLineBuilder.cs +++ b/src/PSRule/Pipeline/CommandLineBuilder.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using System.IO; using PSRule.Configuration; namespace PSRule.Pipeline @@ -58,11 +57,7 @@ public static IInvokePipelineBuilder Assert(string[] module, PSRuleOption option internal static string GetLocalPath() { - if (string.IsNullOrEmpty(AppContext.BaseDirectory) || - string.IsNullOrEmpty(Path.GetDirectoryName(AppContext.BaseDirectory))) - return null; - - return PSRuleOption.GetRootedBasePath(Path.GetDirectoryName(AppContext.BaseDirectory)); + return string.IsNullOrEmpty(AppContext.BaseDirectory) ? null : PSRuleOption.GetRootedBasePath(AppContext.BaseDirectory); } } } From 1362719d6eb82a90274b940d5a0f0cb33d395405 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sun, 20 Nov 2022 14:25:16 +1000 Subject: [PATCH 113/156] Fixes comment handling in baselines #1336 (#1347) * Fixes comment handling in baselines #1336 * Additional comment fixes --- docs/CHANGELOG-v2.md | 8 +- src/PSRule/Common/JsonConverters.cs | 26 ++- tests/PSRule.Tests/Baseline.Rule.jsonc | 253 ++++++++++++------------ tests/PSRule.Tests/Baseline.Rule.yaml | 3 + tests/PSRule.Tests/SelectorTests.cs | 14 +- tests/PSRule.Tests/Selectors.Rule.jsonc | 8 + tests/PSRule.Tests/Selectors.Rule.yaml | 6 + 7 files changed, 176 insertions(+), 142 deletions(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index c53aa5da9e..fe3ba299b7 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,12 +30,18 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since pre-release v2.7.0-B0001: + +- Bug fixes: + - Fixed exception with comments in JSON baselines by @BernieWhite. + [#1336](https://github.com/microsoft/PSRule/issues/1336) + ## v2.7.0-B0001 (pre-release) What's changed since v2.6.0: - Bug fixes: - - Fixes exception calling `RuleSource` value cannot be null by @BernieWhite. + - Fixed exception calling `RuleSource` value cannot be null by @BernieWhite. [#1343](https://github.com/microsoft/PSRule/issues/1343) ## v2.6.0 diff --git a/src/PSRule/Common/JsonConverters.cs b/src/PSRule/Common/JsonConverters.cs index bc04263bd9..de8816873b 100644 --- a/src/PSRule/Common/JsonConverters.cs +++ b/src/PSRule/Common/JsonConverters.cs @@ -489,7 +489,6 @@ private static bool TryKind(JsonReader reader, string propertyName, out string k private static bool TryMetadata(JsonReader reader, JsonSerializer serializer, string propertyName, out ResourceMetadata metadata) { metadata = null; - if (propertyName == FIELD_METADATA) { if (reader.Read() && reader.TokenType == JsonToken.StartObject) @@ -498,7 +497,6 @@ private static bool TryMetadata(JsonReader reader, JsonSerializer serializer, st return true; } } - return false; } @@ -514,16 +512,12 @@ private bool TrySpec( out IResource spec) { spec = null; - - if (propertyName == FIELD_SPEC && _Factory.TryDescriptor( - apiVersion: apiVersion, - name: kind, - descriptor: out var descriptor)) + if (propertyName == FIELD_SPEC && _Factory.TryDescriptor(apiVersion: apiVersion, name: kind, descriptor: out var descriptor)) { if (reader.Read() && reader.TokenType == JsonToken.StartObject) { + reader.SkipComments(out _); var deserializedSpec = serializer.Deserialize(reader, objectType: descriptor.SpecType); - spec = descriptor.CreateInstance( source: RunspaceContext.CurrentThread.Source.File, metadata: metadata, @@ -534,7 +528,6 @@ private bool TrySpec( return true; } } - return false; } } @@ -636,10 +629,13 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist { if (reader.TryConsume(JsonToken.StartArray)) { + reader.SkipComments(out _); var result = new List(); while (reader.TryConsume(JsonToken.String, out var s_object) && s_object is string s) + { result.Add(s); - + reader.SkipComments(out _); + } return result.ToArray(); } else if (reader.TokenType == JsonToken.String && reader.Value is string s) @@ -697,11 +693,12 @@ private LanguageOperator MapOperator(string type, LanguageExpression.PropertyBag { if (TryExpression(type, properties, out LanguageOperator result)) { + reader.SkipComments(out _); + // If and Not if (reader.TryConsume(JsonToken.StartObject)) { result.Add(MapExpression(reader)); - //reader.Consume(JsonToken.EndObject); } // AllOf and AnyOf else if (reader.TryConsume(JsonToken.StartArray)) @@ -718,6 +715,7 @@ private LanguageOperator MapOperator(string type, LanguageExpression.PropertyBag } } reader.Consume(JsonToken.EndArray); + reader.SkipComments(out _); } result.Subselector = subselector; } @@ -741,6 +739,7 @@ private LanguageExpression MapExpression(JsonReader reader) { LanguageExpression result = null; var properties = new LanguageExpression.PropertyBag(); + reader.SkipComments(out _); MapProperty(properties, reader, out var key, out var subselector); if (key != null && TryCondition(key)) { @@ -818,6 +817,7 @@ private void MapProperty(LanguageExpression.PropertyBag properties, JsonReader r name = null; subselector = null; + reader.SkipComments(out _); while (reader.TokenType == JsonToken.PropertyName) { var key = reader.Value.ToString(); @@ -873,6 +873,7 @@ private void MapProperty(LanguageExpression.PropertyBag properties, JsonReader r reader.Read(); } } + reader.SkipComments(out _); } } @@ -932,17 +933,14 @@ reader.Value is string s && private bool TryExpression(string type, LanguageExpression.PropertyBag properties, out T expression) where T : LanguageExpression { expression = null; - if (_Factory.TryDescriptor(type, out var descriptor)) { expression = (T)descriptor.CreateInstance( source: RunspaceContext.CurrentThread.Source.File, properties: properties ); - return expression != null; } - return false; } } diff --git a/tests/PSRule.Tests/Baseline.Rule.jsonc b/tests/PSRule.Tests/Baseline.Rule.jsonc index 6138cbb525..fcd78fe933 100644 --- a/tests/PSRule.Tests/Baseline.Rule.jsonc +++ b/tests/PSRule.Tests/Baseline.Rule.jsonc @@ -1,137 +1,140 @@ [ - { - // Synopsis: This is an example baseline - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Baseline", - "metadata": { - "name": "TestBaseline1", - "annotations": { - "key": "value" - } - }, - "spec": { - "binding": { - "field": { - "kind": [ - "kind" - ], - "uniqueIdentifer": [ - "Id", - "AlternateName" - ] - }, - "targetName": [ - "AlternateName" - ], - "targetType": [ - "kind" - ] - }, - "rule": { - "include": [ - "WithBaseline" - ] - }, - "configuration": { - "key1": "value1", - "key2": [ - { - "value1": "abc" - }, - { - "value2": "def" - } - ] - } - } + { + // Synopsis: This is an example baseline + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Baseline", + "metadata": { + "name": "TestBaseline1", + "annotations": { + "key": "value" + } }, - { - // Synopsis: This is an example baseline - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Baseline", - "metadata": { - "name": "TestBaseline2" + "spec": { + // Additional comment + "binding": { + "field": { + "kind": [ + "kind" + ], + "uniqueIdentifer": [ + "Id", + "AlternateName" + ] }, - "spec": { - "binding": { - "targetName": [ - "metadata.name" - ], - "targetType": [ - "kind" - ] - }, - "rule": { - "include": [ - "" - ] - }, - "configuration": { - "key1": "value1" - } - } + "targetName": [ + "AlternateName" + ], + "targetType": [ + "kind" + ] + }, + "rule": { + "include": [ + // Additional comment + "WithBaseline" + // Additional comment + ] + }, + "configuration": { + "key1": "value1", + "key2": [ + { + "value1": "abc" + }, + { + "value2": "def" + } + ] + } + } + }, + { + // Synopsis: This is an example baseline + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Baseline", + "metadata": { + "name": "TestBaseline2" }, - { - // Synopsis: This is an example baseline - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Baseline", - "metadata": { - "name": "TestBaseline3" - }, - "spec": { - "rule": { - "tag": { - "category": "group2" - } - } + "spec": { + "binding": { + "targetName": [ + "metadata.name" + ], + "targetType": [ + "kind" + ] + }, + "rule": { + "include": [ + "" + ] + }, + "configuration": { + "key1": "value1" + } + } + }, + { + // Synopsis: This is an example baseline + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Baseline", + "metadata": { + "name": "TestBaseline3" + }, + "spec": { + "rule": { + "tag": { + "category": "group2" } + } + } + }, + // Baseline without synopsis. + { + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Baseline", + "metadata": { + "name": "TestBaseline4" }, - // Baseline without synopsis. - { - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Baseline", - "metadata": { - "name": "TestBaseline4" - }, - "spec": { - "rule": { - "tag": { - "severity": [ - "high", - "low" - ] - } - } + "spec": { + "rule": { + "tag": { + "severity": [ + "high", + "low" + ] } + } + } + }, + { + // Synopsis: This is an example obsolete baseline + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Baseline", + "metadata": { + "name": "TestBaseline5", + "annotations": { + "obsolete": true + } }, - { - // Synopsis: This is an example obsolete baseline - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Baseline", - "metadata": { - "name": "TestBaseline5", - "annotations": { - "obsolete": true - } - }, - "spec": {} + "spec": {} + }, + { + // Synopsis: An example of a baseline with taxonomy defined + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Baseline", + "metadata": { + "name": "TestBaseline6" }, - { - // Synopsis: An example of a baseline with taxonomy defined - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Baseline", - "metadata": { - "name": "TestBaseline6" - }, - "spec": { - "rule": { - "labels": { - "framework.v1/control": [ - "c-1", - "c-2" - ] - } - } + "spec": { + "rule": { + "labels": { + "framework.v1/control": [ + "c-1", + "c-2" + ] } + } } + } ] diff --git a/tests/PSRule.Tests/Baseline.Rule.yaml b/tests/PSRule.Tests/Baseline.Rule.yaml index 64b9c050fd..bff3791677 100644 --- a/tests/PSRule.Tests/Baseline.Rule.yaml +++ b/tests/PSRule.Tests/Baseline.Rule.yaml @@ -8,6 +8,7 @@ metadata: annotations: key: value spec: + # Additional comment binding: field: kind: @@ -21,7 +22,9 @@ spec: - kind rule: include: + # Additional comment - 'WithBaseline' + # Additional comment configuration: key1: value1 key2: diff --git a/tests/PSRule.Tests/SelectorTests.cs b/tests/PSRule.Tests/SelectorTests.cs index 145c5090d1..75b02fcb1b 100644 --- a/tests/PSRule.Tests/SelectorTests.cs +++ b/tests/PSRule.Tests/SelectorTests.cs @@ -35,6 +35,7 @@ public sealed class SelectorTests [InlineData("Json", SelectorJsonFileName)] public void ReadSelector(string type, string path) { + var testObject = GetObject((name: "value", value: 3)); var context = new RunspaceContext(PipelineContext.New(GetOption(), null, null, null, null, null, new OptionContext(), null), null); context.Init(GetSource(path)); context.Begin(); @@ -42,8 +43,17 @@ public void ReadSelector(string type, string path) Assert.NotNull(selector); Assert.Equal(92, selector.Length); - Assert.Equal("BasicSelector", selector[0].Name); - Assert.Equal($"{type}AllOf", selector[4].Name); + var actual = selector[0]; + var visitor = new SelectorVisitor(context, actual.Id, actual.Source, actual.Spec.If); + Assert.Equal("BasicSelector", actual.Name); + Assert.NotNull(actual.Spec.If); + Assert.False(visitor.Match(testObject)); + + actual = selector[4]; + visitor = new SelectorVisitor(context, actual.Id, actual.Source, actual.Spec.If); + Assert.Equal($"{type}AllOf", actual.Name); + Assert.NotNull(actual.Spec.If); + Assert.False(visitor.Match(testObject)); } #region Conditions diff --git a/tests/PSRule.Tests/Selectors.Rule.jsonc b/tests/PSRule.Tests/Selectors.Rule.jsonc index 4897db5812..61f1e4734a 100644 --- a/tests/PSRule.Tests/Selectors.Rule.jsonc +++ b/tests/PSRule.Tests/Selectors.Rule.jsonc @@ -11,14 +11,22 @@ }, "spec": { "if": { + // Additional comments "allOf": [ + // Additional comments { + // Additional comments "field": "Name", + // Additional comments "equals": "TargetObject1" + // Additional comments }, { + // Additional comments "field": "Value", + // Additional comments "equals": "value1" + // Additional comments } ] } diff --git a/tests/PSRule.Tests/Selectors.Rule.yaml b/tests/PSRule.Tests/Selectors.Rule.yaml index e0e29153d9..62a9d500b4 100644 --- a/tests/PSRule.Tests/Selectors.Rule.yaml +++ b/tests/PSRule.Tests/Selectors.Rule.yaml @@ -11,11 +11,17 @@ metadata: name: BasicSelector spec: if: + # Additional comments allOf: + # Additional comments - field: Name + # Additional comments equals: TargetObject1 + # Additional comments - field: Value + # Additional comments equals: value1 + # Additional comments --- # Synopsis: A selector to match objects using a specific JSON schema From 6c4d42190d3f52534cee7c8f3e6f0fef7866899b Mon Sep 17 00:00:00 2001 From: Bernie White Date: Tue, 22 Nov 2022 03:28:55 +1000 Subject: [PATCH 114/156] Add support for target scope #1350 (#1351) --- README.md | 1 + docs/CHANGELOG-v2.md | 3 + .../PSRule/en-US/about_PSRule_Expressions.md | 37 +++++++ .../PSRule/en-US/about_PSRule_Variables.md | 1 + schemas/PSRule-language.schema.json | 99 ++++++++++++++++++- src/PSRule/Common/PSObjectExtensions.cs | 30 +++++- src/PSRule/Data/ITargetSourceCollection.cs | 6 ++ src/PSRule/Data/RepositoryInfo.cs | 12 +++ .../Expressions/LanguageExpressions.cs | 24 ++++- src/PSRule/Pipeline/PipelineHookActions.cs | 20 ++-- src/PSRule/Pipeline/TargetObject.cs | 22 +++++ src/PSRule/Runtime/LanguageScope.cs | 13 +++ src/PSRule/Runtime/Operand.cs | 12 ++- src/PSRule/Runtime/PSRule.cs | 5 + src/PSRule/Runtime/PSRuleMemberInfo.cs | 22 ++++- tests/PSRule.Tests/SelectorTests.cs | 75 ++++++++++---- tests/PSRule.Tests/Selectors.Rule.jsonc | 31 ++++++ tests/PSRule.Tests/Selectors.Rule.yaml | 25 ++++- tests/PSRule.Tests/SuppressionGroupTests.cs | 42 +++++++- .../PSRule.Tests/SuppressionGroups.Rule.jsonc | 18 ++++ .../PSRule.Tests/SuppressionGroups.Rule.yaml | 14 +++ 21 files changed, 467 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index e143678622..a8e04c9a43 100644 --- a/README.md +++ b/README.md @@ -270,6 +270,7 @@ The following conceptual topics exist in the `PSRule` module: - [NotMatch](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notmatch) - [NotStartsWith](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notstartswith) - [NotWithinPath](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notwithinpath) + - [Scope](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#scope) - [SetOf](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#setof) - [Source](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#source) - [StartsWith](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#startswith) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index fe3ba299b7..d1a8e09da8 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -32,6 +32,9 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers What's changed since pre-release v2.7.0-B0001: +- General improvements: + - Added support target scope by @BernieWhite. + [#1350](https://github.com/microsoft/PSRule/issues/1350) - Bug fixes: - Fixed exception with comments in JSON baselines by @BernieWhite. [#1336](https://github.com/microsoft/PSRule/issues/1336) diff --git a/docs/concepts/PSRule/en-US/about_PSRule_Expressions.md b/docs/concepts/PSRule/en-US/about_PSRule_Expressions.md index b4722fa442..5e02d594f1 100644 --- a/docs/concepts/PSRule/en-US/about_PSRule_Expressions.md +++ b/docs/concepts/PSRule/en-US/about_PSRule_Expressions.md @@ -66,6 +66,7 @@ The following comparison properties are available: - [Field](#field) - [Name](#name) +- [Scope](#scope) - [Source](#source) - [Type](#type) @@ -1682,6 +1683,42 @@ spec: caseSensitive: true ``` +### Scope + +The comparison property `scope` is used with a condition to evaluate the scope of the object. +The `scope` property must be set to `.`. +Any other value will cause the condition to evaluate to `false`. + +Syntax: + +```yaml +scope: '.' +``` + +For example: + +```yaml +--- +apiVersion: github.com/microsoft/PSRule/v1 +kind: Rule +metadata: + name: 'ExampleScope' +spec: + condition: + scope: '.' + startsWith: '/' + +--- +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: 'ExampleScope' +spec: + if: + scope: '.' + startsWith: '/' +``` + ### SetOf The `setOf` condition can be used to determine if the operand is a set of specified values. diff --git a/docs/concepts/PSRule/en-US/about_PSRule_Variables.md b/docs/concepts/PSRule/en-US/about_PSRule_Variables.md index d433c4b37f..b7b1a51ab1 100644 --- a/docs/concepts/PSRule/en-US/about_PSRule_Variables.md +++ b/docs/concepts/PSRule/en-US/about_PSRule_Variables.md @@ -172,6 +172,7 @@ The following properties are available for read access: See option `Binding.Field` for more information. - `Input` - Allows adding additional input paths to the pipeline. - `Repository` - Provides access to information about the current repository. +- `Scope` - The scope of the object currently being processed by the pipeline. - `Source` - A collection of sources for the object currently being processed on the pipeline. - `TargetObject` - The object currently being processed on the pipeline. - `TargetName` - The name of the object currently being processed on the pipeline. diff --git a/schemas/PSRule-language.schema.json b/schemas/PSRule-language.schema.json index 58d530ee71..964b9a9d91 100644 --- a/schemas/PSRule-language.schema.json +++ b/schemas/PSRule-language.schema.json @@ -478,8 +478,8 @@ "if": { "type": "object", "title": "If", - "description": "A condition is made up of one or more expressions that will determine if an object is selected by the selector.", - "markdownDescription": "A condition is made up of one or more [expressions](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/) that will determine if an object is selected by the selector. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Selectors/)", + "description": "A condition made up of one or more expressions that will determine if an object is selected by the selector.", + "markdownDescription": "A condition made up of one or more [expressions](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/) that will determine if an object is selected by the selector. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Selectors/)", "$ref": "#/definitions/expressions" } }, @@ -551,6 +551,9 @@ }, "if": { "type": "object", + "title": "If", + "description": "A condition made up of one or more expressions that will determine if the rule is suppressed.", + "markdownDescription": "A condition made up of one or more [expressions](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/) that will determine if the rule is suppressed. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_SuppressionGroups/)", "$ref": "#/definitions/expressions" } }, @@ -999,6 +1002,9 @@ }, { "$ref": "#/definitions/expressions/definitions/operands/definitions/source" + }, + { + "$ref": "#/definitions/expressions/definitions/operands/definitions/scope" } ], "definitions": { @@ -1078,6 +1084,17 @@ "required": [ "source" ] + }, + "scope": { + "type": "object", + "properties": { + "scope": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/scope" + } + }, + "required": [ + "scope" + ] } } }, @@ -1151,6 +1168,16 @@ "description": "The source of the object currently being processed by the pipeline.", "markdownDescription": "The source of the object currently being processed by the pipeline. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#source)", "default": "" + }, + "scope": { + "type": "string", + "title": "Scope", + "description": "The scope of the object currently being processed by the pipeline.", + "markdownDescription": "The scope of the object currently being processed by the pipeline. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#scope)", + "default": ".", + "enum": [ + "." + ] } } }, @@ -1315,6 +1342,9 @@ }, "source": { "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + }, + "scope": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/scope" } }, "required": [ @@ -1407,6 +1437,9 @@ }, "source": { "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + }, + "scope": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/scope" } }, "required": [ @@ -1465,6 +1498,9 @@ }, "source": { "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + }, + "scope": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/scope" } }, "required": [ @@ -1500,6 +1536,9 @@ }, "source": { "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + }, + "scope": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/scope" } }, "required": [ @@ -1538,6 +1577,9 @@ }, "source": { "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + }, + "scope": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/scope" } }, "required": [ @@ -1576,6 +1618,9 @@ }, "source": { "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + }, + "scope": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/scope" } }, "required": [ @@ -1751,6 +1796,9 @@ }, "source": { "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + }, + "scope": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/scope" } }, "required": [ @@ -1802,6 +1850,9 @@ }, "source": { "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + }, + "scope": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/scope" } }, "required": [ @@ -1853,6 +1904,9 @@ }, "source": { "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + }, + "scope": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/scope" } }, "required": [ @@ -1904,6 +1958,9 @@ }, "source": { "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + }, + "scope": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/scope" } }, "required": [ @@ -1954,6 +2011,9 @@ }, "source": { "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + }, + "scope": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/scope" } }, "required": [ @@ -2003,6 +2063,9 @@ }, "source": { "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + }, + "scope": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/scope" } }, "required": [ @@ -2053,6 +2116,9 @@ }, "source": { "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + }, + "scope": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/scope" } }, "required": [ @@ -2102,6 +2168,9 @@ }, "source": { "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + }, + "scope": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/scope" } }, "required": [ @@ -2159,6 +2228,9 @@ }, "source": { "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + }, + "scope": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/scope" } }, "required": [ @@ -2209,6 +2281,9 @@ }, "source": { "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + }, + "scope": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/scope" } }, "required": [ @@ -2234,6 +2309,9 @@ "field": { "$ref": "#/definitions/expressions/definitions/properties/definitions/field" }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, "value": { "$ref": "#/definitions/expressions/definitions/properties/definitions/value" } @@ -2246,7 +2324,8 @@ "$ref": "#/definitions/expressions/definitions/operands" } ], - "additionalProperties": false + "additionalProperties": false, + "minProperties": 2 }, "isArray": { "type": "object", @@ -2425,6 +2504,9 @@ "field": { "$ref": "#/definitions/expressions/definitions/properties/definitions/field" }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, "value": { "$ref": "#/definitions/expressions/definitions/properties/definitions/value" } @@ -2453,6 +2535,9 @@ "field": { "$ref": "#/definitions/expressions/definitions/properties/definitions/field" }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + }, "value": { "$ref": "#/definitions/expressions/definitions/properties/definitions/value" } @@ -2618,6 +2703,9 @@ }, "value": { "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" } }, "required": [ @@ -2629,7 +2717,7 @@ } ], "additionalProperties": false, - "maxProperties": 2 + "minProperties": 2 }, "notWithinPath": { "type": "object", @@ -2657,6 +2745,9 @@ }, "value": { "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" } }, "required": [ diff --git a/src/PSRule/Common/PSObjectExtensions.cs b/src/PSRule/Common/PSObjectExtensions.cs index c310d417fb..97796c6873 100644 --- a/src/PSRule/Common/PSObjectExtensions.cs +++ b/src/PSRule/Common/PSObjectExtensions.cs @@ -18,6 +18,9 @@ internal static class PSObjectExtensions { private const string PROPERTY_SOURCE = "source"; private const string PROPERTY_ISSUE = "issue"; + private const string PROPERTY_NAME = "name"; + private const string PROPERTY_TYPE = "type"; + private const string PROPERTY_SCOPE = "scope"; private const string PROPERTY_PATH = "path"; public static T PropertyValue(this PSObject o, string propertyName) @@ -112,6 +115,21 @@ public static TargetIssueInfo[] GetIssueInfo(this PSObject o) return o.TryTargetInfo(out var targetInfo) ? targetInfo.Issue.ToArray() : Array.Empty(); } + public static string GetTargetName(this PSObject o) + { + return o != null && o.TryTargetInfo(out var targetInfo) ? targetInfo.TargetName : null; + } + + public static string GetTargetType(this PSObject o) + { + return o != null && o.TryTargetInfo(out var targetInfo) ? targetInfo.TargetType : null; + } + + public static string GetScope(this PSObject o) + { + return o != null && o.TryTargetInfo(out var targetInfo) ? targetInfo.Scope : null; + } + public static string GetTargetPath(this PSObject o) { if (o == null) @@ -130,10 +148,18 @@ public static void ConvertTargetInfoProperty(this PSObject o) return; UseTargetInfo(o, out var targetInfo); + if (TryProperty(value, PROPERTY_NAME, out string name) && targetInfo.TargetName == null) + targetInfo.TargetName = name; + + if (TryProperty(value, PROPERTY_TYPE, out string type) && targetInfo.TargetType == null) + targetInfo.TargetType = type; + + if (TryProperty(value, PROPERTY_SCOPE, out string scope) && targetInfo.Scope == null) + targetInfo.Scope = scope; + if (TryProperty(value, PROPERTY_PATH, out string path) && targetInfo.Path == null) - { targetInfo.Path = path; - } + if (TryProperty(value, PROPERTY_SOURCE, out Array sources)) { for (var i = 0; i < sources.Length; i++) diff --git a/src/PSRule/Data/ITargetSourceCollection.cs b/src/PSRule/Data/ITargetSourceCollection.cs index 1a6082d7a5..a20ead9a97 100644 --- a/src/PSRule/Data/ITargetSourceCollection.cs +++ b/src/PSRule/Data/ITargetSourceCollection.cs @@ -6,8 +6,14 @@ namespace PSRule.Data { + /// + /// A collection of sources for a target object. + /// public interface ITargetSourceCollection { + /// + /// Get the source details by source type. + /// TargetSourceInfo this[string type] { get; } } diff --git a/src/PSRule/Data/RepositoryInfo.cs b/src/PSRule/Data/RepositoryInfo.cs index 9a13363d67..715a3cad3c 100644 --- a/src/PSRule/Data/RepositoryInfo.cs +++ b/src/PSRule/Data/RepositoryInfo.cs @@ -15,16 +15,28 @@ internal RepositoryInfo(string basePath, string headRef) DisplayName = headRef; } + /// + /// The full path to the repository root. + /// public string FullName { get; } + /// + /// The full path to the repository root. + /// public string BasePath { get; } + /// + /// The HEAD ref. + /// public string DisplayName { get; } + /// string ITargetInfo.TargetName => DisplayName; + /// string ITargetInfo.TargetType => typeof(RepositoryInfo).FullName; + /// TargetSourceInfo ITargetInfo.Source => null; } } diff --git a/src/PSRule/Definitions/Expressions/LanguageExpressions.cs b/src/PSRule/Definitions/Expressions/LanguageExpressions.cs index 1b1e0c28e4..adbf2b7f37 100644 --- a/src/PSRule/Definitions/Expressions/LanguageExpressions.cs +++ b/src/PSRule/Definitions/Expressions/LanguageExpressions.cs @@ -433,12 +433,14 @@ internal sealed class LanguageExpressions private const string PROPERTY_SCHEMA = "$schema"; private const string SOURCE = "source"; private const string VALUE = "value"; + private const string SCOPE = "scope"; // Comparisons private const string LESS_THAN = "<"; private const string LESS_THAN_EQUALS = "<="; private const string GREATER_THAN = ">="; private const string GREATER_THAN_EQUALS = ">="; + private const string DOT = "."; // Define built-ins internal readonly static ILanguageExpresssionDescriptor[] Builtin = new ILanguageExpresssionDescriptor[] @@ -1597,7 +1599,7 @@ private static bool TryName(IExpressionContext context, LanguageExpression.Prope operand = null; if (properties.TryGetString(NAME, out var svalue)) { - if (svalue != "." || context?.Context?.LanguageScope == null) + if (svalue != DOT || context?.Context?.LanguageScope == null) return Invalid(context, svalue); if (!context.Context.LanguageScope.TryGetName(o, out var name, out var path) || @@ -1614,7 +1616,7 @@ private static bool TryType(IExpressionContext context, LanguageExpression.Prope operand = null; if (properties.TryGetString(TYPE, out var svalue)) { - if (svalue != "." || context?.Context?.LanguageScope == null) + if (svalue != DOT || context?.Context?.LanguageScope == null) return Invalid(context, svalue); if (!context.Context.LanguageScope.TryGetType(o, out var type, out var path) || @@ -1651,6 +1653,23 @@ private static bool TryValue(IExpressionContext context, LanguageExpression.Prop return operand != null; } + private static bool TryScope(IExpressionContext context, LanguageExpression.PropertyBag properties, object o, out IOperand operand) + { + operand = null; + if (properties.TryGetString(SCOPE, out var svalue)) + { + if (svalue != DOT || context?.Context?.LanguageScope == null) + return Invalid(context, svalue); + + if (!context.Context.LanguageScope.TryGetScope(o, out var scope) || + string.IsNullOrEmpty(scope)) + return Invalid(context, svalue); + + operand = Operand.FromScope(scope); + } + return operand != null; + } + /// /// Unwrap a function delegate or a literal value. /// @@ -1701,6 +1720,7 @@ private static bool TryOperand(ExpressionContext context, string name, object o, TryName(context, properties, o, out operand) || TrySource(context, properties, out operand) || TryValue(context, properties, out operand) || + TryScope(context, properties, o, out operand) || Invalid(context, name); } diff --git a/src/PSRule/Pipeline/PipelineHookActions.cs b/src/PSRule/Pipeline/PipelineHookActions.cs index 3bee828cd3..2118de5a63 100644 --- a/src/PSRule/Pipeline/PipelineHookActions.cs +++ b/src/PSRule/Pipeline/PipelineHookActions.cs @@ -201,22 +201,26 @@ private static bool TryGetInfoTargetName(object targetObject, out string targetN { targetName = null; var baseObject = ExpressionHelpers.GetBaseObject(targetObject); - if (baseObject is not ITargetInfo info) - return false; + if (targetObject is PSObject pso && pso.TryTargetInfo(out var targetInfoMember) && targetInfoMember.TargetName != null) + targetName = targetInfoMember.TargetName; - targetName = info.TargetName; - return true; + if (baseObject is ITargetInfo info) + targetName = info.TargetName; + + return targetName != null; } private static bool TryGetInfoTargetType(object targetObject, out string targetType) { targetType = null; var baseObject = ExpressionHelpers.GetBaseObject(targetObject); - if (baseObject is not ITargetInfo info) - return false; + if (targetObject is PSObject pso && pso.TryTargetInfo(out var targetInfoMember) && targetInfoMember.TargetType != null) + targetType = targetInfoMember.TargetType; + + if (baseObject is ITargetInfo info) + targetType = info.TargetType; - targetType = info.TargetType; - return true; + return targetType != null; } private static string ValueAsString(object o, string propertyName, bool caseSensitive) diff --git a/src/PSRule/Pipeline/TargetObject.cs b/src/PSRule/Pipeline/TargetObject.cs index 9688fc4354..81ef6cb3bc 100644 --- a/src/PSRule/Pipeline/TargetObject.cs +++ b/src/PSRule/Pipeline/TargetObject.cs @@ -54,17 +54,39 @@ internal TargetObject(PSObject o, TargetSourceCollection source) o.ConvertTargetInfoType(); Source = ReadSourceInfo(o, source); Issue = ReadIssueInfo(o, null); + TargetName = o.GetTargetName(); + TargetType = o.GetTargetType(); + Scope = o.GetScope(); Path = ReadPath(o); Value = Convert(o); _Annotations = new Dictionary(); } + internal TargetObject(PSObject o, string targetName = null, string targetType = null, string scope = null) + : this (o, null) + { + if (targetName != null) + TargetName = targetName; + + if (targetType != null) + TargetType = targetType; + + if (scope != null) + Scope = scope; + } + internal PSObject Value { get; } internal TargetSourceCollection Source { get; private set; } internal TargetIssueCollection Issue { get; private set; } + internal string TargetName { get; } + + internal string TargetType { get; } + + internal string Scope { get; } + internal string Path { get; } internal Hashtable GetData() diff --git a/src/PSRule/Runtime/LanguageScope.cs b/src/PSRule/Runtime/LanguageScope.cs index e9ad168350..e751c22827 100644 --- a/src/PSRule/Runtime/LanguageScope.cs +++ b/src/PSRule/Runtime/LanguageScope.cs @@ -44,6 +44,8 @@ internal interface ILanguageScope : IDisposable bool TryGetType(object o, out string type, out string path); bool TryGetName(object o, out string name, out string path); + + bool TryGetScope(object o, out string scope); } internal sealed class LanguageScope : ILanguageScope @@ -147,6 +149,17 @@ public bool TryGetName(object o, out string name, out string path) return false; } + public bool TryGetScope(object o, out string scope) + { + if (_Context != null && _Context.TargetObject.Value == o) + { + scope = _Context.TargetObject.Scope; + return true; + } + scope = null; + return false; + } + internal static string Normalize(string scope) { return string.IsNullOrEmpty(scope) ? STANDALONE_SCOPENAME : scope; diff --git a/src/PSRule/Runtime/Operand.cs b/src/PSRule/Runtime/Operand.cs index 224e7fe3ec..2359cac3ef 100644 --- a/src/PSRule/Runtime/Operand.cs +++ b/src/PSRule/Runtime/Operand.cs @@ -43,7 +43,12 @@ public enum OperandKind /// /// A literal value or function. /// - Value = 6 + Value = 6, + + /// + /// The object scope. + /// + Scope = 7 } /// @@ -127,6 +132,11 @@ internal static IOperand FromValue(object value) return new Operand(OperandKind.Value, null, value); } + internal static IOperand FromScope(string scope) + { + return new Operand(OperandKind.Scope, scope); + } + internal static string JoinPath(string p1, string p2) { if (IsEmptyPath(p1)) diff --git a/src/PSRule/Runtime/PSRule.cs b/src/PSRule/Runtime/PSRule.cs index ea4c04b7c3..8547fc017e 100644 --- a/src/PSRule/Runtime/PSRule.cs +++ b/src/PSRule/Runtime/PSRule.cs @@ -204,6 +204,11 @@ public IEnumerable Output /// public string TargetType => GetContext().RuleRecord.TargetType; + /// + /// The bound scope of the target object. + /// + public string Scope => GetContext().TargetObject.Scope; + /// /// Attempts to read content from disk. /// diff --git a/src/PSRule/Runtime/PSRuleMemberInfo.cs b/src/PSRule/Runtime/PSRuleMemberInfo.cs index 6eddb1c809..70df2c8b6c 100644 --- a/src/PSRule/Runtime/PSRuleMemberInfo.cs +++ b/src/PSRule/Runtime/PSRuleMemberInfo.cs @@ -18,11 +18,8 @@ internal sealed class PSRuleTargetInfo : PSMemberInfo public PSRuleTargetInfo() { SetMemberName(PropertyName); - if (Source == null) - Source = new List(); - - if (Issue == null) - Issue = new List(); + Source ??= new List(); + Issue ??= new List(); } private PSRuleTargetInfo(PSRuleTargetInfo targetInfo) @@ -31,6 +28,9 @@ private PSRuleTargetInfo(PSRuleTargetInfo targetInfo) if (targetInfo == null) return; + TargetName = targetInfo.TargetName; + TargetType = targetInfo.TargetType; + Scope = targetInfo.Scope; Path = targetInfo.Path; Source = targetInfo.Source; Issue = targetInfo.Issue; @@ -44,6 +44,15 @@ public string File } } + [JsonProperty(PropertyName = "name")] + public string TargetName { get; set; } + + [JsonProperty(PropertyName = "type")] + public string TargetType { get; set; } + + [JsonProperty(PropertyName = "scope")] + public string Scope { get; set; } + [JsonProperty(PropertyName = "path")] public string Path { get; set; } @@ -76,6 +85,9 @@ internal void Combine(PSRuleTargetInfo targetInfo) if (targetInfo == null) return; + TargetName = targetInfo.TargetName; + TargetType = targetInfo.TargetType; + Scope = targetInfo.Scope; Path = targetInfo.Path; Source.AddUnique(targetInfo?.Source); Issue.AddUnique(targetInfo?.Issue); diff --git a/tests/PSRule.Tests/SelectorTests.cs b/tests/PSRule.Tests/SelectorTests.cs index 75b02fcb1b..9840760f4f 100644 --- a/tests/PSRule.Tests/SelectorTests.cs +++ b/tests/PSRule.Tests/SelectorTests.cs @@ -41,7 +41,7 @@ public void ReadSelector(string type, string path) context.Begin(); var selector = HostHelper.GetSelector(GetSource(path), context).ToArray(); Assert.NotNull(selector); - Assert.Equal(92, selector.Length); + Assert.Equal(94, selector.Length); var actual = selector[0]; var visitor = new SelectorVisitor(context, actual.Id, actual.Source, actual.Spec.If); @@ -415,7 +415,7 @@ public void InExpression(string type, string path) var actual1 = GetObject((name: "value", value: new string[] { "Value1" })); var actual2 = GetObject((name: "value", value: new string[] { "Value2" })); var actual3 = GetObject((name: "value", value: new string[] { "Value3" })); - var actual4 = GetObject((name: "value", value: new string[] { })); + var actual4 = GetObject((name: "value", value: Array.Empty())); var actual5 = GetObject((name: "value", value: null)); var actual6 = GetObject(); @@ -451,7 +451,7 @@ public void NotInExpression(string type, string path) var actual1 = GetObject((name: "value", value: new string[] { "Value1" })); var actual2 = GetObject((name: "value", value: new string[] { "Value2" })); var actual3 = GetObject((name: "value", value: new string[] { "Value3" })); - var actual4 = GetObject((name: "value", value: new string[] { })); + var actual4 = GetObject((name: "value", value: Array.Empty())); var actual5 = GetObject((name: "value", value: null)); Assert.False(notIn.Match(actual1)); @@ -487,7 +487,7 @@ public void SetOfExpression(string type, string path) var actual3 = GetObject((name: "value", value: new string[] { "cluster-autoscaler" })); var actual4 = GetObject((name: "value", value: new string[] { "kube-apiserver", "kube-scheduler" })); var actual5 = GetObject((name: "value", value: new string[] { "kube-scheduler" })); - var actual6 = GetObject((name: "value", value: new string[] { })); + var actual6 = GetObject((name: "value", value: Array.Empty())); var actual7 = GetObject((name: "value", value: null)); var actual8 = GetObject(); var actual9 = GetObject((name: "value", value: new string[] { "kube-apiserver", "cluster-autoscaler", "kube-apiserver" })); @@ -516,7 +516,7 @@ public void SubsetExpression(string type, string path) var actual3 = GetObject((name: "value", value: new string[] { "cluster-autoscaler" })); var actual4 = GetObject((name: "value", value: new string[] { "kube-apiserver", "kube-scheduler" })); var actual5 = GetObject((name: "value", value: new string[] { "kube-scheduler" })); - var actual6 = GetObject((name: "value", value: new string[] { })); + var actual6 = GetObject((name: "value", value: Array.Empty())); var actual7 = GetObject((name: "value", value: null)); var actual8 = GetObject(); var actual9 = GetObject((name: "value", value: new string[] { "kube-apiserver", "cluster-autoscaler", "kube-apiserver" })); @@ -545,7 +545,7 @@ public void CountExpression(string type, string path) var actual3 = GetObject((name: "value", value: new string[] { "1" })); var actual4 = GetObject((name: "value", value: new int[] { 2, 3 })); var actual5 = GetObject((name: "value", value: new int[] { 3 })); - var actual6 = GetObject((name: "value", value: new string[] { })); + var actual6 = GetObject((name: "value", value: Array.Empty())); var actual7 = GetObject((name: "value", value: null)); var actual8 = GetObject(); @@ -570,7 +570,7 @@ public void NotCountExpression(string type, string path) var actual3 = GetObject((name: "value", value: new string[] { "1" })); var actual4 = GetObject((name: "value", value: new int[] { 2, 3 })); var actual5 = GetObject((name: "value", value: new int[] { 3 })); - var actual6 = GetObject((name: "value", value: new string[] { })); + var actual6 = GetObject((name: "value", value: Array.Empty())); var actual7 = GetObject((name: "value", value: null)); var actual8 = GetObject(); @@ -593,7 +593,7 @@ public void LessExpression(string type, string path) var actual1 = GetObject((name: "value", value: 3)); var actual2 = GetObject((name: "value", value: 4)); var actual3 = GetObject((name: "value", value: new string[] { "Value3" })); - var actual4 = GetObject((name: "value", value: new string[] { })); + var actual4 = GetObject((name: "value", value: Array.Empty())); var actual5 = GetObject((name: "value", value: null)); var actual6 = GetObject((name: "value", value: 2)); var actual7 = GetObject((name: "value", value: -1)); @@ -635,7 +635,7 @@ public void LessOrEqualsExpression(string type, string path) var actual1 = GetObject((name: "value", value: 3)); var actual2 = GetObject((name: "value", value: 4)); var actual3 = GetObject((name: "value", value: new string[] { "Value3" })); - var actual4 = GetObject((name: "value", value: new string[] { })); + var actual4 = GetObject((name: "value", value: Array.Empty())); var actual5 = GetObject((name: "value", value: null)); var actual6 = GetObject((name: "value", value: 2)); var actual7 = GetObject((name: "value", value: -1)); @@ -677,7 +677,7 @@ public void GreaterExpression(string type, string path) var actual1 = GetObject((name: "value", value: 3)); var actual2 = GetObject((name: "value", value: 4)); var actual3 = GetObject((name: "value", value: new string[] { "Value3" })); - var actual4 = GetObject((name: "value", value: new string[] { })); + var actual4 = GetObject((name: "value", value: Array.Empty())); var actual5 = GetObject((name: "value", value: null)); var actual6 = GetObject((name: "value", value: 2)); var actual7 = GetObject((name: "value", value: -1)); @@ -719,7 +719,7 @@ public void GreaterOrEqualsExpression(string type, string path) var actual1 = GetObject((name: "value", value: 3)); var actual2 = GetObject((name: "value", value: 4)); var actual3 = GetObject((name: "value", value: new string[] { "Value3" })); - var actual4 = GetObject((name: "value", value: new string[] { })); + var actual4 = GetObject((name: "value", value: Array.Empty())); var actual5 = GetObject((name: "value", value: null)); var actual6 = GetObject((name: "value", value: 2)); var actual7 = GetObject((name: "value", value: -1)); @@ -761,7 +761,7 @@ public void StartsWithExpression(string type, string path) var actual1 = GetObject((name: "value", value: "abc")); var actual2 = GetObject((name: "value", value: "efg")); var actual3 = GetObject((name: "value", value: "hij")); - var actual4 = GetObject((name: "value", value: new string[] { })); + var actual4 = GetObject((name: "value", value: Array.Empty())); var actual5 = GetObject((name: "value", value: null)); var actual6 = GetObject(); var actual7 = GetObject((name: "value", value: "EFG")); @@ -803,7 +803,7 @@ public void NotStartsWithExpression(string type, string path) var actual1 = GetObject((name: "value", value: "abc")); var actual2 = GetObject((name: "value", value: "efg")); var actual3 = GetObject((name: "value", value: "hij")); - var actual4 = GetObject((name: "value", value: new string[] { })); + var actual4 = GetObject((name: "value", value: Array.Empty())); var actual5 = GetObject((name: "value", value: null)); var actual6 = GetObject(); var actual7 = GetObject((name: "value", value: "EFG")); @@ -830,7 +830,7 @@ public void EndsWithExpression(string type, string path) var actual1 = GetObject((name: "value", value: "abc")); var actual2 = GetObject((name: "value", value: "efg")); var actual3 = GetObject((name: "value", value: "hij")); - var actual4 = GetObject((name: "value", value: new string[] { })); + var actual4 = GetObject((name: "value", value: Array.Empty())); var actual5 = GetObject((name: "value", value: null)); var actual6 = GetObject(); var actual7 = GetObject((name: "value", value: "EFG")); @@ -889,7 +889,7 @@ public void NotEndsWithExpression(string type, string path) var actual1 = GetObject((name: "value", value: "abc")); var actual2 = GetObject((name: "value", value: "efg")); var actual3 = GetObject((name: "value", value: "hij")); - var actual4 = GetObject((name: "value", value: new string[] { })); + var actual4 = GetObject((name: "value", value: Array.Empty())); var actual5 = GetObject((name: "value", value: null)); var actual6 = GetObject(); var actual7 = GetObject((name: "value", value: "EFG")); @@ -916,7 +916,7 @@ public void ContainsExpression(string type, string path) var actual1 = GetObject((name: "value", value: "abc")); var actual2 = GetObject((name: "value", value: "bcd")); var actual3 = GetObject((name: "value", value: "hij")); - var actual4 = GetObject((name: "value", value: new string[] { })); + var actual4 = GetObject((name: "value", value: Array.Empty())); var actual5 = GetObject((name: "value", value: null)); var actual6 = GetObject(); var actual7 = GetObject((name: "value", value: "BCD")); @@ -958,7 +958,7 @@ public void NotContainsExpression(string type, string path) var actual1 = GetObject((name: "value", value: "abc")); var actual2 = GetObject((name: "value", value: "bcd")); var actual3 = GetObject((name: "value", value: "hij")); - var actual4 = GetObject((name: "value", value: new string[] { })); + var actual4 = GetObject((name: "value", value: Array.Empty())); var actual5 = GetObject((name: "value", value: null)); var actual6 = GetObject(); var actual7 = GetObject((name: "value", value: "BCD")); @@ -985,7 +985,7 @@ public void LikeExpression(string type, string path) var actual1 = GetObject((name: "value", value: "abc")); var actual2 = GetObject((name: "value", value: "efg")); var actual3 = GetObject((name: "value", value: "hij")); - var actual4 = GetObject((name: "value", value: new string[] { })); + var actual4 = GetObject((name: "value", value: Array.Empty())); var actual5 = GetObject((name: "value", value: null)); var actual6 = GetObject(); var actual7 = GetObject((name: "value", value: "EFG")); @@ -1012,7 +1012,7 @@ public void NotLikeExpression(string type, string path) var actual1 = GetObject((name: "value", value: "abc")); var actual2 = GetObject((name: "value", value: "efg")); var actual3 = GetObject((name: "value", value: "hij")); - var actual4 = GetObject((name: "value", value: new string[] { })); + var actual4 = GetObject((name: "value", value: Array.Empty())); var actual5 = GetObject((name: "value", value: null)); var actual6 = GetObject(); var actual7 = GetObject((name: "value", value: "EFG")); @@ -1039,7 +1039,7 @@ public void IsStringExpression(string type, string path) var isStringFalse = GetSelectorVisitor($"{type}IsStringFalse", GetSource(path), out _); var actual1 = GetObject((name: "value", value: "abc")); var actual2 = GetObject((name: "value", value: 4)); - var actual3 = GetObject((name: "value", value: new string[] { })); + var actual3 = GetObject((name: "value", value: Array.Empty())); var actual4 = GetObject((name: "value", value: null)); var actual5 = GetObject(); @@ -1118,7 +1118,7 @@ public void IsBooleanExpression(string type, string path) var actual4 = GetObject((name: "value", value: "false")); var actual5 = GetObject((name: "value", value: null)); var actual6 = GetObject((name: "value", value: PSObject.AsPSObject(true))); - var actual7 = GetObject((name: "value", value: new bool[] { })); + var actual7 = GetObject((name: "value", value: Array.Empty())); var actual8 = GetObject(); // Without conversion @@ -1661,6 +1661,39 @@ public void Name(string type, string path) Assert.True(equals.Match(actual1)); } + [Theory] + [InlineData("Yaml", SelectorYamlFileName)] + [InlineData("Json", SelectorJsonFileName)] + public void Scope(string type, string path) + { + var testObject = GetObject( + (name: "Name", value: "TargetObject1") + ); + + var equals = GetSelectorVisitor($"{type}ScopeEquals", GetSource(path), out var context); + context.EnterTargetObject(new TargetObject(testObject, scope: "/scope1")); + Assert.True(equals.Match(testObject)); + + context.EnterTargetObject(new TargetObject(testObject, scope: "/scope2")); + Assert.False(equals.Match(testObject)); + + context.EnterTargetObject(new TargetObject(testObject)); + Assert.False(equals.Match(testObject)); + + var startsWith = GetSelectorVisitor($"{type}ScopeStartsWith", GetSource(path), out context); + context.EnterTargetObject(new TargetObject(testObject, scope: "/scope1/")); + Assert.True(startsWith.Match(testObject)); + + context.EnterTargetObject(new TargetObject(testObject, scope: "/scope2/")); + Assert.True(startsWith.Match(testObject)); + + context.EnterTargetObject(new TargetObject(testObject, scope: "/scope2")); + Assert.False(startsWith.Match(testObject)); + + context.EnterTargetObject(new TargetObject(testObject)); + Assert.False(startsWith.Match(testObject)); + } + #endregion Properties #region Functions diff --git a/tests/PSRule.Tests/Selectors.Rule.jsonc b/tests/PSRule.Tests/Selectors.Rule.jsonc index 61f1e4734a..d52dad7ccd 100644 --- a/tests/PSRule.Tests/Selectors.Rule.jsonc +++ b/tests/PSRule.Tests/Selectors.Rule.jsonc @@ -1770,5 +1770,36 @@ "caseSensitive": true } } + }, + { + // Synopsis: Test scope property with equals. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonScopeEquals" + }, + "spec": { + "if": { + "scope": ".", + "equals": "/scope1" + } + } + }, + { + // Synopsis: Test scope property with startsWith. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonScopeStartsWith" + }, + "spec": { + "if": { + "scope": ".", + "startsWith": [ + "/scope1/", + "/scope2/" + ] + } + } } ] diff --git a/tests/PSRule.Tests/Selectors.Rule.yaml b/tests/PSRule.Tests/Selectors.Rule.yaml index 62a9d500b4..65eca38205 100644 --- a/tests/PSRule.Tests/Selectors.Rule.yaml +++ b/tests/PSRule.Tests/Selectors.Rule.yaml @@ -1142,6 +1142,8 @@ spec: name: '.' isUpper: true +#endregion With name tests + --- # Synopsis: Test endsWith with source apiVersion: github.com/microsoft/PSRule/v1 @@ -1257,5 +1259,26 @@ spec: - "SECURITY/" caseSensitive: true +--- +# Synopsis: Test scope property with equals. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: YamlScopeEquals +spec: + if: + scope: . + equals: /scope1 -#endregion With name tests +--- +# Synopsis: Test scope property with startsWith. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: YamlScopeStartsWith +spec: + if: + scope: . + startsWith: + - /scope1/ + - /scope2/ diff --git a/tests/PSRule.Tests/SuppressionGroupTests.cs b/tests/PSRule.Tests/SuppressionGroupTests.cs index 05113f59b3..a7fe0e9bce 100644 --- a/tests/PSRule.Tests/SuppressionGroupTests.cs +++ b/tests/PSRule.Tests/SuppressionGroupTests.cs @@ -4,9 +4,13 @@ using System; using System.IO; using System.Linq; +using System.Management.Automation; using PSRule.Configuration; +using PSRule.Definitions; +using PSRule.Definitions.SuppressionGroups; using PSRule.Host; using PSRule.Pipeline; +using PSRule.Rules; using PSRule.Runtime; using Xunit; using Assert = Xunit.Assert; @@ -25,7 +29,7 @@ public void ReadSuppressionGroup(string path) context.Begin(); var suppressionGroup = HostHelper.GetSuppressionGroup(GetSource(path), context).ToArray(); Assert.NotNull(suppressionGroup); - Assert.Equal(4, suppressionGroup.Length); + Assert.Equal(5, suppressionGroup.Length); var actual = suppressionGroup[0]; Assert.Equal("SuppressWithTargetName", actual.Name); @@ -50,8 +54,35 @@ public void ReadSuppressionGroup(string path) Assert.Equal("Suppress with expiry.", actual.Info.Synopsis.Text); Assert.Equal(DateTime.Parse("2022-01-01T00:00:00Z").ToUniversalTime(), actual.Spec.ExpiresOn); Assert.DoesNotContain(context.Pipeline.SuppressionGroup, g => g.Id.Equals(".\\SuppressWithExpiry")); + + actual = suppressionGroup[4]; + Assert.Equal("SuppressByScope", actual.Name); + Assert.Equal("Suppress by scope.", actual.Info.Synopsis.Text); } + //[Theory] + //[InlineData("SuppressionGroups.Rule.yaml")] + //[InlineData("SuppressionGroups.Rule.jsonc")] + //public void EvaluateSuppressionGroup(string path) + //{ + // var context = new RunspaceContext(PipelineContext.New(GetOption(), null, null, PipelineHookActions.BindTargetName, PipelineHookActions.BindTargetType, PipelineHookActions.BindField, GetOptionContext(), null), null); + // context.Init(GetSource(path)); + // context.Begin(); + // var suppressionGroup = HostHelper.GetSuppressionGroup(GetSource(path), context).ToArray(); + // Assert.NotNull(suppressionGroup); + + // var testObject = GetObject((name: "name", value: "TestObject1")); + // context.EnterTargetObject(new TargetObject(testObject, targetName: "TestObject1", scope: "/scope1")); + + // var actual = suppressionGroup[0]; + // var visitor = new SuppressionGroupVisitor(context, actual.Id, actual.Source, actual.Spec, actual.Info); + // Assert.True(visitor.TryMatch(testObject, out _)); + + // actual = suppressionGroup[4]; + // visitor = new SuppressionGroupVisitor(context, actual.Id, actual.Source, actual.Spec, actual.Info); + // //Assert.True(visitor.TryMatch()); + //} + #region Helper methods private static PSRuleOption GetOption() @@ -78,6 +109,15 @@ private static string GetSourcePath(string fileName) return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName); } + private static PSObject 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 result; + } + #endregion Helper methods } } diff --git a/tests/PSRule.Tests/SuppressionGroups.Rule.jsonc b/tests/PSRule.Tests/SuppressionGroups.Rule.jsonc index 37354fd188..5371cc1846 100644 --- a/tests/PSRule.Tests/SuppressionGroups.Rule.jsonc +++ b/tests/PSRule.Tests/SuppressionGroups.Rule.jsonc @@ -83,5 +83,23 @@ ] } } + }, + { + // Synopsis: Suppress by scope. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "SuppressionGroup", + "metadata": { + "name": "SuppressByScope" + }, + "spec": { + "rule": [ + "FromFile1", + "FromFile2" + ], + "if": { + "scope": ".", + "startsWith": "/" + } + } } ] diff --git a/tests/PSRule.Tests/SuppressionGroups.Rule.yaml b/tests/PSRule.Tests/SuppressionGroups.Rule.yaml index 21c6fafac1..e1b285cf74 100644 --- a/tests/PSRule.Tests/SuppressionGroups.Rule.yaml +++ b/tests/PSRule.Tests/SuppressionGroups.Rule.yaml @@ -65,3 +65,17 @@ spec: in: - 'dev' - 'test' + +--- +# Synopsis: Suppress by scope. +apiVersion: github.com/microsoft/PSRule/v1 +kind: SuppressionGroup +metadata: + name: SuppressByScope +spec: + rule: + - 'FromFile1' + - 'FromFile2' + if: + scope: . + startsWith: '/' From 005eb43ad6905a8eb88512ea076008b4edc9e5a9 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Tue, 22 Nov 2022 03:46:24 +1000 Subject: [PATCH 115/156] Pre-release v2.7.0-B0006 (#1352) --- docs/CHANGELOG-v2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index d1a8e09da8..97572896b7 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.7.0-B0006 (pre-release) + What's changed since pre-release v2.7.0-B0001: - General improvements: From f4b55ca8c69af711bcff78be194a7331977ad75b Mon Sep 17 00:00:00 2001 From: Bernie White Date: Wed, 23 Nov 2022 15:41:03 +1000 Subject: [PATCH 116/156] Fixes constrained language mode with PS 7.3 #1348 (#1349) * Fixes constrained language mode with PS 7.3 #1348 * Additional updates * Additional updates --- .azure-pipelines/azure-pipelines.yaml | 9 ++++++++- docs/CHANGELOG-v2.md | 2 ++ src/PSRule/Pipeline/PipelineContext.cs | 12 +++++++----- src/PSRule/Rules/PowerShellCondition.cs | 4 +++- src/PSRule/Runtime/LanguageScriptBlock.cs | 1 + 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/.azure-pipelines/azure-pipelines.yaml b/.azure-pipelines/azure-pipelines.yaml index 56dda80cd7..c9d4e79f1a 100644 --- a/.azure-pipelines/azure-pipelines.yaml +++ b/.azure-pipelines/azure-pipelines.yaml @@ -172,7 +172,14 @@ stages: - template: jobs/testContainer.yaml parameters: - name: ps_7_2_ubuntu_20_04 + name: ps_7_2_ubuntu_22_04 displayName: 'PowerShell 7.2 - ubuntu-22.04' imageName: mcr.microsoft.com/powershell imageTag: 7.2-ubuntu-22.04 + + - template: jobs/testContainer.yaml + parameters: + name: ps_7_3_ubuntu_22_04 + displayName: 'PowerShell 7.3 - ubuntu-22.04' + imageName: mcr.microsoft.com/powershell + imageTag: 7.3-ubuntu-22.04 diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 97572896b7..cfd482d50b 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -40,6 +40,8 @@ What's changed since pre-release v2.7.0-B0001: - Bug fixes: - Fixed exception with comments in JSON baselines by @BernieWhite. [#1336](https://github.com/microsoft/PSRule/issues/1336) + - Fixed handling of constrained language mode with PowerShell 7.3 by @BernieWhite. + [#1348](https://github.com/microsoft/PSRule/issues/1348) ## v2.7.0-B0001 (pre-release) diff --git a/src/PSRule/Pipeline/PipelineContext.cs b/src/PSRule/Pipeline/PipelineContext.cs index 2339cb22ec..cb26342f49 100644 --- a/src/PSRule/Pipeline/PipelineContext.cs +++ b/src/PSRule/Pipeline/PipelineContext.cs @@ -41,6 +41,8 @@ internal sealed class PipelineContext : IDisposable, IBindingContext private Runspace _Runspace; private SHA1Managed _Hash; + private Runspace _OrginalRunspace; + // Track whether Dispose has been called. private bool _Disposed; @@ -65,9 +67,7 @@ public HashAlgorithm ObjectHashAlgorithm { get { - if (_Hash == null) - _Hash = new SHA1Managed(); - + _Hash ??= new SHA1Managed(); return _Hash; } } @@ -143,8 +143,7 @@ internal Runspace GetRunspace() state.LanguageMode = _LanguageMode == LanguageMode.FullLanguage ? PSLanguageMode.FullLanguage : PSLanguageMode.ConstrainedLanguage; _Runspace = RunspaceFactory.CreateRunspace(state); - if (Runspace.DefaultRunspace == null) - Runspace.DefaultRunspace = _Runspace; + Runspace.DefaultRunspace ??= _Runspace; _Runspace.Open(); _Runspace.SessionStateProxy.PSVariable.Set(new PSRuleVariable()); @@ -286,6 +285,9 @@ private void Dispose(bool disposing) { if (disposing) { + if (_OrginalRunspace != null) + Runspace.DefaultRunspace = _OrginalRunspace; + if (_Hash != null) _Hash.Dispose(); diff --git a/src/PSRule/Rules/PowerShellCondition.cs b/src/PSRule/Rules/PowerShellCondition.cs index 9fb2452610..b28faa6471 100644 --- a/src/PSRule/Rules/PowerShellCondition.cs +++ b/src/PSRule/Rules/PowerShellCondition.cs @@ -42,8 +42,10 @@ private void Dispose(bool disposing) if (!_Disposed) { if (disposing) + { + _Condition.Runspace = null; _Condition.Dispose(); - + } _Disposed = true; } } diff --git a/src/PSRule/Runtime/LanguageScriptBlock.cs b/src/PSRule/Runtime/LanguageScriptBlock.cs index b73e71221b..227298f272 100644 --- a/src/PSRule/Runtime/LanguageScriptBlock.cs +++ b/src/PSRule/Runtime/LanguageScriptBlock.cs @@ -30,6 +30,7 @@ private void Dispose(bool disposing) { if (disposing) { + _Block.Runspace = null; _Block.Dispose(); } _Disposed = true; From 7062f29e6e0c803e2099d246e162a630904f4328 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Nov 2022 16:20:19 +1000 Subject: [PATCH 117/156] Bump pymdown-extensions from 9.8 to 9.9 (#1355) Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 9.8 to 9.9. - [Release notes](https://github.com/facelessuser/pymdown-extensions/releases) - [Commits](https://github.com/facelessuser/pymdown-extensions/compare/9.8...9.9) --- updated-dependencies: - dependency-name: pymdown-extensions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index a0814cf7fa..c58faff3b9 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,6 +1,6 @@ mkdocs==1.4.2 mkdocs-material==8.5.10 -pymdown-extensions==9.8 +pymdown-extensions==9.9 mike==1.1.2 mkdocs-simple-hooks==0.1.5 mkdocs-git-revision-date-plugin==0.3.2 From 7743ef2f70d0fbda6b249781dd537917f0d01f45 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Fri, 25 Nov 2022 17:16:24 +1000 Subject: [PATCH 118/156] Fixes job summary directory creation #1353 (#1357) * Fixes job summary directory creation #1353 * Additional check for path * Fix test --- docs/CHANGELOG-v2.md | 6 ++++++ src/PSRule/Pipeline/Output/JobSummaryWriter.cs | 2 +- tests/PSRule.Tests/OutputWriterTests.cs | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index cfd482d50b..afabff6d84 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,12 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since pre-release v2.7.0-B0006: + +- Bug fixes: + - Fixed job summary directory creation by @BernieWhite. + [#1353](https://github.com/microsoft/PSRule/issues/1353) + ## v2.7.0-B0006 (pre-release) What's changed since pre-release v2.7.0-B0001: diff --git a/src/PSRule/Pipeline/Output/JobSummaryWriter.cs b/src/PSRule/Pipeline/Output/JobSummaryWriter.cs index 58f481aee5..7ad101d48c 100644 --- a/src/PSRule/Pipeline/Output/JobSummaryWriter.cs +++ b/src/PSRule/Pipeline/Output/JobSummaryWriter.cs @@ -58,7 +58,7 @@ public sealed override void End() private void Open() { - if (_OutputPath == null || _IsDisposed) + if (string.IsNullOrEmpty(_OutputPath) || _IsDisposed || !CreateFile(_OutputPath)) return; _Stream ??= new FileStream(_OutputPath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read); diff --git a/tests/PSRule.Tests/OutputWriterTests.cs b/tests/PSRule.Tests/OutputWriterTests.cs index ff5dc7bfc1..51ff6dcd76 100644 --- a/tests/PSRule.Tests/OutputWriterTests.cs +++ b/tests/PSRule.Tests/OutputWriterTests.cs @@ -316,7 +316,7 @@ public void JobSummary() result.Add(GetPass()); result.Add(GetFail()); result.Add(GetFail("rid-003", SeverityLevel.Warning, ruleId: "TestModule\\Rule-003")); - var writer = new JobSummaryWriter(output, option, null, outputPath: "", stream: stream); + var writer = new JobSummaryWriter(output, option, null, outputPath: "reports/summary.md", stream: stream); writer.Begin(); writer.WriteObject(result, false); writer.End(); From 096294880e44a21c9a6bc467979a24868ea06efd Mon Sep 17 00:00:00 2001 From: Bernie White Date: Fri, 25 Nov 2022 22:24:47 +1000 Subject: [PATCH 119/156] Fixes same key for ref and name #1354 (#1359) --- docs/CHANGELOG-v2.md | 2 + .../Definitions/DependencyTargetCollection.cs | 2 +- tests/PSRule.Tests/FromFile.Json.schema.json | 52 +-- tests/PSRule.Tests/FromFile.Rule.jsonc | 370 +++++++++--------- tests/PSRule.Tests/FromFile.Rule.yaml | 1 + tests/PSRule.Tests/FromFileAlias.Rule.jsonc | 34 +- tests/PSRule.Tests/RulesTests.cs | 2 +- 7 files changed, 233 insertions(+), 230 deletions(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index afabff6d84..2f2487da9c 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -35,6 +35,8 @@ What's changed since pre-release v2.7.0-B0006: - Bug fixes: - Fixed job summary directory creation by @BernieWhite. [#1353](https://github.com/microsoft/PSRule/issues/1353) + - Fixed same key for ref and name by @BernieWhite + [#1354](https://github.com/microsoft/PSRule/issues/1354) ## v2.7.0-B0006 (pre-release) diff --git a/src/PSRule/Definitions/DependencyTargetCollection.cs b/src/PSRule/Definitions/DependencyTargetCollection.cs index c8b95d856c..38df977ad3 100644 --- a/src/PSRule/Definitions/DependencyTargetCollection.cs +++ b/src/PSRule/Definitions/DependencyTargetCollection.cs @@ -56,7 +56,7 @@ public bool TryAdd(T target) // Add Id, Ref, and aliases to the index. _Index.Add(target.Id, new TargetLink(target, ResourceIdKind.Id)); - if (target.Ref.HasValue) + if (target.Ref.HasValue && target.Id != target.Ref.Value) _Index.Add(target.Ref.Value, new TargetLink(target, ResourceIdKind.Ref)); for (var i = 0; target.Alias != null && i < target.Alias.Length; i++) diff --git a/tests/PSRule.Tests/FromFile.Json.schema.json b/tests/PSRule.Tests/FromFile.Json.schema.json index 24b2ccd1b6..ca547b5a72 100644 --- a/tests/PSRule.Tests/FromFile.Json.schema.json +++ b/tests/PSRule.Tests/FromFile.Json.schema.json @@ -1,30 +1,30 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Unit test schema", - "description": "A schema for PSRule YAML options files.", - "oneOf": [ - { - "$ref": "#/definitions/structure" - } - ], - "definitions": { - "structure": { - "type": "object", - "properties": { - "Name": { - "type": "string", - "enum": [ - "TestObject1" - ] - }, - "Type": { - "type": "string" - } - }, - "required": [ - "Type", - "Name" - ] + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Unit test schema", + "description": "A schema for PSRule YAML options files.", + "oneOf": [ + { + "$ref": "#/definitions/structure" + } + ], + "definitions": { + "structure": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "enum": [ + "TestObject1" + ] + }, + "Type": { + "type": "string" } + }, + "required": [ + "Type", + "Name" + ] } + } } diff --git a/tests/PSRule.Tests/FromFile.Rule.jsonc b/tests/PSRule.Tests/FromFile.Rule.jsonc index b6c15bda82..838439ff13 100644 --- a/tests/PSRule.Tests/FromFile.Rule.jsonc +++ b/tests/PSRule.Tests/FromFile.Rule.jsonc @@ -1,202 +1,202 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. - // // JSON-based rules for unit testing // [ - { - // Synopsis: A YAML rule for testing. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Rule", - "metadata": { - "name": "JsonBasicRule", - "tags": { - "feature": "tag" - }, - "labels": { - "single": "Value", - "multi": [ - "Value1", - "Value2" - ] - } - }, - "spec": { - "condition": { - "allOf": [ - { - "field": "Name", - "equals": "TargetObject1" - }, - { - "field": "Value", - "in": [ - "Value1", - "Value2" - ] - } - ] - } - } + { + // Synopsis: A YAML rule for testing. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "JsonBasicRule", + "ref": "JsonBasicRule", + "tags": { + "feature": "tag" + }, + "labels": { + "single": "Value", + "multi": [ + "Value1", + "Value2" + ] + } }, - { - // Synopsis: A YAML rule for testing. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Rule", - "metadata": { - "name": "RuleJsonTrue", - "tags": { - "release": "GA" - } - }, - "spec": { - "condition": { - "field": "Value", - "equals": 3 - } - } + "spec": { + "condition": { + "allOf": [ + { + "field": "Name", + "equals": "TargetObject1" + }, + { + "field": "Value", + "in": [ + "Value1", + "Value2" + ] + } + ] + } + } + }, + { + // Synopsis: A YAML rule for testing. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "RuleJsonTrue", + "tags": { + "release": "GA" + } }, - { - // Synopsis: A YAML rule for testing. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Rule", - "metadata": { - "name": "RuleJsonFalse", - "tags": { - "release": "GA" - } - }, - "spec": { - "condition": { - "field": "Value", - "greater": 3 - } - } + "spec": { + "condition": { + "field": "Value", + "equals": 3 + } + } + }, + { + // Synopsis: A YAML rule for testing. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "RuleJsonFalse", + "tags": { + "release": "GA" + } }, - { - // Synopsis: A YAML rule for testing. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Rule", - "metadata": { - "name": "RuleJsonWithCustomType", - "tags": { - "release": "GA" - } - }, - "spec": { - "type": [ - "CustomType" - ], - "condition": { - "field": "Value", - "greater": 3 - } - } + "spec": { + "condition": { + "field": "Value", + "greater": 3 + } + } + }, + { + // Synopsis: A YAML rule for testing. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "RuleJsonWithCustomType", + "tags": { + "release": "GA" + } + }, + "spec": { + "type": [ + "CustomType" + ], + "condition": { + "field": "Value", + "greater": 3 + } + } + }, + { + // Synopsis: A YAML rule for testing. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "RuleJsonWithSelector", + "tags": { + "release": "GA" + } }, - { - // Synopsis: A YAML rule for testing. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Rule", - "metadata": { - "name": "RuleJsonWithSelector", - "tags": { - "release": "GA" - } - }, - "spec": { - "with": [ - "Test.Rule.Selector.1" - ], - "condition": { - "field": "notValue", - "greaterOrEquals": 3 - } - } + "spec": { + "with": [ + "Test.Rule.Selector.1" + ], + "condition": { + "field": "notValue", + "greaterOrEquals": 3 + } + } + }, + { + // Synopsis: A selector for YAML rule tests + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Test.Rule.Selector.1" }, - { - // Synopsis: A selector for YAML rule tests - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "Test.Rule.Selector.1" - }, - "spec": { - "if": { - "field": "notValue", - "exists": true - } - } + "spec": { + "if": { + "field": "notValue", + "exists": true + } + } + }, + { + // Synopsis: Test reason from rule. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "JsonReasonTest", + "tags": { + "test": "Reason" + } }, - { - // Synopsis: Test reason from rule. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Rule", - "metadata": { - "name": "JsonReasonTest", - "tags": { - "test": "Reason" - } - }, - "spec": { - "condition": { - "field": "Name", - "equals": "TestValue" - } - } + "spec": { + "condition": { + "field": "Name", + "equals": "TestValue" + } + } + }, + { + // Synopsis: Rule for unit testing of rule error level. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "JsonRuleErrorLevel", + "tags": { + "test": "Level" + } }, - { - // Synopsis: Rule for unit testing of rule error level. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Rule", - "metadata": { - "name": "JsonRuleErrorLevel", - "tags": { - "test": "Level" - } - }, - "spec": { - "level": "Error", - "condition": { - "field": "name", - "equals": "TestObject1" - } - } + "spec": { + "level": "Error", + "condition": { + "field": "name", + "equals": "TestObject1" + } + } + }, + { + // Synopsis: Rule for unit testing of rule warning level. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "JsonRuleWarningLevel", + "tags": { + "test": "Level" + } }, - { - // Synopsis: Rule for unit testing of rule warning level. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Rule", - "metadata": { - "name": "JsonRuleWarningLevel", - "tags": { - "test": "Level" - } - }, - "spec": { - "level": "Warning", - "condition": { - "field": "name", - "equals": "TestObject1" - } - } + "spec": { + "level": "Warning", + "condition": { + "field": "name", + "equals": "TestObject1" + } + } + }, + { + // Synopsis: Rule for unit testing of rule information level. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "JsonRuleInfoLevel", + "tags": { + "test": "Level" + } }, - { - // Synopsis: Rule for unit testing of rule information level. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Rule", - "metadata": { - "name": "JsonRuleInfoLevel", - "tags": { - "test": "Level" - } - }, - "spec": { - "level": "Information", - "condition": { - "field": "name", - "equals": "TestObject1" - } - } + "spec": { + "level": "Information", + "condition": { + "field": "name", + "equals": "TestObject1" + } } + } ] diff --git a/tests/PSRule.Tests/FromFile.Rule.yaml b/tests/PSRule.Tests/FromFile.Rule.yaml index 7680f131e7..2b729f8f0b 100644 --- a/tests/PSRule.Tests/FromFile.Rule.yaml +++ b/tests/PSRule.Tests/FromFile.Rule.yaml @@ -11,6 +11,7 @@ apiVersion: github.com/microsoft/PSRule/v1 kind: Rule metadata: name: YamlBasicRule + ref: YamlBasicRule tags: feature: tag labels: diff --git a/tests/PSRule.Tests/FromFileAlias.Rule.jsonc b/tests/PSRule.Tests/FromFileAlias.Rule.jsonc index 537008695b..f182738c51 100644 --- a/tests/PSRule.Tests/FromFileAlias.Rule.jsonc +++ b/tests/PSRule.Tests/FromFileAlias.Rule.jsonc @@ -1,20 +1,20 @@ [ - { - // Synopsis: A rule with an alias. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Rule", - "metadata": { - "name": "JSON.RuleWithAlias1", - "alias": [ - "JSON.AlternativeName" - ], - "ref": "PSRZZ.0003" - }, - "spec": { - "condition": { - "field": "name", - "exists": true - } - } + { + // Synopsis: A rule with an alias. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "JSON.RuleWithAlias1", + "alias": [ + "JSON.AlternativeName" + ], + "ref": "PSRZZ.0003" + }, + "spec": { + "condition": { + "field": "name", + "exists": true + } } + } ] diff --git a/tests/PSRule.Tests/RulesTests.cs b/tests/PSRule.Tests/RulesTests.cs index abe32802c7..4cc4c48dc1 100644 --- a/tests/PSRule.Tests/RulesTests.cs +++ b/tests/PSRule.Tests/RulesTests.cs @@ -209,7 +209,7 @@ public void ReadJsonRule() Assert.NotNull(rule); Assert.Equal("JsonBasicRule", rule[0].Name); Assert.Equal(PSRuleOption.GetRootedPath(""), rule[0].Source.HelpPath); - Assert.Equal(8, rule[0].Extent.Line); + Assert.Equal(7, rule[0].Extent.Line); // From relative path rule = HostHelper.GetRule(GetSource("../../../FromFile.Rule.jsonc"), context, includeDependencies: false); From 2ff19f5b51c0e36296f21de6e620e2b7abb0963f Mon Sep 17 00:00:00 2001 From: Bernie White Date: Fri, 25 Nov 2022 23:46:50 +1000 Subject: [PATCH 120/156] Release v2.7.0-B0016 (#1360) --- docs/CHANGELOG-v2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 2f2487da9c..8d28069c93 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.7.0-B0016 (pre-release) + What's changed since pre-release v2.7.0-B0006: - Bug fixes: From 1f878e96cede4f80714c04e93a577239d686661c Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sun, 27 Nov 2022 23:50:10 +1000 Subject: [PATCH 121/156] Fixes CLI module and assembly loading #1361 #1362 (#1363) --- docs/CHANGELOG-v2.md | 8 ++++++ src/PSRule.Tool/ClientHelper.cs | 7 ++--- src/PSRule.Tool/ClientHost.cs | 5 +++- src/PSRule/Configuration/PSRuleOption.cs | 14 ++++++++++ src/PSRule/Definitions/IResultRecord.cs | 3 +++ src/PSRule/Definitions/IRuleResultV2.cs | 15 +++++++++++ src/PSRule/Pipeline/CommandLineBuilder.cs | 6 +++++ src/PSRule/Pipeline/GetRulePipelineBuiler.cs | 10 ++++++++ src/PSRule/Pipeline/GetTargetPipeline.cs | 10 ++++++++ src/PSRule/Pipeline/HostContext.cs | 18 +++++++++++++ src/PSRule/Pipeline/SourcePipeline.cs | 27 +++++++++++++++++--- src/PSRule/Pipeline/TargetObject.cs | 2 +- tests/PSRule.Tests/SuppressionGroupTests.cs | 3 --- 13 files changed, 116 insertions(+), 12 deletions(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 8d28069c93..af2b1ae09d 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,14 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since pre-release v2.7.0-B0016: + +- Bug fixes: + - Fixes CLI failed to load required assemblies by @BernieWhite. + [#1361](https://github.com/microsoft/PSRule/issues/1361) + - Fixes CLI ignores modules specified in `Include.Modules` by @BernieWhite. + [#1362](https://github.com/microsoft/PSRule/issues/1362) + ## v2.7.0-B0016 (pre-release) What's changed since pre-release v2.7.0-B0006: diff --git a/src/PSRule.Tool/ClientHelper.cs b/src/PSRule.Tool/ClientHelper.cs index f98406b81c..e301e662ff 100644 --- a/src/PSRule.Tool/ClientHelper.cs +++ b/src/PSRule.Tool/ClientHelper.cs @@ -15,8 +15,8 @@ internal sealed class ClientHelper { public static void RunAnalyze(AnalyzerOptions operationOptions, ClientContext clientContext, InvocationContext invocation) { - var option = GetOption(); var host = new ClientHost(invocation, operationOptions.Verbose, operationOptions.Debug); + var option = GetOption(host); var inputPath = operationOptions.InputPath == null || operationOptions.InputPath.Length == 0 ? new string[] { PSRuleOption.GetWorkingPath() } : operationOptions.InputPath; @@ -40,8 +40,8 @@ public static void RunAnalyze(AnalyzerOptions operationOptions, ClientContext cl public static void RunRestore(RestoreOptions operationOptions, ClientContext clientContext, InvocationContext invocation) { - var option = GetOption(); var host = new ClientHost(invocation, operationOptions.Verbose, operationOptions.Debug); + var option = GetOption(host); var requires = option.Requires.ToArray(); using var pwsh = PowerShell.Create(); @@ -97,8 +97,9 @@ private static void InstallVersion(PowerShell pwsh, string name, string version) pwsh.Invoke(); } - private static PSRuleOption GetOption() + private static PSRuleOption GetOption(ClientHost host) { + PSRuleOption.UseHostContext(host); var option = PSRuleOption.FromFileOrEmpty(); option.Execution.InitialSessionState = Configuration.SessionState.Minimal; option.Input.Format = InputFormat.File; diff --git a/src/PSRule.Tool/ClientHost.cs b/src/PSRule.Tool/ClientHost.cs index 1258b37a90..fc2a5cbdbc 100644 --- a/src/PSRule.Tool/ClientHost.cs +++ b/src/PSRule.Tool/ClientHost.cs @@ -4,6 +4,7 @@ using System.CommandLine; using System.CommandLine.Invocation; using System.CommandLine.IO; +using System.IO; using System.Management.Automation; using PSRule.Pipeline; @@ -11,7 +12,7 @@ namespace PSRule.Tool { internal sealed class ClientHost : HostContext { - private InvocationContext _Invocation; + private readonly InvocationContext _Invocation; private readonly bool _Verbose; private readonly bool _Debug; @@ -20,6 +21,8 @@ public ClientHost(InvocationContext invocation, bool verbose, bool debug) _Invocation = invocation; _Verbose = verbose; _Debug = debug; + + Verbose($"Using working path: {Directory.GetCurrentDirectory()}"); } public override ActionPreference GetPreferenceVariable(string variableName) diff --git a/src/PSRule/Configuration/PSRuleOption.cs b/src/PSRule/Configuration/PSRuleOption.cs index 67cb370d8b..f2dface15b 100644 --- a/src/PSRule/Configuration/PSRuleOption.cs +++ b/src/PSRule/Configuration/PSRuleOption.cs @@ -11,6 +11,7 @@ using System.Threading; using Newtonsoft.Json; using PSRule.Definitions.Baselines; +using PSRule.Pipeline; using PSRule.Resources; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; @@ -395,6 +396,19 @@ public static void UseExecutionContext(EngineIntrinsics executionContext) _GetWorkingPath = () => executionContext.SessionState.Path.CurrentFileSystemLocation.Path; } + /// + /// Set working path from a command-line host environment. + /// + public static void UseHostContext(IHostContext hostContext) + { + if (hostContext == null) + { + _GetWorkingPath = () => Directory.GetCurrentDirectory(); + return; + } + _GetWorkingPath = () => hostContext.GetWorkingPath(); + } + /// /// Configures PSRule to use the culture of the current thread at runtime. /// diff --git a/src/PSRule/Definitions/IResultRecord.cs b/src/PSRule/Definitions/IResultRecord.cs index 47b81d9a00..6d86cf8b1a 100644 --- a/src/PSRule/Definitions/IResultRecord.cs +++ b/src/PSRule/Definitions/IResultRecord.cs @@ -3,6 +3,9 @@ namespace PSRule.Definitions { + /// + /// A base interface for a PSRule result record. + /// public interface IResultRecord { } diff --git a/src/PSRule/Definitions/IRuleResultV2.cs b/src/PSRule/Definitions/IRuleResultV2.cs index 3d4b87a6cb..eff51db125 100644 --- a/src/PSRule/Definitions/IRuleResultV2.cs +++ b/src/PSRule/Definitions/IRuleResultV2.cs @@ -44,6 +44,9 @@ public interface IRuleResultV2 : IResultRecord long Time { get; } } + /// + /// Detailed rule records for PSRule v2. + /// public interface IDetailedRuleResultV2 : IRuleResultV2 { /// @@ -61,8 +64,14 @@ public interface IDetailedRuleResultV2 : IRuleResultV2 /// Hashtable Field { get; } + /// + /// The bound name of the target. + /// string TargetName { get; } + /// + /// The bound type of the target. + /// string TargetType { get; } } @@ -92,8 +101,14 @@ public interface IResultReasonV2 /// string FullPath { get; } + /// + /// The reason message. + /// string Message { get; } + /// + /// Return a formatted reason string. + /// string Format(); } } diff --git a/src/PSRule/Pipeline/CommandLineBuilder.cs b/src/PSRule/Pipeline/CommandLineBuilder.cs index eb8a071d6f..cea81918af 100644 --- a/src/PSRule/Pipeline/CommandLineBuilder.cs +++ b/src/PSRule/Pipeline/CommandLineBuilder.cs @@ -27,6 +27,9 @@ public static IInvokePipelineBuilder Invoke(string[] module, PSRuleOption option for (var i = 0; i < module.Length; i++) sourcePipeline.ModuleByName(module[i]); + for (var i = 0; option.Include.Module != null && i < option.Include.Module.Length; i++) + sourcePipeline.ModuleByName(option.Include.Module[i]); + var source = sourcePipeline.Build(); var pipeline = new InvokeRulePipelineBuilder(source, hostContext); pipeline.Configure(option); @@ -49,6 +52,9 @@ public static IInvokePipelineBuilder Assert(string[] module, PSRuleOption option for (var i = 0; module != null && i < module.Length; i++) sourcePipeline.ModuleByName(module[i]); + for (var i = 0; option.Include.Module != null && i < option.Include.Module.Length; i++) + sourcePipeline.ModuleByName(option.Include.Module[i]); + var source = sourcePipeline.Build(); var pipeline = new AssertPipelineBuilder(source, hostContext); pipeline.Configure(option); diff --git a/src/PSRule/Pipeline/GetRulePipelineBuiler.cs b/src/PSRule/Pipeline/GetRulePipelineBuiler.cs index 609bd9a11b..e3981aae8b 100644 --- a/src/PSRule/Pipeline/GetRulePipelineBuiler.cs +++ b/src/PSRule/Pipeline/GetRulePipelineBuiler.cs @@ -5,8 +5,14 @@ namespace PSRule.Pipeline { + /// + /// A helper to build a get pipeline. + /// public interface IGetPipelineBuilder : IPipelineBuilder { + /// + /// Determines if the returned rules also include rule dependencies. + /// void IncludeDependencies(); } @@ -20,6 +26,7 @@ internal sealed class GetRulePipelineBuilder : PipelineBuilderBase, IGetPipeline internal GetRulePipelineBuilder(Source[] source, IHostContext hostContext) : base(source, hostContext) { } + /// public override IPipelineBuilder Configure(PSRuleOption option) { if (option == null) @@ -36,11 +43,14 @@ public override IPipelineBuilder Configure(PSRuleOption option) return this; } + + /// public void IncludeDependencies() { _IncludeDependencies = true; } + /// public override IPipeline Build(IPipelineWriter writer = null) { return !RequireModules() || !RequireSources() diff --git a/src/PSRule/Pipeline/GetTargetPipeline.cs b/src/PSRule/Pipeline/GetTargetPipeline.cs index 1ae7e2782e..6b757134e0 100644 --- a/src/PSRule/Pipeline/GetTargetPipeline.cs +++ b/src/PSRule/Pipeline/GetTargetPipeline.cs @@ -7,8 +7,14 @@ namespace PSRule.Pipeline { + /// + /// A helper to build a pipeline to return target objects. + /// public interface IGetTargetPipelineBuilder : IPipelineBuilder { + /// + /// Specifies a path for reading input objects from disk. + /// void InputPath(string[] path); } @@ -25,6 +31,7 @@ internal GetTargetPipelineBuilder(Source[] source, IHostContext hostContext) _InputPath = null; } + /// public override IPipelineBuilder Configure(PSRuleOption option) { if (option == null) @@ -41,6 +48,7 @@ public override IPipelineBuilder Configure(PSRuleOption option) return this; } + /// public void InputPath(string[] path) { if (path == null || path.Length == 0) @@ -58,11 +66,13 @@ public void InputPath(string[] path) _InputPath = builder; } + /// public override IPipeline Build(IPipelineWriter writer = null) { return new GetTargetPipeline(PrepareContext(null, null, null), PrepareReader(), writer ?? PrepareWriter()); } + /// protected override PipelineReader PrepareReader() { if (!string.IsNullOrEmpty(Option.Input.ObjectPath)) diff --git a/src/PSRule/Pipeline/HostContext.cs b/src/PSRule/Pipeline/HostContext.cs index ba32ea22de..7f4aa5a483 100644 --- a/src/PSRule/Pipeline/HostContext.cs +++ b/src/PSRule/Pipeline/HostContext.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.IO; using System.Management.Automation; using PSRule.Definitions; @@ -65,6 +66,11 @@ public interface IHostContext /// Determines if a destructive action such as overwriting a file should be processed. /// bool ShouldProcess(string target, string action); + + /// + /// Get the current working path. + /// + string GetWorkingPath(); } internal static class HostContextExtensions @@ -186,6 +192,12 @@ public virtual void Record(IResultRecord record) { } + + /// + public virtual string GetWorkingPath() + { + return Directory.GetCurrentDirectory(); + } } /// @@ -273,5 +285,11 @@ public void Object(object sendToPipeline, bool enumerateCollection) { CmdletContext.WriteObject(sendToPipeline, enumerateCollection); } + + /// + public string GetWorkingPath() + { + return ExecutionContext.SessionState.Path.CurrentFileSystemLocation.Path; + } } } diff --git a/src/PSRule/Pipeline/SourcePipeline.cs b/src/PSRule/Pipeline/SourcePipeline.cs index 0617afc0e6..3b2c8abdc0 100644 --- a/src/PSRule/Pipeline/SourcePipeline.cs +++ b/src/PSRule/Pipeline/SourcePipeline.cs @@ -294,13 +294,14 @@ private static string[] SortModulePath(IEnumerable values) return results; } - private static Source.ModuleInfo LoadManifest(string basePath) + private Source.ModuleInfo LoadManifest(string basePath) { var name = Path.GetFileName(Path.GetDirectoryName(basePath)); var path = Path.Combine(basePath, GetManifestName(name)); if (!File.Exists(path)) return null; + Log("Loading manifest for: {0}", basePath); using var reader = new StreamReader(path); var data = reader.ReadToEnd(); var ast = System.Management.Automation.Language.Parser.ParseInput(data, out _, out _); @@ -316,14 +317,32 @@ private static Source.ModuleInfo LoadManifest(string basePath) var projectUri = psData["ProjectUri"] as string; var prerelease = psData["Prerelease"] as string; - if (manifest["RequiredAssemblies"] is Array requiredAssemblies) + if (TryRequiredAssemblies(manifest["RequiredAssemblies"], out var requiredAssemblies)) { - foreach (var a in requiredAssemblies.OfType()) - Assembly.LoadFile(Path.Combine(basePath, a)); + foreach (var a in requiredAssemblies) + { + var assemblyPath = Path.Combine(basePath, a); + Log("Loading assembly: {0}", assemblyPath); + Assembly.LoadFile(assemblyPath); + } } return new Source.ModuleInfo(basePath, name, version, projectUri, guid, companyName, prerelease); } + private static bool TryRequiredAssemblies(object value, out IEnumerable requiredAssemblies) + { + requiredAssemblies = null; + if (value == null) return false; + + if (value is string s) + requiredAssemblies = new string[] { s }; + + if (value is Array array) + requiredAssemblies = array.OfType().ToArray(); + + return requiredAssemblies != null; + } + private static string GetManifestName(string name) { return string.Concat(name, ".psd1"); diff --git a/src/PSRule/Pipeline/TargetObject.cs b/src/PSRule/Pipeline/TargetObject.cs index 81ef6cb3bc..1344116990 100644 --- a/src/PSRule/Pipeline/TargetObject.cs +++ b/src/PSRule/Pipeline/TargetObject.cs @@ -63,7 +63,7 @@ internal TargetObject(PSObject o, TargetSourceCollection source) } internal TargetObject(PSObject o, string targetName = null, string targetType = null, string scope = null) - : this (o, null) + : this(o, null) { if (targetName != null) TargetName = targetName; diff --git a/tests/PSRule.Tests/SuppressionGroupTests.cs b/tests/PSRule.Tests/SuppressionGroupTests.cs index a7fe0e9bce..205815cdab 100644 --- a/tests/PSRule.Tests/SuppressionGroupTests.cs +++ b/tests/PSRule.Tests/SuppressionGroupTests.cs @@ -6,11 +6,8 @@ using System.Linq; using System.Management.Automation; using PSRule.Configuration; -using PSRule.Definitions; -using PSRule.Definitions.SuppressionGroups; using PSRule.Host; using PSRule.Pipeline; -using PSRule.Rules; using PSRule.Runtime; using Xunit; using Assert = Xunit.Assert; From a56520f0e65684ece922800947ba17e622557e0a Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sat, 3 Dec 2022 02:02:05 +1000 Subject: [PATCH 122/156] Added API version date comparison #1356 (#1365) --- README.md | 2 + docs/CHANGELOG-v2.md | 3 + .../PSRule/en-US/about_PSRule_Assert.md | 83 ++ .../PSRule/en-US/about_PSRule_Expressions.md | 49 ++ schemas/PSRule-language.schema.json | 41 + src/PSRule.Types/Data/DateVersion.cs | 776 ++++++++++++++++++ src/PSRule.Types/Data/ModuleConstraint.cs | 4 +- src/PSRule.Types/Data/SemanticVersion.cs | 10 +- .../Expressions/LanguageExpressions.cs | 27 + src/PSRule/Runtime/Assert.cs | 35 + tests/PSRule.Tests/AssertTests.cs | 37 + tests/PSRule.Tests/DateVersionTests.cs | 184 +++++ tests/PSRule.Tests/SelectorTests.cs | 43 +- tests/PSRule.Tests/Selectors.Rule.jsonc | 43 + tests/PSRule.Tests/Selectors.Rule.yaml | 34 + tests/PSRule.Tests/SemanticVersionTests.cs | 11 +- 16 files changed, 1370 insertions(+), 12 deletions(-) create mode 100644 src/PSRule.Types/Data/DateVersion.cs create mode 100644 tests/PSRule.Tests/DateVersionTests.cs diff --git a/README.md b/README.md index a8e04c9a43..654ca90477 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,7 @@ The following commands exist in the `PSRule` module: The following conceptual topics exist in the `PSRule` module: - [Assert](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Assert/) + - [APIVersion](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Assert/#apiversion) - [Contains](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Assert/#contains) - [Count](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Assert/#count) - [EndsWith](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Assert/#endswith) @@ -235,6 +236,7 @@ The following conceptual topics exist in the `PSRule` module: - [Expressions](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/) - [AllOf](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#allof) - [AnyOf](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#anyof) + - [APIVersion](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#apiversion) - [Contains](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#contains) - [Count](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#count) - [EndsWith](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#endswith) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index af2b1ae09d..5ac2ad1a52 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -32,6 +32,9 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers What's changed since pre-release v2.7.0-B0016: +- New features: + - Added API version date comparison assertion method and expression by @BernieWhite. + [#1356](https://github.com/microsoft/PSRule/issues/1356) - Bug fixes: - Fixes CLI failed to load required assemblies by @BernieWhite. [#1361](https://github.com/microsoft/PSRule/issues/1361) diff --git a/docs/concepts/PSRule/en-US/about_PSRule_Assert.md b/docs/concepts/PSRule/en-US/about_PSRule_Assert.md index c831b7c660..f94ffc3d7e 100644 --- a/docs/concepts/PSRule/en-US/about_PSRule_Assert.md +++ b/docs/concepts/PSRule/en-US/about_PSRule_Assert.md @@ -20,6 +20,7 @@ Each `$Assert` method returns an `AssertResult` object that contains the result The following built-in assertion methods are provided: +- [APIVersion](#apiversion) - The field value must be a date version string. - [Contains](#contains) - The field value must contain at least one of the strings. - [Count](#count) - The field value must contain the specified number of items. - [EndsWith](#endswith) - The field value must match at least one suffix. @@ -138,6 +139,88 @@ Notable differences between object paths and JSONPath are: However member names can not start or end with a dash. i.e. `Properties.dashed-name` and `Properties.'-dashed-name'` are valid. +### APIVersion + +The `APIVersion` assertion method checks the field value is a valid date version. +A constraint can optionally be provided to require the date version to be within a range. + +A date version uses the format `yyyy-MM-dd` (`2015-10-01`). +Additionally an optional string prerelease identifier can be used `yyyy-MM-dd-prerelease` (`2015-10-01-preview.1`). + +The following parameters are accepted: + +- `inputObject` - The object being checked for the specified field. +- `field` - The name of the field to check. + This is a case insensitive compare. +- `constraint` (optional) - A version constraint, see below for details of version constrain format. +- `includePrerelease` (optional) - Determines if prerelease versions are included. + Unless specified this defaults to `$False`. + +The following are supported constraints: + +- `version` - Must match version exactly. This also accepts the prefix `=`. + - e.g. `2015-10-01`, `=2015-10-01` +- `>version` - Must be greater than version. + - e.g. `>2015-10-01` +- `>=version` - Must be greater than or equal to version. + - e.g. `>=2015-10-01` +- `=2015-10-01 <2022-03-01` results in: + - Pass: `2014-01-01`, `2015-10-01`, `2019-06-30`, `2022-02-01`. + - Fail: `2015-01-01`, `2022-09-01`. + +Handling for prerelease versions: + +- Constraints and versions containing prerelease identifiers are supported. + i.e. `>=2015-10-01-preview` or `2015-10-01-preview`. +- A version containing a prerelease identifer follows similar ordering to semantic versioning. + i.e. `2015-10-01-preview` < `2015-10-01-preview.1` < `2015-10-01` < `2022-03-01-preview` < `2022-03-01`. +- A constraint without a prerelease identifer will only match a stable version by default. + Set `includePrerelease` to `$True` to include prerelease versions. + Alternatively use the `@pre` or `@prerelease` flag in a constraint. + +Reasons include: + +- _The parameter 'inputObject' is null._ +- _The parameter 'field' is null or empty._ +- _The field '{0}' does not exist._ +- _The field value '{0}' is not a version string._ +- _The version '{0}' does not match the constraint '{1}'._ + +Examples: + +```powershell +Rule 'ValidAPIVersion' { + $Assert.APIVersion($TargetObject, 'apiVersion') +} + +Rule 'MinimumAPIVersion' { + $Assert.APIVersion($TargetObject, 'apiVersion', '>=2015-10-01') +} + +Rule 'MinimumAPIVersionWithPrerelease' { + $Assert.APIVersion($TargetObject, 'apiVersion', '>=2015-10-01-0', $True) +} + +Rule 'MinimumAPIVersionWithFlag' { + $Assert.APIVersion($TargetObject, 'apiVersion', '@pre >=2015-10-01-0') +} +``` + ### Contains The `Contains` assertion method checks the field value contains the specified string. diff --git a/docs/concepts/PSRule/en-US/about_PSRule_Expressions.md b/docs/concepts/PSRule/en-US/about_PSRule_Expressions.md index 5e02d594f1..f8201d5bad 100644 --- a/docs/concepts/PSRule/en-US/about_PSRule_Expressions.md +++ b/docs/concepts/PSRule/en-US/about_PSRule_Expressions.md @@ -18,6 +18,7 @@ Expressions are comprised of nested conditions, operators, and comparison proper The following conditions are available: +- [APIVersion](#apiversion) - [Contains](#contains) - [Count](#count) - [Equals](#equals) @@ -158,6 +159,54 @@ spec: exists: true ``` +### APIVersion + +The `apiVersion` condition determines if the operand is a valid date version. +A constraint can optionally be provided to require the date version to be within a range. +Supported version constraints for expression are the same as the `$Assert.APIVersion` assertion helper. + +Syntax: + +```yaml +apiVersion: +includePrerelease: +``` + +For example: + +```yaml +--- +apiVersion: github.com/microsoft/PSRule/v1 +kind: Rule +metadata: + name: 'ExampleAPIVersion' +spec: + condition: + field: 'engine.apiVersion' + apiVersion: '>=2015-10-01' + +--- +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: 'ExampleAnyAPIVersion' +spec: + if: + field: 'engine.apiVersion' + apiVersion: '' + +--- +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: 'ExampleAPIVersionIncludingPrerelease' +spec: + if: + field: 'engine.apiVersion' + apiVersion: '>=2015-10-01' + includePrerelease: true +``` + ### Contains The `contains` condition can be used to determine if the operand contains a specified sub-string. diff --git a/schemas/PSRule-language.schema.json b/schemas/PSRule-language.schema.json index 964b9a9d91..b30580c5c6 100644 --- a/schemas/PSRule-language.schema.json +++ b/schemas/PSRule-language.schema.json @@ -913,6 +913,9 @@ { "$ref": "#/definitions/expressions/definitions/conditions/definitions/version" }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/apiVersion" + }, { "$ref": "#/definitions/expressions/definitions/conditions/definitions/hasDefault" }, @@ -1280,6 +1283,9 @@ { "$ref": "#/definitions/expressions/definitions/conditions/definitions/version" }, + { + "$ref": "#/definitions/expressions/definitions/conditions/definitions/apiVersion" + }, { "$ref": "#/definitions/expressions/definitions/conditions/definitions/hasDefault" }, @@ -2638,6 +2644,41 @@ "additionalProperties": false, "minProperties": 2 }, + "apiVersion": { + "type": "object", + "properties": { + "apiVersion": { + "type": "string", + "title": "API Version", + "description": "Must be a valid date version. A constraint can optionally be provided to require the date version to be within a range.", + "markdownDescription": "Must be a valid date version. A constraint can optionally be provided to require the date version to be within a range. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#apiversion)", + "default": "" + }, + "includePrerelease": { + "type": "boolean", + "title": "Include pre-release", + "description": "Determines if pre-release versions are included. By default, pre-release versions are not included.", + "markdownDescription": "Determines if pre-release versions are included. By default, pre-release versions are not included. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#apiversion)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "apiVersion" + ], + "oneOf": [ + { + "$ref": "#/definitions/expressions/definitions/properties" + } + ], + "additionalProperties": false, + "minProperties": 2 + }, "hasDefault": { "type": "object", "properties": { diff --git a/src/PSRule.Types/Data/DateVersion.cs b/src/PSRule.Types/Data/DateVersion.cs new file mode 100644 index 0000000000..7ba5445115 --- /dev/null +++ b/src/PSRule.Types/Data/DateVersion.cs @@ -0,0 +1,776 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; + +namespace PSRule.Data +{ + /// + /// An date version constraint. + /// + public interface IDateVersionConstraint + { + /// + /// Determines if the date version meets the requirments of the constraint. + /// + bool Equals(DateVersion.Version version); + } + + /// + /// A helper for comparing date version strings. + /// An date version is represented as YYYY-MM-DD-prerelease. + /// + public static class DateVersion + { + private const char EQUAL = '='; + private const char GREATER = '>'; + private const char LESS = '<'; + private const char DOT = '.'; + private const char DASH = '-'; + private const char PLUS = '+'; + private const char ZERO = '0'; + private const char PIPE = '|'; + private const char SPACE = ' '; + private const char AT = '@'; + + private const string FLAG_PRERELEASE = "@prerelease "; + private const string FLAG_PRE = "@pre "; + + /// + /// A comparison operation for a version constraint. + /// + [Flags] + internal enum ComparisonOperator + { + None = 0, + + /// + /// YYYY-MM-DD bits must match. + /// + Equals = 1, + + GreaterThan = 8, + + LessThan = 16 + } + + internal enum JoinOperator + { + None = 0, + And = 1, + Or = 2 + } + + [Flags] + internal enum ConstraintModifier + { + None = 0, + + Prerelease = 1 + } + + /// + /// An date version constraint. + /// + public sealed class VersionConstraint : IDateVersionConstraint + { + private List _Constraints; + + /// + public bool Equals(Version version) + { + if (_Constraints == null || _Constraints.Count == 0) + return true; + + var match = false; + var i = 0; + while (!match && i < _Constraints.Count) + { + var result = _Constraints[i].Equals(version); + + // True OR + if (result && _Constraints[i].Join == JoinOperator.Or) + return true; + + // True AND + if (result && _Constraints[i].Join == JoinOperator.And && ++i < _Constraints.Count) + continue; + + // False OR + if (_Constraints[i].Join == JoinOperator.Or && ++i < _Constraints.Count) + continue; + + // False AND + while (++i < _Constraints.Count) + { + // Move to after the next OR. + if (_Constraints[i].Join == JoinOperator.Or) + { + i++; + continue; + } + } + } + return false; + } + + internal void Join(int year, int month, int day, PR prid, ComparisonOperator flag, JoinOperator join, bool includePrerelease) + { + if (_Constraints == null) + _Constraints = new List(); + + _Constraints.Add(new ConstraintExpression( + year, + month, + day, + prid, + flag, + join == JoinOperator.None ? JoinOperator.Or : join, + includePrerelease + )); + } + } + + [DebuggerDisplay("{_Year}.{_Month}.{_Day}")] + internal sealed class ConstraintExpression : IDateVersionConstraint + { + private readonly ComparisonOperator _Flag; + private readonly int _Year; + private readonly int _Month; + private readonly int _Day; + private readonly PR _PRID; + private readonly bool _IncludePrerelease; + + internal ConstraintExpression(int year, int month, int day, PR prid, ComparisonOperator flag, JoinOperator join, bool includePrerelease) + { + _Flag = flag == ComparisonOperator.None ? ComparisonOperator.Equals : flag; + _Year = year; + _Month = month; + _Day = day; + _PRID = prid; + Join = join; + _IncludePrerelease = includePrerelease; + } + + public bool Stable => IsStable(_PRID); + + public JoinOperator Join { get; } + + public static bool TryParse(string value, out IDateVersionConstraint constraint) + { + return TryParseConstraint(value, out constraint); + } + + public bool Equals(Version version) + { + return Equals(version.Year, version.Month, version.Day, version.Prerelease); + } + + public bool Equals(int year, int month, int day, PR prid) + { + if (_Flag == ComparisonOperator.Equals) + return EQ(year, month, day, prid); + + // Fail when pre-release should not be included + if (GuardPRID(prid)) + return false; + + // Fail when not greater + if (GuardGreater(year, month, day, prid)) + return false; + + // Fail when not greater or equal to + if (GuardGreaterOrEqual(year, month, day, prid)) + return false; + + // Fail when not less + if (GaurdLess(year, month, day, prid)) + return false; + + // Fail with not less or equal to + return !GuardLessOrEqual(year, month, day, prid); + } + + private bool GuardLessOrEqual(int year, int month, int day, PR prid) + { + return _Flag == (ComparisonOperator.LessThan | ComparisonOperator.Equals) && !(LT(year, month, day, prid) || EQ(year, month, day, prid)); + } + + private bool GaurdLess(int year, int month, int day, PR prid) + { + return _Flag == ComparisonOperator.LessThan && !LT(year, month, day, prid); + } + + private bool GuardGreaterOrEqual(int year, int month, int day, PR prid) + { + return _Flag == (ComparisonOperator.GreaterThan | ComparisonOperator.Equals) && !(GT(year, month, day, prid) || EQ(year, month, day, prid)); + } + + private bool GuardGreater(int year, int month, int day, PR prid) + { + return _Flag == ComparisonOperator.GreaterThan && !GT(year, month, day, prid); + } + + private bool GuardPRID(PR prid) + { + return !_IncludePrerelease && Stable && !IsStable(prid); + } + + private bool EQ(int year, int month, int day, PR prid) + { + return EQCore(year, month, day) && PR(prid) == 0; + } + + private bool EQCore(int year, int month, int day) + { + return (_Year == -1 || _Year == year) && + (_Month == -1 || _Month == month) && + (_Day == -1 || _Day == day); + } + + private bool GTCore(int year, int month, int day) + { + return (year > _Year) || + (year == _Year && month > _Month) || + (year == _Year && month == _Month && day > _Day); + } + + private bool LTCore(int year, int month, int day) + { + return (year < _Year) || + (year == _Year && month < _Month) || + (year == _Year && month == _Month && day < _Day); + } + + /// + /// Greater Than. + /// + private bool GT(int year, int month, int day, PR prid) + { + return !IsStable(prid) && !_IncludePrerelease + ? EQCore(year, month, day) && PR(prid) < 0 + : GTCore(year, month, day) || (EQCore(year, month, day) && PR(prid) < 0); + } + + /// + /// Less Than. + /// + private bool LT(int year, int month, int day, PR prid) + { + return !IsStable(prid) && !_IncludePrerelease + ? EQCore(year, month, day) && PR(prid) > 0 + : LTCore(year, month, day) || (EQCore(year, month, day) && PR(prid) > 0); + } + + /// + /// Compare pre-release. + /// + private int PR(PR prid) + { + return _PRID.CompareTo(prid); + } + + private static bool IsStable(PR prid) + { + return prid == null || prid.Stable; + } + } + + /// + /// An date version. + /// + public sealed class Version : IComparable, IEquatable + { + /// + /// The year part of the version. + /// + public readonly int Year; + + /// + /// The month part of the version. + /// + public readonly int Month; + + /// + /// The day part of the version. + /// + public readonly int Day; + + /// + /// The pre-release part of the version. + /// + public readonly PR Prerelease; + + internal Version(int year, int month, int day, PR prerelease) + { + Year = year; + Month = month; + Day = day; + Prerelease = prerelease; + } + + /// + public override string ToString() + { + return string.Concat(Year, DASH, Month, DASH, Day); + } + + /// + public override bool Equals(object obj) + { + return obj is Version version && Equals(version); + } + + /// + public override int GetHashCode() + { + unchecked // Overflow is fine + { + var hash = 17; + hash = hash * 23 + Year.GetHashCode(); + hash = hash * 23 + Month.GetHashCode(); + hash = hash * 23 + Day.GetHashCode(); + hash = hash * 23 + (Prerelease != null ? Prerelease.GetHashCode() : 0); + return hash; + } + } + + /// + /// Compare the version against another version. + /// + public bool Equals(Version other) + { + return other != null && + Equals(other.Year, other.Month, other.Day); + } + + /// + /// Compare the version against another version based on YYYY-MM-DD. + /// + public bool Equals(int year, int month, int day) + { + return year == Year && + month == Month && + day == Day; + } + + /// + /// Compare the version against another version. + /// + public int CompareTo(Version other) + { + if (other == null) + return 1; + + if (Year != other.Year) + return Year > other.Year ? 32 : -32; + + if (Month != other.Month) + return Month > other.Month ? 16 : -16; + + if (Day != other.Day) + return Day > other.Day ? 8 : -8; + + if ((Prerelease == null || Prerelease.Stable) && (other.Prerelease == null || other.Prerelease.Stable)) + return 0; + + if (Prerelease != null && !Prerelease.Stable && other.Prerelease != null && !other.Prerelease.Stable) + return Prerelease.CompareTo(other.Prerelease); + + return Prerelease == null || Prerelease.Stable ? 1 : -1; + } + } + + /// + /// An date version pre-release identifier. + /// + [DebuggerDisplay("{Value}")] + public sealed class PR + { + internal static readonly PR Empty = new PR(); + private static readonly char[] SEPARATORS = new char[] { DOT }; + + private readonly string[] _Identifiers; + + private PR() + { + Value = string.Empty; + _Identifiers = null; + } + + internal PR(string value) + { + Value = string.IsNullOrEmpty(value) ? string.Empty : value; + _Identifiers = string.IsNullOrEmpty(value) ? null : value.Split(SEPARATORS, StringSplitOptions.RemoveEmptyEntries); + } + + /// + /// The string value of a pre-release identifier. + /// + public string Value { get; } + + /// + /// Is the pre-release identifier empty, indicating a stable release. + /// + public bool Stable => _Identifiers == null; + + /// + /// Compare the pre-release identifer to another pre-release identifier. + /// + public int CompareTo(PR pr) + { + if (pr == null || pr.Stable) + return Stable ? 0 : -1; + else if (Stable) + return 1; + + var i = -1; + var left = _Identifiers; + var right = pr._Identifiers; + + while (++i < left.Length && i < right.Length) + { + var leftNumeric = false; + var rightNumeric = false; + if (long.TryParse(left[i], out var l)) + leftNumeric = true; + + if (long.TryParse(right[i], out var r)) + rightNumeric = true; + + if (leftNumeric != rightNumeric) + return leftNumeric ? -1 : 1; + + if (leftNumeric && rightNumeric && l == r) + continue; + + if (leftNumeric && rightNumeric) + return l.CompareTo(r); + + var result = string.Compare(left[i], right[i], StringComparison.Ordinal); + if (result == 0) + continue; + + return result; + } + if (left.Length == right.Length) + return 0; + + return left.Length > right.Length ? 1 : -1; + } + + /// + public override bool Equals(object obj) + { + return obj is PR prerelease && Value.Equals(prerelease.Value); + } + + /// + public override int GetHashCode() + { + return Value.GetHashCode(); + } + + /// + public override string ToString() + { + return Value.ToString(); + } + } + + [DebuggerDisplay("Current = {_Current}, Position = {_Position}, Value = {_Value}")] + private sealed class VersionStream + { + private readonly string _Value; + private int _Position; + private char _Current; + + internal VersionStream(string value) + { + _Value = value; + _Position = 0; + _Current = _Value.Length > 0 ? _Value[0] : char.MinValue; + } + + internal bool EOF => _Position >= _Value.Length; + + internal void Next() + { + Next(1); + } + + private void Next(int count) + { + _Position += count; + if (EOF || _Position >= _Value.Length) + return; + + _Current = _Value[_Position]; + } + + internal void Operator(out ComparisonOperator comparison) + { + comparison = ComparisonOperator.None; + while (!EOF && IsConstraint(_Current)) + { + if (_Current == EQUAL) + comparison |= ComparisonOperator.Equals; + else if (_Current == GREATER) + comparison |= ComparisonOperator.GreaterThan; + else if (_Current == LESS) + comparison |= ComparisonOperator.LessThan; + + Next(); + } + } + + internal void Flags(out ConstraintModifier flag) + { + flag = ConstraintModifier.None; + if (EOF || _Current != AT) + return; + + if (HasFlag(FLAG_PRE) || HasFlag(FLAG_PRERELEASE)) + flag |= ConstraintModifier.Prerelease; + } + + private bool HasFlag(string value) + { + if (string.Compare(_Value, _Position, value, 0, value.Length, false) != 0) + return false; + + Next(value.Length); + return true; + } + + internal bool TryDigit(out int digit) + { + var pos = _Position; + var count = 0; + while (!EOF && IsVersionDigit(_Current)) + { + count++; + Next(); + } + digit = count > 0 ? int.Parse(_Value.Substring(pos, count), Thread.CurrentThread.CurrentCulture) : 0; + return count > 0; + } + + internal bool TrySegments(out int[] segments) + { + segments = new int[] { -1, -1, -1 }; + var segmentIndex = 0; + while (!EOF) + { + if (!IsAllowedChar(_Current)) + return false; + + if (TryDigit(out var digit)) + { + segments[segmentIndex++] = digit; + if (segments.Length <= segmentIndex) + return true; + } + + if (IsSeparator(_Current)) + Next(); + + if (IsWildcard(_Current)) + { + segments[segmentIndex++] = -1; + Next(); + } + + if (IsIdentifier(_Current)) + return true; + + if (IsJoin(_Current)) + return true; + } + return segmentIndex > 0; + } + + internal bool Prerelease(out PR identifier) + { + identifier = PR.Empty; + if (EOF || _Current != DASH) + return true; + + Next(); + var start = _Position; + if (EOF) + return false; + + var numeric = true; + while (!EOF && IsPrereleaseChar(_Current, ref numeric)) + Next(); + + if (_Position - start == 0) + return false; + + // No leading 0 if numeric + var id = _Value.Substring(start, _Position - start); + if (numeric && id.Length > 1 && id[0] == ZERO) + return false; + + identifier = new PR(id); + return true; + } + + internal void Build(out string label) + { + label = string.Empty; + if (EOF || _Current != PLUS) + return; + + Next(); + var start = _Position; + if (_Current == ZERO) + return; + + while (!EOF && IsBuildChar(_Current)) + Next(); + + label = _Value.Substring(start, _Position - start); + } + + /// + /// 1.2.3 || 3.4.5 + /// >=1.2.3 <3.4.5 + /// + internal JoinOperator GetJoin() + { + var result = JoinOperator.None; + while ((_Current == SPACE || _Current == PIPE) && !EOF) + { + if (result == JoinOperator.None && _Current == SPACE) + result = JoinOperator.And; + + if (_Current == PIPE) + result = JoinOperator.Or; + + Next(); + } + return _Current != SPACE || _Current != PIPE ? result : JoinOperator.Or; + } + + [DebuggerStepThrough()] + private static bool IsConstraint(char c) + { + return c == EQUAL || c == GREATER || c == LESS; + } + + [DebuggerStepThrough()] + private static bool IsVersionDigit(char c) + { + return char.IsDigit(c); + } + + [DebuggerStepThrough()] + private static bool IsWildcard(char c) + { + return c == '*' || c == 'X' || c == 'x'; + } + + [DebuggerStepThrough()] + private static bool IsSeparator(char c) + { + return c == DASH; + } + + [DebuggerStepThrough()] + private static bool IsAllowedChar(char c) + { + return IsVersionDigit(c) || IsSeparator(c) || IsWildcard(c) || IsIdentifier(c); + } + + [DebuggerStepThrough()] + private static bool IsPrereleaseChar(char c, ref bool numeric) + { + if (numeric && char.IsDigit(c)) + return true; + + numeric = false; + return char.IsDigit(c) || IsLetter(c) || c == DASH || c == DOT; + } + + [DebuggerStepThrough()] + private static bool IsBuildChar(char c) + { + return char.IsDigit(c) || IsLetter(c); + } + + [DebuggerStepThrough()] + private static bool IsIdentifier(char c) + { + return c == DASH || c == PLUS; + } + + [DebuggerStepThrough()] + private static bool IsJoin(char c) + { + return c == SPACE || c == PIPE; + } + + /// + /// Is the character within the reduced set of allowed characters. a-z or A-Z. + /// + [DebuggerStepThrough()] + private static bool IsLetter(char c) + { + var nc = (int)c; + return (nc >= 0x41 && nc <= 0x5a) || (nc >= 0x61 && nc <= 0x7a); + } + } + + /// + /// Try to parse a version constraint from the provided string. + /// + public static bool TryParseConstraint(string value, out IDateVersionConstraint constraint, bool includePrerelease = false) + { + var c = new VersionConstraint(); + constraint = c; + if (string.IsNullOrEmpty(value)) + return true; + + var stream = new VersionStream(value); + stream.Flags(out var flags); + if (flags.HasFlag(ConstraintModifier.Prerelease)) + includePrerelease = true; + + while (!stream.EOF) + { + stream.Operator(out var comparison); + if (!stream.TrySegments(out var segments)) + return false; + + stream.Prerelease(out var prerelease); + c.Join(segments[0], segments[1], segments[2], prerelease, comparison, stream.GetJoin(), includePrerelease); + } + return true; + } + + /// + /// Try to parse a version from the provided string. + /// + public static bool TryParseVersion(string value, out Version version) + { + version = null; + if (string.IsNullOrEmpty(value)) + return false; + + var stream = new VersionStream(value); + if (!stream.TrySegments(out var segments)) + return false; + + if (!stream.Prerelease(out var prerelease)) + return false; + + version = new Version(segments[0], segments[1], segments[2], prerelease); + return true; + } + } +} diff --git a/src/PSRule.Types/Data/ModuleConstraint.cs b/src/PSRule.Types/Data/ModuleConstraint.cs index 4e572dc458..b0c27f1396 100644 --- a/src/PSRule.Types/Data/ModuleConstraint.cs +++ b/src/PSRule.Types/Data/ModuleConstraint.cs @@ -13,7 +13,7 @@ public sealed class ModuleConstraint /// /// The name of the module. /// The version constraint of the module. - public ModuleConstraint(string module, IVersionConstraint constraint) + public ModuleConstraint(string module, ISemanticVersionConstraint constraint) { Module = module; Constraint = constraint; @@ -27,6 +27,6 @@ public ModuleConstraint(string module, IVersionConstraint constraint) /// /// The version constraint of the module. /// - public IVersionConstraint Constraint { get; } + public ISemanticVersionConstraint Constraint { get; } } } diff --git a/src/PSRule.Types/Data/SemanticVersion.cs b/src/PSRule.Types/Data/SemanticVersion.cs index b51852ad9e..8782106b04 100644 --- a/src/PSRule.Types/Data/SemanticVersion.cs +++ b/src/PSRule.Types/Data/SemanticVersion.cs @@ -11,7 +11,7 @@ namespace PSRule.Data /// /// A semantic version constraint. /// - public interface IVersionConstraint + public interface ISemanticVersionConstraint { /// /// Determines if the semantic version meets the requirments of the constraint. @@ -88,7 +88,7 @@ internal enum ConstraintModifier /// /// A semantic version constraint. /// - public sealed class VersionConstraint : IVersionConstraint + public sealed class VersionConstraint : ISemanticVersionConstraint { private List _Constraints; @@ -148,7 +148,7 @@ internal void Join(int major, int minor, int patch, PR prid, ComparisonOperator } [DebuggerDisplay("{_Major}.{_Minor}.{_Patch}")] - internal sealed class ConstraintExpression : IVersionConstraint + internal sealed class ConstraintExpression : ISemanticVersionConstraint { private readonly ComparisonOperator _Flag; private readonly int _Major; @@ -172,7 +172,7 @@ internal ConstraintExpression(int major, int minor, int patch, PR prid, Comparis public JoinOperator Join { get; } - public static bool TryParse(string value, out IVersionConstraint constraint) + public static bool TryParse(string value, out ISemanticVersionConstraint constraint) { return TryParseConstraint(value, out constraint); } @@ -790,7 +790,7 @@ private static bool IsLetter(char c) /// /// Try to parse a version constraint from the provided string. /// - public static bool TryParseConstraint(string value, out IVersionConstraint constraint, bool includePrerelease = false) + public static bool TryParseConstraint(string value, out ISemanticVersionConstraint constraint, bool includePrerelease = false) { var c = new VersionConstraint(); constraint = c; diff --git a/src/PSRule/Definitions/Expressions/LanguageExpressions.cs b/src/PSRule/Definitions/Expressions/LanguageExpressions.cs index adbf2b7f37..11cfae9a0d 100644 --- a/src/PSRule/Definitions/Expressions/LanguageExpressions.cs +++ b/src/PSRule/Definitions/Expressions/LanguageExpressions.cs @@ -410,6 +410,7 @@ internal sealed class LanguageExpressions private const string COUNT = "count"; private const string NOTCOUNT = "notCount"; private const string VERSION = "version"; + private const string APIVERSION = "apiVersion"; private const string WITHINPATH = "withinPath"; private const string NOTWITHINPATH = "notWithinPath"; private const string LIKE = "like"; @@ -484,6 +485,7 @@ internal sealed class LanguageExpressions new LanguageExpresssionDescriptor(NOTCOUNT, LanguageExpressionType.Condition, NotCount), new LanguageExpresssionDescriptor(HASSCHEMA, LanguageExpressionType.Condition, HasSchema), new LanguageExpresssionDescriptor(VERSION, LanguageExpressionType.Condition, Version), + new LanguageExpresssionDescriptor(APIVERSION, LanguageExpressionType.Condition, APIVersion), new LanguageExpresssionDescriptor(HASDEFAULT, LanguageExpressionType.Condition, HasDefault), new LanguageExpresssionDescriptor(WITHINPATH, LanguageExpressionType.Condition, WithinPath), new LanguageExpresssionDescriptor(NOTWITHINPATH, LanguageExpressionType.Condition, NotWithinPath), @@ -1468,6 +1470,31 @@ internal static bool Version(ExpressionContext context, ExpressionInfo info, obj return Invalid(context, VERSION); } + internal static bool APIVersion(ExpressionContext context, ExpressionInfo info, object[] args, object o) + { + var properties = GetProperties(args); + if (TryPropertyString(properties, APIVERSION, out var expectedValue) && + TryOperand(context, APIVERSION, o, properties, out var operand) && + TryPropertyBoolOrDefault(properties, INCLUDEPRERELEASE, out var includePrerelease, false)) + { + context.ExpressionTrace(APIVERSION, operand.Value, expectedValue); + if (!ExpressionHelpers.TryString(operand.Value, out var version)) + return NotString(context, operand); + + if (!DateVersion.TryParseVersion(version, out var actualVersion)) + return Fail(context, operand, ReasonStrings.Version, operand.Value); + + if (!DateVersion.TryParseConstraint(expectedValue, out var constraint, includePrerelease)) + throw new RuleException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.VersionConstraintInvalid, expectedValue)); + + if (constraint != null && !constraint.Equals(actualVersion)) + return Fail(context, operand, ReasonStrings.VersionContraint, actualVersion, constraint); + + return Pass(); + } + return Invalid(context, APIVERSION); + } + #endregion Conditions #region Helper methods diff --git a/src/PSRule/Runtime/Assert.cs b/src/PSRule/Runtime/Assert.cs index 1c0cf1a7bc..aa09023c8a 100644 --- a/src/PSRule/Runtime/Assert.cs +++ b/src/PSRule/Runtime/Assert.cs @@ -847,6 +847,30 @@ public AssertResult Version(PSObject inputObject, string field, string constrain return c != null && !c.Equals(value) ? Fail(Operand.FromPath(field), ReasonStrings.VersionContraint, value, constraint) : Pass(); } + /// + /// The APIVersion assertion method checks the field value is a valid date version. + /// A constraint can optionally be provided to require the date version to be within a range. + /// + /// + /// + /// Only applies to strings. + /// + public AssertResult APIVersion(PSObject inputObject, string field, string constraint = null, bool includePrerelease = false) + { + // Guard parameters + if (GuardNullParam(inputObject, nameof(inputObject), out var result) || + GuardNullOrEmptyParam(field, nameof(field), out result) || + GuardField(inputObject, field, false, out var fieldValue, out result) || + GuardDateVersion(Operand.FromPath(field), fieldValue, out var value, out result)) + return result; + + if (!DateVersion.TryParseConstraint(constraint, out var c, includePrerelease)) + throw new RuleException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.VersionConstraintInvalid, value)); + + // Assert + return c != null && !c.Equals(value) ? Fail(Operand.FromPath(field), ReasonStrings.VersionContraint, value, constraint) : Pass(); + } + /// /// The Greater assertion method checks the field value is greater than the specified value. /// The field value can either be an integer, float, array, or string. @@ -1360,6 +1384,17 @@ private bool GuardSemanticVersion(IOperand operand, object fieldValue, out Seman return true; } + private bool GuardDateVersion(IOperand operand, object fieldValue, out DateVersion.Version value, out AssertResult result) + { + result = null; + value = null; + if (ExpressionHelpers.TryString(fieldValue, out var sversion) && DateVersion.TryParseVersion(sversion, out value)) + return false; + + result = Fail(operand, ReasonStrings.Version, fieldValue); + return true; + } + /// /// Fails if the field is not enumerable. /// diff --git a/tests/PSRule.Tests/AssertTests.cs b/tests/PSRule.Tests/AssertTests.cs index 42e57c1930..487d70a347 100644 --- a/tests/PSRule.Tests/AssertTests.cs +++ b/tests/PSRule.Tests/AssertTests.cs @@ -716,6 +716,43 @@ public void Version() Assert.Equal("notversion", assert.Version(value, "notversion", null).ToResultReason().FirstOrDefault().Path); } + [Fact] + public void APIVersion() + { + SetContext(); + var assert = GetAssertionHelper(); + var value = GetObject( + (name: "version", value: "2015-10-01"), + (name: "version2", value: "2015-10-01-preview"), + (name: "version3", value: "2022-01-01-preview"), + (name: "notversion", value: "x.y.z") + ); + + Assert.True(assert.APIVersion(value, "version", "2015-10-01").Result); + Assert.True(assert.APIVersion(value, "version", "=2015-10-01").Result); + Assert.True(assert.APIVersion(value, "version", ">=2015-10-01").Result); + Assert.False(assert.APIVersion(value, "version", "<2015-10-01").Result); + Assert.True(assert.APIVersion(value, "version", ">2014-01-01").Result); + Assert.True(assert.APIVersion(value, "version", ">=2015-10-01-preview").Result); + Assert.True(assert.APIVersion(value, "version", ">=2015-10-01-preview", includePrerelease: true).Result); + + Assert.False(assert.APIVersion(value, "version2", "2015-10-01").Result); + Assert.False(assert.APIVersion(value, "version2", "=2015-10-01").Result); + Assert.False(assert.APIVersion(value, "version2", ">=2015-10-01").Result); + Assert.False(assert.APIVersion(value, "version2", "<2015-10-01").Result); + Assert.False(assert.APIVersion(value, "version2", ">2014-01-01").Result); + Assert.True(assert.APIVersion(value, "version2", ">=2015-10-01-preview").Result); + Assert.True(assert.APIVersion(value, "version2", ">=2015-10-01-preview", includePrerelease: true).Result); + + Assert.False(assert.APIVersion(value, "version3", "2015-10-01").Result); + Assert.False(assert.APIVersion(value, "version3", "=2015-10-01").Result); + Assert.False(assert.APIVersion(value, "version3", ">=2015-10-01").Result); + Assert.False(assert.APIVersion(value, "version3", "<2015-10-01").Result); + Assert.False(assert.APIVersion(value, "version3", ">2014-01-01").Result); + Assert.False(assert.APIVersion(value, "version3", ">=2015-10-01-preview").Result); + Assert.True(assert.APIVersion(value, "version3", ">=2015-10-01-preview", includePrerelease: true).Result); + } + [Fact] public void Greater() { diff --git a/tests/PSRule.Tests/DateVersionTests.cs b/tests/PSRule.Tests/DateVersionTests.cs new file mode 100644 index 0000000000..7f9021a030 --- /dev/null +++ b/tests/PSRule.Tests/DateVersionTests.cs @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using PSRule.Data; +using Xunit; + +namespace PSRule +{ + /// + /// Tests for date version comparison. + /// + public sealed class DateVersionTests + { + /// + /// Test parsing of versions. + /// + [Fact] + public void Version() + { + Assert.True(DateVersion.TryParseVersion("2015-10-01", out var actual)); + Assert.Equal(2015, actual.Year); + Assert.Equal(10, actual.Month); + Assert.Equal(1, actual.Day); + Assert.Equal(string.Empty, actual.Prerelease.Value); + + Assert.True(DateVersion.TryParseVersion("2015-1-01-prerelease", out actual)); + Assert.Equal(2015, actual.Year); + Assert.Equal(1, actual.Month); + Assert.Equal(1, actual.Day); + Assert.Equal("prerelease", actual.Prerelease.Value); + } + + /// + /// Test ordering of versions by comparison. + /// + [Fact] + public void VersionOrder() + { + Assert.True(DateVersion.TryParseVersion("2015-10-01", out var actual1)); + Assert.True(DateVersion.TryParseVersion("2015-10-01-prerelease", out var actual2)); + Assert.True(DateVersion.TryParseVersion("2022-03-01", out var actual3)); + Assert.True(DateVersion.TryParseVersion("2022-01-03", out var actual4)); + + Assert.True(actual1.CompareTo(actual1) == 0); + Assert.True(actual1.CompareTo(actual2) > 0); + Assert.True(actual1.CompareTo(actual3) < 0); + Assert.True(actual1.CompareTo(actual4) < 0); + Assert.True(actual2.CompareTo(actual2) == 0); + Assert.True(actual2.CompareTo(actual1) < 0); + Assert.True(actual2.CompareTo(actual3) < 0); + Assert.True(actual2.CompareTo(actual4) < 0); + Assert.True(actual3.CompareTo(actual4) > 0); + Assert.True(actual1.CompareTo(actual4) < 0); + } + + /// + /// Test parsing of constraints. + /// + [Fact] + public void Constraint() + { + // Versions + Assert.True(DateVersion.TryParseVersion("2015-10-01", out var version1)); + Assert.True(DateVersion.TryParseVersion("2015-10-01-alpha.9", out var version2)); + Assert.True(DateVersion.TryParseVersion("2022-03-01", out var version3)); + Assert.False(DateVersion.TryParseVersion("2022-03-01-", out var _)); + Assert.True(DateVersion.TryParseVersion("2022-03-01-0", out var _)); + + // Constraints + Assert.True(DateVersion.TryParseConstraint("2015-10-01", out var actual1)); + Assert.True(DateVersion.TryParseConstraint("2015-10-01-alpha.3", out var actual2)); + Assert.True(DateVersion.TryParseConstraint(">2015-10-01-alpha.3", out var actual3)); + Assert.True(DateVersion.TryParseConstraint(">2015-10-01-alpha.1", out var actual4)); + Assert.True(DateVersion.TryParseConstraint("<2015-10-01-beta", out var actual5)); + Assert.True(DateVersion.TryParseConstraint("<2022-03-01", out var actual7)); + Assert.True(DateVersion.TryParseConstraint("=2015-10-01", out var actual8)); + Assert.True(DateVersion.TryParseConstraint(">=2015-10-01", out var actual9)); + Assert.True(DateVersion.TryParseConstraint(">=2015-10-01-0", out var actual10)); + Assert.True(DateVersion.TryParseConstraint("<2022-03-01", out var actual11)); + Assert.True(DateVersion.TryParseConstraint("<2022-03-01-9999999999", out var actual12)); + Assert.True(DateVersion.TryParseConstraint("<2015-10-01-0", out var actual14)); + Assert.True(DateVersion.TryParseConstraint("2015-10-01|| >=2022-03-01-0 2022-03-01", out var actual15)); + Assert.True(DateVersion.TryParseConstraint("2015-10-01 ||>=2022-03-01-0 || 2022-03-01", out var actual16)); + Assert.True(DateVersion.TryParseConstraint("2015-10-01||2022-03-01", out var actual17)); + Assert.True(DateVersion.TryParseConstraint(">=2015-09-01", out var actual18, includePrerelease: true)); + Assert.True(DateVersion.TryParseConstraint("<=2022-03-01-0", out var actual19, includePrerelease: true)); + Assert.True(DateVersion.TryParseConstraint("@pre >=2015-09-01", out var actual20)); + Assert.True(DateVersion.TryParseConstraint("@prerelease <=2022-03-01-0", out var actual21)); + + // Version1 - 2015-10-01 + Assert.True(actual1.Equals(version1)); + Assert.False(actual2.Equals(version1)); + Assert.True(actual3.Equals(version1)); + Assert.True(actual4.Equals(version1)); + Assert.False(actual5.Equals(version1)); + Assert.True(actual7.Equals(version1)); + Assert.True(actual8.Equals(version1)); + Assert.True(actual9.Equals(version1)); + Assert.True(actual10.Equals(version1)); + Assert.True(actual11.Equals(version1)); + Assert.True(actual12.Equals(version1)); + Assert.False(actual14.Equals(version1)); + Assert.True(actual15.Equals(version1)); + Assert.True(actual16.Equals(version1)); + Assert.True(actual17.Equals(version1)); + Assert.True(actual18.Equals(version1)); + Assert.True(actual19.Equals(version1)); + Assert.True(actual20.Equals(version1)); + Assert.True(actual21.Equals(version1)); + + // Version3 - 2015-10-01-alpha.9 + Assert.False(actual1.Equals(version2)); + Assert.False(actual2.Equals(version2)); + Assert.True(actual3.Equals(version2)); + Assert.True(actual4.Equals(version2)); + Assert.True(actual5.Equals(version2)); + Assert.False(actual7.Equals(version2)); + Assert.False(actual8.Equals(version2)); + Assert.False(actual9.Equals(version2)); + Assert.True(actual10.Equals(version2)); + Assert.False(actual11.Equals(version2)); + Assert.False(actual12.Equals(version2)); + Assert.False(actual14.Equals(version2)); + Assert.False(actual15.Equals(version2)); + Assert.False(actual16.Equals(version2)); + Assert.False(actual17.Equals(version2)); + Assert.True(actual18.Equals(version2)); + Assert.True(actual19.Equals(version2)); + Assert.True(actual20.Equals(version2)); + Assert.True(actual21.Equals(version2)); + + // Version4 - 2022-03-01 + Assert.False(actual1.Equals(version3)); + Assert.False(actual2.Equals(version3)); + Assert.True(actual3.Equals(version3)); + Assert.True(actual4.Equals(version3)); + Assert.False(actual5.Equals(version3)); + Assert.False(actual7.Equals(version3)); + Assert.False(actual8.Equals(version3)); + Assert.True(actual9.Equals(version3)); + Assert.True(actual10.Equals(version3)); + Assert.False(actual11.Equals(version3)); + Assert.False(actual12.Equals(version3)); + Assert.False(actual14.Equals(version3)); + Assert.True(actual15.Equals(version3)); + Assert.True(actual16.Equals(version3)); + Assert.True(actual17.Equals(version3)); + Assert.True(actual18.Equals(version3)); + Assert.False(actual19.Equals(version3)); + Assert.True(actual20.Equals(version3)); + Assert.False(actual21.Equals(version3)); + } + + /// + /// Test parsing and order of pre-releases. + /// + [Fact] + public void Prerelease() + { + var actual1 = new DateVersion.PR(null); + var actual2 = new DateVersion.PR("alpha"); + var actual3 = new DateVersion.PR("alpha.1"); + var actual4 = new DateVersion.PR("alpha.beta"); + var actual5 = new DateVersion.PR("beta"); + var actual6 = new DateVersion.PR("beta.2"); + var actual7 = new DateVersion.PR("beta.11"); + var actual8 = new DateVersion.PR("rc.1"); + var actual9 = new DateVersion.PR("alpha.9"); + + Assert.True(actual1.CompareTo(actual1) == 0); + Assert.True(actual1.CompareTo(actual2) > 0); + Assert.True(actual1.CompareTo(actual6) > 0); + Assert.True(actual2.CompareTo(actual3) < 0); + Assert.True(actual3.CompareTo(actual4) < 0); + Assert.True(actual4.CompareTo(actual5) < 0); + Assert.True(actual5.CompareTo(actual6) < 0); + Assert.True(actual6.CompareTo(actual7) < 0); + Assert.True(actual7.CompareTo(actual8) < 0); + Assert.True(actual8.CompareTo(actual1) < 0); + Assert.True(actual8.CompareTo(actual2) > 0); + Assert.True(actual9.CompareTo(actual3) > 0); + } + } +} diff --git a/tests/PSRule.Tests/SelectorTests.cs b/tests/PSRule.Tests/SelectorTests.cs index 9840760f4f..a5d143ba67 100644 --- a/tests/PSRule.Tests/SelectorTests.cs +++ b/tests/PSRule.Tests/SelectorTests.cs @@ -41,7 +41,7 @@ public void ReadSelector(string type, string path) context.Begin(); var selector = HostHelper.GetSelector(GetSource(path), context).ToArray(); Assert.NotNull(selector); - Assert.Equal(94, selector.Length); + Assert.Equal(97, selector.Length); var actual = selector[0]; var visitor = new SelectorVisitor(context, actual.Id, actual.Source, actual.Spec.If); @@ -1548,6 +1548,47 @@ public void Version(string type, string path) Assert.False(version.Match(actual7)); } + [Theory] + [InlineData("Yaml", SelectorYamlFileName)] + [InlineData("Json", SelectorJsonFileName)] + public void APIVersion(string type, string path) + { + var actual1 = GetObject((name: "dateVersion", value: "2015-10-01")); + var actual2 = GetObject((name: "dateVersion", value: "2014-01-01")); + var actual3 = GetObject((name: "dateVersion", value: "2022-01-01")); + var actual4 = GetObject((name: "dateVersion", value: "2015-10-01-preview")); + var actual5 = GetObject((name: "dateVersion", value: "2022-01-01-preview")); + var actual6 = GetObject(); + var actual7 = GetObject((name: "dateVersion", value: "a-b-c")); + + var version = GetSelectorVisitor($"{type}APIVersion", GetSource(path), out _); + Assert.True(version.Match(actual1)); + Assert.False(version.Match(actual2)); + Assert.True(version.Match(actual3)); + Assert.False(version.Match(actual4)); + Assert.False(version.Match(actual5)); + Assert.False(version.Match(actual6)); + Assert.False(version.Match(actual7)); + + version = GetSelectorVisitor($"{type}APIVersionWithPrerelease", GetSource(path), out _); + Assert.True(version.Match(actual1)); + Assert.False(version.Match(actual2)); + Assert.True(version.Match(actual3)); + Assert.False(version.Match(actual4)); + Assert.True(version.Match(actual5)); + Assert.False(version.Match(actual6)); + Assert.False(version.Match(actual7)); + + version = GetSelectorVisitor($"{type}APIVersionAnyVersion", GetSource(path), out _); + Assert.True(version.Match(actual1)); + Assert.True(version.Match(actual2)); + Assert.True(version.Match(actual3)); + Assert.True(version.Match(actual4)); + Assert.True(version.Match(actual5)); + Assert.False(version.Match(actual6)); + Assert.False(version.Match(actual7)); + } + [Theory] [InlineData("Yaml", SelectorYamlFileName)] [InlineData("Json", SelectorJsonFileName)] diff --git a/tests/PSRule.Tests/Selectors.Rule.jsonc b/tests/PSRule.Tests/Selectors.Rule.jsonc index d52dad7ccd..00e567efeb 100644 --- a/tests/PSRule.Tests/Selectors.Rule.jsonc +++ b/tests/PSRule.Tests/Selectors.Rule.jsonc @@ -1801,5 +1801,48 @@ ] } } + }, + { + // Synopsis: Test comparison with apiVersion. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonAPIVersion" + }, + "spec": { + "if": { + "field": "dateVersion", + "apiVersion": ">=2015-10-01" + } + } + }, + { + // Synopsis: Test comparison with apiVersion. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonAPIVersionWithPrerelease" + }, + "spec": { + "if": { + "field": "dateVersion", + "apiVersion": ">=2015-10-01", + "includePrerelease": true + } + } + }, + { + // Synopsis: Test comparison with apiVersion. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonAPIVersionAnyVersion" + }, + "spec": { + "if": { + "field": "dateVersion", + "apiVersion": "" + } + } } ] diff --git a/tests/PSRule.Tests/Selectors.Rule.yaml b/tests/PSRule.Tests/Selectors.Rule.yaml index 65eca38205..c8cadf8009 100644 --- a/tests/PSRule.Tests/Selectors.Rule.yaml +++ b/tests/PSRule.Tests/Selectors.Rule.yaml @@ -1282,3 +1282,37 @@ spec: startsWith: - /scope1/ - /scope2/ + +--- +# Synopsis: Test comparison with apiVersion. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: YamlAPIVersion +spec: + if: + field: dateVersion + apiVersion: '>=2015-10-01' + +--- +# Synopsis: Test comparison with apiVersion. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: YamlAPIVersionWithPrerelease +spec: + if: + field: dateVersion + apiVersion: '>=2015-10-01' + includePrerelease: true + +--- +# Synopsis: Test comparison with apiVersion. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: YamlAPIVersionAnyVersion +spec: + if: + field: dateVersion + apiVersion: '' diff --git a/tests/PSRule.Tests/SemanticVersionTests.cs b/tests/PSRule.Tests/SemanticVersionTests.cs index 13f864300a..6e8efea1c3 100644 --- a/tests/PSRule.Tests/SemanticVersionTests.cs +++ b/tests/PSRule.Tests/SemanticVersionTests.cs @@ -6,6 +6,9 @@ namespace PSRule { + /// + /// Tests for semantic version comparison. + /// public sealed class SemanticVersionTests { /// @@ -40,10 +43,10 @@ public void Version() [Fact] public void VersionOrder() { - SemanticVersion.TryParseVersion("1.0.0", out var actual1); - SemanticVersion.TryParseVersion("1.2.0", out var actual2); - SemanticVersion.TryParseVersion("10.0.0", out var actual3); - SemanticVersion.TryParseVersion("1.0.2", out var actual4); + Assert.True(SemanticVersion.TryParseVersion("1.0.0", out var actual1)); + Assert.True(SemanticVersion.TryParseVersion("1.2.0", out var actual2)); + Assert.True(SemanticVersion.TryParseVersion("10.0.0", out var actual3)); + Assert.True(SemanticVersion.TryParseVersion("1.0.2", out var actual4)); Assert.True(actual1.CompareTo(actual1) == 0); Assert.True(actual1.CompareTo(actual2) < 0); From ea9f21ce5373510f59b4f19badff9cf71898c666 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 3 Dec 2022 03:43:54 +1000 Subject: [PATCH 123/156] Bump mkdocs-material from 8.5.10 to 8.5.11 (#1364) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.5.10 to 8.5.11. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.5.10...8.5.11) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bernie White --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index c58faff3b9..a9d34391bc 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.4.2 -mkdocs-material==8.5.10 +mkdocs-material==8.5.11 pymdown-extensions==9.9 mike==1.1.2 mkdocs-simple-hooks==0.1.5 From 9b5fe161cabb68c0142d44d002f8883541a1dd05 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Tue, 6 Dec 2022 17:04:43 +1000 Subject: [PATCH 124/156] Added new function support #1227 (#1368) --- docs/CHANGELOG-v2.md | 3 + docs/expressions/functions.md | 11 +- schemas/PSRule-language.schema.json | 168 +++++++++ src/PSRule/Common/ArrayExtensions.cs | 23 ++ src/PSRule/Common/ExpressionHelpers.cs | 82 ++++- src/PSRule/Common/StringExtensions.cs | 29 +- .../Definitions/Expressions/Functions.cs | 100 +++++ tests/PSRule.Tests/FunctionTests.cs | 190 ++++++++++ tests/PSRule.Tests/Functions.Rule.jsonc | 345 ++++++++++++------ tests/PSRule.Tests/Functions.Rule.yaml | 79 ++++ tests/PSRule.Tests/SelectorTests.cs | 21 ++ tests/PSRule.Tests/StringExtensionsTests.cs | 30 ++ 12 files changed, 955 insertions(+), 126 deletions(-) create mode 100644 src/PSRule/Common/ArrayExtensions.cs create mode 100644 tests/PSRule.Tests/StringExtensionsTests.cs diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 5ac2ad1a52..4f83e19a7f 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -35,6 +35,9 @@ What's changed since pre-release v2.7.0-B0016: - New features: - Added API version date comparison assertion method and expression by @BernieWhite. [#1356](https://github.com/microsoft/PSRule/issues/1356) + - Added support for new functions by @BernieWhite. + [#1227](https://github.com/microsoft/PSRule/issues/1227) + - Added support for `trim`, `replace`, `split`, `first`, and `last`. - Bug fixes: - Fixes CLI failed to load required assemblies by @BernieWhite. [#1361](https://github.com/microsoft/PSRule/issues/1361) diff --git a/docs/expressions/functions.md b/docs/expressions/functions.md index 312dc77a06..7269c74910 100644 --- a/docs/expressions/functions.md +++ b/docs/expressions/functions.md @@ -22,12 +22,17 @@ Functions cover two (2) main scenarios: It may be necessary to perform minor transformation before evaluating a condition. - `boolean` - Convert a value to a boolean. -- `string` - Convert a value to a string. -- `integer` - Convert a value to an integer. - `concat` - Concatenate multiple values. -- `substring` - Extract a substring from a string. - `configuration` - Get a configuration value. +- `first` - Return the first element in an array or the first character of a string. +- `integer` - Convert a value to an integer. +- `last` - Return the last element in an array or the last character of a string. - `path` - Get a value from an object path. +- `replace` - Replace an old string with a new string. +- `split` - Split a string into an array by a delimiter. +- `string` - Convert a value to a string. +- `substring` - Extract a substring from a string. +- `trim` - Remove whitespace from the start and end of a string. ## Supported conditions diff --git a/schemas/PSRule-language.schema.json b/schemas/PSRule-language.schema.json index b30580c5c6..d34c8c3597 100644 --- a/schemas/PSRule-language.schema.json +++ b/schemas/PSRule-language.schema.json @@ -3034,6 +3034,21 @@ }, { "$ref": "#/definitions/fn/definitions/function/definitions/configuration" + }, + { + "$ref": "#/definitions/fn/definitions/function/definitions/replace" + }, + { + "$ref": "#/definitions/fn/definitions/function/definitions/trim" + }, + { + "$ref": "#/definitions/fn/definitions/function/definitions/first" + }, + { + "$ref": "#/definitions/fn/definitions/function/definitions/last" + }, + { + "$ref": "#/definitions/fn/definitions/function/definitions/split" } ], "definitions": { @@ -3305,6 +3320,159 @@ "required": [ "configuration" ] + }, + "replace": { + "type": "object", + "properties": { + "replace": { + "type": "object", + "title": "Trim", + "description": "The replace function searches for a string and replaces them with a new string.", + "markdownDescription": "The `replace` function searches for a string and replaces them with a new string.", + "$ref": "#/definitions/fn/definitions/function" + }, + "oldString": { + "type": "string", + "description": "The string to replace.", + "minLength": 1 + }, + "newString": { + "type": "string", + "description": "The new string to replace oldString." + }, + "caseSensitive": { + "type": "boolean", + "title": "Case-sensitive", + "description": "Determines if the replace is case-sensitive. By default, a case-insensitive comparison is performed.", + "default": false + } + }, + "additionalProperties": false, + "required": [ + "replace", + "oldString", + "newString" + ] + }, + "trim": { + "type": "object", + "properties": { + "trim": { + "type": "object", + "title": "Trim", + "description": "Trim a string value by removing leading and trailings whitespace.", + "markdownDescription": "Trim a string value by removing leading and trailings whitespace.", + "$ref": "#/definitions/fn/definitions/function" + } + }, + "additionalProperties": false, + "required": [ + "trim" + ] + }, + "first": { + "type": "object", + "properties": { + "first": { + "oneOf": [ + { + "type": "array", + "description": "The first function returns the first element in the array.", + "markdownDescription": "The `first` function returns the first element in the array.", + "items": { + "$ref": "#/definitions/fn/definitions/function" + }, + "additionalItems": false, + "minItems": 2 + }, + { + "type": "object", + "description": "The first function returns the first element of arrays or the first character of a string.", + "markdownDescription": "The `first` function returns the first element of arrays or the first character of a string.", + "$ref": "#/definitions/fn/definitions/function" + } + ] + } + }, + "additionalProperties": false, + "required": [ + "first" + ] + }, + "last": { + "type": "object", + "properties": { + "last": { + "oneOf": [ + { + "type": "array", + "description": "The last function returns the last element in the array.", + "markdownDescription": "The `last` function returns the last element in the array.", + "items": { + "$ref": "#/definitions/fn/definitions/function" + }, + "additionalItems": false, + "minItems": 2 + }, + { + "type": "object", + "description": "The last function returns the last element of arrays or the last character of a string.", + "markdownDescription": "The `last` function returns the last element of arrays or the last character of a string.", + "$ref": "#/definitions/fn/definitions/function" + } + ] + } + }, + "additionalProperties": false, + "required": [ + "last" + ] + }, + "split": { + "type": "object", + "properties": { + "split": { + "oneOf": [ + { + "type": "string", + "description": "The split function returns converts a string into an array by spliting based on a delimiter.", + "markdownDescription": "The `split` function returns converts a string into an array by spliting based on a delimiter." + }, + { + "type": "object", + "description": "The split function returns converts a string into an array by spliting based on a delimiter.", + "markdownDescription": "The `split` function returns converts a string into an array by spliting based on a delimiter.", + "$ref": "#/definitions/fn/definitions/function" + } + ], + "default": {} + }, + "delimiter": { + "oneOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 1, + "additionalItems": false + } + ], + "default": [ + "" + ] + } + }, + "additionalProperties": false, + "required": [ + "split", + "delimiter" + ] } } } diff --git a/src/PSRule/Common/ArrayExtensions.cs b/src/PSRule/Common/ArrayExtensions.cs new file mode 100644 index 0000000000..2caf42f5eb --- /dev/null +++ b/src/PSRule/Common/ArrayExtensions.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace PSRule.Common +{ + /// + /// Extension methods for arrays. + /// + internal static class ArrayExtensions + { + internal static object Last(this Array array) + { + return array.Length > 0 ? array.GetValue(array.Length - 1) : null; + } + + internal static object First(this Array array) + { + return array.Length > 0 ? array.GetValue(0) : null; + } + } +} diff --git a/src/PSRule/Common/ExpressionHelpers.cs b/src/PSRule/Common/ExpressionHelpers.cs index 3f6e51f8c4..8cd3e47a69 100644 --- a/src/PSRule/Common/ExpressionHelpers.cs +++ b/src/PSRule/Common/ExpressionHelpers.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Management.Automation; +using System.Reflection; using System.Text.RegularExpressions; using System.Threading; using Newtonsoft.Json.Linq; @@ -57,6 +58,9 @@ internal static bool Equal(object expectedValue, object actualValue, bool caseSe if (TryInt(expectedValue, convertExpected, out var i1) && TryInt(actualValue, convertActual, out var i2)) return i1 == i2; + if (TryArray(expectedValue, out var a1) && TryArray(actualValue, out var a2)) + return SequenceEqual(a1, a2); + var expectedBase = GetBaseObject(expectedValue); var actualBase = GetBaseObject(actualValue); if (expectedBase == null || actualBase == null) @@ -65,6 +69,69 @@ internal static bool Equal(object expectedValue, object actualValue, bool caseSe return expectedBase.Equals(actualBase) || expectedValue.Equals(actualValue); } + internal static bool SequenceEqual(Array array1, Array array2) + { + if (array1.Length != array2.Length) + return false; + + for (var i = 0; i < array1.Length; i++) + { + if (!Equal(array1.GetValue(i), array2.GetValue(i))) + return false; + } + return true; + } + + internal static bool Equal(object o1, object o2) + { + // One null + if (o1 == null || o2 == null) + return o1 == o2; + + // Arrays + if (o1 is Array array1 && o2 is Array array2) + return SequenceEqual(array1, array2); + else if (o1 is Array || o2 is Array) + return false; + + // String and int + if (TryString(o1, out var s1) && TryString(o2, out var s2)) + return s1 == s2; + else if (TryString(o1, out _) || TryString(o2, out _)) + return false; + else if (TryLong(o1, false, out var i1) && TryLong(o2, false, out var i2)) + return i1 == i2; + else if (TryLong(o1, false, out var _) || TryLong(o2, false, out var _)) + return false; + + // JTokens + if (o1 is JToken t1 && o2 is JToken t2) + return JTokenEquals(t1, t2); + + // Objects + return ObjectEquals(o1, o2); + } + + private static bool JTokenEquals(JToken t1, JToken t2) + { + return JToken.DeepEquals(t1, t2); + } + + internal static bool ObjectEquals(object o1, object o2) + { + var objectType = o1.GetType(); + if (objectType != o2.GetType()) + return false; + + var props = objectType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty); + for (var i = 0; i < props.Length; i++) + { + if (!object.Equals(props[i].GetValue(o1), props[i].GetValue(o2))) + return false; + } + return true; + } + internal static int Compare(object left, object right) { if (TryString(left, out var stringLeft) && TryString(right, out var stringRight)) @@ -162,13 +229,18 @@ internal static bool TryString(object o, bool convert, out string value) internal static bool TryArray(object o, out Array value) { o = GetBaseObject(o); + value = null; + if (o is string) return false; if (o is Array a) - { value = a; - return true; - } - value = null; - return false; + + else if (o is JArray jArray) + value = jArray.Values().ToArray(); + + else if (o is IEnumerable e) + value = e.OfType().ToArray(); + + return value != null; } internal static bool TryConvertStringArray(object o, out string[] value) diff --git a/src/PSRule/Common/StringExtensions.cs b/src/PSRule/Common/StringExtensions.cs index 4e4b02c694..d2a4af20fc 100644 --- a/src/PSRule/Common/StringExtensions.cs +++ b/src/PSRule/Common/StringExtensions.cs @@ -1,8 +1,9 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; using System.Collections.Generic; +using System.Text; namespace PSRule { @@ -78,5 +79,31 @@ public static bool Contains(this string source, string value, StringComparison c { return source?.IndexOf(value, comparison) >= 0; } + + public static string Replace(this string s, string oldString, string newString, bool caseSensitive) + { + if (string.IsNullOrEmpty(s) || string.IsNullOrEmpty(oldString) || s.Length < oldString.Length) + return s; + + if (caseSensitive) + return s.Replace(oldString, newString); + + var sb = new StringBuilder(s.Length); + var pos = 0; + var replaceWithEmpty = string.IsNullOrEmpty(newString); + int indexAt; + while ((indexAt = s.IndexOf(oldString, pos, StringComparison.OrdinalIgnoreCase)) != -1) + { + sb.Append(s, pos, indexAt - pos); + if (!replaceWithEmpty) + sb.Append(newString); + + pos = indexAt + oldString.Length; + } + if (pos < s.Length) + sb.Append(s, pos, s.Length - pos); + + return sb.ToString(); + } } } diff --git a/src/PSRule/Definitions/Expressions/Functions.cs b/src/PSRule/Definitions/Expressions/Functions.cs index 6262145d1b..d260714f61 100644 --- a/src/PSRule/Definitions/Expressions/Functions.cs +++ b/src/PSRule/Definitions/Expressions/Functions.cs @@ -1,8 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; +using System.Linq; using System.Text; using System.Threading; +using PSRule.Common; using PSRule.Resources; using PSRule.Runtime; using static PSRule.Definitions.Expressions.LanguageExpression; @@ -22,6 +25,15 @@ internal static class Functions private const string CONFIGURATION = "configuration"; private const string PATH = "path"; private const string LENGTH = "length"; + private const string REPLACE = "replace"; + private const string TRIM = "trim"; + private const string FIRST = "first"; + private const string LAST = "last"; + private const string SPLIT = "split"; + private const string DELIMITER = "delimiter"; + private const string OLDSTRING = "oldstring"; + private const string NEWSTRING = "newstring"; + private const string CASESENSITIVE = "casesensitive"; /// /// The available built-in functions. @@ -35,6 +47,11 @@ internal static class Functions new FunctionDescriptor(INTEGER, Integer), new FunctionDescriptor(CONCAT, Concat), new FunctionDescriptor(SUBSTRING, Substring), + new FunctionDescriptor(REPLACE, Replace), + new FunctionDescriptor(TRIM, Trim), + new FunctionDescriptor(FIRST, First), + new FunctionDescriptor(LAST, Last), + new FunctionDescriptor(SPLIT, Split), }; private static ExpressionFnOuter Boolean(IExpressionContext context, PropertyBag properties) @@ -144,6 +161,89 @@ private static ExpressionFnOuter Substring(IExpressionContext context, PropertyB }; } + private static ExpressionFnOuter Replace(IExpressionContext context, PropertyBag properties) + { + if (properties == null || + properties.Count == 0 || + !properties.TryGetString(OLDSTRING, out var oldString) || + !properties.TryGetString(NEWSTRING, out var newString) || + !TryProperty(properties, REPLACE, out ExpressionFnOuter next)) + return null; + + var caseSensitive = properties.TryGetBool(CASESENSITIVE, out var cs) && cs.HasValue && cs.Value; + return (context) => + { + var value = next(context); + if (ExpressionHelpers.TryString(value, out var originalString)) + return originalString.Length > 0 && oldString.Length > 0 ? originalString.Replace(oldString, newString, caseSensitive) : originalString; + + return null; + }; + } + + private static ExpressionFnOuter Trim(IExpressionContext context, PropertyBag properties) + { + if (properties == null || + properties.Count == 0 || + !TryProperty(properties, TRIM, out ExpressionFnOuter next)) + return null; + + return (context) => + { + var value = next(context); + return ExpressionHelpers.TryString(value, out var s) ? s.Trim() : null; + }; + } + + private static ExpressionFnOuter First(IExpressionContext context, PropertyBag properties) + { + if (properties == null || + properties.Count == 0 || + !TryProperty(properties, FIRST, out ExpressionFnOuter next)) + return null; + + return (context) => + { + var value = next(context); + if (ExpressionHelpers.TryString(value, out var s)) + return s.Length > 0 ? new string(s[0], 1) : null; + + return ExpressionHelpers.TryArray(value, out var array) ? Value(context, array.First()) : null; + }; + } + + private static ExpressionFnOuter Last(IExpressionContext context, PropertyBag properties) + { + if (properties == null || + properties.Count == 0 || + !TryProperty(properties, LAST, out ExpressionFnOuter next)) + return null; + + return (context) => + { + var value = next(context); + if (ExpressionHelpers.TryString(value, out var s)) + return s.Length > 0 ? new string(s[s.Length - 1], 1) : null; + + return ExpressionHelpers.TryArray(value, out var array) ? Value(context, array.Last()) : null; + }; + } + + private static ExpressionFnOuter Split(IExpressionContext context, PropertyBag properties) + { + if (properties == null || + properties.Count == 0 || + !properties.TryGetStringArray(DELIMITER, out var delimiter) || + !TryProperty(properties, SPLIT, out ExpressionFnOuter next)) + return null; + + return (context) => + { + var value = next(context); + return ExpressionHelpers.TryString(value, out var s) ? s.Split(delimiter, options: StringSplitOptions.None) : null; + }; + } + #region Helper functions private static bool TryProperty(PropertyBag properties, string name, out int? value) diff --git a/tests/PSRule.Tests/FunctionTests.cs b/tests/PSRule.Tests/FunctionTests.cs index 87772ce402..036fe41e61 100644 --- a/tests/PSRule.Tests/FunctionTests.cs +++ b/tests/PSRule.Tests/FunctionTests.cs @@ -237,6 +237,196 @@ public void Path() Assert.Null(fn(context, properties)(context)); } + [Fact] + public void Replace() + { + var context = GetContext(); + var fn = GetFunction("replace"); + + var properties = new LanguageExpression.PropertyBag + { + { "oldString", "12" }, + { "newString", "" }, + { "replace", "Test123" } + }; + Assert.Equal("Test3", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "oldString", "456" }, + { "newString", "" }, + { "replace", "Test123" } + }; + Assert.Equal("Test123", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "oldString", "456" }, + { "newString", "" }, + { "replace", "" } + }; + Assert.Equal("", fn(context, properties)(context)); + } + + [Fact] + public void Trim() + { + var context = GetContext(); + var fn = GetFunction("trim"); + + var properties = new LanguageExpression.PropertyBag + { + { "trim", " test " } + }; + Assert.Equal("test", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "trim", "test" } + }; + Assert.Equal("test", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "trim", "\r\ntest\r\n" } + }; + Assert.Equal("test", fn(context, properties)(context)); + } + + [Fact] + public void First() + { + var context = GetContext(); + var fn = GetFunction("first"); + + // String + var properties = new LanguageExpression.PropertyBag + { + { "first", "Test123" } + }; + Assert.Equal("T", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "first", "" } + }; + Assert.Null(fn(context, properties)(context)); + + // Array + properties = new LanguageExpression.PropertyBag + { + { "first", new string[] { "one", "two", "three" } } + }; + Assert.Equal("one", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "first", new int[] { 1, 2, 3 } } + }; + Assert.Equal(1, fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "first", Array.Empty() } + }; + Assert.Null(fn(context, properties)(context)); + } + + [Fact] + public void Last() + { + var context = GetContext(); + var fn = GetFunction("last"); + + // String + var properties = new LanguageExpression.PropertyBag + { + { "last", "Test123" } + }; + Assert.Equal("3", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "last", "" } + }; + Assert.Null(fn(context, properties)(context)); + + // Array + properties = new LanguageExpression.PropertyBag + { + { "last", new string[] { "one", "two", "three" } } + }; + Assert.Equal("three", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "last", new int[] { 1, 2, 3 } } + }; + Assert.Equal(3, fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "last", Array.Empty() } + }; + Assert.Null(fn(context, properties)(context)); + } + + [Fact] + public void Split() + { + var context = GetContext(); + var fn = GetFunction("split"); + + var properties = new LanguageExpression.PropertyBag + { + { "split", "One Two Three" }, + { "delimiter", " " } + }; + Assert.Equal(new string[] { "One", "Two", "Three" }, fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "split", "One Two Three" }, + { "delimiter", " Two " } + }; + Assert.Equal(new string[] { "One", "Three" }, fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "split", "One Two Three" }, + { "delimiter", "/" } + }; + Assert.Equal(new string[] { "One Two Three" }, fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "split", "" }, + { "delimiter", "/" } + }; + Assert.Equal(new string[] { "" }, fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "split", "One Two Three" }, + { "delimiter", new string[] { " Two " } } + }; + Assert.Equal(new string[] { "One", "Three" }, fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "split", "One Two Three" }, + { "delimiter", new string[] { " ", "Two" } } + }; + Assert.Equal(new string[] { "One", "", "", "Three" }, fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "split", null }, + { "delimiter", new string[] { " ", "Two" } } + }; + Assert.Null(fn(context, properties)(context)); + } + #region Helper methods private static ExpressionBuilderFn GetFunction(string name) diff --git a/tests/PSRule.Tests/Functions.Rule.jsonc b/tests/PSRule.Tests/Functions.Rule.jsonc index 4caba42731..452c3ca9f6 100644 --- a/tests/PSRule.Tests/Functions.Rule.jsonc +++ b/tests/PSRule.Tests/Functions.Rule.jsonc @@ -1,133 +1,244 @@ [ - { - // Synopsis: An expression function example. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "Json.Fn.Example1" + { + // Synopsis: An expression function example. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.Example1" + }, + "spec": { + "if": { + "value": { + "$": { + "substring": { + "path": "name" + }, + "length": 7 + } }, - "spec": { - "if": { - "value": { - "$": { - "substring": { - "path": "name" - }, - "length": 7 - } - }, - "equals": "TestObj" - } - } + "equals": "TestObj" + } + } + }, + { + // Synopsis: An expression function example. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.Example2" }, - { - // Synopsis: An expression function example. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "Json.Fn.Example2" + "spec": { + "if": { + "value": { + "$": { + "configuration": "ConfigArray" + } }, - "spec": { - "if": { - "value": { - "$": { - "configuration": "ConfigArray" - } - }, - "count": 5 - } - } + "count": 5 + } + } + }, + { + // Synopsis: An expression function example. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.Example3" }, - { - // Synopsis: An expression function example. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "Json.Fn.Example3" + "spec": { + "if": { + "value": { + "$": { + "boolean": true + } }, - "spec": { - "if": { - "value": { - "$": { - "boolean": true - } - }, - "equals": true - } - } + "equals": true + } + } + }, + { + // Synopsis: An expression function example. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.Example4" }, - { - // Synopsis: An expression function example. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "Json.Fn.Example4" + "spec": { + "if": { + "value": { + "$": { + "concat": [ + { + "path": "name" + }, + { + "string": "-" + }, + { + "path": "name" + } + ] + } }, - "spec": { - "if": { - "value": { - "$": { - "concat": [ - { - "path": "name" - }, - { - "string": "-" - }, - { - "path": "name" - } - ] - } - }, - "equals": "TestObject1-TestObject1" - } - } + "equals": "TestObject1-TestObject1" + } + } + }, + { + // Synopsis: An expression function example. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.Example5" }, - { - // Synopsis: An expression function example. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "Json.Fn.Example5" + "spec": { + "if": { + "value": { + "$": { + "integer": 6 + } }, - "spec": { - "if": { - "value": { - "$": { - "integer": 6 - } - }, - "greater": 5 - } + "greater": 5 + } + } + }, + { + // Synopsis: An expression function example. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.Example6" + }, + "spec": { + "if": { + "value": "TestObject1-TestObject1", + "equals": { + "$": { + "concat": [ + { + "path": "name" + }, + { + "string": "-" + }, + { + "path": "name" + } + ] + } } + } + } + }, + { + // Synopsis: A test for the replace function. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.Replace" }, - { - // Synopsis: An expression function example. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "Json.Fn.Example6" + "spec": { + "if": { + "value": { + "$": { + "replace": { + "string": " test one " + }, + "oldString": " ", + "newString": "" + } }, - "spec": { - "if": { - "value": "TestObject1-TestObject1", - "equals": { - "$": { - "concat": [ - { - "path": "name" - }, - { - "string": "-" - }, - { - "path": "name" - } - ] - } - } + "equals": "testone" + } + } + }, + { + // Synopsis: A test for the trim function. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.Trim" + }, + "spec": { + "if": { + "value": { + "$": { + "trim": { + "string": " test " } - } + } + }, + "equals": "test" + } + } + }, + { + // Synopsis: A test for the first function. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.First" + }, + "spec": { + "if": { + "value": { + "$": { + "first": [ + "abc", + "def" + ] + } + }, + "equals": "abc" + } + } + }, + { + // Synopsis: A test for the last function. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.Last" + }, + "spec": { + "if": { + "value": { + "$": { + "last": [ + "abc", + "def" + ] + } + }, + "equals": "def" + } + } + }, + { + // Synopsis: A test for the split function. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.Split" + }, + "spec": { + "if": { + "value": { + "$": { + "split": { + "string": "One Two Three" + }, + "delimiter": [ + " " + ] + } + }, + "equals": [ + "One", + "Two", + "Three" + ] + } } + } ] diff --git a/tests/PSRule.Tests/Functions.Rule.yaml b/tests/PSRule.Tests/Functions.Rule.yaml index ba3f42f0eb..5d3b64752e 100644 --- a/tests/PSRule.Tests/Functions.Rule.yaml +++ b/tests/PSRule.Tests/Functions.Rule.yaml @@ -86,3 +86,82 @@ spec: - path: name - string: '-' - path: name + +--- +# Synopsis: A test for the replace function. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: Yaml.Fn.Replace +spec: + if: + value: + $: + replace: + string: ' test one ' + oldString: ' ' + newString: '' + equals: testone + +--- +# Synopsis: A test for the trim function. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: Yaml.Fn.Trim +spec: + if: + value: + $: + trim: + string: ' test ' + equals: test + +--- +# Synopsis: A test for the first function. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: Yaml.Fn.First +spec: + if: + value: + $: + first: + - string: abc + - string: def + equals: abc + +--- +# Synopsis: A test for the last function. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: Yaml.Fn.Last +spec: + if: + value: + $: + last: + - string: abc + - string: def + equals: def + +--- +# Synopsis: A test for the split function. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: Yaml.Fn.Split +spec: + if: + value: + $: + split: + string: One Two Three + delimiter: + - ' ' + equals: + - One + - Two + - Three diff --git a/tests/PSRule.Tests/SelectorTests.cs b/tests/PSRule.Tests/SelectorTests.cs index a5d143ba67..b7f64822b7 100644 --- a/tests/PSRule.Tests/SelectorTests.cs +++ b/tests/PSRule.Tests/SelectorTests.cs @@ -1762,6 +1762,27 @@ public void WithFunction(string type, string path) Assert.True(example6.Match(actual1)); } + [Theory] + [InlineData("Yaml", FunctionsYamlFileName)] + [InlineData("Json", FunctionsJsonFileName)] + public void WithFunctionSpecific(string type, string path) + { + var example1 = GetSelectorVisitor($"{type}.Fn.Replace", GetSource(path), out _); + var example2 = GetSelectorVisitor($"{type}.Fn.Trim", GetSource(path), out _); + var example3 = GetSelectorVisitor($"{type}.Fn.First", GetSource(path), out _); + var example4 = GetSelectorVisitor($"{type}.Fn.Last", GetSource(path), out _); + var example5 = GetSelectorVisitor($"{type}.Fn.Split", GetSource(path), out _); + var actual1 = GetObject( + (name: "Name", value: "TestObject1") + ); + + Assert.True(example1.Match(actual1)); + Assert.True(example2.Match(actual1)); + Assert.True(example3.Match(actual1)); + Assert.True(example4.Match(actual1)); + Assert.True(example5.Match(actual1)); + } + #endregion Functions #region Helper methods diff --git a/tests/PSRule.Tests/StringExtensionsTests.cs b/tests/PSRule.Tests/StringExtensionsTests.cs new file mode 100644 index 0000000000..b3bc833010 --- /dev/null +++ b/tests/PSRule.Tests/StringExtensionsTests.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Xunit; + +namespace PSRule +{ + public sealed class StringExtensionsTests + { + [Fact] + public void Replace() + { + var actual = "One Two Three"; + Assert.Equal("OneTwoThree", actual.Replace(" ", "", caseSensitive: true)); + Assert.Equal("OneTwoThree", actual.Replace(" ", "", caseSensitive: false)); + Assert.Equal("One Two ", actual.Replace("Three", "", caseSensitive: true)); + Assert.Equal("One Two ", actual.Replace("Three", "", caseSensitive: false)); + Assert.Equal(" Two Three", actual.Replace("One", "", caseSensitive: true)); + Assert.Equal(" Two Three", actual.Replace("One", "", caseSensitive: false)); + Assert.Equal("One 2 Three", actual.Replace("Two", "2", caseSensitive: true)); + Assert.Equal("One 2 Three", actual.Replace("Two", "2", caseSensitive: false)); + Assert.Equal("One Two Three", actual.Replace("two", "2", caseSensitive: true)); + Assert.Equal("One 2 Three", actual.Replace("two", "2", caseSensitive: false)); + Assert.Equal("One Two Three", actual.Replace("three", "3", caseSensitive: true)); + Assert.Equal("One Two 3", actual.Replace("three", "3", caseSensitive: false)); + Assert.Equal("One Two Three", actual.Replace("one", "1", caseSensitive: true)); + Assert.Equal("1 Two Three", actual.Replace("one", "1", caseSensitive: false)); + } + } +} From 4e271b24505d07d74363224b7359454f98000a54 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Tue, 6 Dec 2022 18:41:24 +1000 Subject: [PATCH 125/156] Pre-release v2.7.0-B0031 (#1369) --- docs/CHANGELOG-v2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 4f83e19a7f..8ea64c9197 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.7.0-B0031 (pre-release) + What's changed since pre-release v2.7.0-B0016: - New features: From cac453be4cfb028bb71c72fd74663ded391a5f9c Mon Sep 17 00:00:00 2001 From: Bernie White Date: Thu, 8 Dec 2022 00:57:44 +1000 Subject: [PATCH 126/156] Fixes loop stuck parsing JSON in rule #1370 (#1371) --- docs/CHANGELOG-v2.md | 8 ++++++-- src/PSRule/Common/JsonConverters.cs | 19 +++++++++--------- src/PSRule/Common/JsonReaderExtensions.cs | 2 +- .../Resources/PSRuleResources.Designer.cs | 2 +- src/PSRule/Resources/PSRuleResources.resx | 2 +- tests/PSRule.Tests/FromFile.Rule.jsonc | 20 +++++++++++++++++++ tests/PSRule.Tests/FromFile.Rule.yaml | 13 ++++++++++++ 7 files changed, 51 insertions(+), 15 deletions(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 8ea64c9197..120aa76fb0 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,10 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +- Bug fixes: + - Fixed loop stuck parsing JSON allOf not rule condition by @BernieWhite. + [#1370](https://github.com/microsoft/PSRule/issues/1370) + ## v2.7.0-B0031 (pre-release) What's changed since pre-release v2.7.0-B0016: @@ -41,9 +45,9 @@ What's changed since pre-release v2.7.0-B0016: [#1227](https://github.com/microsoft/PSRule/issues/1227) - Added support for `trim`, `replace`, `split`, `first`, and `last`. - Bug fixes: - - Fixes CLI failed to load required assemblies by @BernieWhite. + - Fixed CLI failed to load required assemblies by @BernieWhite. [#1361](https://github.com/microsoft/PSRule/issues/1361) - - Fixes CLI ignores modules specified in `Include.Modules` by @BernieWhite. + - Fixed CLI ignores modules specified in `Include.Modules` by @BernieWhite. [#1362](https://github.com/microsoft/PSRule/issues/1362) ## v2.7.0-B0016 (pre-release) diff --git a/src/PSRule/Common/JsonConverters.cs b/src/PSRule/Common/JsonConverters.cs index de8816873b..fddaf6dd6f 100644 --- a/src/PSRule/Common/JsonConverters.cs +++ b/src/PSRule/Common/JsonConverters.cs @@ -42,7 +42,7 @@ protected static void ReadObject(PSObject value, JsonReader reader, bool bindTar SkipComments(reader); var path = reader.Path; if (reader.TokenType != JsonToken.StartObject || !reader.Read()) - throw new PipelineSerializationException(PSRuleResources.ReadJsonFailedExpectedToken, Enum.GetName(typeof(JsonToken), reader.TokenType)); + throw new PipelineSerializationException(PSRuleResources.ReadJsonFailedExpectedToken, Enum.GetName(typeof(JsonToken), reader.TokenType), reader.Path); string name = null; var lineNumber = 0; @@ -92,7 +92,7 @@ protected static void ReadObject(PSObject value, JsonReader reader, bool bindTar break; } if (!reader.Read() || reader.TokenType == JsonToken.None) - throw new PipelineSerializationException(PSRuleResources.ReadJsonFailedExpectedToken, Enum.GetName(typeof(JsonToken), reader.TokenType)); + throw new PipelineSerializationException(PSRuleResources.ReadJsonFailedExpectedToken, Enum.GetName(typeof(JsonToken), reader.TokenType), reader.Path); } if (bindTargetInfo) { @@ -116,7 +116,7 @@ protected static PSObject[] ReadArray(JsonReader reader) { SkipComments(reader); if (reader.TokenType != JsonToken.StartArray || !reader.Read()) - throw new PipelineSerializationException(PSRuleResources.ReadJsonFailedExpectedToken, Enum.GetName(typeof(JsonToken), reader.TokenType)); + throw new PipelineSerializationException(PSRuleResources.ReadJsonFailedExpectedToken, Enum.GetName(typeof(JsonToken), reader.TokenType), reader.Path); var result = new List(); @@ -147,7 +147,7 @@ protected static PSObject[] ReadArray(JsonReader reader) break; } if (!reader.Read() || reader.TokenType == JsonToken.None) - throw new PipelineSerializationException(PSRuleResources.ReadJsonFailedExpectedToken, Enum.GetName(typeof(JsonToken), reader.TokenType)); + throw new PipelineSerializationException(PSRuleResources.ReadJsonFailedExpectedToken, Enum.GetName(typeof(JsonToken), reader.TokenType), reader.Path); } return result.ToArray(); } @@ -247,7 +247,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist { SkipComments(reader); if (reader.TokenType != JsonToken.StartObject && reader.TokenType != JsonToken.StartArray) - throw new PipelineSerializationException(PSRuleResources.ReadJsonFailedExpectedToken, Enum.GetName(typeof(JsonToken), reader.TokenType)); + throw new PipelineSerializationException(PSRuleResources.ReadJsonFailedExpectedToken, Enum.GetName(typeof(JsonToken), reader.TokenType), reader.Path); var result = new List(); var isArray = reader.TokenType == JsonToken.StartArray; @@ -549,9 +549,7 @@ public override bool CanConvert(Type objectType) public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { var fieldMap = existingValue as FieldMap ?? new FieldMap(); - ReadFieldMap(fieldMap, reader); - return fieldMap; } @@ -563,12 +561,9 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s private static void ReadFieldMap(FieldMap map, JsonReader reader) { if (reader.TokenType != JsonToken.StartObject || !reader.Read()) - { throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed); - } string propertyName = null; - while (reader.TokenType != JsonToken.EndObject) { if (reader.TokenType == JsonToken.PropertyName) @@ -699,6 +694,8 @@ private LanguageOperator MapOperator(string type, LanguageExpression.PropertyBag if (reader.TryConsume(JsonToken.StartObject)) { result.Add(MapExpression(reader)); + if (type != "if") + reader.Consume(JsonToken.EndObject); } // AllOf and AnyOf else if (reader.TryConsume(JsonToken.StartArray)) @@ -713,6 +710,8 @@ private LanguageOperator MapOperator(string type, LanguageExpression.PropertyBag result.Add(MapExpression(reader)); reader.Consume(JsonToken.EndObject); } + if (reader.TokenType == JsonToken.EndObject) + throw new PipelineSerializationException(PSRuleResources.ReadJsonFailedExpectedToken, Enum.GetName(typeof(JsonToken), reader.TokenType), reader.Path); } reader.Consume(JsonToken.EndArray); reader.SkipComments(out _); diff --git a/src/PSRule/Common/JsonReaderExtensions.cs b/src/PSRule/Common/JsonReaderExtensions.cs index 4b49263e05..5d6c513cb1 100644 --- a/src/PSRule/Common/JsonReaderExtensions.cs +++ b/src/PSRule/Common/JsonReaderExtensions.cs @@ -60,7 +60,7 @@ public static bool TryConsume(this JsonReader reader, JsonToken token, out objec public static void Consume(this JsonReader reader, JsonToken token) { if (reader.TokenType != token) - throw new PipelineSerializationException(PSRuleResources.ReadJsonFailedExpectedToken, Enum.GetName(typeof(JsonToken), reader.TokenType)); + throw new PipelineSerializationException(PSRuleResources.ReadJsonFailedExpectedToken, Enum.GetName(typeof(JsonToken), reader.TokenType), reader.Path); reader.Read(); } diff --git a/src/PSRule/Resources/PSRuleResources.Designer.cs b/src/PSRule/Resources/PSRuleResources.Designer.cs index 8c939522e4..7b3e4e84e5 100644 --- a/src/PSRule/Resources/PSRuleResources.Designer.cs +++ b/src/PSRule/Resources/PSRuleResources.Designer.cs @@ -574,7 +574,7 @@ internal static string ReadJsonFailed { } /// - /// Looks up a localized string similar to Read JSON failed because the token ({0}) was not expected.. + /// Looks up a localized string similar to Read JSON failed because the token ({0}) was not expected at path '{1}'.. /// internal static string ReadJsonFailedExpectedToken { get { diff --git a/src/PSRule/Resources/PSRuleResources.resx b/src/PSRule/Resources/PSRuleResources.resx index a16cd27d4d..0372c2b7c6 100644 --- a/src/PSRule/Resources/PSRuleResources.resx +++ b/src/PSRule/Resources/PSRuleResources.resx @@ -380,7 +380,7 @@ Object path not found. - Read JSON failed because the token ({0}) was not expected. + Read JSON failed because the token ({0}) was not expected at path '{1}'. Target failed sub-selector pre-condition. diff --git a/tests/PSRule.Tests/FromFile.Rule.jsonc b/tests/PSRule.Tests/FromFile.Rule.jsonc index 838439ff13..0d73cd9adc 100644 --- a/tests/PSRule.Tests/FromFile.Rule.jsonc +++ b/tests/PSRule.Tests/FromFile.Rule.jsonc @@ -198,5 +198,25 @@ "equals": "TestObject1" } } + }, + { + // Synopsis: Rule for unit testing of allOf not. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "JsonRuleAllOfNot" + }, + "spec": { + "condition": { + "allOf": [ + { + "not": { + "field": "name", + "equals": "TestObject1" + } + } + ] + } + } } ] diff --git a/tests/PSRule.Tests/FromFile.Rule.yaml b/tests/PSRule.Tests/FromFile.Rule.yaml index 2b729f8f0b..7e89f5e12d 100644 --- a/tests/PSRule.Tests/FromFile.Rule.yaml +++ b/tests/PSRule.Tests/FromFile.Rule.yaml @@ -163,3 +163,16 @@ spec: condition: field: name equals: TestObject1 + +--- +# Synopsis: Rule for unit testing of allOf not. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Rule +metadata: + name: YamlRuleAllOfNot +spec: + condition: + allOf: + - not: + field: name + equals: TestObject1 From 95990b95e7636d28b175fbaa2800a8fb7c09c592 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Thu, 8 Dec 2022 01:39:00 +1000 Subject: [PATCH 127/156] Fixes handling of ulong with LessOrEqual #1366 (#1372) --- docs/CHANGELOG-v2.md | 6 +++- src/PSRule/Common/ExpressionHelpers.cs | 10 ++++++ tests/PSRule.Tests/AssertTests.cs | 44 ++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 120aa76fb0..356f141691 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,9 +30,13 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since pre-release v2.7.0-B0031: + - Bug fixes: - - Fixed loop stuck parsing JSON allOf not rule condition by @BernieWhite. + - Fixed loop stuck parsing JSON `allOf` `not` rule condition by @BernieWhite. [#1370](https://github.com/microsoft/PSRule/issues/1370) + - Fixed handling of uint64 with `LessOrEqual` assertion method by @BernieWhite. + [#1366](https://github.com/microsoft/PSRule/issues/1366) ## v2.7.0-B0031 (pre-release) diff --git a/src/PSRule/Common/ExpressionHelpers.cs b/src/PSRule/Common/ExpressionHelpers.cs index 8cd3e47a69..d1896e17c6 100644 --- a/src/PSRule/Common/ExpressionHelpers.cs +++ b/src/PSRule/Common/ExpressionHelpers.cs @@ -380,11 +380,21 @@ internal static bool TryLong(object o, bool convert, out long value) value = i; return true; } + else if (o is uint ui) + { + value = (long)ui; + return true; + } else if (o is long l) { value = l; return true; } + else if (o is ulong ul && ul <= long.MaxValue) + { + value = (long)ul; + return true; + } else if (o is JToken token && token.Type == JTokenType.Integer) { value = token.Value(); diff --git a/tests/PSRule.Tests/AssertTests.cs b/tests/PSRule.Tests/AssertTests.cs index 487d70a347..4a09326592 100644 --- a/tests/PSRule.Tests/AssertTests.cs +++ b/tests/PSRule.Tests/AssertTests.cs @@ -771,6 +771,17 @@ public void Greater() Assert.Equal("value", assert.Greater(value, "value", 3).ToResultReason().FirstOrDefault().Path); Assert.Equal("jvalue", assert.Greater(value, "jvalue", 4).ToResultReason().FirstOrDefault().Path); + // ULong + value = GetObject((name: "value", value: (ulong)3), (name: "jvalue", value: new JValue((ulong)3))); + Assert.True(assert.Greater(value, "value", 2).Result); + Assert.False(assert.Greater(value, "value", 3).Result); + Assert.False(assert.Greater(value, "value", 4).Result); + Assert.True(assert.Greater(value, "value", 0).Result); + Assert.True(assert.Greater(value, "value", -1).Result); + Assert.False(assert.Greater(value, "jvalue", 4).Result); + + Assert.Equal("value", assert.Greater(value, "value", 3).ToResultReason().FirstOrDefault().Path); + // String value = GetObject((name: "value", value: "abc")); Assert.True(assert.Greater(value, "value", 2).Result); @@ -828,6 +839,17 @@ public void GreaterOrEqual() Assert.Equal("jvalue", assert.GreaterOrEqual(value, "jvalue", 4).ToResultReason().FirstOrDefault().Path); + // ULong + value = GetObject((name: "value", value: (ulong)3), (name: "jvalue", value: new JValue((ulong)3))); + Assert.True(assert.GreaterOrEqual(value, "value", 2).Result); + Assert.True(assert.GreaterOrEqual(value, "value", 3).Result); + Assert.False(assert.GreaterOrEqual(value, "value", 4).Result); + Assert.True(assert.GreaterOrEqual(value, "value", 0).Result); + Assert.True(assert.GreaterOrEqual(value, "value", -1).Result); + Assert.False(assert.GreaterOrEqual(value, "jvalue", 4).Result); + + Assert.Equal("jvalue", assert.GreaterOrEqual(value, "jvalue", 4).ToResultReason().FirstOrDefault().Path); + // String value = GetObject((name: "value", value: "abc")); Assert.True(assert.GreaterOrEqual(value, "value", 2).Result); @@ -885,6 +907,17 @@ public void Less() Assert.Equal("value", assert.Less(value, "value", -1).ToResultReason().FirstOrDefault().Path); + // ULong + value = GetObject((name: "value", value: (ulong)3), (name: "jvalue", value: new JValue((ulong)3))); + Assert.False(assert.Less(value, "value", 2).Result); + Assert.False(assert.Less(value, "value", 3).Result); + Assert.True(assert.Less(value, "value", 4).Result); + Assert.False(assert.Less(value, "value", 0).Result); + Assert.False(assert.Less(value, "value", -1).Result); + Assert.True(assert.Less(value, "jvalue", 4).Result); + + Assert.Equal("value", assert.Less(value, "value", -1).ToResultReason().FirstOrDefault().Path); + // String value = GetObject((name: "value", value: "abc")); Assert.False(assert.Less(value, "value", 2).Result); @@ -942,6 +975,17 @@ public void LessOrEqual() Assert.Equal("value", assert.LessOrEqual(value, "value", 0).ToResultReason().FirstOrDefault().Path); + // ULong + value = GetObject((name: "value", value: (ulong)3), (name: "jvalue", value: new JValue((ulong)3))); + Assert.False(assert.LessOrEqual(value, "value", 2).Result); + Assert.True(assert.LessOrEqual(value, "value", 3).Result); + Assert.True(assert.LessOrEqual(value, "value", 4).Result); + Assert.False(assert.LessOrEqual(value, "value", 0).Result); + Assert.False(assert.LessOrEqual(value, "value", -1).Result); + Assert.True(assert.LessOrEqual(value, "jvalue", 4).Result); + + Assert.Equal("value", assert.LessOrEqual(value, "value", 0).ToResultReason().FirstOrDefault().Path); + // String value = GetObject((name: "value", value: "abc")); Assert.False(assert.LessOrEqual(value, "value", 2).Result); From 9ca1885d340bf041d1b9f6bf3c2557e1011f058f Mon Sep 17 00:00:00 2001 From: Bernie White Date: Thu, 8 Dec 2022 11:33:00 +1000 Subject: [PATCH 128/156] Pre-release v2.7.0-B0049 (#1373) --- docs/CHANGELOG-v2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 356f141691..45ba89c644 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.7.0-B0049 (pre-release) + What's changed since pre-release v2.7.0-B0031: - Bug fixes: From ea30409c66699d91afd444efefed696b8c131baf Mon Sep 17 00:00:00 2001 From: Bernie White Date: Wed, 14 Dec 2022 00:34:09 +1000 Subject: [PATCH 129/156] Fixed object path wildcard selector #1376 (#1377) --- docs/CHANGELOG-v2.md | 6 + .../ObjectPath/PathExpressionBuilder.cs | 12 ++ tests/PSRule.Tests/ObjectFromFile.json | 170 ++++++++++-------- tests/PSRule.Tests/PSRule.Common.Tests.ps1 | 2 +- tests/PSRule.Tests/PSRule.Tests.csproj | 8 +- tests/PSRule.Tests/PathExpressionTests.cs | 26 +++ 6 files changed, 145 insertions(+), 79 deletions(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 45ba89c644..b99d785381 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,12 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since pre-release v2.7.0-B0049: + +- Bug fixes: + - Fixed object path fails to iterate JSON object with wildcard selector by @BernieWhite. + [#1376](https://github.com/microsoft/PSRule/issues/1376) + ## v2.7.0-B0049 (pre-release) What's changed since pre-release v2.7.0-B0031: diff --git a/src/PSRule/Runtime/ObjectPath/PathExpressionBuilder.cs b/src/PSRule/Runtime/ObjectPath/PathExpressionBuilder.cs index 21fb404b30..1f22582bec 100644 --- a/src/PSRule/Runtime/ObjectPath/PathExpressionBuilder.cs +++ b/src/PSRule/Runtime/ObjectPath/PathExpressionBuilder.cs @@ -386,6 +386,12 @@ private static IEnumerable GetAll(object o) if (IsSimpleType(baseObject)) return DEFAULT_EMPTY_ARRAY; + if (baseObject is JObject jObject) + return GetAllField(jObject); + + if (baseObject is JArray jArray) + return GetAllIndex(jArray); + return baseObject is IEnumerable ? GetAllIndex(baseObject) : GetAllField(baseObject); } @@ -438,6 +444,12 @@ private static IEnumerable GetAllField(object o) foreach (var property in psObject.Properties) yield return property.Value; } + // Handle JObject + else if (o is JObject jObject) + { + foreach (var property in jObject.Properties()) + yield return property.Value; + } // Handle DynamicObjects else if (o is DynamicObject dynamicObject) { diff --git a/tests/PSRule.Tests/ObjectFromFile.json b/tests/PSRule.Tests/ObjectFromFile.json index dacc0ed321..361bab9561 100644 --- a/tests/PSRule.Tests/ObjectFromFile.json +++ b/tests/PSRule.Tests/ObjectFromFile.json @@ -1,82 +1,104 @@ // Some comments [ + // Some comments + { // Some comments - { - // Some comments - "TargetName": "TestObject1", - "Spec": { // Some comments - "": { - "Key": "value" - }, - "Properties": { - "Value1": 1, - "Kind": "Test", - "array": [ - { - "id": "1" - }, - { - "id": "2" - } - ], - "array2": [ - "1", - "2", - "3" - ], - "array3": [ - [ - "nested" - ], - [ - 1, - 2, - 3 - ] - ], - "from": null - } - }, // Some comments - "_PSRule": { - "path": "master.items[0]", - "source": [ - { - "file": "some-file.json", - "line": 1, - "type": "Origin" - } - ], - "issue": [ - { - "type": "Downstream.Issue", - "name": "Custom.NoNesting", - "message": "A custom downstream issue reporting nesting not allowed." - } + "TargetName": "TestObject1", + "Spec": { // Some comments + "": { + "Key": "value" + }, + "Properties": { + "Value1": 1, + "Kind": "Test", + "array": [ + { + "id": "1" + }, + { + "id": "2" + } + ], + "array2": [ + "1", + "2", + "3" + ], + "array3": [ + [ + "nested" + ], + [ + 1, + 2, + 3 + ] + ], + "from": null + }, + "config": { + "example": [ + { + "prop": [ + { + "public": true + } ] + } + ] + } + }, // Some comments + "_PSRule": { + "path": "master.items[0]", + "source": [ + { + "file": "some-file.json", + "line": 1, + "type": "Origin" } - }, - { - "TargetName": "TestObject2", - "Spec": { - "Properties": { - "Value2": 2, - "Kind": "Test", - "array": [ - { - "id": "1" - }, - { - "id": "2" - }, - null - ], - "array2": [ - "1", - "2", - "3" - ], - "from": "abc" - } + ], + "issue": [ + { + "type": "Downstream.Issue", + "name": "Custom.NoNesting", + "message": "A custom downstream issue reporting nesting not allowed." } + ] } + }, + { + "TargetName": "TestObject2", + "Spec": { + "Properties": { + "Value2": 2, + "Kind": "Test", + "array": [ + { + "id": "1" + }, + { + "id": "2" + }, + null + ], + "array2": [ + "1", + "2", + "3" + ], + "from": "abc" + }, + "config": { + "example": [ + { + "prop": [ + { + "public": false + } + ] + } + ] + } + } + } ] diff --git a/tests/PSRule.Tests/PSRule.Common.Tests.ps1 b/tests/PSRule.Tests/PSRule.Common.Tests.ps1 index 4de8358a34..a5b4eca0e5 100644 --- a/tests/PSRule.Tests/PSRule.Common.Tests.ps1 +++ b/tests/PSRule.Tests/PSRule.Common.Tests.ps1 @@ -852,7 +852,7 @@ Describe 'Invoke-PSRule' -Tag 'Invoke-PSRule','Common' { $result[0].Source[0].File | Should -Be 'some-file.json'; $result[0].Source[0].Line | Should -Be 1; $result[1].Source[0].File.Split([char[]]@('\', '/'))[-1] | Should -Be 'ObjectFromFile.json'; - $result[1].Source[0].Line | Should -Be 59; + $result[1].Source[0].Line | Should -Be 70; # Multiple file $result = @(Invoke-PSRule -Path $ruleFilePath -Name 'WithFormat' -InputPath $inputFiles); diff --git a/tests/PSRule.Tests/PSRule.Tests.csproj b/tests/PSRule.Tests/PSRule.Tests.csproj index 7274b1f51b..3af12608eb 100644 --- a/tests/PSRule.Tests/PSRule.Tests.csproj +++ b/tests/PSRule.Tests/PSRule.Tests.csproj @@ -6,9 +6,9 @@ true false PSRule - portable - false - false + Full + @@ -17,7 +17,7 @@ all - runtime; build; native; contentfiles; analyzers + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/PSRule.Tests/PathExpressionTests.cs b/tests/PSRule.Tests/PathExpressionTests.cs index d8bfcccd26..1cd3267374 100644 --- a/tests/PSRule.Tests/PathExpressionTests.cs +++ b/tests/PSRule.Tests/PathExpressionTests.cs @@ -307,6 +307,32 @@ public void WithDescendant() Assert.False(expression.TryGet(pso, true, out object[] _)); } + [Fact] + public void WithArrayExpand() + { + var testObject = GetJsonContent() as JArray; + var expression = PathExpression.Create("Spec.config.[*][*].prop[*].public"); + Assert.True(expression.IsArray); + Assert.True(expression.TryGet(testObject[0], true, out object[] actual)); + Assert.Equal(true, actual[0]); + Assert.True(expression.TryGet(testObject[1], true, out actual)); + Assert.Equal(false, actual[0]); + + expression = PathExpression.Create("Spec.config[*][*].prop[*].public"); + Assert.True(expression.IsArray); + Assert.True(expression.TryGet(testObject[0], true, out actual)); + Assert.Equal(true, actual[0]); + Assert.True(expression.TryGet(testObject[1], true, out actual)); + Assert.Equal(false, actual[0]); + + expression = PathExpression.Create("Spec.config.[*].[*].prop[*].public"); + Assert.True(expression.IsArray); + Assert.True(expression.TryGet(testObject[0], true, out actual)); + Assert.Equal(true, actual[0]); + Assert.True(expression.TryGet(testObject[1], true, out actual)); + Assert.Equal(false, actual[0]); + } + #region Helper methods private static object GetJsonContent() From a75771368360c347c010ddfcc14827cbed1502fc Mon Sep 17 00:00:00 2001 From: Bernie White Date: Wed, 14 Dec 2022 22:50:53 +1000 Subject: [PATCH 130/156] Fixes rule annotation from definition #1378 (#1379) * Fixes rule annotation from definition #1387 * Fix change log --- docs/CHANGELOG-v2.md | 2 ++ src/PSRule/Host/HostHelper.cs | 15 +++++++++++++++ tests/PSRule.Tests/FromFile.Rule.jsonc | 3 +++ tests/PSRule.Tests/FromFile.Rule.yaml | 2 ++ tests/PSRule.Tests/RulesTests.cs | 10 ++++++++++ 5 files changed, 32 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index b99d785381..2720115d1a 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -35,6 +35,8 @@ What's changed since pre-release v2.7.0-B0049: - Bug fixes: - Fixed object path fails to iterate JSON object with wildcard selector by @BernieWhite. [#1376](https://github.com/microsoft/PSRule/issues/1376) + - Fixed rule annotations are not included from YAML/ JSON definition by @BernieWhite. + [#1378](https://github.com/microsoft/PSRule/issues/1378) ## v2.7.0-B0049 (pre-release) diff --git a/src/PSRule/Host/HostHelper.cs b/src/PSRule/Host/HostHelper.cs index d397c91481..a0aaa5f416 100644 --- a/src/PSRule/Host/HostHelper.cs +++ b/src/PSRule/Host/HostHelper.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; @@ -543,6 +544,7 @@ private static DependencyTargetCollection ToRuleBlockV1(ILanguageBloc block.Source.Module, synopsis: new InfoString(block.Synopsis) ); + MergeAnnotations(info, block.Metadata); results.TryAdd(new RuleBlock ( @@ -571,6 +573,19 @@ private static DependencyTargetCollection ToRuleBlockV1(ILanguageBloc 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; + } + } + private static RuleHelpInfo[] ToRuleHelp(IEnumerable blocks, RunspaceContext context) { // Index rules by RuleId diff --git a/tests/PSRule.Tests/FromFile.Rule.jsonc b/tests/PSRule.Tests/FromFile.Rule.jsonc index 0d73cd9adc..12c6c79df6 100644 --- a/tests/PSRule.Tests/FromFile.Rule.jsonc +++ b/tests/PSRule.Tests/FromFile.Rule.jsonc @@ -20,6 +20,9 @@ "Value1", "Value2" ] + }, + "annotations": { + "test_value": "test123" } }, "spec": { diff --git a/tests/PSRule.Tests/FromFile.Rule.yaml b/tests/PSRule.Tests/FromFile.Rule.yaml index 7e89f5e12d..6eafde1c8c 100644 --- a/tests/PSRule.Tests/FromFile.Rule.yaml +++ b/tests/PSRule.Tests/FromFile.Rule.yaml @@ -19,6 +19,8 @@ metadata: multi: - Value1 - Value2 + annotations: + test_value: test123 spec: condition: allOf: diff --git a/tests/PSRule.Tests/RulesTests.cs b/tests/PSRule.Tests/RulesTests.cs index 4cc4c48dc1..a55cca5586 100644 --- a/tests/PSRule.Tests/RulesTests.cs +++ b/tests/PSRule.Tests/RulesTests.cs @@ -45,6 +45,11 @@ public void ReadYamlRule() var hashtable = rule[0].Tag.ToHashtable(); Assert.Equal("tag", hashtable["feature"]); + + var block = HostHelper.GetRuleBlockGraph(GetSource(), context).GetAll(); + var actual = block.FirstOrDefault(b => b.Name == "YamlBasicRule"); + Assert.NotNull(actual.Info.Annotations); + Assert.Equal("test123", actual.Info.Annotations["test_value"]); } /// @@ -219,6 +224,11 @@ public void ReadJsonRule() 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"]); } /// From 965a872dba3ddcbebce9d4450d5efea3675cea31 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Wed, 14 Dec 2022 23:10:56 +1000 Subject: [PATCH 131/156] Pre-release v2.7.0-B0070 (#1380) --- docs/CHANGELOG-v2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 2720115d1a..7693842dd9 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.7.0-B0070 (pre-release) + What's changed since pre-release v2.7.0-B0049: - Bug fixes: From 9d012ec0eda16099054c182989512ba4b6fd984a Mon Sep 17 00:00:00 2001 From: Bernie White Date: Mon, 19 Dec 2022 14:21:29 +1000 Subject: [PATCH 132/156] Improvements to scope and string #1382 #1383 #1384 (#1385) --- docs/CHANGELOG-v2.md | 10 ++ .../PSRule/en-US/about_PSRule_Assert.md | 18 ++- .../PSRule/en-US/about_PSRule_Expressions.md | 2 +- .../PSRule/en-US/about_PSRule_Variables.md | 2 +- schemas/PSRule-language.schema.json | 11 +- src/PSRule/Common/DictionaryExtensions.cs | 4 +- src/PSRule/Common/ExpressionHelpers.cs | 12 +- src/PSRule/Common/HashtableExtensions.cs | 2 +- src/PSRule/Common/PSObjectExtensions.cs | 4 +- .../Expressions/LanguageExpressions.cs | 114 ++++++++++-------- src/PSRule/Pipeline/TargetObject.cs | 6 +- src/PSRule/Runtime/Assert.cs | 83 +++++++++---- src/PSRule/Runtime/LanguageScope.cs | 4 +- src/PSRule/Runtime/Operand.cs | 2 +- src/PSRule/Runtime/PSRule.cs | 2 +- src/PSRule/Runtime/PSRuleMemberInfo.cs | 2 +- tests/PSRule.Tests/SelectorTests.cs | 38 +++++- tests/PSRule.Tests/Selectors.Rule.jsonc | 32 ++++- tests/PSRule.Tests/Selectors.Rule.yaml | 25 +++- 19 files changed, 263 insertions(+), 110 deletions(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 7693842dd9..92fcc69003 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,16 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since pre-release v2.7.0-B0070: + +- General improvements: + - Added support for `hasValue` expression with `scope` by @BernieWhite. + [#1382](https://github.com/microsoft/PSRule/issues/1382) + - Return target object scope as an array by @BernieWhite. + [#1383](https://github.com/microsoft/PSRule/issues/1383) + - Improve support of string comparisons to support an array of strings by @BernieWhite. + [#1384](https://github.com/microsoft/PSRule/issues/1384) + ## v2.7.0-B0070 (pre-release) What's changed since pre-release v2.7.0-B0049: diff --git a/docs/concepts/PSRule/en-US/about_PSRule_Assert.md b/docs/concepts/PSRule/en-US/about_PSRule_Assert.md index f94ffc3d7e..0195b60777 100644 --- a/docs/concepts/PSRule/en-US/about_PSRule_Assert.md +++ b/docs/concepts/PSRule/en-US/about_PSRule_Assert.md @@ -223,7 +223,8 @@ Rule 'MinimumAPIVersionWithFlag' { ### Contains -The `Contains` assertion method checks the field value contains the specified string. +The `Contains` assertion method checks the operand contains the specified string. +If the operand is an array of strings, only one string must contain the specified string. Optionally a case-sensitive compare can be used, however case is ignored by default. The following parameters are accepted: @@ -284,7 +285,8 @@ Rule 'Count' { ### EndsWith -The `EndsWith` assertion method checks the field value ends with the specified suffix. +The `EndsWith` assertion method checks the operand ends with the specified suffix. +If the operand is an array of strings, only one string must end with the specified suffix. Optionally a case-sensitive compare can be used, however case is ignored by default. The following parameters are accepted: @@ -1070,8 +1072,9 @@ Rule 'Match' { ### NotContains -The `NotContains` assertion method checks the field value contains the specified string. +The `NotContains` assertion method checks the operand contains the specified string. This condition fails when any of the specified sub-strings are found. +If the operand is an array of strings, this condition fails if any of the strings contain the specified string. Optionally a case-sensitive compare can be used, however case is ignored by default. The following parameters are accepted: @@ -1130,8 +1133,9 @@ Rule 'NotCount' { ### NotEndsWith -The `NotEndsWith` assertion method checks the field value ends with the specified suffix. +The `NotEndsWith` assertion method checks the operand ends with the specified suffix. This condition fails when any of the specified sub-strings are found at the end of the operand. +If the operand is an array of strings, this condition fails if any of the strings ends with the specified suffix. Optionally a case-sensitive compare can be used, however case is ignored by default. The following parameters are accepted: @@ -1313,8 +1317,9 @@ Rule 'NotNull' { ### NotStartsWith -The `NotStartsWith` assertion method checks the field value starts with the specified prefix. +The `NotStartsWith` assertion method checks the operand starts with the specified prefix. This condition fails when any of the specified sub-strings are found at the start of the operand. +If the operand is an array of strings, this condition fails if any of the strings start with the specified prefix. Optionally a case-sensitive compare can be used, however case is ignored by default. The following parameters are accepted: @@ -1507,7 +1512,8 @@ Rule 'Subset' { ### StartsWith -The `StartsWith` assertion method checks the field value starts with the specified prefix. +The `StartsWith` assertion method checks the operand starts with the specified prefix. +If the operand is an array of strings, only one string must start with the specified prefix. Optionally a case-sensitive compare can be used, however case is ignored by default. The following parameters are accepted: diff --git a/docs/concepts/PSRule/en-US/about_PSRule_Expressions.md b/docs/concepts/PSRule/en-US/about_PSRule_Expressions.md index f8201d5bad..a6c5696bcb 100644 --- a/docs/concepts/PSRule/en-US/about_PSRule_Expressions.md +++ b/docs/concepts/PSRule/en-US/about_PSRule_Expressions.md @@ -1734,7 +1734,7 @@ spec: ### Scope -The comparison property `scope` is used with a condition to evaluate the scope of the object. +The comparison property `scope` is used with a condition to evaluate any scopes assigned to the object. The `scope` property must be set to `.`. Any other value will cause the condition to evaluate to `false`. diff --git a/docs/concepts/PSRule/en-US/about_PSRule_Variables.md b/docs/concepts/PSRule/en-US/about_PSRule_Variables.md index b7b1a51ab1..f81fe9f16f 100644 --- a/docs/concepts/PSRule/en-US/about_PSRule_Variables.md +++ b/docs/concepts/PSRule/en-US/about_PSRule_Variables.md @@ -172,7 +172,7 @@ The following properties are available for read access: See option `Binding.Field` for more information. - `Input` - Allows adding additional input paths to the pipeline. - `Repository` - Provides access to information about the current repository. -- `Scope` - The scope of the object currently being processed by the pipeline. +- `Scope` - Any scopes assigned to the object currently being processed by the pipeline. - `Source` - A collection of sources for the object currently being processed on the pipeline. - `TargetObject` - The object currently being processed on the pipeline. - `TargetName` - The name of the object currently being processed on the pipeline. diff --git a/schemas/PSRule-language.schema.json b/schemas/PSRule-language.schema.json index d34c8c3597..713fc5672d 100644 --- a/schemas/PSRule-language.schema.json +++ b/schemas/PSRule-language.schema.json @@ -1175,8 +1175,8 @@ "scope": { "type": "string", "title": "Scope", - "description": "The scope of the object currently being processed by the pipeline.", - "markdownDescription": "The scope of the object currently being processed by the pipeline. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#scope)", + "description": "Any scopes assigned to the object currently being processed by the pipeline.", + "markdownDescription": "Any scopes assigned to the object currently being processed by the pipeline.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#scope)", "default": ".", "enum": [ "." @@ -1463,12 +1463,15 @@ "hasValue": { "type": "boolean", "title": "Has Value", - "description": "Must have a non-empty value.", - "markdownDescription": "Must have a non-empty value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasvalue)", + "description": "When set to true, the operand must have a non-empty value. When set to false the operand must be null or empty.", + "markdownDescription": "When set to `true`, the operand must have a non-empty value. When set to `false` the operand must be `null` or empty.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#hasvalue)", "default": true }, "field": { "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "scope": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/scope" } }, "required": [ diff --git a/src/PSRule/Common/DictionaryExtensions.cs b/src/PSRule/Common/DictionaryExtensions.cs index ffc365b185..db7e31fefd 100644 --- a/src/PSRule/Common/DictionaryExtensions.cs +++ b/src/PSRule/Common/DictionaryExtensions.cs @@ -58,7 +58,7 @@ public static bool TryPopString(this IDictionary dictionary, str public static bool TryPopStringArray(this IDictionary dictionary, string key, out string[] value) { value = default; - return TryPopValue(dictionary, key, out var v) && ExpressionHelpers.TryConvertStringArray(v, out value); + return TryPopValue(dictionary, key, out var v) && ExpressionHelpers.TryStringOrArray(v, convert: true, value: out value); } [DebuggerStepThrough] @@ -140,7 +140,7 @@ public static bool TryGetEnumerable(this IDictionary dictionary, public static bool TryGetStringArray(this IDictionary dictionary, string key, out string[] value) { value = null; - return dictionary.TryGetValue(key, out var o) && ExpressionHelpers.TryConvertStringArray(o, out value); + return dictionary.TryGetValue(key, out var o) && ExpressionHelpers.TryStringOrArray(o, convert: true, value: out value); } [DebuggerStepThrough] diff --git a/src/PSRule/Common/ExpressionHelpers.cs b/src/PSRule/Common/ExpressionHelpers.cs index d1896e17c6..cd1cd8a3e4 100644 --- a/src/PSRule/Common/ExpressionHelpers.cs +++ b/src/PSRule/Common/ExpressionHelpers.cs @@ -243,20 +243,20 @@ internal static bool TryArray(object o, out Array value) return value != null; } - internal static bool TryConvertStringArray(object o, out string[] value) + internal static bool TryStringOrArray(object o, bool convert, out string[] value) { // Handle single string - if (TryString(o, convert: true, value: out var s)) + if (TryString(o, convert, value: out var s)) { value = new string[] { s }; return true; } // Handle multiple strings - return TryStringArray(o, out value); + return TryStringArray(o, convert, out value); } - internal static bool TryStringArray(object o, out string[] value) + internal static bool TryStringArray(object o, bool convert, out string[] value) { value = null; if (o is Array array) @@ -264,7 +264,7 @@ internal static bool TryStringArray(object o, out string[] value) value = new string[array.Length]; for (var i = 0; i < array.Length; i++) { - if (TryString(array.GetValue(i), out var s)) + if (TryString(array.GetValue(i), convert, value: out var s)) value[i] = s; } } @@ -273,7 +273,7 @@ internal static bool TryStringArray(object o, out string[] value) value = new string[jArray.Count]; for (var i = 0; i < jArray.Count; i++) { - if (TryString(jArray[i], out var s)) + if (TryString(jArray[i], convert, out var s)) value[i] = s; } } diff --git a/src/PSRule/Common/HashtableExtensions.cs b/src/PSRule/Common/HashtableExtensions.cs index 89a09f0a07..e9dd8ee3d8 100644 --- a/src/PSRule/Common/HashtableExtensions.cs +++ b/src/PSRule/Common/HashtableExtensions.cs @@ -24,7 +24,7 @@ public static IDictionary ToDictionary(this Hashtable hashtable) public static bool TryGetStringArray(this Hashtable hashtable, string key, out string[] value) { value = null; - return hashtable.TryGetValue(key, out var o) && ExpressionHelpers.TryConvertStringArray(o, out value); + return hashtable.TryGetValue(key, out var o) && ExpressionHelpers.TryStringOrArray(o, convert: true, value: out value); } [DebuggerStepThrough] diff --git a/src/PSRule/Common/PSObjectExtensions.cs b/src/PSRule/Common/PSObjectExtensions.cs index 97796c6873..9102cea7df 100644 --- a/src/PSRule/Common/PSObjectExtensions.cs +++ b/src/PSRule/Common/PSObjectExtensions.cs @@ -125,7 +125,7 @@ public static string GetTargetType(this PSObject o) return o != null && o.TryTargetInfo(out var targetInfo) ? targetInfo.TargetType : null; } - public static string GetScope(this PSObject o) + public static string[] GetScope(this PSObject o) { return o != null && o.TryTargetInfo(out var targetInfo) ? targetInfo.Scope : null; } @@ -154,7 +154,7 @@ public static void ConvertTargetInfoProperty(this PSObject o) if (TryProperty(value, PROPERTY_TYPE, out string type) && targetInfo.TargetType == null) targetInfo.TargetType = type; - if (TryProperty(value, PROPERTY_SCOPE, out string scope) && targetInfo.Scope == null) + if (TryProperty(value, PROPERTY_SCOPE, out string[] scope) && targetInfo.Scope == null) targetInfo.Scope = scope; if (TryProperty(value, PROPERTY_PATH, out string path) && targetInfo.Path == null) diff --git a/src/PSRule/Definitions/Expressions/LanguageExpressions.cs b/src/PSRule/Definitions/Expressions/LanguageExpressions.cs index 11cfae9a0d..55f206c995 100644 --- a/src/PSRule/Definitions/Expressions/LanguageExpressions.cs +++ b/src/PSRule/Definitions/Expressions/LanguageExpressions.cs @@ -968,13 +968,17 @@ internal static bool StartsWith(ExpressionContext context, ExpressionInfo info, GetCaseSensitive(properties, out var caseSensitive)) { context.ExpressionTrace(STARTSWITH, operand.Value, propertyValue); - if (!ExpressionHelpers.TryString(operand.Value, convert, out var value)) + if (!ExpressionHelpers.TryStringOrArray(operand.Value, convert, out var value)) return NotString(context, operand); - for (var i = 0; propertyValue != null && i < propertyValue.Length; i++) - if (ExpressionHelpers.StartsWith(value, propertyValue[i], caseSensitive)) - return Pass(); - + for (var i_propertyValue = 0; propertyValue != null && i_propertyValue < propertyValue.Length; i_propertyValue++) + { + for (var i_value = 0; i_value < value.Length; i_value++) + { + if (ExpressionHelpers.StartsWith(value[i_value], propertyValue[i_propertyValue], caseSensitive)) + return Pass(); + } + } return Fail( context, operand, @@ -995,18 +999,21 @@ internal static bool NotStartsWith(ExpressionContext context, ExpressionInfo inf GetCaseSensitive(properties, out var caseSensitive)) { context.ExpressionTrace(NOTSTARTSWITH, operand.Value, propertyValue); - if (ExpressionHelpers.TryString(operand.Value, convert, out var value)) + if (ExpressionHelpers.TryStringOrArray(operand.Value, convert, out var value)) { - for (var i = 0; propertyValue != null && i < propertyValue.Length; i++) + for (var i_propertyValue = 0; propertyValue != null && i_propertyValue < propertyValue.Length; i_propertyValue++) { - if (ExpressionHelpers.StartsWith(value, propertyValue[i], caseSensitive)) - return Fail( - context, - operand, - ReasonStrings.Assert_StartsWith, - value, - propertyValue[i] - ); + for (var i_value = 0; i_value < value.Length; i_value++) + { + if (ExpressionHelpers.StartsWith(value[i_value], propertyValue[i_propertyValue], caseSensitive)) + return Fail( + context, + operand, + ReasonStrings.Assert_StartsWith, + value[i_value], + propertyValue[i_propertyValue] + ); + } } } return Pass(); @@ -1023,13 +1030,17 @@ internal static bool EndsWith(ExpressionContext context, ExpressionInfo info, ob GetCaseSensitive(properties, out var caseSensitive)) { context.ExpressionTrace(ENDSWITH, operand.Value, propertyValue); - if (!ExpressionHelpers.TryString(operand.Value, convert, out var value)) + if (!ExpressionHelpers.TryStringOrArray(operand.Value, convert, out var value)) return NotString(context, operand); - for (var i = 0; propertyValue != null && i < propertyValue.Length; i++) - if (ExpressionHelpers.EndsWith(value, propertyValue[i], caseSensitive)) - return Pass(); - + for (var i_propertyValue = 0; propertyValue != null && i_propertyValue < propertyValue.Length; i_propertyValue++) + { + for (var i_value = 0; i_value < value.Length; i_value++) + { + if (ExpressionHelpers.EndsWith(value[i_value], propertyValue[i_propertyValue], caseSensitive)) + return Pass(); + } + } return Fail( context, operand, @@ -1050,18 +1061,21 @@ internal static bool NotEndsWith(ExpressionContext context, ExpressionInfo info, GetCaseSensitive(properties, out var caseSensitive)) { context.ExpressionTrace(NOTENDSWITH, operand.Value, propertyValue); - if (ExpressionHelpers.TryString(operand.Value, convert, out var value)) + if (ExpressionHelpers.TryStringOrArray(operand.Value, convert, out var value)) { - for (var i = 0; propertyValue != null && i < propertyValue.Length; i++) + for (var i_propertyValue = 0; propertyValue != null && i_propertyValue < propertyValue.Length; i_propertyValue++) { - if (ExpressionHelpers.EndsWith(value, propertyValue[i], caseSensitive)) - return Fail( - context, - operand, - ReasonStrings.Assert_EndsWith, - value, - propertyValue[i] - ); + for (var i_value = 0; i_value < value.Length; i_value++) + { + if (ExpressionHelpers.EndsWith(value[i_value], propertyValue[i_propertyValue], caseSensitive)) + return Fail( + context, + operand, + ReasonStrings.Assert_EndsWith, + value[i_value], + propertyValue[i_propertyValue] + ); + } } } return Pass(); @@ -1078,13 +1092,17 @@ internal static bool Contains(ExpressionContext context, ExpressionInfo info, ob GetCaseSensitive(properties, out var caseSensitive)) { context.ExpressionTrace(CONTAINS, operand.Value, propertyValue); - if (!ExpressionHelpers.TryString(operand.Value, convert, out var value)) + if (!ExpressionHelpers.TryStringOrArray(operand.Value, convert, out var value)) return NotString(context, operand); - for (var i = 0; propertyValue != null && i < propertyValue.Length; i++) - if (ExpressionHelpers.Contains(value, propertyValue[i], caseSensitive)) - return Pass(); - + for (var i_propertyValue = 0; propertyValue != null && i_propertyValue < propertyValue.Length; i_propertyValue++) + { + for (var i_value = 0; i_value < value.Length; i_value++) + { + if (ExpressionHelpers.Contains(value[i_value], propertyValue[i_propertyValue], caseSensitive)) + return Pass(); + } + } return Fail( context, operand, @@ -1105,18 +1123,21 @@ internal static bool NotContains(ExpressionContext context, ExpressionInfo info, GetCaseSensitive(properties, out var caseSensitive)) { context.ExpressionTrace(NOTCONTAINS, operand.Value, propertyValue); - if (ExpressionHelpers.TryString(operand.Value, convert, out var value)) + if (ExpressionHelpers.TryStringOrArray(operand.Value, convert, out var value)) { - for (var i = 0; propertyValue != null && i < propertyValue.Length; i++) + for (var i_propertyValue = 0; propertyValue != null && i_propertyValue < propertyValue.Length; i_propertyValue++) { - if (ExpressionHelpers.Contains(value, propertyValue[i], caseSensitive)) - return Fail( - context, - operand, - ReasonStrings.Assert_Contains, - value, - propertyValue[i] - ); + for (var i_value = 0; i_value < value.Length; i_value++) + { + if (ExpressionHelpers.Contains(value[i_value], propertyValue[i_propertyValue], caseSensitive)) + return Fail( + context, + operand, + ReasonStrings.Assert_Contains, + value[i_value], + propertyValue[i_propertyValue] + ); + } } } return Pass(); @@ -1688,8 +1709,7 @@ private static bool TryScope(IExpressionContext context, LanguageExpression.Prop if (svalue != DOT || context?.Context?.LanguageScope == null) return Invalid(context, svalue); - if (!context.Context.LanguageScope.TryGetScope(o, out var scope) || - string.IsNullOrEmpty(scope)) + if (!context.Context.LanguageScope.TryGetScope(o, out var scope)) return Invalid(context, svalue); operand = Operand.FromScope(scope); diff --git a/src/PSRule/Pipeline/TargetObject.cs b/src/PSRule/Pipeline/TargetObject.cs index 1344116990..ec8a3c7672 100644 --- a/src/PSRule/Pipeline/TargetObject.cs +++ b/src/PSRule/Pipeline/TargetObject.cs @@ -62,7 +62,7 @@ internal TargetObject(PSObject o, TargetSourceCollection source) _Annotations = new Dictionary(); } - internal TargetObject(PSObject o, string targetName = null, string targetType = null, string scope = null) + internal TargetObject(PSObject o, string targetName = null, string targetType = null, string[] scope = null) : this(o, null) { if (targetName != null) @@ -71,7 +71,7 @@ internal TargetObject(PSObject o, string targetName = null, string targetType = if (targetType != null) TargetType = targetType; - if (scope != null) + if (scope != null && scope.Length > 0) Scope = scope; } @@ -85,7 +85,7 @@ internal TargetObject(PSObject o, string targetName = null, string targetType = internal string TargetType { get; } - internal string Scope { get; } + internal string[] Scope { get; } internal string Path { get; } diff --git a/src/PSRule/Runtime/Assert.cs b/src/PSRule/Runtime/Assert.cs index aa09023c8a..c041fe79da 100644 --- a/src/PSRule/Runtime/Assert.cs +++ b/src/PSRule/Runtime/Assert.cs @@ -449,17 +449,20 @@ public AssertResult StartsWith(PSObject inputObject, string field, string[] pref GuardNullOrEmptyParam(field, nameof(field), out result) || GuardNullParam(prefix, nameof(prefix), out result) || GuardField(inputObject, field, false, out var fieldValue, out result) || - GuardString(Operand.FromPath(field), fieldValue, out var value, out result)) + GuardStringOrArray(Operand.FromPath(field), fieldValue, out var value, out result)) return result; if (prefix == null || prefix.Length == 0) return Pass(); // Assert - for (var i = 0; i < prefix.Length; i++) + for (var i_prefix = 0; i_prefix < prefix.Length; i_prefix++) { - if (ExpressionHelpers.StartsWith(value, prefix[i], caseSensitive)) - return Pass(); + for (var i_value = 0; i_value < value.Length; i_value++) + { + if (ExpressionHelpers.StartsWith(value[i_value], prefix[i_prefix], caseSensitive)) + return Pass(); + } } return Fail(Operand.FromPath(field), ReasonStrings.StartsWith, field, FormatArray(prefix)); } @@ -483,14 +486,17 @@ public AssertResult NotStartsWith(PSObject inputObject, string field, string[] p GuardField(inputObject, field, false, out var fieldValue, out result)) return result; - if (prefix == null || prefix.Length == 0 || GuardString(Operand.FromPath(field), fieldValue, out var value, out _)) + if (prefix == null || prefix.Length == 0 || GuardStringOrArray(Operand.FromPath(field), fieldValue, out var value, out _)) return Pass(); // Assert - for (var i = 0; i < prefix.Length; i++) + for (var i_prefix = 0; i_prefix < prefix.Length; i_prefix++) { - if (ExpressionHelpers.StartsWith(value, prefix[i], caseSensitive)) - return Fail(Operand.FromPath(field), ReasonStrings.Assert_StartsWith, value, prefix[i]); + for (var i_value = 0; i_value < value.Length; i_value++) + { + if (ExpressionHelpers.StartsWith(value[i_value], prefix[i_prefix], caseSensitive)) + return Fail(Operand.FromPath(field), ReasonStrings.Assert_StartsWith, value, prefix[i_prefix]); + } } return Pass(); } @@ -505,17 +511,20 @@ public AssertResult EndsWith(PSObject inputObject, string field, string[] suffix GuardNullOrEmptyParam(field, nameof(field), out result) || GuardNullParam(suffix, nameof(suffix), out result) || GuardField(inputObject, field, false, out var fieldValue, out result) || - GuardString(Operand.FromPath(field), fieldValue, out var value, out result)) + GuardStringOrArray(Operand.FromPath(field), fieldValue, out var value, out result)) return result; if (suffix == null || suffix.Length == 0) return Pass(); // Assert - for (var i = 0; i < suffix.Length; i++) + for (var i_suffix = 0; i_suffix < suffix.Length; i_suffix++) { - if (ExpressionHelpers.EndsWith(value, suffix[i], caseSensitive)) - return Pass(); + for (var i_value = 0; i_value < value.Length; i_value++) + { + if (ExpressionHelpers.EndsWith(value[i_value], suffix[i_suffix], caseSensitive)) + return Pass(); + } } return Fail(Operand.FromPath(field), ReasonStrings.EndsWith, field, FormatArray(suffix)); } @@ -539,14 +548,17 @@ public AssertResult NotEndsWith(PSObject inputObject, string field, string[] suf GuardField(inputObject, field, false, out var fieldValue, out result)) return result; - if (suffix == null || suffix.Length == 0 || GuardString(Operand.FromPath(field), fieldValue, out var value, out _)) + if (suffix == null || suffix.Length == 0 || GuardStringOrArray(Operand.FromPath(field), fieldValue, out var value, out _)) return Pass(); // Assert - for (var i = 0; i < suffix.Length; i++) + for (var i_suffix = 0; i_suffix < suffix.Length; i_suffix++) { - if (ExpressionHelpers.EndsWith(value, suffix[i], caseSensitive)) - return Fail(Operand.FromPath(field), ReasonStrings.Assert_EndsWith, value, suffix); + for (var i_value = 0; i_value < value.Length; i_value++) + { + if (ExpressionHelpers.EndsWith(value[i_value], suffix[i_suffix], caseSensitive)) + return Fail(Operand.FromPath(field), ReasonStrings.Assert_EndsWith, value, suffix[i_suffix]); + } } return Pass(); } @@ -561,17 +573,20 @@ public AssertResult Contains(PSObject inputObject, string field, string[] text, GuardNullOrEmptyParam(field, nameof(field), out result) || GuardNullParam(text, nameof(text), out result) || GuardField(inputObject, field, false, out var fieldValue, out result) || - GuardString(Operand.FromPath(field), fieldValue, out var value, out result)) + GuardStringOrArray(Operand.FromPath(field), fieldValue, out var value, out result)) return result; if (text == null || text.Length == 0) return Pass(); // Assert - for (var i = 0; i < text.Length; i++) + for (var i_text = 0; i_text < text.Length; i_text++) { - if (ExpressionHelpers.Contains(value, text[i], caseSensitive)) - return Pass(); + for (var i_value = 0; i_value < value.Length; i_value++) + { + if (ExpressionHelpers.Contains(value[i_value], text[i_text], caseSensitive)) + return Pass(); + } } return Fail(Operand.FromPath(field), ReasonStrings.Contains, field, FormatArray(text)); } @@ -595,14 +610,17 @@ public AssertResult NotContains(PSObject inputObject, string field, string[] tex GuardField(inputObject, field, false, out var fieldValue, out result)) return result; - if (text == null || text.Length == 0 || GuardString(Operand.FromPath(field), fieldValue, out var value, out _)) + if (text == null || text.Length == 0 || GuardStringOrArray(Operand.FromPath(field), fieldValue, out var value, out _)) return Pass(); // Assert - for (var i = 0; i < text.Length; i++) + for (var i_text = 0; i_text < text.Length; i_text++) { - if (ExpressionHelpers.Contains(value, text[i], caseSensitive)) - return Fail(Operand.FromPath(field), ReasonStrings.Assert_Contains, value, text); + for (var i_value = 0; i_value < value.Length; i_value++) + { + if (ExpressionHelpers.Contains(value[i_value], text[i_text], caseSensitive)) + return Fail(Operand.FromPath(field), ReasonStrings.Assert_Contains, value, text[i_text]); + } } return Pass(); } @@ -1429,6 +1447,23 @@ private bool GuardString(IOperand operand, object fieldValue, out string value, return true; } + /// + /// Fails if the field value is not a string or an array of strings. + /// + /// Returns true if the field value is not a string or an array of strings. + /// + /// Reason: The field value '{0}' is not a string. + /// + private bool GuardStringOrArray(IOperand operand, object fieldValue, out string[] value, out AssertResult result) + { + result = null; + if (ExpressionHelpers.TryStringOrArray(fieldValue, convert: false, value: out value)) + return false; + + result = Fail(operand, ReasonStrings.Type, TYPENAME_STRING, GetTypeName(fieldValue), fieldValue); + return true; + } + /// /// Fields if the field value is null. /// diff --git a/src/PSRule/Runtime/LanguageScope.cs b/src/PSRule/Runtime/LanguageScope.cs index e751c22827..3560e8c191 100644 --- a/src/PSRule/Runtime/LanguageScope.cs +++ b/src/PSRule/Runtime/LanguageScope.cs @@ -45,7 +45,7 @@ internal interface ILanguageScope : IDisposable bool TryGetName(object o, out string name, out string path); - bool TryGetScope(object o, out string scope); + bool TryGetScope(object o, out string[] scope); } internal sealed class LanguageScope : ILanguageScope @@ -149,7 +149,7 @@ public bool TryGetName(object o, out string name, out string path) return false; } - public bool TryGetScope(object o, out string scope) + public bool TryGetScope(object o, out string[] scope) { if (_Context != null && _Context.TargetObject.Value == o) { diff --git a/src/PSRule/Runtime/Operand.cs b/src/PSRule/Runtime/Operand.cs index 2359cac3ef..911edbfdf6 100644 --- a/src/PSRule/Runtime/Operand.cs +++ b/src/PSRule/Runtime/Operand.cs @@ -132,7 +132,7 @@ internal static IOperand FromValue(object value) return new Operand(OperandKind.Value, null, value); } - internal static IOperand FromScope(string scope) + internal static IOperand FromScope(string[] scope) { return new Operand(OperandKind.Scope, scope); } diff --git a/src/PSRule/Runtime/PSRule.cs b/src/PSRule/Runtime/PSRule.cs index 8547fc017e..2a7c2c6518 100644 --- a/src/PSRule/Runtime/PSRule.cs +++ b/src/PSRule/Runtime/PSRule.cs @@ -207,7 +207,7 @@ public IEnumerable Output /// /// The bound scope of the target object. /// - public string Scope => GetContext().TargetObject.Scope; + public string[] Scope => GetContext().TargetObject.Scope; /// /// Attempts to read content from disk. diff --git a/src/PSRule/Runtime/PSRuleMemberInfo.cs b/src/PSRule/Runtime/PSRuleMemberInfo.cs index 70df2c8b6c..afd5aab203 100644 --- a/src/PSRule/Runtime/PSRuleMemberInfo.cs +++ b/src/PSRule/Runtime/PSRuleMemberInfo.cs @@ -51,7 +51,7 @@ public string File public string TargetType { get; set; } [JsonProperty(PropertyName = "scope")] - public string Scope { get; set; } + public string[] Scope { get; set; } [JsonProperty(PropertyName = "path")] public string Path { get; set; } diff --git a/tests/PSRule.Tests/SelectorTests.cs b/tests/PSRule.Tests/SelectorTests.cs index b7f64822b7..754cf92877 100644 --- a/tests/PSRule.Tests/SelectorTests.cs +++ b/tests/PSRule.Tests/SelectorTests.cs @@ -41,7 +41,7 @@ public void ReadSelector(string type, string path) context.Begin(); var selector = HostHelper.GetSelector(GetSource(path), context).ToArray(); Assert.NotNull(selector); - Assert.Equal(97, selector.Length); + Assert.Equal(99, selector.Length); var actual = selector[0]; var visitor = new SelectorVisitor(context, actual.Id, actual.Source, actual.Spec.If); @@ -767,6 +767,7 @@ public void StartsWithExpression(string type, string path) var actual7 = GetObject((name: "value", value: "EFG")); var actual8 = GetObject((name: "value", value: "abc"), (name: "OtherValue", value: TestEnumValue.All)); var actual9 = GetObject((name: "value", value: "abc"), (name: "OtherValue", value: TestEnumValue.None)); + var actual10 = GetObject((name: "value", value: new string[] { "hij", "abc" })); Assert.True(startsWith.Match(actual1)); Assert.True(startsWith.Match(actual2)); @@ -777,6 +778,7 @@ public void StartsWithExpression(string type, string path) Assert.False(startsWith.Match(actual7)); Assert.True(startsWith.Match(actual8)); Assert.False(startsWith.Match(actual9)); + Assert.True(startsWith.Match(actual10)); // With name var withName = GetSelectorVisitor($"{type}NameStartsWith", GetSource(path), out var context); @@ -809,6 +811,7 @@ public void NotStartsWithExpression(string type, string path) var actual7 = GetObject((name: "value", value: "EFG")); var actual8 = GetObject((name: "value", value: "abc"), (name: "OtherValue", value: TestEnumValue.All)); var actual9 = GetObject((name: "value", value: "hij"), (name: "OtherValue", value: TestEnumValue.None)); + var actual10 = GetObject((name: "value", value: new string[] { "hij", "abc" })); Assert.False(notStartsWith.Match(actual1)); Assert.False(notStartsWith.Match(actual2)); @@ -819,6 +822,7 @@ public void NotStartsWithExpression(string type, string path) Assert.False(notStartsWith.Match(actual7)); Assert.False(notStartsWith.Match(actual8)); Assert.True(notStartsWith.Match(actual9)); + Assert.False(notStartsWith.Match(actual10)); } [Theory] @@ -836,6 +840,7 @@ public void EndsWithExpression(string type, string path) var actual7 = GetObject((name: "value", value: "EFG")); var actual8 = GetObject((name: "value", value: "abc"), (name: "OtherValue", value: TestEnumValue.All)); var actual9 = GetObject((name: "value", value: "abc"), (name: "OtherValue", value: TestEnumValue.None)); + var actual10 = GetObject((name: "value", value: new string[] { "hij", "abc" })); Assert.True(endsWith.Match(actual1)); Assert.True(endsWith.Match(actual2)); @@ -846,6 +851,7 @@ public void EndsWithExpression(string type, string path) Assert.False(endsWith.Match(actual7)); Assert.True(endsWith.Match(actual8)); Assert.False(endsWith.Match(actual9)); + Assert.True(endsWith.Match(actual10)); // With name var withName = GetSelectorVisitor($"{type}NameEndsWith", GetSource(path), out var context); @@ -895,6 +901,7 @@ public void NotEndsWithExpression(string type, string path) var actual7 = GetObject((name: "value", value: "EFG")); var actual8 = GetObject((name: "value", value: "abc"), (name: "OtherValue", value: TestEnumValue.All)); var actual9 = GetObject((name: "value", value: "hij"), (name: "OtherValue", value: TestEnumValue.None)); + var actual10 = GetObject((name: "value", value: new string[] { "hij", "abc" })); Assert.False(notEndsWith.Match(actual1)); Assert.False(notEndsWith.Match(actual2)); @@ -905,6 +912,7 @@ public void NotEndsWithExpression(string type, string path) Assert.False(notEndsWith.Match(actual7)); Assert.False(notEndsWith.Match(actual8)); Assert.True(notEndsWith.Match(actual9)); + Assert.False(notEndsWith.Match(actual10)); } [Theory] @@ -922,6 +930,7 @@ public void ContainsExpression(string type, string path) var actual7 = GetObject((name: "value", value: "BCD")); var actual8 = GetObject((name: "value", value: "abc"), (name: "OtherValue", value: TestEnumValue.All)); var actual9 = GetObject((name: "value", value: "abc"), (name: "OtherValue", value: TestEnumValue.None)); + var actual10 = GetObject((name: "value", value: new string[] { "hij", "abc" })); Assert.True(contains.Match(actual1)); Assert.True(contains.Match(actual2)); @@ -932,6 +941,7 @@ public void ContainsExpression(string type, string path) Assert.False(contains.Match(actual7)); Assert.True(contains.Match(actual8)); Assert.False(contains.Match(actual9)); + Assert.True(contains.Match(actual10)); // With name var withName = GetSelectorVisitor($"{type}NameContains", GetSource(path), out var context); @@ -964,6 +974,7 @@ public void NotContainsExpression(string type, string path) var actual7 = GetObject((name: "value", value: "BCD")); var actual8 = GetObject((name: "value", value: "abc"), (name: "OtherValue", value: TestEnumValue.All)); var actual9 = GetObject((name: "value", value: "hij"), (name: "OtherValue", value: TestEnumValue.None)); + var actual10 = GetObject((name: "value", value: new string[] { "hij", "abc" })); Assert.False(notContains.Match(actual1)); Assert.False(notContains.Match(actual2)); @@ -974,6 +985,7 @@ public void NotContainsExpression(string type, string path) Assert.False(notContains.Match(actual7)); Assert.False(notContains.Match(actual8)); Assert.True(notContains.Match(actual9)); + Assert.False(notContains.Match(actual10)); } [Theory] @@ -1712,27 +1724,41 @@ public void Scope(string type, string path) ); var equals = GetSelectorVisitor($"{type}ScopeEquals", GetSource(path), out var context); - context.EnterTargetObject(new TargetObject(testObject, scope: "/scope1")); + context.EnterTargetObject(new TargetObject(testObject, scope: new string[] { "/scope1" })); Assert.True(equals.Match(testObject)); - context.EnterTargetObject(new TargetObject(testObject, scope: "/scope2")); + context.EnterTargetObject(new TargetObject(testObject, scope: new string[] { "/scope2" })); Assert.False(equals.Match(testObject)); context.EnterTargetObject(new TargetObject(testObject)); Assert.False(equals.Match(testObject)); var startsWith = GetSelectorVisitor($"{type}ScopeStartsWith", GetSource(path), out context); - context.EnterTargetObject(new TargetObject(testObject, scope: "/scope1/")); + context.EnterTargetObject(new TargetObject(testObject, scope: new string[] { "/scope1/" })); Assert.True(startsWith.Match(testObject)); - context.EnterTargetObject(new TargetObject(testObject, scope: "/scope2/")); + context.EnterTargetObject(new TargetObject(testObject, scope: new string[] { "/scope2/" })); Assert.True(startsWith.Match(testObject)); - context.EnterTargetObject(new TargetObject(testObject, scope: "/scope2")); + context.EnterTargetObject(new TargetObject(testObject, scope: new string[] { "/scope2" })); Assert.False(startsWith.Match(testObject)); context.EnterTargetObject(new TargetObject(testObject)); Assert.False(startsWith.Match(testObject)); + + var hasValueFalse = GetSelectorVisitor($"{type}ScopeHasValueFalse", GetSource(path), out context); + context.EnterTargetObject(new TargetObject(testObject, scope: new string[] { "/scope1" })); + Assert.False(hasValueFalse.Match(testObject)); + + context.EnterTargetObject(new TargetObject(testObject)); + Assert.True(hasValueFalse.Match(testObject)); + + var hasValueTrue = GetSelectorVisitor($"{type}ScopeHasValueTrue", GetSource(path), out context); + context.EnterTargetObject(new TargetObject(testObject, scope: new string[] { "/scope1" })); + Assert.True(hasValueTrue.Match(testObject)); + + context.EnterTargetObject(new TargetObject(testObject)); + Assert.False(hasValueTrue.Match(testObject)); } #endregion Properties diff --git a/tests/PSRule.Tests/Selectors.Rule.jsonc b/tests/PSRule.Tests/Selectors.Rule.jsonc index 00e567efeb..87ba19bdae 100644 --- a/tests/PSRule.Tests/Selectors.Rule.jsonc +++ b/tests/PSRule.Tests/Selectors.Rule.jsonc @@ -1781,7 +1781,9 @@ "spec": { "if": { "scope": ".", - "equals": "/scope1" + "equals": [ + "/scope1" + ] } } }, @@ -1802,6 +1804,34 @@ } } }, + { + // Synopsis: Test scope property with hasValue. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonScopeHasValueFalse" + }, + "spec": { + "if": { + "scope": ".", + "hasValue": false + } + } + }, + { + // Synopsis: Test scope property with hasValue. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonScopeHasValueTrue" + }, + "spec": { + "if": { + "scope": ".", + "hasValue": true + } + } + }, { // Synopsis: Test comparison with apiVersion. "apiVersion": "github.com/microsoft/PSRule/v1", diff --git a/tests/PSRule.Tests/Selectors.Rule.yaml b/tests/PSRule.Tests/Selectors.Rule.yaml index c8cadf8009..7b0d9f6b6a 100644 --- a/tests/PSRule.Tests/Selectors.Rule.yaml +++ b/tests/PSRule.Tests/Selectors.Rule.yaml @@ -1268,7 +1268,8 @@ metadata: spec: if: scope: . - equals: /scope1 + equals: + - /scope1 --- # Synopsis: Test scope property with startsWith. @@ -1283,6 +1284,28 @@ spec: - /scope1/ - /scope2/ +--- +# Synopsis: Test scope property with hasValue. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: YamlScopeHasValueFalse +spec: + if: + scope: . + hasValue: false + +--- +# Synopsis: Test scope property with hasValue. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: YamlScopeHasValueTrue +spec: + if: + scope: . + hasValue: true + --- # Synopsis: Test comparison with apiVersion. apiVersion: github.com/microsoft/PSRule/v1 From 51926b069370b768b44265b0cd3cc2845decc46c Mon Sep 17 00:00:00 2001 From: Bernie White Date: Mon, 19 Dec 2022 19:55:51 +1000 Subject: [PATCH 133/156] Added help properties to rule #1386 (#1387) * Added help properties to rule #1386 * Update help --- docs/CHANGELOG-v2.md | 2 + docs/authoring/writing-rule-help.md | 264 +++++++++++++++++- docs/hooks.py | 13 +- schemas/PSRule-language.schema.json | 57 +++- .../Commands/NewRuleDefinitionCommand.cs | 2 +- .../Common/ResourceHelpInfoExtensions.cs | 3 +- src/PSRule/Definitions/IResourceHelpInfo.cs | 3 +- src/PSRule/Definitions/InfoString.cs | 8 + src/PSRule/Definitions/Resource.cs | 15 + src/PSRule/Definitions/Rules/Rule.cs | 11 + src/PSRule/Definitions/SpecFactory.cs | 2 +- src/PSRule/Host/HostHelper.cs | 26 +- src/PSRule/Rules/Rule.cs | 2 + src/PSRule/Rules/RuleBlock.cs | 2 + src/PSRule/Rules/RuleHelpInfo.cs | 25 ++ tests/PSRule.Tests/FromFile.Rule.jsonc | 6 +- tests/PSRule.Tests/FromFile.Rule.yaml | 4 + tests/PSRule.Tests/RulesTests.cs | 8 + 18 files changed, 420 insertions(+), 33 deletions(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 92fcc69003..f9e59f65a6 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -39,6 +39,8 @@ What's changed since pre-release v2.7.0-B0070: [#1383](https://github.com/microsoft/PSRule/issues/1383) - Improve support of string comparisons to support an array of strings by @BernieWhite. [#1384](https://github.com/microsoft/PSRule/issues/1384) + - Added help properties to rules from YAML/ JSON resources by @BernieWhite. + [#1386](https://github.com/microsoft/PSRule/issues/1386) ## v2.7.0-B0070 (pre-release) diff --git a/docs/authoring/writing-rule-help.md b/docs/authoring/writing-rule-help.md index a5e7f9e6e7..1695c39319 100644 --- a/docs/authoring/writing-rule-help.md +++ b/docs/authoring/writing-rule-help.md @@ -9,24 +9,262 @@ This scenario covers the following: - Writing markdown documentation - Localizing documentation files -## Using inline help +## Inline help with YAML and JSON + +With authoring rules in YAML and JSON, PSRule provides the following syntax features: + +- Synopsis resource comment. +- `metadata.displayName` property. +- `metadata.description` property. +- `metadata.link` property. +- `spec.recommend` property. + +### Synopsis resource comment + +Specify the synopsis of the rule with the `Synopsis` comment above the rule properties. + +=== "YAML" + + ```yaml hl_lines="2" + --- + # Synopsis: An example rule to require TLS. + apiVersion: github.com/microsoft/PSRule/v1 + kind: Rule + metadata: + name: 'Local.YAML.RequireTLS' + spec: + condition: + field: 'configure.supportsHttpsTrafficOnly' + equals: true + ``` + +=== "JSON" + + ```json hl_lines="3" + [ + { + // Synopsis: An example rule to require TLS. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "Local.JSON.RequireTLS" + }, + "spec": { + "condition": { + "field": "configure.supportsHttpsTrafficOnly", + "equals": true + } + } + } + ] + ``` + +!!! Note + The resource comment is not localized. + Use [markdown documentation](#writing-markdown-documentation) for a localized synopsis. + +### Display name property + +Specify the display name of the rule with the `metadata.displayName` property. + +=== "YAML" + + ```yaml hl_lines="7" + --- + # Synopsis: An example rule to require TLS. + apiVersion: github.com/microsoft/PSRule/v1 + kind: Rule + metadata: + name: 'Local.YAML.RequireTLS' + displayName: Require TLS + spec: + condition: + field: 'configure.supportsHttpsTrafficOnly' + equals: true + ``` + +=== "JSON" + + ```json hl_lines="8" + [ + { + // Synopsis: An example rule to require TLS. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "Local.JSON.RequireTLS", + "displayName": "Require TLS" + }, + "spec": { + "condition": { + "field": "configure.supportsHttpsTrafficOnly", + "equals": true + } + } + } + ] + ``` + +!!! Note + This property is not localized. + Use [markdown documentation](#writing-markdown-documentation) for a localized display name. + +### Description property + +Specify the description of the rule with the `metadata.description` property. + +=== "YAML" + + ```yaml hl_lines="7" + --- + # Synopsis: An example rule to require TLS. + apiVersion: github.com/microsoft/PSRule/v1 + kind: Rule + metadata: + name: 'Local.YAML.RequireTLS' + description: The resource should only use TLS. + spec: + condition: + field: 'configure.supportsHttpsTrafficOnly' + equals: true + ``` + +=== "JSON" + + ```json hl_lines="8" + [ + { + // Synopsis: An example rule to require TLS. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "Local.JSON.RequireTLS", + "description": "The resource should only use TLS." + }, + "spec": { + "condition": { + "field": "configure.supportsHttpsTrafficOnly", + "equals": true + } + } + } + ] + ``` + +!!! Note + This property is not localized. + Use [markdown documentation](#writing-markdown-documentation) for a localized description. + +### Link property + +Specify the online help URL of the rule with the `metadata.link` property. + +=== "YAML" + + ```yaml hl_lines="7" + --- + # Synopsis: An example rule to require TLS. + apiVersion: github.com/microsoft/PSRule/v1 + kind: Rule + metadata: + name: 'Local.YAML.RequireTLS' + link: https://aka.ms/ps-rule + spec: + condition: + field: 'configure.supportsHttpsTrafficOnly' + equals: true + ``` + +=== "JSON" + + ```json hl_lines="8" + [ + { + // Synopsis: An example rule to require TLS. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "Local.JSON.RequireTLS", + "link": "https://aka.ms/ps-rule" + }, + "spec": { + "condition": { + "field": "configure.supportsHttpsTrafficOnly", + "equals": true + } + } + } + ] + ``` + +!!! Note + This property is not localized. + Use [markdown documentation](#writing-markdown-documentation) for a localized online help URL. + +### Recommend property + +Specify the rule recommendation with the `spec.recommend` property. + +=== "YAML" + + ```yaml hl_lines="8" + --- + # Synopsis: An example rule to require TLS. + apiVersion: github.com/microsoft/PSRule/v1 + kind: Rule + metadata: + name: 'Local.YAML.RequireTLS' + spec: + recommend: The resource should only use TLS. + condition: + field: 'configure.supportsHttpsTrafficOnly' + equals: true + ``` + +=== "JSON" + + ```json hl_lines="10" + [ + { + // Synopsis: An example rule to require TLS. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "Local.JSON.RequireTLS" + }, + "spec": { + "recommend": "", + "condition": { + "field": "configure.supportsHttpsTrafficOnly", + "equals": true + } + } + } + ] + ``` + +!!! Note + This property is not localized. + Use [markdown documentation](#writing-markdown-documentation) for a localized recommendation. + +## Inline help with PowerShell When authoring rules in PowerShell, PSRule provides the following syntax features: -- Comment metadata. +- Synopsis script comment. - `Recommend` keyword. - `Reason` keyword. These features are each describe in detail in the following sections. -### Comment metadata +### Synopsis script comment Comment metadata can be included directly above a rule block by using the syntax `# Synopsis: `. This is only supported for populating a rule synopsis. For example: -```powershell +```powershell title="PowerShell" hl_lines="1" # Synopsis: Must have the app.kubernetes.io/name label Rule 'metadata.Name' -Type 'Deployment', 'Service' { Exists "metadata.labels.'app.kubernetes.io/name'" @@ -43,6 +281,10 @@ The synopsis can not break over multiple lines. The key limitation of _only_ using comment metadata is that it can not be localized for multiple languages. Consider using comment metadata and also using markdown documentation for a multi-language experience. +!!! Note + The script comment is not localized. + Use [markdown documentation](#writing-markdown-documentation) for a localized synopsis. + ### Recommend keyword The `Recommend` keyword sets the recommendation for a rule. @@ -53,7 +295,7 @@ When packaging rules in a module consider using markdown help instead. For example: -```powershell +```powershell title="PowerShell" hl_lines="3" # Synopsis: Must have the app.kubernetes.io/name label Rule 'metadata.Name' -Type 'Deployment', 'Service' { Recommend 'Consider setting the recommended label ''app.kubernetes.io/name'' on deployment and service resources.' @@ -72,7 +314,7 @@ Localized recommendations can set by using the `$LocalizedData`. For example: -```powershell +```powershell title="PowerShell" hl_lines="3" # Synopsis: Must have the app.kubernetes.io/name label Rule 'metadata.Name' -Type 'Deployment', 'Service' { Recommend $LocalizedData.RecommendNameLabel @@ -91,7 +333,7 @@ Use `-OutputFormat Wide` to display reason messages. To set a reason use the `Reason` keyword followed by the reason. For example: -```powershell +```powershell title="PowerShell" hl_lines="6" # Synopsis: Must have the app.kubernetes.io/name label Rule 'metadata.Name' -Type 'Deployment', 'Service' { Recommend $LocalizedData.RecommendNameLabel @@ -106,7 +348,7 @@ Additionally the reason messages can be localized by using the `$LocalizedData` For example: -```powershell +```powershell title="PowerShell" hl_lines="7" # Synopsis: Must have the app.kubernetes.io/name label Rule 'metadata.Name' -Type 'Deployment', 'Service' { Recommend $LocalizedData.RecommendNameLabel @@ -168,6 +410,8 @@ The basic structure of markdown help is as follows: The PSRule [Visual Studio Code extension][extension] includes snippets for writing markdown documentation. + [extension]: https://marketplace.visualstudio.com/items?itemName=bewhite.psrule-vscode-preview + ### Annotations The annotation front matter at the top of the markdown document, is a set of key value pairs. @@ -259,7 +503,7 @@ Any text following the heading is interpreted by PSRule and included in output. The recommendation is displayed when using the `Invoke-PSRule` and `Get-PSRuleHelp` cmdlets. The _recommendation_ is intended to identify corrective actions that can be taken to address any failures. -Avoid using URLs within the recommendations. +Avoid using URLs within the recommendation. Use the _links_ section to include references to external sources. PSRule supports semantic line breaks, and will automatically run together lines into a single paragraph. @@ -384,5 +628,3 @@ Module file structure: - [kubernetes.Rule.ps1](writing-rule-help/kubernetes.Rule.ps1) - An example rule for validating name label. - [metadata.Name](writing-rule-help/en-US/metadata.Name.md) - An example markdown documentation file. - -[extension]: https://marketplace.visualstudio.com/items?itemName=bewhite.psrule-vscode-preview diff --git a/docs/hooks.py b/docs/hooks.py index faed8b41c2..7c47025e09 100644 --- a/docs/hooks.py +++ b/docs/hooks.py @@ -42,12 +42,13 @@ def replace_maml(markdown: str, page: mkdocs.structure.nav.Page, config: mkdocs. markdown = markdown.replace("# PSRule_Variables", "# Variables") # Rules - markdown = markdown.replace("## SYNOPSIS", "") - markdown = markdown.replace("## DESCRIPTION", "## Description") - markdown = markdown.replace("## RECOMMENDATION", "## Recommendation") - markdown = markdown.replace("## NOTES", "## Notes") - markdown = markdown.replace("## EXAMPLES", "## Examples") - markdown = markdown.replace("## LINKS", "## Links") + if page.canonical_url.__contains__("/concepts/PSRule/") or page.canonical_url.__contains__("/keywords/PSRule/"): + markdown = markdown.replace("## SYNOPSIS", "") + markdown = markdown.replace("## DESCRIPTION", "## Description") + markdown = markdown.replace("## RECOMMENDATION", "## Recommendation") + markdown = markdown.replace("## NOTES", "## Notes") + markdown = markdown.replace("## EXAMPLES", "## Examples") + markdown = markdown.replace("## LINKS", "## Links") # Conceptual topics markdown = markdown.replace("## SHORT DESCRIPTION", "") diff --git a/schemas/PSRule-language.schema.json b/schemas/PSRule-language.schema.json index 713fc5672d..ec17640822 100644 --- a/schemas/PSRule-language.schema.json +++ b/schemas/PSRule-language.schema.json @@ -30,8 +30,30 @@ "type": "string", "title": "Name", "description": "The name of the resource. This must be unique.", + "markdownDescription": "The name of the resource. This must be unique.\n\n[See help](https://microsoft.github.io/PSRule/v2/authoring/storing-rules/#naming-rules)", "$ref": "#/definitions/resourceName" }, + "description": { + "type": "string", + "title": "Description", + "description": "A non-localized description of the resource. The description is intended to be a verbose description of the resource. If your resource documentation needs to include background information include it here. For localized documentation define markdown help.", + "markdownDescription": "A non-localized description of the resource.\n\nThe description is intended to be a verbose description of the resource. If your resource documentation needs to include background information include it here.\n\nFor localized documentation define markdown help.\n\n[See help](https://microsoft.github.io/PSRule/v2/authoring/writing-rule-help/)", + "minLength": 1 + }, + "displayName": { + "type": "string", + "title": "Display name", + "description": "A non-localized display name for the resource. For localized documentation define markdown help.", + "markdownDescription": "A non-localized display name for the resource. For localized documentation define markdown help.\n\n[See help](https://microsoft.github.io/PSRule/v2/authoring/writing-rule-help/)", + "minLength": 1 + }, + "link": { + "type": "string", + "title": "Online help", + "description": "A non-localized URL to documentation for the resource. URLs should be prefixed with https://. Relative URLs are not supported.", + "markdownDescription": "A non-localized URL to documentation for the resource. URLs should be prefixed with `https://`. Relative URLs are not supported.\n\n[See help](https://microsoft.github.io/PSRule/v2/authoring/writing-rule-help/)", + "pattern": "^https://" + }, "annotations": { "type": "object", "title": "Annotations", @@ -60,7 +82,8 @@ }, "required": [ "name" - ] + ], + "additionalProperties": false }, "baseline-v1": { "type": "object", @@ -630,6 +653,13 @@ ], "default": "Error" }, + "recommend": { + "type": "string", + "title": "Recommendation", + "description": "A non-localized recommendation for the rule. The recommendation is intended to identify corrective actions that can be taken to address any failures. Avoid using URLs within the recommendation. For localized documentation define markdown help.", + "markdownDescription": "A non-localized recommendation for the rule.\n\nThe recommendation is intended to identify corrective actions that can be taken to address any failures. Avoid using URLs within the recommendation.\n\nFor localized documentation define markdown help.\n\n[See help](https://microsoft.github.io/PSRule/v2/authoring/writing-rule-help/)", + "minLength": 1 + }, "type": { "type": "array", "title": "Type pre-condition", @@ -679,8 +709,30 @@ "type": "string", "title": "Name", "description": "The name of the resource. This must be unique.", + "markdownDescription": "The name of the resource. This must be unique.\n\n[See help](https://microsoft.github.io/PSRule/v2/authoring/storing-rules/#naming-rules)", "$ref": "#/definitions/resourceName" }, + "description": { + "type": "string", + "title": "Description", + "description": "A non-localized description of the resource. The description is intended to be a verbose description of the resource. If your resource documentation needs to include background information include it here. For localized documentation define markdown help.", + "markdownDescription": "A non-localized description of the resource.\n\nThe description is intended to be a verbose description of the resource. If your resource documentation needs to include background information include it here.\n\nFor localized documentation define markdown help.\n\n[See help](https://microsoft.github.io/PSRule/v2/authoring/writing-rule-help/)", + "minLength": 1 + }, + "displayName": { + "type": "string", + "title": "Display name", + "description": "A non-localized display name for the resource. For localized documentation define markdown help.", + "markdownDescription": "A non-localized display name for the resource. For localized documentation define markdown help.\n\n[See help](https://microsoft.github.io/PSRule/v2/authoring/writing-rule-help/)", + "minLength": 1 + }, + "link": { + "type": "string", + "title": "Online help", + "description": "A non-localized URL to documentation for the resource. URLs should be prefixed with https://. Relative URLs are not supported.", + "markdownDescription": "A non-localized URL to documentation for the resource. URLs should be prefixed with `https://`. Relative URLs are not supported.\n\n[See help](https://microsoft.github.io/PSRule/v2/authoring/writing-rule-help/)", + "pattern": "^https://" + }, "ref": { "title": "Reference", "description": "An optional stable opaque identifier of this resource for lookup. This must be unique if set.", @@ -752,7 +804,8 @@ }, "required": [ "name" - ] + ], + "additionalProperties": false }, "selectorExpressionValueMultiString": { "oneOf": [ diff --git a/src/PSRule/Commands/NewRuleDefinitionCommand.cs b/src/PSRule/Commands/NewRuleDefinitionCommand.cs index 616878c12c..671bf31191 100644 --- a/src/PSRule/Commands/NewRuleDefinitionCommand.cs +++ b/src/PSRule/Commands/NewRuleDefinitionCommand.cs @@ -127,7 +127,7 @@ protected override void ProcessRecord() CheckDependsOn(); var ps = GetCondition(context, id, source, errorPreference); - var info = PSRule.Host.HostHelper.GetRuleHelpInfo(context, Name, metadata.Synopsis) ?? new RuleHelpInfo( + var info = PSRule.Host.HostHelper.GetRuleHelpInfo(context, Name, metadata.Synopsis, null, null, null) ?? new RuleHelpInfo( name: Name, displayName: Name, moduleName: source.Module diff --git a/src/PSRule/Common/ResourceHelpInfoExtensions.cs b/src/PSRule/Common/ResourceHelpInfoExtensions.cs index 8d861f0527..b24ebdc65f 100644 --- a/src/PSRule/Common/ResourceHelpInfoExtensions.cs +++ b/src/PSRule/Common/ResourceHelpInfoExtensions.cs @@ -13,8 +13,7 @@ internal static void Update(this IResourceHelpInfo info, IResourceHelpInfo other return; info.Synopsis.Update(other.Synopsis); - if (other.Description != null && other.Description.HasValue) - info.Description.Text = other.Description.Text; + info.Description.Update(other.Description); } } } diff --git a/src/PSRule/Definitions/IResourceHelpInfo.cs b/src/PSRule/Definitions/IResourceHelpInfo.cs index 3a47686cb7..bfd6f662c2 100644 --- a/src/PSRule/Definitions/IResourceHelpInfo.cs +++ b/src/PSRule/Definitions/IResourceHelpInfo.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using Newtonsoft.Json; namespace PSRule.Definitions @@ -54,7 +55,7 @@ internal ResourceHelpInfo(string name, string displayName, InfoString synopsis, public InfoString Synopsis { get; } /// - [JsonProperty(PropertyName = "synopsis")] + [JsonProperty(PropertyName = "description")] public InfoString Description { get; } } } diff --git a/src/PSRule/Definitions/InfoString.cs b/src/PSRule/Definitions/InfoString.cs index 02d23ea36e..98f2016bbf 100644 --- a/src/PSRule/Definitions/InfoString.cs +++ b/src/PSRule/Definitions/InfoString.cs @@ -55,5 +55,13 @@ public string Markdown _Markdown = value; } } + + /// + /// Create an info string when not null or empty. + /// + internal static InfoString Create(string text, string markdown = null) + { + return string.IsNullOrEmpty(text) && string.IsNullOrEmpty(markdown) ? null : new InfoString(text, markdown); + } } } diff --git a/src/PSRule/Definitions/Resource.cs b/src/PSRule/Definitions/Resource.cs index b591033df5..6da6ed1953 100644 --- a/src/PSRule/Definitions/Resource.cs +++ b/src/PSRule/Definitions/Resource.cs @@ -413,6 +413,16 @@ public ResourceMetadata() /// public string Name { get; set; } + /// + /// A non-localized display name for the resource. + /// + public string DisplayName { get; set; } + + /// + /// A non-localized description of the resource. + /// + public string Description { get; set; } + /// /// A opaque reference for the resource. /// @@ -440,6 +450,11 @@ public ResourceMetadata() /// [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitEmptyCollections)] public ResourceLabels Labels { get; set; } + + /// + /// A URL to documentation for the resource. + /// + public string Link { get; set; } } /// diff --git a/src/PSRule/Definitions/Rules/Rule.cs b/src/PSRule/Definitions/Rules/Rule.cs index 2862a5e664..958cfcc0b2 100644 --- a/src/PSRule/Definitions/Rules/Rule.cs +++ b/src/PSRule/Definitions/Rules/Rule.cs @@ -52,6 +52,11 @@ public interface IRuleV1 : IResource, IDependencyTarget /// SeverityLevel Level { get; } + /// + /// A recommendation for the rule. + /// + InfoString Recommendation { get; } + /// /// A short description of the rule. /// @@ -167,6 +172,9 @@ public RuleV1(string apiVersion, SourceFile source, ResourceMetadata metadata, I /// string IRuleV1.Description => Info.Synopsis.Text; + + /// + InfoString IRuleV1.Recommendation => InfoString.Create(Spec?.Recommend); } /// @@ -180,6 +188,9 @@ internal sealed class RuleV1Spec : Spec, IRuleSpec /// public SeverityLevel? Level { get; set; } + /// + public string Recommend { get; set; } + /// public string[] Type { get; set; } diff --git a/src/PSRule/Definitions/SpecFactory.cs b/src/PSRule/Definitions/SpecFactory.cs index 9b3148e273..d042b1e100 100644 --- a/src/PSRule/Definitions/SpecFactory.cs +++ b/src/PSRule/Definitions/SpecFactory.cs @@ -75,7 +75,7 @@ public SpecDescriptor(string apiVersion, string name) public IResource CreateInstance(SourceFile source, ResourceMetadata metadata, CommentMetadata comment, ISourceExtent extent, object spec) { - var info = new ResourceHelpInfo(metadata.Name, metadata.Name, new InfoString(comment?.Synopsis), new InfoString()); + var info = new ResourceHelpInfo(metadata.Name, metadata.DisplayName, new InfoString(comment?.Synopsis), InfoString.Create(metadata.Description)); return (IResource)Activator.CreateInstance(typeof(T), ApiVersion, source, metadata, info, extent, spec); } } diff --git a/src/PSRule/Host/HostHelper.cs b/src/PSRule/Host/HostHelper.cs index a0aaa5f416..4bfb74966a 100644 --- a/src/PSRule/Host/HostHelper.cs +++ b/src/PSRule/Host/HostHelper.cs @@ -5,6 +5,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Data; using System.IO; using System.Linq; using System.Management.Automation; @@ -466,7 +467,7 @@ private static DependencyTargetCollection ToRuleV1(ILanguageBlock[] blo context.WarnDuplicateRuleName(duplicateName); context.EnterSourceScope(block.Source); - var info = GetRuleHelpInfo(context, block.Name, block.Synopsis); + var info = GetRuleHelpInfo(context, block); results.TryAdd(new Rule { Id = block.Id, @@ -538,7 +539,7 @@ private static DependencyTargetCollection ToRuleBlockV1(ILanguageBloc } context.EnterSourceScope(block.Source); - var info = GetRuleHelpInfo(context, ruleName, block.Synopsis) ?? new RuleHelpInfo( + var info = GetRuleHelpInfo(context, block) ?? new RuleHelpInfo( ruleName, ruleName, block.Source.Module, @@ -584,6 +585,8 @@ private static void MergeAnnotations(RuleHelpInfo info, ResourceMetadata metadat 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) @@ -804,22 +807,24 @@ private static IConvention[] Sort(RunspaceContext context, IConvention[] convent return conventions; } - internal static RuleHelpInfo GetRuleHelpInfo(RunspaceContext context, string name, string defaultSynopsis) + internal static RuleHelpInfo GetRuleHelpInfo(RunspaceContext context, string name, string defaultSynopsis, string defaultDisplayName, InfoString defaultDescription, InfoString defaultRecommendation) { return !TryHelpPath(context, name, out var path, out var culture) || !TryDocument(path, culture, out var document) ? new RuleHelpInfo( name: name, - displayName: name, + displayName: defaultDisplayName ?? name, moduleName: context.Source.File.Module, - synopsis: new InfoString(defaultSynopsis) + synopsis: InfoString.Create(defaultSynopsis), + description: defaultDescription, + recommendation: defaultRecommendation ) : new RuleHelpInfo( name: name, - displayName: document.Name ?? name, + displayName: document.Name ?? defaultDisplayName ?? name, moduleName: context.Source.File.Module, synopsis: document.Synopsis ?? new InfoString(defaultSynopsis), - description: document.Description, - recommendation: document.Recommendation ?? document.Synopsis ?? new InfoString(defaultSynopsis) + description: document.Description ?? defaultDescription, + recommendation: document.Recommendation ?? defaultRecommendation ?? document.Synopsis ?? InfoString.Create(defaultSynopsis) ) { Notes = document.Notes?.Text, @@ -828,6 +833,11 @@ 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(RunspaceContext context, IResource resource) { if (context == null || resource == null || !TryHelpPath(context, resource.Name, out var path, out var culture) || !TryHelpInfo(path, culture, out var info)) diff --git a/src/PSRule/Rules/Rule.cs b/src/PSRule/Rules/Rule.cs index 76a4395752..43043a7393 100644 --- a/src/PSRule/Rules/Rule.cs +++ b/src/PSRule/Rules/Rule.cs @@ -138,6 +138,8 @@ public sealed class Rule : IDependencyTarget, ITargetInfo, IResource, IRuleV1 [Obsolete("Use Source property instead.")] string ILanguageBlock.Module => Source.Module; + InfoString IRuleV1.Recommendation => ((IRuleHelpInfoV2)Info)?.Recommendation; + /// [JsonIgnore, YamlIgnore] public ResourceId? Ref { get; set; } diff --git a/src/PSRule/Rules/RuleBlock.cs b/src/PSRule/Rules/RuleBlock.cs index a82d3b3423..0b575cfdca 100644 --- a/src/PSRule/Rules/RuleBlock.cs +++ b/src/PSRule/Rules/RuleBlock.cs @@ -134,6 +134,8 @@ internal RuleBlock(SourceFile source, ResourceId id, ResourceId? @ref, SeverityL string IRuleV1.Description => Info.Synopsis; + InfoString IRuleV1.Recommendation => ((IRuleHelpInfoV2)Info)?.Recommendation; + #region IDisposable public void Dispose() diff --git a/src/PSRule/Rules/RuleHelpInfo.cs b/src/PSRule/Rules/RuleHelpInfo.cs index ebf8b1826f..a1794da360 100644 --- a/src/PSRule/Rules/RuleHelpInfo.cs +++ b/src/PSRule/Rules/RuleHelpInfo.cs @@ -66,6 +66,28 @@ public static string GetOnlineHelpUrl(this IRuleHelpInfoV2 info) !info.Annotations.ContainsKey(ONLINE_HELP_LINK_ANNOTATION) ? null : info.Annotations[ONLINE_HELP_LINK_ANNOTATION].ToString(); } + + /// + /// Determines if the online help link is set. + /// + /// Returns true when the online help link is set. Otherwise this method returns false. + internal static bool HasOnlineHelp(this IRuleHelpInfoV2 info) + { + return info != null && + info.Annotations != null && + info.Annotations.ContainsKey(ONLINE_HELP_LINK_ANNOTATION); + } + + /// + /// Set the online help link from the parameter. + /// + /// The info object. + /// A URL to the online help location. + internal static void SetOnlineHelpUrl(this IRuleHelpInfoV2 info, string url) + { + if (info == null || info.Annotations == null) return; + info.Annotations[ONLINE_HELP_LINK_ANNOTATION] = url; + } } /// @@ -166,12 +188,15 @@ internal RuleHelpInfo(string name, string displayName, string moduleName, InfoSt [JsonProperty(PropertyName = "annotations")] public Hashtable Annotations { get; internal set; } + /// [JsonIgnore, YamlIgnore] InfoString IRuleHelpInfoV2.Recommendation => _Recommendation; + /// [JsonIgnore, YamlIgnore] InfoString IResourceHelpInfo.Synopsis => _Synopsis; + /// [JsonIgnore, YamlIgnore] InfoString IResourceHelpInfo.Description => _Description; diff --git a/tests/PSRule.Tests/FromFile.Rule.jsonc b/tests/PSRule.Tests/FromFile.Rule.jsonc index 12c6c79df6..1bedde4461 100644 --- a/tests/PSRule.Tests/FromFile.Rule.jsonc +++ b/tests/PSRule.Tests/FromFile.Rule.jsonc @@ -23,9 +23,13 @@ }, "annotations": { "test_value": "test123" - } + }, + "displayName": "Basic JSON rule", + "description": "This is a description of a basic rule.", + "link": "https://aka.ms/ps-rule" }, "spec": { + "recommend": "A JSON rule recommendation for testing.", "condition": { "allOf": [ { diff --git a/tests/PSRule.Tests/FromFile.Rule.yaml b/tests/PSRule.Tests/FromFile.Rule.yaml index 6eafde1c8c..f2e068797b 100644 --- a/tests/PSRule.Tests/FromFile.Rule.yaml +++ b/tests/PSRule.Tests/FromFile.Rule.yaml @@ -21,7 +21,11 @@ metadata: - Value2 annotations: test_value: test123 + displayName: Basic YAML rule + description: This is a description of a basic rule. + link: https://aka.ms/ps-rule spec: + recommend: A YAML rule recommendation for testing. condition: allOf: - field: 'Name' diff --git a/tests/PSRule.Tests/RulesTests.cs b/tests/PSRule.Tests/RulesTests.cs index a55cca5586..8800080143 100644 --- a/tests/PSRule.Tests/RulesTests.cs +++ b/tests/PSRule.Tests/RulesTests.cs @@ -50,6 +50,10 @@ public void ReadYamlRule() 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()); } /// @@ -229,6 +233,10 @@ public void ReadJsonRule() 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()); } /// From 806bde76190a2bd6f16fe10098363d75552605b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Dec 2022 20:37:07 +1000 Subject: [PATCH 134/156] Bump Newtonsoft.Json from 13.0.1 to 13.0.2 (#1358) * Bump Newtonsoft.Json from 13.0.1 to 13.0.2 Bumps [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json) from 13.0.1 to 13.0.2. - [Release notes](https://github.com/JamesNK/Newtonsoft.Json/releases) - [Commits](https://github.com/JamesNK/Newtonsoft.Json/compare/13.0.1...13.0.2) --- updated-dependencies: - dependency-name: Newtonsoft.Json dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Bump change log Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bernie White --- docs/CHANGELOG-v2.md | 3 +++ src/PSRule.Badges/PSRule.Badges.csproj | 2 +- src/PSRule.BuildTool/PSRule.BuildTool.csproj | 2 +- src/PSRule.Types/PSRule.Types.csproj | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index f9e59f65a6..e5acf8e404 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -41,6 +41,9 @@ What's changed since pre-release v2.7.0-B0070: [#1384](https://github.com/microsoft/PSRule/issues/1384) - Added help properties to rules from YAML/ JSON resources by @BernieWhite. [#1386](https://github.com/microsoft/PSRule/issues/1386) +- Engineering: + - Bump Newtonsoft.Json to v13.0.2. + [#1358](https://github.com/microsoft/PSRule/pull/1358) ## v2.7.0-B0070 (pre-release) diff --git a/src/PSRule.Badges/PSRule.Badges.csproj b/src/PSRule.Badges/PSRule.Badges.csproj index 01899a3811..002959b250 100644 --- a/src/PSRule.Badges/PSRule.Badges.csproj +++ b/src/PSRule.Badges/PSRule.Badges.csproj @@ -18,7 +18,7 @@ - + diff --git a/src/PSRule.BuildTool/PSRule.BuildTool.csproj b/src/PSRule.BuildTool/PSRule.BuildTool.csproj index 4cbb9ed573..05165f421f 100644 --- a/src/PSRule.BuildTool/PSRule.BuildTool.csproj +++ b/src/PSRule.BuildTool/PSRule.BuildTool.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/PSRule.Types/PSRule.Types.csproj b/src/PSRule.Types/PSRule.Types.csproj index 2ee366f9e5..3b04181d65 100644 --- a/src/PSRule.Types/PSRule.Types.csproj +++ b/src/PSRule.Types/PSRule.Types.csproj @@ -14,7 +14,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + From 29a4206425e1964b2dbd4622c43bd307299e8e52 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Dec 2022 20:57:30 +1000 Subject: [PATCH 135/156] Bump System.Drawing.Common from 6.0.0 to 7.0.0 (#1332) * Bump System.Drawing.Common from 6.0.0 to 7.0.0 Bumps [System.Drawing.Common](https://github.com/dotnet/runtime) from 6.0.0 to 7.0.0. - [Release notes](https://github.com/dotnet/runtime/releases) - [Commits](https://github.com/dotnet/runtime/commits) --- updated-dependencies: - dependency-name: System.Drawing.Common dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * Bump change log Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bernie White --- docs/CHANGELOG-v2.md | 2 ++ src/PSRule.BuildTool/PSRule.BuildTool.csproj | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index e5acf8e404..faff67e7e5 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -44,6 +44,8 @@ What's changed since pre-release v2.7.0-B0070: - Engineering: - Bump Newtonsoft.Json to v13.0.2. [#1358](https://github.com/microsoft/PSRule/pull/1358) + - Bump System.Drawing.Common to v7.0.0. + [#1332](https://github.com/microsoft/PSRule/pull/1332) ## v2.7.0-B0070 (pre-release) diff --git a/src/PSRule.BuildTool/PSRule.BuildTool.csproj b/src/PSRule.BuildTool/PSRule.BuildTool.csproj index 05165f421f..daf31d3337 100644 --- a/src/PSRule.BuildTool/PSRule.BuildTool.csproj +++ b/src/PSRule.BuildTool/PSRule.BuildTool.csproj @@ -11,7 +11,7 @@ - + From 34f4330d06f9c6f834ed172f159a86533150f1b7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Dec 2022 21:19:50 +1000 Subject: [PATCH 136/156] Bump Microsoft.NET.Test.Sdk from 17.4.0 to 17.4.1 (#1389) * Bump Microsoft.NET.Test.Sdk from 17.4.0 to 17.4.1 Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 17.4.0 to 17.4.1. - [Release notes](https://github.com/microsoft/vstest/releases) - [Changelog](https://github.com/microsoft/vstest/blob/main/docs/releases.md) - [Commits](https://github.com/microsoft/vstest/compare/v17.4.0...v17.4.1) --- updated-dependencies: - dependency-name: Microsoft.NET.Test.Sdk dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Bump change log Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bernie White --- docs/CHANGELOG-v2.md | 2 ++ tests/PSRule.Tests/PSRule.Tests.csproj | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index faff67e7e5..0b9c3219e8 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -46,6 +46,8 @@ What's changed since pre-release v2.7.0-B0070: [#1358](https://github.com/microsoft/PSRule/pull/1358) - Bump System.Drawing.Common to v7.0.0. [#1332](https://github.com/microsoft/PSRule/pull/1332) + - Bump Microsoft.NET.Test.Sdk to v17.4.1. + [#1389](https://github.com/microsoft/PSRule/pull/1389) ## v2.7.0-B0070 (pre-release) diff --git a/tests/PSRule.Tests/PSRule.Tests.csproj b/tests/PSRule.Tests/PSRule.Tests.csproj index 3af12608eb..f80a90b275 100644 --- a/tests/PSRule.Tests/PSRule.Tests.csproj +++ b/tests/PSRule.Tests/PSRule.Tests.csproj @@ -12,7 +12,7 @@ - + From 55aceeda9664a379e4fe94a334b594adb4654882 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Mon, 19 Dec 2022 21:34:00 +1000 Subject: [PATCH 137/156] Pre-release v2.7.0-B0097 (#1390) --- docs/CHANGELOG-v2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 0b9c3219e8..4c875d6350 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.7.0-B0097 (pre-release) + What's changed since pre-release v2.7.0-B0070: - General improvements: From 350695e2e30722449e5076d326c811eee1b02f86 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Wed, 21 Dec 2022 00:30:51 +1000 Subject: [PATCH 138/156] Added fixes for link and sub-selectors #1393 #1394 (#1395) --- docs/CHANGELOG-v2.md | 8 ++++++++ .../Expressions/ExpressionContext.cs | 4 ++-- .../Expressions/LanguageExpressions.cs | 17 +++++++++++++---- src/PSRule/Rules/RuleHelpInfo.cs | 2 +- src/PSRule/Runtime/RunspaceContext.cs | 6 +++--- 5 files changed, 27 insertions(+), 10 deletions(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 4c875d6350..f3c361d7d7 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,14 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since pre-release v2.7.0-B0097: + +- Bug fixes: + - Fixed null reference for link property by @BernieWhite. + [#1393](https://github.com/microsoft/PSRule/issues/1393) + - Fixed reason are emitted for pre-condition sub-selectors by @BernieWhite. + [#1394](https://github.com/microsoft/PSRule/issues/1394) + ## v2.7.0-B0097 (pre-release) What's changed since pre-release v2.7.0-B0070: diff --git a/src/PSRule/Definitions/Expressions/ExpressionContext.cs b/src/PSRule/Definitions/Expressions/ExpressionContext.cs index d0687e6067..624c7c6bea 100644 --- a/src/PSRule/Definitions/Expressions/ExpressionContext.cs +++ b/src/PSRule/Definitions/Expressions/ExpressionContext.cs @@ -80,7 +80,7 @@ internal void PopScope(RunspaceScope scope) public void Reason(IOperand operand, string text, params object[] args) { - if (string.IsNullOrEmpty(text)) + if (string.IsNullOrEmpty(text) || !RunspaceContext.CurrentThread.IsScope(RunspaceScope.Rule)) return; _Reason ??= new List(); @@ -89,7 +89,7 @@ public void Reason(IOperand operand, string text, params object[] args) public void Reason(string text, params object[] args) { - if (string.IsNullOrEmpty(text)) + if (string.IsNullOrEmpty(text) || !RunspaceContext.CurrentThread.IsScope(RunspaceScope.Rule)) return; _Reason ??= new List(); diff --git a/src/PSRule/Definitions/Expressions/LanguageExpressions.cs b/src/PSRule/Definitions/Expressions/LanguageExpressions.cs index 55f206c995..c4c3a37c76 100644 --- a/src/PSRule/Definitions/Expressions/LanguageExpressions.cs +++ b/src/PSRule/Definitions/Expressions/LanguageExpressions.cs @@ -205,11 +205,20 @@ private static LanguageExpressionOuterFn PreconditionSubselector(LanguageExpress { return (context, o) => { - // Evalute sub-selector pre-condition - if (!AcceptsSubselector(context, subselector, o)) + try { - context.Debug(PSRuleResources.DebugTargetSubselectorMismatch); - return null; + context.PushScope(RunspaceScope.Precondition); + + // Evalute sub-selector pre-condition + if (!AcceptsSubselector(context, subselector, o)) + { + context.Debug(PSRuleResources.DebugTargetSubselectorMismatch); + return null; + } + } + finally + { + context.PopScope(RunspaceScope.Precondition); } return fn(context, o); }; diff --git a/src/PSRule/Rules/RuleHelpInfo.cs b/src/PSRule/Rules/RuleHelpInfo.cs index a1794da360..d238b35bf2 100644 --- a/src/PSRule/Rules/RuleHelpInfo.cs +++ b/src/PSRule/Rules/RuleHelpInfo.cs @@ -85,7 +85,7 @@ internal static bool HasOnlineHelp(this IRuleHelpInfoV2 info) /// A URL to the online help location. internal static void SetOnlineHelpUrl(this IRuleHelpInfoV2 info, string url) { - if (info == null || info.Annotations == null) return; + if (info == null || info.Annotations == null || string.IsNullOrEmpty(url)) return; info.Annotations[ONLINE_HELP_LINK_ANNOTATION] = url; } } diff --git a/src/PSRule/Runtime/RunspaceContext.cs b/src/PSRule/Runtime/RunspaceContext.cs index 0b248e14ea..7ed138e5a6 100644 --- a/src/PSRule/Runtime/RunspaceContext.cs +++ b/src/PSRule/Runtime/RunspaceContext.cs @@ -576,12 +576,12 @@ internal SourceScope EnterSourceScope(SourceFile source) { // TODO: Look at scope caching, and a scope stack. - if (!source.Exists()) - throw new FileNotFoundException(PSRuleResources.ScriptNotFound, source.Path); - if (Source != null && Source.File == source) return Source; + if (!source.Exists()) + throw new FileNotFoundException(PSRuleResources.ScriptNotFound, source.Path); + _LanguageScopes.UseScope(source.Module); // Change scope From 73fda60a54a7dbd143d548c15a9417a23f461bd4 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Wed, 21 Dec 2022 01:59:06 +1000 Subject: [PATCH 139/156] Pre-release v2.7.0-B0126 (#1398) --- docs/CHANGELOG-v2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index f3c361d7d7..0a68cb2694 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.7.0-B0126 (pre-release) + What's changed since pre-release v2.7.0-B0097: - Bug fixes: From 7c852064cc00f0b18b0b35bbfe3661162718dc79 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Wed, 4 Jan 2023 01:33:47 +1000 Subject: [PATCH 140/156] Release v2.7.0 (#1401) --- docs/CHANGELOG-v2.md | 60 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 0a68cb2694..fe982e56c4 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,6 +30,66 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +## v2.7.0 + +What's changed since pre-release v2.6.0: + +- New features: + - Added API version date comparison assertion method and expression by @BernieWhite. + [#1356](https://github.com/microsoft/PSRule/issues/1356) + - Added support for new functions by @BernieWhite. + [#1227](https://github.com/microsoft/PSRule/issues/1227) + - Added support for `trim`, `replace`, `split`, `first`, and `last`. +- General improvements: + - Added support target scope by @BernieWhite. + [#1350](https://github.com/microsoft/PSRule/issues/1350) + - Added support for `hasValue` expression with `scope` by @BernieWhite. + [#1382](https://github.com/microsoft/PSRule/issues/1382) + - Return target object scope as an array by @BernieWhite. + [#1383](https://github.com/microsoft/PSRule/issues/1383) + - Improve support of string comparisons to support an array of strings by @BernieWhite. + [#1384](https://github.com/microsoft/PSRule/issues/1384) + - Added help properties to rules from YAML/ JSON resources by @BernieWhite. + [#1386](https://github.com/microsoft/PSRule/issues/1386) +- Engineering: + - Bump Newtonsoft.Json to v13.0.2. + [#1358](https://github.com/microsoft/PSRule/pull/1358) + - Bump System.Drawing.Common to v7.0.0. + [#1332](https://github.com/microsoft/PSRule/pull/1332) + - Bump Microsoft.NET.Test.Sdk to v17.4.1. + [#1389](https://github.com/microsoft/PSRule/pull/1389) +- Bug fixes: + - Fixed exception with comments in JSON baselines by @BernieWhite. + [#1336](https://github.com/microsoft/PSRule/issues/1336) + - Fixed handling of constrained language mode with PowerShell 7.3 by @BernieWhite. + [#1348](https://github.com/microsoft/PSRule/issues/1348) + - Fixed exception calling `RuleSource` value cannot be null by @BernieWhite. + [#1343](https://github.com/microsoft/PSRule/issues/1343) + - Fixed null reference for link property by @BernieWhite. + [#1393](https://github.com/microsoft/PSRule/issues/1393) + - Fixed reason are emitted for pre-condition sub-selectors by @BernieWhite. + [#1394](https://github.com/microsoft/PSRule/issues/1394) + - Fixed CLI failed to load required assemblies by @BernieWhite. + [#1361](https://github.com/microsoft/PSRule/issues/1361) + - Fixed CLI ignores modules specified in `Include.Modules` by @BernieWhite. + [#1362](https://github.com/microsoft/PSRule/issues/1362) + - Fixed job summary directory creation by @BernieWhite. + [#1353](https://github.com/microsoft/PSRule/issues/1353) + - Fixed same key for ref and name by @BernieWhite + [#1354](https://github.com/microsoft/PSRule/issues/1354) + - Fixed object path fails to iterate JSON object with wildcard selector by @BernieWhite. + [#1376](https://github.com/microsoft/PSRule/issues/1376) + - Fixed rule annotations are not included from YAML/ JSON definition by @BernieWhite. + [#1378](https://github.com/microsoft/PSRule/issues/1378) + - Fixed loop stuck parsing JSON `allOf` `not` rule condition by @BernieWhite. + [#1370](https://github.com/microsoft/PSRule/issues/1370) + - Fixed handling of uint64 with `LessOrEqual` assertion method by @BernieWhite. + [#1366](https://github.com/microsoft/PSRule/issues/1366) + +What's changed since pre-release v2.7.0-B0126: + +- No additional changes. + ## v2.7.0-B0126 (pre-release) What's changed since pre-release v2.7.0-B0097: From 2c0f274af5da96cf952547ce17b40d36caee8885 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Jan 2023 12:41:31 +1000 Subject: [PATCH 141/156] Bump mkdocs-material from 8.5.11 to 9.0.2 (#1408) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.5.11 to 9.0.2. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Upgrade guide](https://github.com/squidfunk/mkdocs-material/blob/master/docs/upgrade.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.5.11...9.0.2) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index a9d34391bc..4fb8d14247 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.4.2 -mkdocs-material==8.5.11 +mkdocs-material==9.0.2 pymdown-extensions==9.9 mike==1.1.2 mkdocs-simple-hooks==0.1.5 From b8879c1c540ef47adf0d3c204b85dae6f3f14a83 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Jan 2023 13:17:32 +1000 Subject: [PATCH 142/156] Bump actions/stale from 6 to 7 (#1400) Bumps [actions/stale](https://github.com/actions/stale) from 6 to 7. - [Release notes](https://github.com/actions/stale/releases) - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/stale/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/stale dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/stale.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index fa5e64c685..0cbfc63026 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -22,7 +22,7 @@ jobs: issues: write steps: - - uses: actions/stale@v6 + - uses: actions/stale@v7 with: stale-issue-message: > This issue has been automatically marked as stale because it has not had From 72e0fcd6025ec9c22ee4fd695c03b931590d7f00 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Jan 2023 00:29:11 +1000 Subject: [PATCH 143/156] Bump mkdocs-material from 9.0.2 to 9.0.3 (#1409) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.0.2 to 9.0.3. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.0.2...9.0.3) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 4fb8d14247..2fead0553c 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.4.2 -mkdocs-material==9.0.2 +mkdocs-material==9.0.3 pymdown-extensions==9.9 mike==1.1.2 mkdocs-simple-hooks==0.1.5 From 0f68d0ac1bfd2beebf065c56ba31ecb9984a9edb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 14 Jan 2023 16:01:01 +1000 Subject: [PATCH 144/156] Bump mkdocs-material from 9.0.3 to 9.0.4 (#1410) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.0.3 to 9.0.4. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.0.3...9.0.4) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 2fead0553c..dad18781ca 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.4.2 -mkdocs-material==9.0.3 +mkdocs-material==9.0.4 pymdown-extensions==9.9 mike==1.1.2 mkdocs-simple-hooks==0.1.5 From fee6f70567bedbd23bc1b17622b24773bb5d486b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 14 Jan 2023 16:23:56 +1000 Subject: [PATCH 145/156] Bump pymdown-extensions from 9.9 to 9.9.1 (#1411) Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 9.9 to 9.9.1. - [Release notes](https://github.com/facelessuser/pymdown-extensions/releases) - [Commits](https://github.com/facelessuser/pymdown-extensions/compare/9.9...9.9.1) --- updated-dependencies: - dependency-name: pymdown-extensions dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index dad18781ca..a92f4c0da0 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,6 +1,6 @@ mkdocs==1.4.2 mkdocs-material==9.0.4 -pymdown-extensions==9.9 +pymdown-extensions==9.9.1 mike==1.1.2 mkdocs-simple-hooks==0.1.5 mkdocs-git-revision-date-plugin==0.3.2 From 7ef9aa03570dafa5bf348c55eb33cbfa0ef6a7fb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Jan 2023 11:13:16 +1000 Subject: [PATCH 146/156] Bump mkdocs-material from 9.0.4 to 9.0.5 (#1415) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.0.4 to 9.0.5. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.0.4...9.0.5) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index a92f4c0da0..6dbe33bb65 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.4.2 -mkdocs-material==9.0.4 +mkdocs-material==9.0.5 pymdown-extensions==9.9.1 mike==1.1.2 mkdocs-simple-hooks==0.1.5 From 5ade938d4cb98b951891357e32af6f41a6ef5aa1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 21 Jan 2023 15:27:58 +1000 Subject: [PATCH 147/156] Bump mkdocs-material from 9.0.5 to 9.0.6 (#1416) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.0.5 to 9.0.6. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.0.5...9.0.6) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 6dbe33bb65..275154e0be 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.4.2 -mkdocs-material==9.0.5 +mkdocs-material==9.0.6 pymdown-extensions==9.9.1 mike==1.1.2 mkdocs-simple-hooks==0.1.5 From 2fbf4511c2204ca46cbeb31c16a656a1b76ef06f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Jan 2023 04:00:00 +1000 Subject: [PATCH 148/156] Bump pymdown-extensions from 9.9.1 to 9.9.2 (#1417) Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 9.9.1 to 9.9.2. - [Release notes](https://github.com/facelessuser/pymdown-extensions/releases) - [Commits](https://github.com/facelessuser/pymdown-extensions/compare/9.9.1...9.9.2) --- updated-dependencies: - dependency-name: pymdown-extensions dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 275154e0be..a1c69b670e 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,6 +1,6 @@ mkdocs==1.4.2 mkdocs-material==9.0.6 -pymdown-extensions==9.9.1 +pymdown-extensions==9.9.2 mike==1.1.2 mkdocs-simple-hooks==0.1.5 mkdocs-git-revision-date-plugin==0.3.2 From 92660ebebd07e33eac29a87099784e0fc1af36b9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 27 Jan 2023 14:46:57 +1000 Subject: [PATCH 149/156] Bump PowerShell dependencies (#1414) * Update ./modules.json * Bump change log Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Bernie White --- docs/CHANGELOG-v2.md | 8 +++++++- modules.json | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index fe982e56c4..544697d0ca 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -30,9 +30,15 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since v2.7.0: + +- Engineering: + - Bump Pester to v5.4.0. + [#1414](https://github.com/microsoft/PSRule/pull/1414) + ## v2.7.0 -What's changed since pre-release v2.6.0: +What's changed since v2.6.0: - New features: - Added API version date comparison assertion method and expression by @BernieWhite. diff --git a/modules.json b/modules.json index e82f14c092..ae1faa7485 100644 --- a/modules.json +++ b/modules.json @@ -2,7 +2,7 @@ "dependencies": {}, "devDependencies": { "Pester": { - "version": "5.3.3" + "version": "5.4.0" }, "platyPS": { "version": "0.14.2" From a1a37e7c1ea9edbc9e690b83bd72d1c4bd2cff72 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Jan 2023 19:34:00 +1000 Subject: [PATCH 150/156] Bump mkdocs-material from 9.0.6 to 9.0.8 (#1419) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.0.6 to 9.0.8. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.0.6...9.0.8) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index a1c69b670e..7e46d48114 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.4.2 -mkdocs-material==9.0.6 +mkdocs-material==9.0.8 pymdown-extensions==9.9.2 mike==1.1.2 mkdocs-simple-hooks==0.1.5 From d01eb60e22e2732d581045b367e7e791261cc271 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Feb 2023 11:42:56 +1000 Subject: [PATCH 151/156] Bump mkdocs-material from 9.0.8 to 9.0.9 (#1420) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.0.8 to 9.0.9. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.0.8...9.0.9) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 7e46d48114..bd5ee3211f 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.4.2 -mkdocs-material==9.0.8 +mkdocs-material==9.0.9 pymdown-extensions==9.9.2 mike==1.1.2 mkdocs-simple-hooks==0.1.5 From dbf764bbc2e778b5b7952fd140f88ab1b176b0af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 4 Feb 2023 00:47:41 +1000 Subject: [PATCH 152/156] Bump mkdocs-material from 9.0.9 to 9.0.10 (#1421) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.0.9 to 9.0.10. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.0.9...9.0.10) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index bd5ee3211f..99bbc91e0b 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.4.2 -mkdocs-material==9.0.9 +mkdocs-material==9.0.10 pymdown-extensions==9.9.2 mike==1.1.2 mkdocs-simple-hooks==0.1.5 From bc06ea47ce60b836f63b2c5346c1ae6ba4fac88d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 5 Feb 2023 00:28:01 +1000 Subject: [PATCH 153/156] Bump Microsoft.CodeAnalysis.NetAnalyzers from 6.0.0 to 7.0.0 (#1374) * Bump Microsoft.CodeAnalysis.NetAnalyzers from 6.0.0 to 7.0.0 Bumps [Microsoft.CodeAnalysis.NetAnalyzers](https://github.com/dotnet/roslyn-analyzers) from 6.0.0 to 7.0.0. - [Release notes](https://github.com/dotnet/roslyn-analyzers/releases) - [Changelog](https://github.com/dotnet/roslyn-analyzers/blob/main/PostReleaseActivities.md) - [Commits](https://github.com/dotnet/roslyn-analyzers/compare/6.0.0...7.0.0) --- updated-dependencies: - dependency-name: Microsoft.CodeAnalysis.NetAnalyzers dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * Bump change log --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bernie White --- docs/CHANGELOG-v2.md | 2 ++ src/PSRule.Tool/PSRule.Tool.csproj | 2 +- src/PSRule.Types/PSRule.Types.csproj | 2 +- src/PSRule/PSRule.csproj | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 544697d0ca..ef2532a24b 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -35,6 +35,8 @@ What's changed since v2.7.0: - Engineering: - Bump Pester to v5.4.0. [#1414](https://github.com/microsoft/PSRule/pull/1414) + - Bump Microsoft.CodeAnalysis.NetAnalyzers to v7.0.0. + [#1374](https://github.com/microsoft/PSRule/pull/1374) ## v2.7.0 diff --git a/src/PSRule.Tool/PSRule.Tool.csproj b/src/PSRule.Tool/PSRule.Tool.csproj index 0b487cee8b..dc04a03869 100644 --- a/src/PSRule.Tool/PSRule.Tool.csproj +++ b/src/PSRule.Tool/PSRule.Tool.csproj @@ -18,7 +18,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/PSRule.Types/PSRule.Types.csproj b/src/PSRule.Types/PSRule.Types.csproj index 3b04181d65..2ae376ce7a 100644 --- a/src/PSRule.Types/PSRule.Types.csproj +++ b/src/PSRule.Types/PSRule.Types.csproj @@ -10,7 +10,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/PSRule/PSRule.csproj b/src/PSRule/PSRule.csproj index 8c47e91749..a9894de9e0 100644 --- a/src/PSRule/PSRule.csproj +++ b/src/PSRule/PSRule.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 6d0e13080caf5dc42e04c162919f4af50a80ec41 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sun, 5 Feb 2023 13:53:08 +1000 Subject: [PATCH 154/156] Fixes dot numeric separator in tests #1405 (#1424) --- docs/CHANGELOG-v2.md | 3 +++ tests/PSRule.Tests/AssertTests.cs | 9 +++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index ef2532a24b..7ade98cbc3 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -37,6 +37,9 @@ What's changed since v2.7.0: [#1414](https://github.com/microsoft/PSRule/pull/1414) - Bump Microsoft.CodeAnalysis.NetAnalyzers to v7.0.0. [#1374](https://github.com/microsoft/PSRule/pull/1374) +- Bug fixes: + - Fixes handling of numerics in tests for that are impacted by regional format by @BernieWhite. + [#1405](https://github.com/microsoft/PSRule/issues/1405) ## v2.7.0 diff --git a/tests/PSRule.Tests/AssertTests.cs b/tests/PSRule.Tests/AssertTests.cs index 4a09326592..b013d25b67 100644 --- a/tests/PSRule.Tests/AssertTests.cs +++ b/tests/PSRule.Tests/AssertTests.cs @@ -815,7 +815,7 @@ public void Greater() value = GetObject((name: "value", value: "3")); Assert.True(assert.Greater(value, "value", 2, convert: true).Result); Assert.False(assert.Greater(value, "value", 2, convert: false).Result); - value = GetObject((name: "value", value: "4.5")); + value = GetObject((name: "value", value: (4.5).ToString())); Assert.True(assert.Greater(value, "value", 2, convert: true).Result); Assert.False(assert.Greater(value, "value", 4, convert: false).Result); @@ -883,7 +883,7 @@ public void GreaterOrEqual() value = GetObject((name: "value", value: "3")); Assert.True(assert.GreaterOrEqual(value, "value", 2, convert: true).Result); Assert.False(assert.GreaterOrEqual(value, "value", 2, convert: false).Result); - value = GetObject((name: "value", value: "4.5")); + value = GetObject((name: "value", value: (4.5).ToString())); Assert.True(assert.GreaterOrEqual(value, "value", 2, convert: true).Result); Assert.False(assert.GreaterOrEqual(value, "value", 4, convert: false).Result); @@ -951,7 +951,8 @@ public void Less() value = GetObject((name: "value", value: "3")); Assert.False(assert.Less(value, "value", 2, convert: true).Result); Assert.True(assert.Less(value, "value", 2, convert: false).Result); - value = GetObject((name: "value", value: "4.5")); + value = GetObject((name: "value", value: (4.5).ToString())); + Assert.False(assert.Less(value, "value", 4, convert: true).Result); Assert.True(assert.Less(value, "value", 4, convert: false).Result); @@ -1019,7 +1020,7 @@ public void LessOrEqual() value = GetObject((name: "value", value: "3")); Assert.False(assert.LessOrEqual(value, "value", 2, convert: true).Result); Assert.True(assert.LessOrEqual(value, "value", 2, convert: false).Result); - value = GetObject((name: "value", value: "4.5")); + value = GetObject((name: "value", value: (4.5).ToString())); Assert.False(assert.LessOrEqual(value, "value", 4, convert: true).Result); Assert.True(assert.LessOrEqual(value, "value", 4, convert: false).Result); From 3484587c56aabb77ee67189cbc9e73668d45e738 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 5 Feb 2023 15:30:31 +1000 Subject: [PATCH 155/156] Bump Microsoft.CodeAnalysis.Common from 4.2.0 to 4.4.0 (#1341) * Bump Microsoft.CodeAnalysis.Common from 4.2.0 to 4.4.0 Bumps [Microsoft.CodeAnalysis.Common](https://github.com/dotnet/roslyn) from 4.2.0 to 4.4.0. - [Release notes](https://github.com/dotnet/roslyn/releases) - [Changelog](https://github.com/dotnet/roslyn/blob/main/docs/Breaking%20API%20Changes.md) - [Commits](https://github.com/dotnet/roslyn/compare/v4.2.0...Visual-Studio-2019-Version-16.0-Preview-4.4) --- updated-dependencies: - dependency-name: Microsoft.CodeAnalysis.Common dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Bump change log --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bernie White --- docs/CHANGELOG-v2.md | 2 ++ src/PSRule.BuildTask/PSRule.BuildTask.csproj | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 7ade98cbc3..f7bedc07ba 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -37,6 +37,8 @@ What's changed since v2.7.0: [#1414](https://github.com/microsoft/PSRule/pull/1414) - Bump Microsoft.CodeAnalysis.NetAnalyzers to v7.0.0. [#1374](https://github.com/microsoft/PSRule/pull/1374) + Bump Microsoft.CodeAnalysis.Common to v4.4.0. + [#1341](https://github.com/microsoft/PSRule/pull/1341) - Bug fixes: - Fixes handling of numerics in tests for that are impacted by regional format by @BernieWhite. [#1405](https://github.com/microsoft/PSRule/issues/1405) diff --git a/src/PSRule.BuildTask/PSRule.BuildTask.csproj b/src/PSRule.BuildTask/PSRule.BuildTask.csproj index 4829f15289..dae83f3ea8 100644 --- a/src/PSRule.BuildTask/PSRule.BuildTask.csproj +++ b/src/PSRule.BuildTask/PSRule.BuildTask.csproj @@ -7,7 +7,7 @@ - +