From 05ec6e5a93fbb0c3f62190b44677b1bd30a3736c Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sun, 4 Jun 2023 00:47:29 +1000 Subject: [PATCH] Added support for baseline groups #1541 --- .vscode/settings.json | 3 + README.md | 1 + docs/CHANGELOG-v2.md | 10 + .../commands/PSRule/en-US/New-PSRuleOption.md | 51 ++-- .../commands/PSRule/en-US/Set-PSRuleOption.md | 34 ++- .../PSRule/en-US/about_PSRule_Options.md | 49 ++++ docs/concepts/baselines.md | 80 ++++++ docs/troubleshooting.md | 19 ++ mkdocs.yml | 1 + schemas/PSRule-options.schema.json | 32 ++- src/PSRule.Badges/PSRule.Badges.csproj | 1 + src/PSRule.SDK/PSRule.SDK.csproj | 1 + src/PSRule.Tool/PSRule.Tool.csproj | 2 +- src/PSRule.Types/Converters/TypeConverter.cs | 264 ++++++++++++++++++ .../Converters/Yaml/StringArrayConverter.cs | 48 ++++ .../Yaml/StringArrayMapConverter.cs | 81 ++++++ src/PSRule.Types/Data/StringArrayMap.cs | 134 +++++++++ .../DictionaryExtensions.cs | 80 +++++- src/PSRule.Types/Environment.cs | 211 ++++++++++++++ src/PSRule.Types/HashtableExtensions.cs | 30 ++ src/PSRule.Types/Options/BaselineOption.cs | 105 +++++++ src/PSRule.Types/PSRule.Types.csproj | 1 + src/PSRule.Types/Properties/AssemblyInfo.cs | 3 +- .../Common/BaselineYamlSerializationMapper.cs | 2 +- src/PSRule/Common/EnvironmentHelper.cs | 143 ---------- src/PSRule/Common/ExpressionHelpers.cs | 158 +---------- src/PSRule/Common/ExternalToolHelper.cs | 4 +- src/PSRule/Common/GitHelper.cs | 30 +- src/PSRule/Common/JsonCommentWriter.cs | 5 +- src/PSRule/Common/KeyMapDictionary.cs | 7 +- src/PSRule/Common/LoggerExtensions.cs | 15 + src/PSRule/Common/YamlConverters.cs | 34 --- src/PSRule/Configuration/BaselineOption.cs | 24 +- src/PSRule/Configuration/BindingOption.cs | 21 ++ .../Configuration/ConfigurationOption.cs | 4 +- src/PSRule/Configuration/ConventionOption.cs | 4 +- src/PSRule/Configuration/ExecutionOption.cs | 32 +-- src/PSRule/Configuration/IncludeOption.cs | 6 +- src/PSRule/Configuration/InputOption.cs | 18 +- src/PSRule/Configuration/LoggingOption.cs | 10 +- src/PSRule/Configuration/OutputOption.cs | 26 +- src/PSRule/Configuration/PSRuleOption.cs | 37 ++- src/PSRule/Configuration/RepositoryOption.cs | 6 +- src/PSRule/Configuration/RequiresOption.cs | 4 +- .../Expressions/LanguageExpressions.cs | 4 +- src/PSRule/Definitions/Resource.cs | 6 +- src/PSRule/Help/HelpLexer.cs | 4 +- src/PSRule/Help/MarkdownStream.cs | 2 +- src/PSRule/Host/Host.cs | 4 +- src/PSRule/Host/HostHelper.cs | 4 +- src/PSRule/PSRule.psm1 | 27 +- .../Pipeline/ExportBaselinePipelineBuilder.cs | 5 +- .../Pipeline/Formatters/AssertFormatter.cs | 4 +- .../Pipeline/GetBaselinePipelineBuilder.cs | 7 +- ...ineBuiler.cs => GetRulePipelineBuilder.cs} | 2 +- src/PSRule/Pipeline/InvokePipelineBuilder.cs | 4 +- src/PSRule/Pipeline/Output/CsvOutputWriter.cs | 7 +- .../Pipeline/Output/NUnit3OutputWriter.cs | 12 +- src/PSRule/Pipeline/PipelineBuilder.cs | 43 ++- src/PSRule/Pipeline/PipelineContext.cs | 24 +- .../Pipeline/PipelineWriterExtensions.cs | 8 - src/PSRule/Pipeline/Source.cs | 1 - src/PSRule/Pipeline/SourcePipeline.cs | 2 +- .../Resources/PSRuleResources.Designer.cs | 27 +- src/PSRule/Resources/PSRuleResources.resx | 9 +- src/PSRule/Rules/RuleHelpInfo.cs | 2 +- src/PSRule/Rules/RuleRecord.cs | 3 +- src/PSRule/Runtime/ILogger.cs | 7 + src/PSRule/Runtime/RunspaceContext.cs | 13 +- tests/PSRule.Tests/OutputWriterTests.cs | 5 +- tests/PSRule.Tests/PSRule.Baseline.Tests.ps1 | 24 ++ tests/PSRule.Tests/PSRule.Options.Tests.ps1 | 41 +++ tests/PSRule.Tests/PSRule.Tests.csproj | 3 + tests/PSRule.Tests/PSRule.Tests.yml | 4 + tests/PSRule.Tests/PSRuleOptionTests.cs | 18 +- tests/PSRule.Tests/PipelineTests.cs | 30 +- tests/PSRule.Tests/SelectorTests.cs | 2 +- 77 files changed, 1635 insertions(+), 554 deletions(-) create mode 100644 docs/concepts/baselines.md create mode 100644 src/PSRule.Types/Converters/TypeConverter.cs create mode 100644 src/PSRule.Types/Converters/Yaml/StringArrayConverter.cs create mode 100644 src/PSRule.Types/Converters/Yaml/StringArrayMapConverter.cs create mode 100644 src/PSRule.Types/Data/StringArrayMap.cs rename src/{PSRule/Common => PSRule.Types}/DictionaryExtensions.cs (66%) create mode 100644 src/PSRule.Types/Environment.cs create mode 100644 src/PSRule.Types/HashtableExtensions.cs create mode 100644 src/PSRule.Types/Options/BaselineOption.cs delete mode 100644 src/PSRule/Common/EnvironmentHelper.cs rename src/PSRule/Pipeline/{GetRulePipelineBuiler.cs => GetRulePipelineBuilder.cs} (97%) diff --git a/.vscode/settings.json b/.vscode/settings.json index fc6bbbc17a..cac8e90ff0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -57,6 +57,9 @@ "document" ] }, + "[markdown]": { + "editor.formatOnSave": false + }, "[powershell]": { "editor.formatOnSave": false, "editor.tabSize": 4 diff --git a/README.md b/README.md index e83277091b..b4b8569298 100644 --- a/README.md +++ b/README.md @@ -281,6 +281,7 @@ The following conceptual topics exist in the `PSRule` module: - [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://aka.ms/ps-rule/options) + - [Baseline.Group](https://aka.ms/ps-rule/options#baselinegroup) - [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) diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 2416b26523..8fd6d44888 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -19,6 +19,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers **Experimental features**: +- Baseline groups allow you to use a friendly name to reference baselines. + See [baselines][6] for more information. - 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. @@ -29,11 +31,19 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers [3]: expressions/functions.md [4]: expressions/sub-selectors.md [5]: creating-your-pipeline.md#processing-changed-files-only + [6]: concepts/baselines.md ## Unreleased What's changed since pre-release v2.9.0-B0033: +- New features: + - **Experimental**: Added support for baseline groups by @BernieWhite. + [#1541](https://github.com/microsoft/PSRule/issues/1541) + - Baseline groups allow you to reference a baseline by a friendly name. + - Update the baseline group to point to a new baseline. + - Currently only a single baseline can be referenced by a baseline group. + - See [baselines][6] for more information. - Engineering: - Bump Microsoft.NET.Test.Sdk to v17.6.1. [#1540](https://github.com/microsoft/PSRule/pull/1540) diff --git a/docs/commands/PSRule/en-US/New-PSRuleOption.md b/docs/commands/PSRule/en-US/New-PSRuleOption.md index 37225b880f..51440be336 100644 --- a/docs/commands/PSRule/en-US/New-PSRuleOption.md +++ b/docs/commands/PSRule/en-US/New-PSRuleOption.md @@ -18,10 +18,10 @@ Create options to configure PSRule execution. ```text New-PSRuleOption [[-Path] ] [-Configuration ] [-SuppressTargetName ] [-BindTargetName ] - [-BindTargetType ] [-BindingIgnoreCase ] [-BindingField ] - [-BindingNameSeparator ] [-BindingPreferTargetInfo ] [-TargetName ] - [-TargetType ] [-BindingUseQualifiedName ] [-Convention ] - [-AliasReferenceWarning ] [-DuplicateResourceId ] + [-BindTargetType ] [-BaselineGroup ] [-BindingIgnoreCase ] + [-BindingField ] [-BindingNameSeparator ] [-BindingPreferTargetInfo ] + [-TargetName ] [-TargetType ] [-BindingUseQualifiedName ] + [-Convention ] [-AliasReferenceWarning ] [-DuplicateResourceId ] [-InconclusiveWarning ] [-InvariantCultureWarning ] [-InitialSessionState ] [-NotProcessedWarning ] [-SuppressedRuleWarning ] [-SuppressionGroupExpired ] [-ExecutionRuleExcluded ] @@ -46,10 +46,10 @@ New-PSRuleOption [[-Path] ] [-Configuration ] ```text New-PSRuleOption [-Option] [-Configuration ] [-SuppressTargetName ] [-BindTargetName ] - [-BindTargetType ] [-BindingIgnoreCase ] [-BindingField ] - [-BindingNameSeparator ] [-BindingPreferTargetInfo ] [-TargetName ] - [-TargetType ] [-BindingUseQualifiedName ] [-Convention ] - [-AliasReferenceWarning ] [-DuplicateResourceId ] + [-BindTargetType ] [-BaselineGroup ] [-BindingIgnoreCase ] + [-BindingField ] [-BindingNameSeparator ] [-BindingPreferTargetInfo ] + [-TargetName ] [-TargetType ] [-BindingUseQualifiedName ] + [-Convention ] [-AliasReferenceWarning ] [-DuplicateResourceId ] [-InconclusiveWarning ] [-InvariantCultureWarning ] [-InitialSessionState ] [-NotProcessedWarning ] [-SuppressedRuleWarning ] [-SuppressionGroupExpired ] [-ExecutionRuleExcluded ] @@ -73,14 +73,15 @@ New-PSRuleOption [-Option] [-Configuration ] ```text New-PSRuleOption [-Default] [-Configuration ] [-SuppressTargetName ] - [-BindTargetName ] [-BindTargetType ] [-BindingIgnoreCase ] - [-BindingField ] [-BindingNameSeparator ] [-BindingPreferTargetInfo ] - [-TargetName ] [-TargetType ] [-BindingUseQualifiedName ] - [-Convention ] [-AliasReferenceWarning ] [-DuplicateResourceId ] - [-InconclusiveWarning ] [-InvariantCultureWarning ] [-InitialSessionState ] - [-NotProcessedWarning ] [-SuppressedRuleWarning ] - [-SuppressionGroupExpired ] [-ExecutionRuleExcluded ] - [-ExecutionRuleSuppressed ] [-ExecutionAliasReference ] + [-BindTargetName ] [-BindTargetType ] [-BaselineGroup ] + [-BindingIgnoreCase ] [-BindingField ] [-BindingNameSeparator ] + [-BindingPreferTargetInfo ] [-TargetName ] [-TargetType ] + [-BindingUseQualifiedName ] [-Convention ] [-AliasReferenceWarning ] + [-DuplicateResourceId ] [-InconclusiveWarning ] + [-InvariantCultureWarning ] [-InitialSessionState ] [-NotProcessedWarning ] + [-SuppressedRuleWarning ] [-SuppressionGroupExpired ] + [-ExecutionRuleExcluded ] [-ExecutionRuleSuppressed ] + [-ExecutionAliasReference ] [-ExecutionRuleInconclusive ] [-ExecutionInvariantCulture ] [-ExecutionUnprocessedObject ] [-IncludeModule ] @@ -259,6 +260,24 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -BaselineGroup + +Sets the option `Baseline.Group`. +The option `Baseline.Group` allows a named group of baselines to be defined and later referenced. +See about_PSRule_Options for more information. + +```yaml +Type: Hashtable +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -BindTargetType Configures a custom function to use to bind TargetType of an object. diff --git a/docs/commands/PSRule/en-US/Set-PSRuleOption.md b/docs/commands/PSRule/en-US/Set-PSRuleOption.md index 18caaa4214..e4a509cc19 100644 --- a/docs/commands/PSRule/en-US/Set-PSRuleOption.md +++ b/docs/commands/PSRule/en-US/Set-PSRuleOption.md @@ -15,14 +15,14 @@ Sets options that configure PSRule execution. ```text Set-PSRuleOption [[-Path] ] [-Option ] [-PassThru] [-Force] [-AllowClobber] - [-BindingIgnoreCase ] [-BindingField ] [-BindingNameSeparator ] - [-BindingPreferTargetInfo ] [-TargetName ] [-TargetType ] - [-BindingUseQualifiedName ] [-Convention ] [-AliasReferenceWarning ] - [-DuplicateResourceId ] [-InconclusiveWarning ] - [-InvariantCultureWarning ] [-InitialSessionState ] [-NotProcessedWarning ] - [-SuppressedRuleWarning ] [-SuppressionGroupExpired ] - [-ExecutionRuleExcluded ] [-ExecutionRuleSuppressed ] - [-ExecutionAliasReference ] + [-BaselineGroup ] [-BindingIgnoreCase ] [-BindingField ] + [-BindingNameSeparator ] [-BindingPreferTargetInfo ] [-TargetName ] + [-TargetType ] [-BindingUseQualifiedName ] [-Convention ] + [-AliasReferenceWarning ] [-DuplicateResourceId ] + [-InconclusiveWarning ] [-InvariantCultureWarning ] [-InitialSessionState ] + [-NotProcessedWarning ] [-SuppressedRuleWarning ] + [-SuppressionGroupExpired ] [-ExecutionRuleExcluded ] + [-ExecutionRuleSuppressed ] [-ExecutionAliasReference ] [-ExecutionRuleInconclusive ] [-ExecutionInvariantCulture ] [-ExecutionUnprocessedObject ] [-IncludeModule ] @@ -155,6 +155,24 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -BaselineGroup + +Sets the option `Baseline.Group`. +The option `Baseline.Group` allows a named group of baselines to be defined and later referenced. +See about_PSRule_Options for more information. + +```yaml +Type: Hashtable +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -BindingIgnoreCase Sets the option `Binding.IgnoreCase`. diff --git a/docs/concepts/PSRule/en-US/about_PSRule_Options.md b/docs/concepts/PSRule/en-US/about_PSRule_Options.md index 5418af226c..8fc560c861 100644 --- a/docs/concepts/PSRule/en-US/about_PSRule_Options.md +++ b/docs/concepts/PSRule/en-US/about_PSRule_Options.md @@ -13,6 +13,7 @@ This topic describes what options are available, when to and how to use them. The following workspace options are available for use: +- [Baseline.Group](#baselinegroup) - [Convention.Include](#conventioninclude) - [Execution.AliasReference](#executionaliasreference) - [Execution.AliasReferenceWarning](#executionaliasreferencewarning) @@ -169,6 +170,54 @@ Boolean values are case-insensitive. - String array values can specify multiple items by using a semi-colon separator. For example `PSRULE_INPUT_TARGETTYPE` could be set to `virtualMachine;virtualNetwork`. +### Baseline.Group + +You can use a baseline group to provide a friendly name to an existing baseline. +When you run PSRule you can opt to use the baseline group name as an alternative name for the baseline. +To indicate a baseline group, prefix the group name with `@` where you would use the name of a baseline. + +Baseline groups can be specified using: + +```powershell +# PowerShell: Using the BaselineGroup parameter +$option = New-PSRuleOption -BaselineGroup @{ latest = 'YourBaseline' }; +``` + +```powershell +# PowerShell: Using the Baseline.Group hashtable key +$option = New-PSRuleOption -Option @{ 'Baseline.Group' = @{ latest = 'YourBaseline' } }; +``` + +```powershell +# PowerShell: Using the BaselineGroup parameter to set YAML +Set-PSRuleOption -BaselineGroup @{ latest = 'YourBaseline' }; +``` + +```yaml +# YAML: Using the baseline/group property +baseline: + group: + latest: YourBaseline +``` + +```bash +# Bash: Using environment variable +export PSRULE_BASELINE_GROUP='latest=YourBaseline' +``` + +```yaml +# GitHub Actions: Using environment variable +env: + PSRULE_BASELINE_GROUP: 'latest=YourBaseline' +``` + +```yaml +# Azure Pipelines: Using environment variable +variables: +- name: PSRULE_BASELINE_GROUP + value: 'latest=YourBaseline' +``` + ### Binding.Field When an object is passed from the pipeline, PSRule automatically extracts fields from object properties. diff --git a/docs/concepts/baselines.md b/docs/concepts/baselines.md new file mode 100644 index 0000000000..49f740f707 --- /dev/null +++ b/docs/concepts/baselines.md @@ -0,0 +1,80 @@ +# Baselines + +!!! Abstract + A _baseline_ is a set of rules and configuration options. + You can define a named baseline to run a set of rules for a specific use case. + +Baselines cover two (2) main scenarios: + +- **Rules** — PSRule supports running rules by name or tag. + However, when working with a large number of rules it is often easier to group and run rules based on a name. +- **Configuration** — A baseline allows you to run any included rules with a predefined configuration by name. + +## Defining baselines + +A baseline is defined as a resource within YAML or JSON. +Baselines can be defined side-by-side with rules you create or included separately as a custom baseline. + +Continue reading [baseline][1] reference. + + [1]: ./PSRule/en-US/about_PSRule_Baseline.md + +## Baseline groups + +:octicons-milestone-24: v2.9.0 + +In addition to regular baselines, you can use a baseline group to provide a friendly name to an existing baseline. +A baseline groups are set by configuring the [Baseline.Group][2] option. + +!!! Experimental + _Baseline groups_ are a work in progress and subject to change. + Currently, _baseline groups_ allow only a single baseline to be referenced. + [Join or start a disucssion][3] to let us know how we can improve this feature going forward. + +!!! Tip + You can use baseline groups to reference a baseline. + If a new baseline is made available in the future, update your baseline group in one place to start using the new baseline. + +In the following example, two baseline groups `latest` and `preview` are defined: + +```yaml title="ps-rule.yaml" +baseline: + group: + latest: PSRule.Rules.Azure\Azure.GA_2023_03 + preview: PSRule.Rules.Azure\Azure.Preview_2023_03 +``` + +- The `latest` baseline group is set to `Azure.GA_2023_03` within the `PSRule.Rules.Azure` module. +- The `preview` baseline group is set to `Azure.Preview_2023_03` within the `PSRule.Rules.Azure` module. + +To use the baseline group, prefix the group name with `@` when running PSRule. +For example: + +=== "GitHub Actions" + + ```yaml + - name: Run PSRule + uses: microsoft/ps-rule@v2.8.1 + with: + modules: 'PSRule.Rules.Azure' + baseline: '@latest' + ``` + +=== "Azure Pipelines" + + ```yaml + - task: ps-rule-assert@2 + displayName: Run PSRule + inputs: + modules: 'PSRule.Rules.Azure' + baseline: '@latest' + ``` + +=== "Generic with PowerShell" + + ```powershell + Assert-PSRule -InputPath '.' -Baseline '@latest' -Module PSRule.Rules.Azure -Format File; + ``` + + [2]: ./PSRule/en-US/about_PSRule_Options.md#baselinegroup + [3]: https://github.com/microsoft/PSRule/discussions diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 0c5498a8a0..3d1c8975ee 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -88,3 +88,22 @@ Choose to use one or the other. If you have a specific use case your would like to enable, please start a [discussion][3]. [3]: https://github.com/microsoft/PSRule/discussions + +## PSR0003 - The specified baseline group is not known + +!!! Error + + PSR0003: The specified baseline group 'latest' is not known. + +This error is caused by attempting to reference a baseline group which has not been defined. +To define a baseline group, see [Baseline.Group][4] option. + + [4]: https://aka.ms/ps-rule/options#baselinegroup + +## PSR0004 - The specified resource is not known + +!!! Error + + PSR0004: The specified Baseline resource 'TestModule4\Module4' is not known. + +This error is caused when you attempt to reference a resource such as a baseline, rule, or selector which has not been defined. diff --git a/mkdocs.yml b/mkdocs.yml index c76ad0ccf8..fc7af9826d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -61,6 +61,7 @@ nav: - Kubernetes resource validation example: scenarios/kubernetes-resources/kubernetes-resources.md - Using PSRule from a Container: scenarios/containers/container-execution.md - Concepts: + - Baselines: concepts/baselines.md - Functions: expressions/functions.md - Grouping rules: concepts/grouping-rules.md - Sub-selectors: expressions/sub-selectors.md diff --git a/schemas/PSRule-options.schema.json b/schemas/PSRule-options.schema.json index e0024e2cbf..0d0efa7d94 100644 --- a/schemas/PSRule-options.schema.json +++ b/schemas/PSRule-options.schema.json @@ -204,6 +204,32 @@ }, "additionalProperties": false }, + "baseline-option": { + "type": "object", + "title": "Baseline options", + "description": "Options that configure baselines.", + "properties": { + "group": { + "type": "object", + "title": "Baseline group", + "description": "Configure baseline group names.", + "additionalProperties": { + "title": "Group mapping", + "description": "Maps a group name to a baseline.", + "type": "string" + }, + "defaultSnippets": [ + { + "label": "Baseline group", + "body": { + "${1:Group}": "${2:Baseline}" + } + } + ] + } + }, + "additionalProperties": false + }, "binding-option": { "type": "object", "title": "Object binding", @@ -851,6 +877,10 @@ }, "options": { "properties": { + "baseline": { + "type": "object", + "$ref": "#/definitions/baseline-option" + }, "binding": { "type": "object", "oneOf": [ @@ -966,7 +996,7 @@ }, "rule-option": { "type": "object", - "title": "Baseline options", + "title": "Rule options", "description": "Options that include/ exclude and configure rules.", "properties": { "include": { diff --git a/src/PSRule.Badges/PSRule.Badges.csproj b/src/PSRule.Badges/PSRule.Badges.csproj index 5339c5174a..4dc36456d2 100644 --- a/src/PSRule.Badges/PSRule.Badges.csproj +++ b/src/PSRule.Badges/PSRule.Badges.csproj @@ -7,6 +7,7 @@ Library {309bed8b-4e60-4c42-a2b4-37a2e7ebef3f} README.md + True diff --git a/src/PSRule.SDK/PSRule.SDK.csproj b/src/PSRule.SDK/PSRule.SDK.csproj index 1be7cf1c8b..66adb69620 100644 --- a/src/PSRule.SDK/PSRule.SDK.csproj +++ b/src/PSRule.SDK/PSRule.SDK.csproj @@ -6,6 +6,7 @@ README.md false {07a84e67-1ca3-4766-b9ea-1fdd9df6516f} + True diff --git a/src/PSRule.Tool/PSRule.Tool.csproj b/src/PSRule.Tool/PSRule.Tool.csproj index 6769a911ca..34f0358e23 100644 --- a/src/PSRule.Tool/PSRule.Tool.csproj +++ b/src/PSRule.Tool/PSRule.Tool.csproj @@ -9,7 +9,7 @@ PSRule.Tool.Program true true - false + True true ps-rule README.md diff --git a/src/PSRule.Types/Converters/TypeConverter.cs b/src/PSRule.Types/Converters/TypeConverter.cs new file mode 100644 index 0000000000..4722c27efe --- /dev/null +++ b/src/PSRule.Types/Converters/TypeConverter.cs @@ -0,0 +1,264 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Newtonsoft.Json.Linq; + +namespace PSRule.Converters +{ + internal static class TypeConverter + { + public static bool TryString(object o, out string value) + { + if (o is string s) + { + value = s; + return true; + } + else if (o is JToken token && token.Type == JTokenType.String) + { + value = token.Value(); + return true; + } + value = null; + return false; + } + + public static bool TryString(object o, bool convert, out string value) + { + if (TryString(o, out value)) + return true; + + if (convert && o is Enum evalue) + { + value = evalue.ToString(); + return true; + } + 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 = 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; + } + + public static bool TryArray(object o, out Array value) + { + value = null; + if (o is string) return false; + if (o is Array a) + value = a; + + else if (o is JArray jArray) + value = jArray.Values().ToArray(); + + else if (o is IEnumerable e) + value = e.OfType().ToArray(); + + return value != null; + } + + public static bool TryStringOrArray(object o, bool convert, out string[] value) + { + // Handle single string + if (TryString(o, convert, value: out var s)) + { + value = new string[] { s }; + return true; + } + + // Handle multiple strings + return TryStringArray(o, convert, out value); + } + + public static bool TryStringArray(object o, bool convert, out string[] value) + { + value = null; + if (o is Array array) + { + value = new string[array.Length]; + for (var i = 0; i < array.Length; i++) + { + if (TryString(array.GetValue(i), convert, value: out var s)) + value[i] = s; + } + } + else if (o is JArray jArray) + { + value = new string[jArray.Count]; + for (var i = 0; i < jArray.Count; i++) + { + if (TryString(jArray[i], convert, 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; + } + + /// + /// Try to get an int from the existing object. + /// + public static bool TryInt(object o, bool convert, out int value) + { + if (o is int ivalue) + { + value = ivalue; + return true; + } + if (o is long lvalue && lvalue <= int.MaxValue && lvalue >= int.MinValue) + { + value = (int)lvalue; + return true; + } + else if (o is JToken token && token.Type == JTokenType.Integer) + { + value = token.Value(); + return true; + } + else if (convert && TryString(o, out var s) && int.TryParse(s, out ivalue)) + { + value = ivalue; + return true; + } + value = default; + return false; + } + + public static bool TryBool(object o, bool convert, out bool value) + { + if (o is bool bvalue) + { + value = bvalue; + return true; + } + else if (o is JToken token && token.Type == JTokenType.Boolean) + { + value = token.Value(); + return true; + } + else if (convert && TryString(o, out var s) && bool.TryParse(s, out bvalue)) + { + value = bvalue; + return true; + } + else if (convert && TryLong(o, convert: false, out var lvalue)) + { + value = lvalue > 0; + return true; + } + value = default; + return false; + } + + public static bool TryByte(object o, bool convert, out byte value) + { + if (o is byte bvalue) + { + value = bvalue; + return true; + } + else if (o is JToken token && token.Type == JTokenType.Integer) + { + value = token.Value(); + return true; + } + else if (convert && TryString(o, out var s) && byte.TryParse(s, out bvalue)) + { + value = bvalue; + return true; + } + value = default; + return false; + } + + public static bool TryLong(object o, bool convert, out long value) + { + if (o is byte b) + { + value = b; + return true; + } + else if (o is int i) + { + 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(); + return true; + } + else if (convert && TryString(o, out var s) && long.TryParse(s, out l)) + { + value = l; + return true; + } + value = default; + return false; + } + + public static bool TryFloat(object o, bool convert, out float value) + { + if (o is float fvalue || (convert && o is string s && float.TryParse(s, out fvalue))) + { + value = fvalue; + return true; + } + else if (convert && o is int ivalue) + { + value = ivalue; + return true; + } + value = default; + return false; + } + + public static bool TryDouble(object o, bool convert, out double value) + { + if (o is double dvalue || (convert && o is string s && double.TryParse(s, out dvalue))) + { + value = dvalue; + return true; + } + value = default; + return false; + } + } +} diff --git a/src/PSRule.Types/Converters/Yaml/StringArrayConverter.cs b/src/PSRule.Types/Converters/Yaml/StringArrayConverter.cs new file mode 100644 index 0000000000..e22d544195 --- /dev/null +++ b/src/PSRule.Types/Converters/Yaml/StringArrayConverter.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace PSRule.Converters.Yaml +{ + /// + /// A YAML converter for deserializing a string array. + /// + public sealed class StringArrayConverter : IYamlTypeConverter + { + /// + bool IYamlTypeConverter.Accepts(Type type) + { + return type == typeof(string[]); + } + + /// + object IYamlTypeConverter.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; + } + + /// + void IYamlTypeConverter.WriteYaml(IEmitter emitter, object value, Type type) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/PSRule.Types/Converters/Yaml/StringArrayMapConverter.cs b/src/PSRule.Types/Converters/Yaml/StringArrayMapConverter.cs new file mode 100644 index 0000000000..aa0b814fc0 --- /dev/null +++ b/src/PSRule.Types/Converters/Yaml/StringArrayMapConverter.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using PSRule.Data; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace PSRule.Converters.Yaml +{ + /// + /// A YAML converter for de/serializing . + /// + public sealed class StringArrayMapConverter : IYamlTypeConverter + { + /// + bool IYamlTypeConverter.Accepts(Type type) + { + return type == typeof(StringArrayMap); + } + + /// + object IYamlTypeConverter.ReadYaml(IParser parser, Type type) + { + var result = new StringArrayMap(); + if (parser.TryConsume(out _)) + { + while (parser.TryConsume(out Scalar scalar)) + { + var key = scalar.Value; + if (parser.TryConsume(out _)) + { + var values = new List(); + while (!parser.Accept(out _)) + { + if (parser.TryConsume(out scalar)) + values.Add(scalar.Value); + } + result[key] = values.ToArray(); + parser.Require(); + parser.MoveNext(); + } + else if (parser.TryConsume(out scalar)) + { + result[key] = new string[] { scalar.Value }; + } + } + parser.Require(); + parser.MoveNext(); + } + return result; + } + + /// + void IYamlTypeConverter.WriteYaml(IEmitter emitter, object value, Type type) + { + if (type == typeof(StringArrayMap) && value == null) + { + emitter.Emit(new MappingStart()); + emitter.Emit(new MappingEnd()); + } + if (value is not StringArrayMap map) + return; + + emitter.Emit(new MappingStart()); + foreach (var field in map) + { + emitter.Emit(new Scalar(field.Key)); + emitter.Emit(new SequenceStart(null, null, false, SequenceStyle.Block)); + for (var i = 0; i < field.Value.Length; i++) + { + emitter.Emit(new Scalar(field.Value[i])); + } + emitter.Emit(new SequenceEnd()); + } + emitter.Emit(new MappingEnd()); + } + } +} diff --git a/src/PSRule.Types/Data/StringArrayMap.cs b/src/PSRule.Types/Data/StringArrayMap.cs new file mode 100644 index 0000000000..b37d178bc8 --- /dev/null +++ b/src/PSRule.Types/Data/StringArrayMap.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using PSRule.Converters; + +namespace PSRule.Data +{ + /// + /// A mapping of string to string arrays. + /// + public sealed class StringArrayMap : IEnumerable> + { + private readonly Dictionary _Map; + + /// + /// Create an empty instance. + /// + public StringArrayMap() + { + _Map = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + /// + /// Create an instance by copying an existing . + /// + internal StringArrayMap(StringArrayMap map) + { + _Map = new Dictionary(map._Map, StringComparer.OrdinalIgnoreCase); + } + + /// + /// Create an instance by copying mapped keys from a string dictionary. + /// + internal StringArrayMap(Dictionary map) + { + _Map = new Dictionary(map, StringComparer.OrdinalIgnoreCase); + } + + /// + /// Create an instance by copying mapped keys from a . + /// + /// + internal StringArrayMap(Hashtable map) + : this() + { + if (map != null) Load(this, map.IndexByString()); + } + + /// + /// The number of mapped keys. + /// + public int Count => _Map.Count; + + /// + /// Get or set mapping for a specified key. + /// + public string[] this[string key] + { + get + { + return !string.IsNullOrEmpty(key) && _Map.TryGetValue(key, out var value) ? value : Array.Empty(); + } + set + { + if (!string.IsNullOrEmpty(key)) + _Map[key] = value; + } + } + + /// + /// + /// + /// + public static implicit operator StringArrayMap(Hashtable hashtable) + { + return new StringArrayMap(hashtable); + } + + /// + /// Try to get a mapping by key. + /// + /// The key. + /// Returns an array of mapped keys. + /// Returns true if the key was found. Otherwise false is returned. + public bool TryGetValue(string key, out string[] value) + { + return _Map.TryGetValue(key, out value); + } + + /// + /// Load a key map from an existing dictionary. + /// + internal static void Load(StringArrayMap map, IDictionary properties) + { + foreach (var property in properties) + { + if (TypeConverter.TryStringOrArray(property.Value, convert: true, out var values)) + map[property.Key] = values; + } + } + + /// + public IEnumerator> GetEnumerator() + { + return ((IEnumerable>)_Map).GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Convert the instance into a dictionary. + /// + /// + public IDictionary ToDictionary() + { + return _Map; + } + + /// + /// Convert a hashtable into a instance. + /// + public static StringArrayMap FromHashtable(Hashtable hashtable) + { + return new StringArrayMap(hashtable); + } + } +} diff --git a/src/PSRule/Common/DictionaryExtensions.cs b/src/PSRule.Types/DictionaryExtensions.cs similarity index 66% rename from src/PSRule/Common/DictionaryExtensions.cs rename to src/PSRule.Types/DictionaryExtensions.cs index b0f14c7fa8..0471647c0b 100644 --- a/src/PSRule/Common/DictionaryExtensions.cs +++ b/src/PSRule.Types/DictionaryExtensions.cs @@ -5,17 +5,28 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; +using PSRule.Converters; +using PSRule.Data; namespace PSRule { - internal static class DictionaryExtensions + /// + /// Extension methods for . + /// + public static class DictionaryExtensions { + /// + /// Try to get a value and remove it from the dictionary. + /// [DebuggerStepThrough] public static bool TryPopValue(this IDictionary dictionary, string key, out object value) { return dictionary.TryGetValue(key, out value) && dictionary.Remove(key); } + /// + /// Try to get a value and remove it from the dictionary. + /// [DebuggerStepThrough] public static bool TryPopValue(this IDictionary dictionary, string key, out T value) { @@ -28,6 +39,9 @@ public static bool TryPopValue(this IDictionary dictionary, s return false; } + /// + /// Try to get a and remove it from the dictionary. + /// [DebuggerStepThrough] public static bool TryPopBool(this IDictionary dictionary, string key, out bool value) { @@ -35,6 +49,9 @@ public static bool TryPopBool(this IDictionary dictionary, strin return TryPopValue(dictionary, key, out var v) && bool.TryParse(v.ToString(), out value); } + /// + /// Try to get a and remove it from the dictionary. + /// [DebuggerStepThrough] public static bool TryPopEnum(this IDictionary dictionary, string key, out TEnum value) where TEnum : struct { @@ -42,6 +59,9 @@ public static bool TryPopEnum(this IDictionary dictionary return TryPopValue(dictionary, key, out var v) && Enum.TryParse(v.ToString(), ignoreCase: true, result: out value); } + /// + /// Try to get a and remove it from the dictionary. + /// [DebuggerStepThrough] public static bool TryPopString(this IDictionary dictionary, string key, out string value) { @@ -54,13 +74,39 @@ public static bool TryPopString(this IDictionary dictionary, str return false; } + /// + /// Try to get an array of strings and remove it from the dictionary. + /// [DebuggerStepThrough] public static bool TryPopStringArray(this IDictionary dictionary, string key, out string[] value) { value = default; - return TryPopValue(dictionary, key, out var v) && ExpressionHelpers.TryStringOrArray(v, convert: true, value: out value); + return TryPopValue(dictionary, key, out var v) && TypeConverter.TryStringOrArray(v, convert: true, value: out value); } + /// + /// Try to get a and remove it from the dictionary. + /// + [DebuggerStepThrough] + public static bool TryPopStringArrayMap(this IDictionary dictionary, string key, out StringArrayMap value) + { + value = default; + if (TryPopValue(dictionary, key, out var v) && v is StringArrayMap svalue) + { + value = svalue; + return true; + } + if (v is Hashtable hashtable) + { + value = StringArrayMap.FromHashtable(hashtable); + return true; + } + return false; + } + + /// + /// Try to get the value as a . + /// [DebuggerStepThrough] public static bool TryGetBool(this IDictionary dictionary, string key, out bool? value) { @@ -76,6 +122,9 @@ public static bool TryGetBool(this IDictionary dictionary, strin return false; } + /// + /// Try to get the value as a . + /// [DebuggerStepThrough] public static bool TryGetLong(this IDictionary dictionary, string key, out long? value) { @@ -83,7 +132,7 @@ public static bool TryGetLong(this IDictionary dictionary, strin if (!dictionary.TryGetValue(key, out var o)) return false; - if (ExpressionHelpers.TryLong(o, true, out var i_value)) + if (TypeConverter.TryLong(o, convert: true, value: out var i_value)) { value = i_value; return true; @@ -91,6 +140,9 @@ public static bool TryGetLong(this IDictionary dictionary, strin return false; } + /// + /// Try to get the value as a . + /// [DebuggerStepThrough] public static bool TryGetInt(this IDictionary dictionary, string key, out int? value) { @@ -98,7 +150,7 @@ public static bool TryGetInt(this IDictionary dictionary, string if (!dictionary.TryGetValue(key, out var o)) return false; - if (ExpressionHelpers.TryInt(o, true, out var i_value)) + if (TypeConverter.TryInt(o, convert: true, value: out var i_value)) { value = i_value; return true; @@ -106,6 +158,9 @@ public static bool TryGetInt(this IDictionary dictionary, string return false; } + /// + /// Try to get the value as a . + /// [DebuggerStepThrough] public static bool TryGetChar(this IDictionary dictionary, string key, out char? value) { @@ -126,6 +181,9 @@ public static bool TryGetChar(this IDictionary dictionary, strin return false; } + /// + /// Try to get the value as a . + /// [DebuggerStepThrough] public static bool TryGetString(this IDictionary dictionary, string key, out string value) { @@ -141,6 +199,9 @@ public static bool TryGetString(this IDictionary dictionary, str return false; } + /// + /// Try to get the value as an . + /// [DebuggerStepThrough] public static bool TryGetEnumerable(this IDictionary dictionary, string key, out IEnumerable value) { @@ -156,19 +217,28 @@ public static bool TryGetEnumerable(this IDictionary dictionary, return false; } + /// + /// Try to get the value as an array of strings. + /// [DebuggerStepThrough] public static bool TryGetStringArray(this IDictionary dictionary, string key, out string[] value) { value = null; - return dictionary.TryGetValue(key, out var o) && ExpressionHelpers.TryStringOrArray(o, convert: true, value: out value); + return dictionary.TryGetValue(key, out var o) && TypeConverter.TryStringOrArray(o, convert: true, value: out value); } + /// + /// Add unique keys to the dictionary. + /// Duplicate keys are ignored. + /// [DebuggerStepThrough] public static void AddUnique(this IDictionary dictionary, IEnumerable> values) { foreach (var kv in values) + { if (!dictionary.ContainsKey(kv.Key)) dictionary.Add(kv.Key, kv.Value); + } } internal static SortedDictionary ToSortedDictionary(this IDictionary dictionary) diff --git a/src/PSRule.Types/Environment.cs b/src/PSRule.Types/Environment.cs new file mode 100644 index 0000000000..ac1256dbcc --- /dev/null +++ b/src/PSRule.Types/Environment.cs @@ -0,0 +1,211 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Security; +using PSRule.Data; + +namespace PSRule +{ + /// + /// A helper for accessing environment variables. + /// + public static class Environment + { + private static readonly char[] STRINGARRAYMAP_ITEMSEPARATOR = 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 char STRINGARRYAMAP_PAIRSEPARATOR = '='; + private const string PATH_ENV = "PATH"; + private const string DEFAULT_CREDENTIAL_USERNAME = "na"; + private const string TF_BUILD = "TF_BUILD"; + private const string GITHUB_ACTIONS = "GITHUB_ACTIONS"; + + /// + /// Determine if the environment is running within Azure Pipelines. + /// + public static bool IsAzurePipelines() + { + return TryBool(TF_BUILD, out var azp) && azp; + } + + /// + /// Determines if the environment is running within GitHub Actions. + /// + public static bool IsGitHubActions() + { + return TryBool(GITHUB_ACTIONS, out var gh) && gh; + } + + /// + /// Determine if the environment is running within Visual Studio Code. + /// + public static bool IsVisualStudioCode() + { + return TryString("TERM_PROGRAM", out var term) && term == "vscode"; + } + + /// + /// Get the run identifier for the current environment. + /// + public static string GetRunId() + { + if (TryString("PSRULE_RUN_ID", out var runId)) + return runId; + + return TryString("BUILD_REPOSITORY_NAME", out var prefix) && TryString("BUILD_BUILDID", out var suffix) || + TryString("GITHUB_REPOSITORY", out prefix) && TryString("GITHUB_RUN_ID", out suffix) + ? string.Concat(prefix, "/", suffix) + : null; + } + + /// + /// Try to get the environment variable as a . + /// + public static bool TryString(string key, out string value) + { + return TryVariable(key, out value) && !string.IsNullOrEmpty(value); + } + + /// + /// Try to get the environment variable as a . + /// + public static bool TrySecureString(string key, out SecureString value) + { + value = null; + if (!TryString(key, out var variable)) + return false; + + value = new NetworkCredential(DEFAULT_CREDENTIAL_USERNAME, variable).SecurePassword; + return true; + } + + /// + /// Try to get the environment variable as an . + /// + public static bool TryInt(string key, out int value) + { + value = default; + return TryVariable(key, out var variable) && int.TryParse(variable, out value); + } + + /// + /// Try to get the environment variable as a . + /// + public static bool TryBool(string key, out bool value) + { + value = default; + return TryVariable(key, out var variable) && TryParseBool(variable, out value); + } + + /// + /// Try to get the environment variable as a enum of type . + /// + public static bool TryEnum(string key, out TEnum value) where TEnum : struct + { + value = default; + return TryVariable(key, out var variable) && Enum.TryParse(variable, ignoreCase: true, out value); + } + + /// + /// Try to get the environment variable as an array of strings. + /// + public static bool TryStringArray(string key, out string[] value) + { + value = default; + if (!TryVariable(key, out var variable)) + return false; + + value = variable.Split(STRINGARRAY_SEPARATOR, options: StringSplitOptions.RemoveEmptyEntries); + return value != null; + } + + /// + /// Try to get the environment variable as a . + /// + public static bool TryStringArrayMap(string key, out StringArrayMap value) + { + value = default; + if (!TryVariable(key, out var variable)) + return false; + + var pairs = variable.Split(STRINGARRAY_SEPARATOR, options: StringSplitOptions.RemoveEmptyEntries); + if (pairs == null) + return false; + + var map = new StringArrayMap(); + for (var i = 0; i < pairs.Length; i++) + { + var index = pairs[i].IndexOf(STRINGARRYAMAP_PAIRSEPARATOR); + if (index < 1 || index + 1 >= pairs[i].Length) continue; + + var left = pairs[i].Substring(0, index); + var right = pairs[i].Substring(index + 1); + var pair = right.Split(STRINGARRAYMAP_ITEMSEPARATOR, StringSplitOptions.RemoveEmptyEntries); + map[left] = pair; + } + value = map; + return true; + } + + /// + /// Try to get the PATH environment variable. + /// + public static bool TryPathEnvironmentVariable(out string[] value) + { + return TryPathEnvironmentVariable(PATH_ENV, out value); + } + + /// + /// Try to get a PATH environment variable with a specific name. + /// + public static bool TryPathEnvironmentVariable(string key, out string[] value) + { + value = default; + if (!TryVariable(key, out var variable)) + return false; + + var separator = System.Environment.OSVersion.Platform == PlatformID.Win32NT ? WINDOWS_PATH_ENV_SEPARATOR : LINUX_PATH_ENV_SEPARATOR; + value = variable.Split(separator, options: StringSplitOptions.RemoveEmptyEntries); + return value != null; + } + + /// + /// Try to get any environment variable with a specific prefix. + /// + public static IEnumerable> GetByPrefix(string prefix) + { + var env = System.Environment.GetEnvironmentVariables(); + var enumerator = env.GetEnumerator(); + while (enumerator.MoveNext()) + { + var key = enumerator.Key.ToString(); + if (key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + yield return new KeyValuePair(key, enumerator.Value); + } + } + + private static bool TryVariable(string key, out string variable) + { + variable = System.Environment.GetEnvironmentVariable(key); + return variable != null; + } + + private static bool TryParseBool(string variable, out bool value) + { + if (bool.TryParse(variable, out value)) + return true; + + if (int.TryParse(variable, out var ivalue)) + { + value = ivalue > 0; + return true; + } + return false; + } + } +} diff --git a/src/PSRule.Types/HashtableExtensions.cs b/src/PSRule.Types/HashtableExtensions.cs new file mode 100644 index 0000000000..4cb8ea9667 --- /dev/null +++ b/src/PSRule.Types/HashtableExtensions.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; + +namespace PSRule +{ + /// + /// Extension methods for . + /// + public static class HashtableExtensions + { + /// + /// Map the hashtable into a dictionary string a string key. + /// + [DebuggerStepThrough] + public static IDictionary IndexByString(this Hashtable hashtable, bool ignoreCase = true) + { + var comparer = ignoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; + var index = new Dictionary(comparer); + foreach (DictionaryEntry entry in hashtable) + index.Add(entry.Key.ToString(), entry.Value); + + return index; + } + } +} diff --git a/src/PSRule.Types/Options/BaselineOption.cs b/src/PSRule.Types/Options/BaselineOption.cs new file mode 100644 index 0000000000..77047fbecc --- /dev/null +++ b/src/PSRule.Types/Options/BaselineOption.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using PSRule.Data; + +namespace PSRule.Options +{ + /// + /// Options that configure baselines. + /// + public sealed class BaselineOption : IEquatable + { + internal static readonly BaselineOption Default = new() + { + }; + + /// + /// Create an option instance. + /// + public BaselineOption() + { + Group = null; + } + + /// + /// Create an option instance based on an existing object. + /// + /// The existing object to copy. + public BaselineOption(BaselineOption option) + { + if (option == null) + return; + + Group = option.Group; + } + + /// + public override bool Equals(object obj) + { + return obj is BaselineOption option && Equals(option); + } + + /// + public bool Equals(BaselineOption other) + { + return other != null && + Group == other.Group; + } + + /// + public override int GetHashCode() + { + unchecked // Overflow is fine + { + var hash = 17; + hash = hash * 23 + (Group != null ? Group.GetHashCode() : 0); + return hash; + } + } + + /// + /// Combines two option instances into a new merged instance. + /// The new instance uses any non-null values from . + /// Any null values from are replaced with . + /// + public static BaselineOption Combine(BaselineOption o1, BaselineOption o2) + { + var result = new BaselineOption(o1) + { + Group = o1.Group ?? o2.Group, + }; + return result; + } + + /// + /// A mapping of baseline group names to baselines. + /// + /// + /// See . + /// + [DefaultValue(null)] + public StringArrayMap Group { get; set; } + + /// + /// Load from environment variables. + /// + internal void Load() + { + if (Environment.TryStringArrayMap("PSRULE_BASELINE_GROUP", out var group)) + Group = group; + } + + /// + /// Load from a dictionary. + /// + internal void Load(Dictionary index) + { + if (index.TryPopStringArrayMap("Baseline.Group", out var group)) + Group = group; + } + } +} diff --git a/src/PSRule.Types/PSRule.Types.csproj b/src/PSRule.Types/PSRule.Types.csproj index 984f938318..8b99a1db53 100644 --- a/src/PSRule.Types/PSRule.Types.csproj +++ b/src/PSRule.Types/PSRule.Types.csproj @@ -7,6 +7,7 @@ Library {5fe4db0b-63d1-4ddb-9762-9c0d29168bc9} README.md + True diff --git a/src/PSRule.Types/Properties/AssemblyInfo.cs b/src/PSRule.Types/Properties/AssemblyInfo.cs index d20fb5e7fc..efee02b75d 100644 --- a/src/PSRule.Types/Properties/AssemblyInfo.cs +++ b/src/PSRule.Types/Properties/AssemblyInfo.cs @@ -1,6 +1,7 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("Microsoft.PSRule.Core")] [assembly: InternalsVisibleTo("PSRule.Tests")] diff --git a/src/PSRule/Common/BaselineYamlSerializationMapper.cs b/src/PSRule/Common/BaselineYamlSerializationMapper.cs index ddd85109ae..0b57d496aa 100644 --- a/src/PSRule/Common/BaselineYamlSerializationMapper.cs +++ b/src/PSRule/Common/BaselineYamlSerializationMapper.cs @@ -226,4 +226,4 @@ private static void MapPSObjectArraySequence(IEmitter emitter, PSObject[] sequen emitter.Emit(new SequenceEnd()); } } -} \ No newline at end of file +} diff --git a/src/PSRule/Common/EnvironmentHelper.cs b/src/PSRule/Common/EnvironmentHelper.cs deleted file mode 100644 index 080e03ef0d..0000000000 --- a/src/PSRule/Common/EnvironmentHelper.cs +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Net; -using System.Security; - -namespace PSRule -{ - internal static class EnvironmentHelperExtensions - { - public static bool IsAzurePipelines(this EnvironmentHelper helper) - { - return helper.TryBool("TF_BUILD", out var azp) && azp; - } - - public static bool IsGitHubActions(this EnvironmentHelper helper) - { - return helper.TryBool("GITHUB_ACTIONS", out var gh) && gh; - } - - public static bool IsVisualStudioCode(this EnvironmentHelper helper) - { - return helper.TryString("TERM_PROGRAM", out var term) && term == "vscode"; - } - - public static string GetRunId(this EnvironmentHelper helper) - { - if (helper.TryString("PSRULE_RUN_ID", out var runId)) - return runId; - - return helper.TryString("BUILD_REPOSITORY_NAME", out var prefix) && helper.TryString("BUILD_BUILDID", out var suffix) || - helper.TryString("GITHUB_REPOSITORY", out prefix) && helper.TryString("GITHUB_RUN_ID", out suffix) - ? string.Concat(prefix, "/", suffix) - : null; - } - } - - internal sealed class EnvironmentHelper - { - 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(); - - internal bool TryString(string key, out string value) - { - return TryVariable(key, out value) && !string.IsNullOrEmpty(value); - } - - internal bool TrySecureString(string key, out SecureString value) - { - value = null; - if (!TryString(key, out var variable)) - return false; - - value = new NetworkCredential(DEFAULT_CREDENTIAL_USERNAME, variable).SecurePassword; - return true; - } - - internal bool TryInt(string key, out int value) - { - value = default; - return TryVariable(key, out var variable) && int.TryParse(variable, out value); - } - - internal bool TryBool(string key, out bool value) - { - value = default; - return TryVariable(key, out var variable) && TryParseBool(variable, out value); - } - - internal bool TryEnum(string key, out TEnum value) where TEnum : struct - { - value = default; - return TryVariable(key, out var variable) && Enum.TryParse(variable, ignoreCase: true, out value); - } - - internal bool TryStringArray(string key, out string[] value) - { - value = default; - if (!TryVariable(key, out var variable)) - return false; - - value = variable.Split(STRINGARRAY_SEPARATOR, options: StringSplitOptions.RemoveEmptyEntries); - 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; - 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); - return variable != null; - } - - private static bool TryParseBool(string variable, out bool value) - { - if (bool.TryParse(variable, out value)) - return true; - - if (int.TryParse(variable, out var ivalue)) - { - value = ivalue > 0; - return true; - } - return false; - } - - internal IEnumerable> WithPrefix(string prefix) - { - var env = Environment.GetEnvironmentVariables(); - var enumerator = env.GetEnumerator(); - while (enumerator.MoveNext()) - { - var key = enumerator.Key.ToString(); - if (key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) - { - yield return new KeyValuePair(key, enumerator.Value); - } - } - } - } -} diff --git a/src/PSRule/Common/ExpressionHelpers.cs b/src/PSRule/Common/ExpressionHelpers.cs index 41314f4cf8..07d348130c 100644 --- a/src/PSRule/Common/ExpressionHelpers.cs +++ b/src/PSRule/Common/ExpressionHelpers.cs @@ -12,6 +12,7 @@ using System.Threading; using Newtonsoft.Json.Linq; using PSRule.Configuration; +using PSRule.Converters; using PSRule.Data; using PSRule.Pipeline; using PSRule.Runtime; @@ -186,19 +187,7 @@ internal static bool CompareNumeric(object actual, object expected, bool convert internal static bool TryString(object o, out string value) { - o = GetBaseObject(o); - if (o is string s) - { - value = s; - return true; - } - else if (o is JToken token && token.Type == JTokenType.String) - { - value = token.Value(); - return true; - } - value = null; - return false; + return TypeConverter.TryString(GetBaseObject(o), out value); } internal static bool TryString(object o, bool convert, out string value) @@ -231,19 +220,7 @@ 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; - - else if (o is JArray jArray) - value = jArray.Values().ToArray(); - - else if (o is IEnumerable e) - value = e.OfType().ToArray(); - - return value != null; + return TypeConverter.TryArray(GetBaseObject(o), out value); } internal static bool TryStringOrArray(object o, bool convert, out string[] value) @@ -296,149 +273,32 @@ internal static bool TryStringArray(object o, bool convert, out string[] value) /// internal static bool TryInt(object o, bool convert, out int value) { - o = GetBaseObject(o); - if (o is int ivalue) - { - value = ivalue; - return true; - } - if (o is long lvalue && lvalue <= int.MaxValue && lvalue >= int.MinValue) - { - value = (int)lvalue; - return true; - } - else if (o is JToken token && token.Type == JTokenType.Integer) - { - value = token.Value(); - return true; - } - else if (convert && TryString(o, out var s) && int.TryParse(s, out ivalue)) - { - value = ivalue; - return true; - } - value = default; - return false; + return TypeConverter.TryInt(GetBaseObject(o), convert, out value); } internal static bool TryBool(object o, bool convert, out bool value) { - o = GetBaseObject(o); - if (o is bool bvalue) - { - value = bvalue; - return true; - } - else if (o is JToken token && token.Type == JTokenType.Boolean) - { - value = token.Value(); - return true; - } - else if (convert && TryString(o, out var s) && bool.TryParse(s, out bvalue)) - { - value = bvalue; - return true; - } - else if (convert && TryLong(o, convert: false, out var lvalue)) - { - value = lvalue > 0; - return true; - } - value = default; - return false; + return TypeConverter.TryBool(GetBaseObject(o), convert, out value); } internal static bool TryByte(object o, bool convert, out byte value) { - o = GetBaseObject(o); - if (o is byte bvalue) - { - value = bvalue; - return true; - } - else if (o is JToken token && token.Type == JTokenType.Integer) - { - value = token.Value(); - return true; - } - else if (convert && TryString(o, out var s) && byte.TryParse(s, out bvalue)) - { - value = bvalue; - return true; - } - value = default; - return false; + return TypeConverter.TryByte(GetBaseObject(o), convert, out value); } internal static bool TryLong(object o, bool convert, out long value) { - o = GetBaseObject(o); - if (o is byte b) - { - value = b; - return true; - } - else if (o is int i) - { - 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(); - return true; - } - else if (convert && TryString(o, out var s) && long.TryParse(s, out l)) - { - value = l; - return true; - } - value = default; - return false; + return TypeConverter.TryLong(GetBaseObject(o), convert, out value); } internal static bool TryFloat(object o, bool convert, out float value) { - o = GetBaseObject(o); - if (o is float fvalue || (convert && o is string s && float.TryParse(s, out fvalue))) - { - value = fvalue; - return true; - } - else if (convert && o is int ivalue) - { - value = ivalue; - return true; - } - value = default; - return false; + return TypeConverter.TryFloat(GetBaseObject(o), convert, out value); } internal static bool TryDouble(object o, bool convert, out double value) { - o = GetBaseObject(o); - if (o is double dvalue || (convert && o is string s && double.TryParse(s, out dvalue))) - { - value = dvalue; - return true; - } - value = default; - return false; + return TypeConverter.TryDouble(GetBaseObject(o), convert, out value); } internal static bool TryStringLength(object o, out int value) diff --git a/src/PSRule/Common/ExternalToolHelper.cs b/src/PSRule/Common/ExternalToolHelper.cs index fa0bebdaaf..00880027ea 100644 --- a/src/PSRule/Common/ExternalToolHelper.cs +++ b/src/PSRule/Common/ExternalToolHelper.cs @@ -119,7 +119,7 @@ private void Bicep_ErrorDataReceived(object sender, DataReceivedEventArgs e) private static string[] GetErrorLine(string input) { - var lines = input.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + var lines = input.Split(new string[] { System.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 ")) @@ -131,7 +131,7 @@ private static string[] GetErrorLine(string input) private static bool TryPathFromEnvironment(string binary, out string binaryPath) { binaryPath = null; - if (!EnvironmentHelper.Default.TryPathEnvironmentVariable(out var path)) + if (!Environment.TryPathEnvironmentVariable(out var path)) return false; for (var i = 0; path != null && i < path.Length; i++) diff --git a/src/PSRule/Common/GitHelper.cs b/src/PSRule/Common/GitHelper.cs index 58c0febdac..f74f2afbd5 100644 --- a/src/PSRule/Common/GitHelper.cs +++ b/src/PSRule/Common/GitHelper.cs @@ -50,17 +50,17 @@ internal static class GitHelper public static bool TryHeadRef(out string value, string path = null) { // Try PSRule - if (EnvironmentHelper.Default.TryString(ENV_PSRULE_REPO_REF, out value) || - EnvironmentHelper.Default.TryString(ENV_PSRULE_REPO_HEADREF, out value)) + if (Environment.TryString(ENV_PSRULE_REPO_REF, out value) || + Environment.TryString(ENV_PSRULE_REPO_HEADREF, out value)) return true; // Try Azure Pipelines - if (EnvironmentHelper.Default.TryString(ENV_ADO_REPO_REF, out value)) + if (Environment.TryString(ENV_ADO_REPO_REF, out value)) return true; // Try GitHub Actions - if (EnvironmentHelper.Default.TryString(ENV_GITHUB_REPO_REF, out value) || - EnvironmentHelper.Default.TryString(ENV_GITHUB_REPO_HEADREF, out value)) + if (Environment.TryString(ENV_GITHUB_REPO_REF, out value) || + Environment.TryString(ENV_GITHUB_REPO_HEADREF, out value)) return true; // Try .git/ @@ -82,15 +82,15 @@ public static bool TryHeadBranch(out string value, string path = null) public static bool TryBaseRef(out string value, string path = null) { // Try PSRule - if (EnvironmentHelper.Default.TryString(ENV_PSRULE_REPO_BASEREF, out value)) + if (Environment.TryString(ENV_PSRULE_REPO_BASEREF, out value)) return true; // Try Azure Pipelines - if (EnvironmentHelper.Default.TryString(ENV_ADO_REPO_BASEREF, out value)) + if (Environment.TryString(ENV_ADO_REPO_BASEREF, out value)) return true; // Try GitHub Actions - if (EnvironmentHelper.Default.TryString(ENV_GITHUB_REPO_BASEREF, out value)) + if (Environment.TryString(ENV_GITHUB_REPO_BASEREF, out value)) return true; // Try .git/ @@ -100,15 +100,15 @@ public static bool TryBaseRef(out string value, string path = null) public static bool TryRevision(out string value, string path = null) { // Try PSRule - if (EnvironmentHelper.Default.TryString(ENV_PSRULE_REPO_REVISION, out value)) + if (Environment.TryString(ENV_PSRULE_REPO_REVISION, out value)) return true; // Try Azure Pipelines - if (EnvironmentHelper.Default.TryString(ENV_ADO_REPO_REVISION, out value)) + if (Environment.TryString(ENV_ADO_REPO_REVISION, out value)) return true; // Try GitHub Actions - if (EnvironmentHelper.Default.TryString(ENV_GITHUB_REPO_REVISION, out value)) + if (Environment.TryString(ENV_GITHUB_REPO_REVISION, out value)) return true; // Try .git/ @@ -118,15 +118,15 @@ public static bool TryRevision(out string value, string path = null) public static bool TryRepository(out string value, string path = null) { // Try PSRule - if (EnvironmentHelper.Default.TryString(ENV_PSRULE_REPO_URL, out value)) + if (Environment.TryString(ENV_PSRULE_REPO_URL, out value)) return true; // Try Azure Pipelines - if (EnvironmentHelper.Default.TryString(ENV_ADO_REPO_URL, out value)) + if (Environment.TryString(ENV_ADO_REPO_URL, out value)) return true; // Try GitHub Actions - if (EnvironmentHelper.Default.TryString(ENV_GITHUB_REPO_URL, out value)) + if (Environment.TryString(ENV_GITHUB_REPO_URL, out value)) { value = string.Concat(GITHUB_BASE_URL, value); return true; @@ -150,7 +150,7 @@ public static bool TryGetChangedFiles(string baseRef, string filter, string opti if (!tool.WaitForExit(args, out var exitCode) || exitCode != 0) return false; - files = tool.GetOutput().Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + files = tool.GetOutput().Split(new string[] { System.Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); return true; } diff --git a/src/PSRule/Common/JsonCommentWriter.cs b/src/PSRule/Common/JsonCommentWriter.cs index 61c562fe25..fe6d6648e1 100644 --- a/src/PSRule/Common/JsonCommentWriter.cs +++ b/src/PSRule/Common/JsonCommentWriter.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.IO; using Newtonsoft.Json; @@ -18,12 +17,12 @@ public override void WriteComment(string text) if (Indentation > 0 && Formatting == Formatting.Indented) WriteIndent(); else - WriteRaw(Environment.NewLine); + WriteRaw(System.Environment.NewLine); WriteRaw("// "); WriteRaw(text); if (Indentation == 0 || Formatting == Formatting.None) - WriteRaw(Environment.NewLine); + WriteRaw(System.Environment.NewLine); } } } diff --git a/src/PSRule/Common/KeyMapDictionary.cs b/src/PSRule/Common/KeyMapDictionary.cs index c4c6ed58d4..cbea199dd9 100644 --- a/src/PSRule/Common/KeyMapDictionary.cs +++ b/src/PSRule/Common/KeyMapDictionary.cs @@ -177,12 +177,9 @@ protected void Load(Hashtable hashtable) /// 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) + internal void Load(string prefix, Func format = null) { - if (env == null) - throw new ArgumentNullException(nameof(env)); - - foreach (var variable in env.WithPrefix(prefix)) + foreach (var variable in Environment.GetByPrefix(prefix)) { if (TryKeyPrefix(variable.Key, prefix, out var suffix)) { diff --git a/src/PSRule/Common/LoggerExtensions.cs b/src/PSRule/Common/LoggerExtensions.cs index ba696bb70c..901882c529 100644 --- a/src/PSRule/Common/LoggerExtensions.cs +++ b/src/PSRule/Common/LoggerExtensions.cs @@ -2,7 +2,9 @@ // Licensed under the MIT License. using System; +using System.Threading; using PSRule.Definitions; +using PSRule.Pipeline; using PSRule.Resources; using PSRule.Runtime; @@ -17,5 +19,18 @@ internal static void WarnResourceObsolete(this ILogger logger, ResourceKind kind logger.Warning(PSRuleResources.ResourceObsolete, Enum.GetName(typeof(ResourceKind), kind), id); } + + internal static void ErrorResourceUnresolved(this ILogger logger, ResourceKind kind, string id) + { + if (logger == null || !logger.ShouldLog(LogLevel.Error)) + return; + + logger.Error(new PipelineBuilderException(string.Format( + Thread.CurrentThread.CurrentCulture, + PSRuleResources.PSR0004, + Enum.GetName(typeof(ResourceKind), + kind), id + )), "PSR0004"); + } } } diff --git a/src/PSRule/Common/YamlConverters.cs b/src/PSRule/Common/YamlConverters.cs index 1a8879b4c2..5b1452d71a 100644 --- a/src/PSRule/Common/YamlConverters.cs +++ b/src/PSRule/Common/YamlConverters.cs @@ -133,40 +133,6 @@ 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. /// diff --git a/src/PSRule/Configuration/BaselineOption.cs b/src/PSRule/Configuration/BaselineOption.cs index e55512957c..a48f3b0bfd 100644 --- a/src/PSRule/Configuration/BaselineOption.cs +++ b/src/PSRule/Configuration/BaselineOption.cs @@ -88,44 +88,44 @@ public static BaselineOption FromString(string value) return string.IsNullOrEmpty(value) ? null : new BaselineRef(value); } - internal static void Load(IBaselineV1Spec option, EnvironmentHelper env) + internal static void Load(IBaselineV1Spec option) { // Binding.Field - currently not supported - if (env.TryBool("PSRULE_BINDING_IGNORECASE", out var ignoreCase)) + if (Environment.TryBool("PSRULE_BINDING_IGNORECASE", out var ignoreCase)) option.Binding.IgnoreCase = ignoreCase; - if (env.TryString("PSRULE_BINDING_NAMESEPARATOR", out var nameSeparator)) + if (Environment.TryString("PSRULE_BINDING_NAMESEPARATOR", out var nameSeparator)) option.Binding.NameSeparator = nameSeparator; - if (env.TryBool("PSRULE_BINDING_PREFERTARGETINFO", out var preferTargetInfo)) + if (Environment.TryBool("PSRULE_BINDING_PREFERTARGETINFO", out var preferTargetInfo)) option.Binding.PreferTargetInfo = preferTargetInfo; - if (env.TryStringArray("PSRULE_BINDING_TARGETNAME", out var targetName)) + if (Environment.TryStringArray("PSRULE_BINDING_TARGETNAME", out var targetName)) option.Binding.TargetName = targetName; - if (env.TryStringArray("PSRULE_BINDING_TARGETTYPE", out var targetType)) + if (Environment.TryStringArray("PSRULE_BINDING_TARGETTYPE", out var targetType)) option.Binding.TargetType = targetType; - if (env.TryBool("PSRULE_BINDING_USEQUALIFIEDNAME", out var useQualifiedName)) + if (Environment.TryBool("PSRULE_BINDING_USEQUALIFIEDNAME", out var useQualifiedName)) option.Binding.UseQualifiedName = useQualifiedName; - if (env.TryString("PSRULE_RULE_BASELINE", out var baseline)) + if (Environment.TryString("PSRULE_RULE_BASELINE", out var baseline)) option.Rule.Baseline = baseline; - if (env.TryStringArray("PSRULE_RULE_EXCLUDE", out var exclude)) + if (Environment.TryStringArray("PSRULE_RULE_EXCLUDE", out var exclude)) option.Rule.Exclude = exclude; - if (env.TryBool("PSRULE_RULE_INCLUDELOCAL", out var includeLocal)) + if (Environment.TryBool("PSRULE_RULE_INCLUDELOCAL", out var includeLocal)) option.Rule.IncludeLocal = includeLocal; - if (env.TryStringArray("PSRULE_RULE_INCLUDE", out var include)) + if (Environment.TryStringArray("PSRULE_RULE_INCLUDE", out var include)) option.Rule.Include = include; // Rule.Tag - currently not supported // Process configuration values - option.Configuration.Load(env); + option.Configuration.Load(); } /// diff --git a/src/PSRule/Configuration/BindingOption.cs b/src/PSRule/Configuration/BindingOption.cs index 5f955b4941..638f500235 100644 --- a/src/PSRule/Configuration/BindingOption.cs +++ b/src/PSRule/Configuration/BindingOption.cs @@ -114,42 +114,63 @@ internal static BindingOption Combine(BindingOption o1, BindingOption o2) /// /// One or more custom fields to bind. /// + /// + /// See . + /// [DefaultValue(null)] public FieldMap Field { get; set; } /// /// Determines if custom binding uses ignores case when matching properties. /// + /// + /// See . + /// [DefaultValue(null)] public bool? IgnoreCase { get; set; } /// /// Configures the separator to use for building a qualified name. /// + /// + /// See . + /// [DefaultValue(null)] public string NameSeparator { get; set; } /// /// Determines if binding prefers target info provided by the object over custom configuration. /// + /// + /// See . + /// [DefaultValue(null)] public bool? PreferTargetInfo { get; set; } /// /// Property names to use to bind TargetName. /// + /// + /// See . + /// [DefaultValue(null)] public string[] TargetName { get; set; } /// /// Property names to use to bind TargetType. /// + /// + /// See . + /// [DefaultValue(null)] public string[] TargetType { get; set; } /// /// Determines if a qualified TargetName is used. /// + /// + /// See . + /// [DefaultValue(null)] public bool? UseQualifiedName { get; set; } } diff --git a/src/PSRule/Configuration/ConfigurationOption.cs b/src/PSRule/Configuration/ConfigurationOption.cs index 308cbd37ee..b9376773ca 100644 --- a/src/PSRule/Configuration/ConfigurationOption.cs +++ b/src/PSRule/Configuration/ConfigurationOption.cs @@ -57,9 +57,9 @@ internal static ConfigurationOption Combine(ConfigurationOption o1, Configuratio /// 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) + internal void Load() { - Load(ENVIRONMENT_PREFIX, env); + Load(ENVIRONMENT_PREFIX); } /// diff --git a/src/PSRule/Configuration/ConventionOption.cs b/src/PSRule/Configuration/ConventionOption.cs index 12f6f741a6..fb2f48afc1 100644 --- a/src/PSRule/Configuration/ConventionOption.cs +++ b/src/PSRule/Configuration/ConventionOption.cs @@ -76,9 +76,9 @@ internal static ConventionOption Combine(ConventionOption o1, ConventionOption o [DefaultValue(null)] public string[] Include { get; set; } - internal void Load(EnvironmentHelper env) + internal void Load() { - if (env.TryStringArray("PSRULE_CONVENTION_INCLUDE", out var include)) + if (Environment.TryStringArray("PSRULE_CONVENTION_INCLUDE", out var include)) Include = include; } diff --git a/src/PSRule/Configuration/ExecutionOption.cs b/src/PSRule/Configuration/ExecutionOption.cs index 268016db95..0d6ba26fa0 100644 --- a/src/PSRule/Configuration/ExecutionOption.cs +++ b/src/PSRule/Configuration/ExecutionOption.cs @@ -330,53 +330,53 @@ internal static ExecutionOption Combine(ExecutionOption o1, ExecutionOption o2) /// /// Load from environment variables. /// - internal void Load(EnvironmentHelper env) + internal void Load() { #pragma warning disable CS0618 // Type or member is obsolete - if (env.TryBool("PSRULE_EXECUTION_ALIASREFERENCEWARNING", out var bvalue)) + if (Environment.TryBool("PSRULE_EXECUTION_ALIASREFERENCEWARNING", out var bvalue)) AliasReferenceWarning = bvalue; - if (env.TryEnum("PSRULE_EXECUTION_DUPLICATERESOURCEID", out ExecutionActionPreference duplicateResourceId)) + if (Environment.TryEnum("PSRULE_EXECUTION_DUPLICATERESOURCEID", out ExecutionActionPreference duplicateResourceId)) DuplicateResourceId = duplicateResourceId; - if (env.TryEnum("PSRULE_EXECUTION_LANGUAGEMODE", out LanguageMode languageMode)) + if (Environment.TryEnum("PSRULE_EXECUTION_LANGUAGEMODE", out LanguageMode languageMode)) LanguageMode = languageMode; - if (env.TryBool("PSRULE_EXECUTION_INCONCLUSIVEWARNING", out bvalue)) + if (Environment.TryBool("PSRULE_EXECUTION_INCONCLUSIVEWARNING", out bvalue)) InconclusiveWarning = bvalue; - if (env.TryBool("PSRULE_EXECUTION_INVARIANTCULTUREWARNING", out bvalue)) + if (Environment.TryBool("PSRULE_EXECUTION_INVARIANTCULTUREWARNING", out bvalue)) InvariantCultureWarning = bvalue; - if (env.TryEnum("PSRULE_EXECUTION_INITIALSESSIONSTATE", out SessionState initialSessionState)) + if (Environment.TryEnum("PSRULE_EXECUTION_INITIALSESSIONSTATE", out SessionState initialSessionState)) InitialSessionState = initialSessionState; - if (env.TryBool("PSRULE_EXECUTION_NOTPROCESSEDWARNING", out bvalue)) + if (Environment.TryBool("PSRULE_EXECUTION_NOTPROCESSEDWARNING", out bvalue)) NotProcessedWarning = bvalue; - if (env.TryBool("PSRULE_EXECUTION_SUPPRESSEDRULEWARNING", out bvalue)) + if (Environment.TryBool("PSRULE_EXECUTION_SUPPRESSEDRULEWARNING", out bvalue)) SuppressedRuleWarning = bvalue; #pragma warning restore CS0618 // Type or member is obsolete - if (env.TryEnum("PSRULE_EXECUTION_SUPPRESSIONGROUPEXPIRED", out ExecutionActionPreference suppressionGroupExpired)) + if (Environment.TryEnum("PSRULE_EXECUTION_SUPPRESSIONGROUPEXPIRED", out ExecutionActionPreference suppressionGroupExpired)) SuppressionGroupExpired = suppressionGroupExpired; - if (env.TryEnum("PSRULE_EXECUTION_RULEEXCLUDED", out ExecutionActionPreference ruleExcluded)) + if (Environment.TryEnum("PSRULE_EXECUTION_RULEEXCLUDED", out ExecutionActionPreference ruleExcluded)) RuleExcluded = ruleExcluded; - if (env.TryEnum("PSRULE_EXECUTION_RULESUPPRESSED", out ExecutionActionPreference ruleSuppressed)) + if (Environment.TryEnum("PSRULE_EXECUTION_RULESUPPRESSED", out ExecutionActionPreference ruleSuppressed)) RuleSuppressed = ruleSuppressed; - if (env.TryEnum("PSRULE_EXECUTION_ALIASREFERENCE", out ExecutionActionPreference aliasReference)) + if (Environment.TryEnum("PSRULE_EXECUTION_ALIASREFERENCE", out ExecutionActionPreference aliasReference)) AliasReference = aliasReference; - if (env.TryEnum("PSRULE_EXECUTION_RULEINCONCLUSIVE", out ExecutionActionPreference ruleInconclusive)) + if (Environment.TryEnum("PSRULE_EXECUTION_RULEINCONCLUSIVE", out ExecutionActionPreference ruleInconclusive)) RuleInconclusive = ruleInconclusive; - if (env.TryEnum("PSRULE_EXECUTION_INVARIANTCULTURE", out ExecutionActionPreference invariantCulture)) + if (Environment.TryEnum("PSRULE_EXECUTION_INVARIANTCULTURE", out ExecutionActionPreference invariantCulture)) InvariantCulture = invariantCulture; - if (env.TryEnum("PSRULE_EXECUTION_UNPROCESSEDOBJECT", out ExecutionActionPreference unprocessedObject)) + if (Environment.TryEnum("PSRULE_EXECUTION_UNPROCESSEDOBJECT", out ExecutionActionPreference unprocessedObject)) UnprocessedObject = unprocessedObject; } diff --git a/src/PSRule/Configuration/IncludeOption.cs b/src/PSRule/Configuration/IncludeOption.cs index 456bfb7991..2581fc1237 100644 --- a/src/PSRule/Configuration/IncludeOption.cs +++ b/src/PSRule/Configuration/IncludeOption.cs @@ -91,12 +91,12 @@ internal static IncludeOption Combine(IncludeOption o1, IncludeOption o2) [DefaultValue(null)] public string[] Module { get; set; } - internal void Load(EnvironmentHelper env) + internal void Load() { - if (env.TryStringArray("PSRULE_INCLUDE_PATH", out var path)) + if (Environment.TryStringArray("PSRULE_INCLUDE_PATH", out var path)) Path = path; - if (env.TryStringArray("PSRULE_INCLUDE_MODULE", out var module)) + if (Environment.TryStringArray("PSRULE_INCLUDE_MODULE", out var module)) Module = module; } diff --git a/src/PSRule/Configuration/InputOption.cs b/src/PSRule/Configuration/InputOption.cs index 9758aaab42..3508318bcc 100644 --- a/src/PSRule/Configuration/InputOption.cs +++ b/src/PSRule/Configuration/InputOption.cs @@ -173,30 +173,30 @@ internal static InputOption Combine(InputOption o1, InputOption o2) [DefaultValue(null)] public string[] TargetType { get; set; } - internal void Load(EnvironmentHelper env) + internal void Load() { - if (env.TryEnum("PSRULE_INPUT_FORMAT", out InputFormat format)) + if (Environment.TryEnum("PSRULE_INPUT_FORMAT", out InputFormat format)) Format = format; - if (env.TryBool("PSRULE_INPUT_IGNOREGITPATH", out var ignoreGitPath)) + if (Environment.TryBool("PSRULE_INPUT_IGNOREGITPATH", out var ignoreGitPath)) IgnoreGitPath = ignoreGitPath; - if (env.TryBool("PSRULE_INPUT_IGNOREOBJECTSOURCE", out var ignoreObjectSource)) + if (Environment.TryBool("PSRULE_INPUT_IGNOREOBJECTSOURCE", out var ignoreObjectSource)) IgnoreObjectSource = ignoreObjectSource; - if (env.TryBool("PSRULE_INPUT_IGNOREREPOSITORYCOMMON", out var ignoreRepositoryCommon)) + if (Environment.TryBool("PSRULE_INPUT_IGNOREREPOSITORYCOMMON", out var ignoreRepositoryCommon)) IgnoreRepositoryCommon = ignoreRepositoryCommon; - if (env.TryBool("PSRULE_INPUT_IGNOREUNCHANGEDPATH", out var ignoreUnchangedPath)) + if (Environment.TryBool("PSRULE_INPUT_IGNOREUNCHANGEDPATH", out var ignoreUnchangedPath)) IgnoreUnchangedPath = ignoreUnchangedPath; - if (env.TryString("PSRULE_INPUT_OBJECTPATH", out var objectPath)) + if (Environment.TryString("PSRULE_INPUT_OBJECTPATH", out var objectPath)) ObjectPath = objectPath; - if (env.TryStringArray("PSRULE_INPUT_PATHIGNORE", out var pathIgnore)) + if (Environment.TryStringArray("PSRULE_INPUT_PATHIGNORE", out var pathIgnore)) PathIgnore = pathIgnore; - if (env.TryStringArray("PSRULE_INPUT_TARGETTYPE", out var targetType)) + if (Environment.TryStringArray("PSRULE_INPUT_TARGETTYPE", out var targetType)) TargetType = targetType; } diff --git a/src/PSRule/Configuration/LoggingOption.cs b/src/PSRule/Configuration/LoggingOption.cs index 67281e8d65..b8ab77eb04 100644 --- a/src/PSRule/Configuration/LoggingOption.cs +++ b/src/PSRule/Configuration/LoggingOption.cs @@ -115,18 +115,18 @@ internal static LoggingOption Combine(LoggingOption o1, LoggingOption o2) [DefaultValue(null)] public OutcomeLogStream? RulePass { get; set; } - internal void Load(EnvironmentHelper env) + internal void Load() { - if (env.TryStringArray("PSRULE_LOGGING_LIMITDEBUG", out var limitDebug)) + if (Environment.TryStringArray("PSRULE_LOGGING_LIMITDEBUG", out var limitDebug)) LimitDebug = limitDebug; - if (env.TryStringArray("PSRULE_LOGGING_LIMITVERBOSE", out var limitVerbose)) + if (Environment.TryStringArray("PSRULE_LOGGING_LIMITVERBOSE", out var limitVerbose)) LimitVerbose = limitVerbose; - if (env.TryEnum("PSRULE_LOGGING_RULEFAIL", out OutcomeLogStream ruleFail)) + if (Environment.TryEnum("PSRULE_LOGGING_RULEFAIL", out OutcomeLogStream ruleFail)) RuleFail = ruleFail; - if (env.TryEnum("PSRULE_LOGGING_RULEPASS", out OutcomeLogStream rulePass)) + if (Environment.TryEnum("PSRULE_LOGGING_RULEPASS", out OutcomeLogStream rulePass)) RulePass = rulePass; } diff --git a/src/PSRule/Configuration/OutputOption.cs b/src/PSRule/Configuration/OutputOption.cs index 5cb94a05b6..980c065052 100644 --- a/src/PSRule/Configuration/OutputOption.cs +++ b/src/PSRule/Configuration/OutputOption.cs @@ -215,42 +215,42 @@ internal static OutputOption Combine(OutputOption o1, OutputOption o2) [DefaultValue(null)] public bool? SarifProblemsOnly { get; set; } - internal void Load(EnvironmentHelper env) + internal void Load() { - if (env.TryEnum("PSRULE_OUTPUT_AS", out ResultFormat value)) + if (Environment.TryEnum("PSRULE_OUTPUT_AS", out ResultFormat value)) As = value; - if (env.TryEnum("PSRULE_OUTPUT_BANNER", out BannerFormat banner)) + if (Environment.TryEnum("PSRULE_OUTPUT_BANNER", out BannerFormat banner)) Banner = banner; - if (env.TryStringArray("PSRULE_OUTPUT_CULTURE", out var culture)) + if (Environment.TryStringArray("PSRULE_OUTPUT_CULTURE", out var culture)) Culture = culture; - if (env.TryEnum("PSRULE_OUTPUT_ENCODING", out OutputEncoding encoding)) + if (Environment.TryEnum("PSRULE_OUTPUT_ENCODING", out OutputEncoding encoding)) Encoding = encoding; - if (env.TryEnum("PSRULE_OUTPUT_FOOTER", out FooterFormat footer)) + if (Environment.TryEnum("PSRULE_OUTPUT_FOOTER", out FooterFormat footer)) Footer = footer; - if (env.TryEnum("PSRULE_OUTPUT_FORMAT", out OutputFormat format)) + if (Environment.TryEnum("PSRULE_OUTPUT_FORMAT", out OutputFormat format)) Format = format; - if (env.TryString("PSRULE_OUTPUT_JOBSUMMARYPATH", out var jobSummaryPath)) + if (Environment.TryString("PSRULE_OUTPUT_JOBSUMMARYPATH", out var jobSummaryPath)) JobSummaryPath = jobSummaryPath; - if (env.TryInt("PSRULE_OUTPUT_JSONINDENT", out var jsonIndent)) + if (Environment.TryInt("PSRULE_OUTPUT_JSONINDENT", out var jsonIndent)) JsonIndent = jsonIndent; - if (env.TryEnum("PSRULE_OUTPUT_OUTCOME", out RuleOutcome outcome)) + if (Environment.TryEnum("PSRULE_OUTPUT_OUTCOME", out RuleOutcome outcome)) Outcome = outcome; - if (env.TryString("PSRULE_OUTPUT_PATH", out var path)) + if (Environment.TryString("PSRULE_OUTPUT_PATH", out var path)) Path = path; - if (env.TryEnum("PSRULE_OUTPUT_STYLE", out OutputStyle style)) + if (Environment.TryEnum("PSRULE_OUTPUT_STYLE", out OutputStyle style)) Style = style; - if (env.TryBool("PSRULE_OUTPUT_SARIFPROBLEMSONLY", out var sarifProblemsOnly)) + if (Environment.TryBool("PSRULE_OUTPUT_SARIFPROBLEMSONLY", out var sarifProblemsOnly)) SarifProblemsOnly = sarifProblemsOnly; } diff --git a/src/PSRule/Configuration/PSRuleOption.cs b/src/PSRule/Configuration/PSRuleOption.cs index bc01003651..3b470277b2 100644 --- a/src/PSRule/Configuration/PSRuleOption.cs +++ b/src/PSRule/Configuration/PSRuleOption.cs @@ -10,6 +10,7 @@ using System.Management.Automation; using System.Threading; using Newtonsoft.Json; +using PSRule.Converters.Yaml; using PSRule.Definitions.Baselines; using PSRule.Pipeline; using PSRule.Resources; @@ -44,6 +45,7 @@ public sealed class PSRuleOption : IEquatable, IBaselineV1Spec private static readonly PSRuleOption Default = new() { + Baseline = Options.BaselineOption.Default, Binding = BindingOption.Default, Convention = ConventionOption.Default, Execution = ExecutionOption.Default, @@ -70,6 +72,7 @@ public sealed class PSRuleOption : IEquatable, IBaselineV1Spec public PSRuleOption() { // Set defaults + Baseline = new Options.BaselineOption(); Binding = new BindingOption(); Configuration = new ConfigurationOption(); Convention = new ConventionOption(); @@ -90,6 +93,7 @@ private PSRuleOption(string sourcePath, PSRuleOption option) _SourcePath = sourcePath; // Set from existing option instance + Baseline = new Options.BaselineOption(option?.Baseline); Binding = new BindingOption(option?.Binding); Configuration = new ConfigurationOption(option?.Configuration); Convention = new ConventionOption(option?.Convention); @@ -105,6 +109,11 @@ private PSRuleOption(string sourcePath, PSRuleOption option) Suppression = new SuppressionOption(option?.Suppression); } + /// + /// Options that configure baselines. + /// + public Options.BaselineOption Baseline { get; set; } + /// /// Options that affect property binding. /// @@ -189,7 +198,7 @@ public string ToYaml() Thread.CurrentThread.CurrentCulture, PSRuleResources.OptionsSourceComment, _SourcePath), - Environment.NewLine, + System.Environment.NewLine, yaml); } @@ -219,6 +228,7 @@ public static PSRuleOption FromDefault() private static PSRuleOption Combine(PSRuleOption o1, PSRuleOption o2) { var result = new PSRuleOption(o1?._SourcePath ?? o2?._SourcePath, o1); + result.Baseline = Options.BaselineOption.Combine(result.Baseline, o2?.Baseline); result.Binding = BindingOption.Combine(result.Binding, o2?.Binding); result.Configuration = ConfigurationOption.Combine(result.Configuration, o2?.Configuration); result.Convention = ConventionOption.Combine(result.Convention, o2?.Convention); @@ -318,6 +328,7 @@ private static PSRuleOption FromYaml(string path, string yaml) .IgnoreUnmatchedProperties() .WithNamingConvention(CamelCaseNamingConvention.Instance) .WithTypeConverter(new FieldMapYamlTypeConverter()) + .WithTypeConverter(new StringArrayMapConverter()) .WithTypeConverter(new SuppressionRuleYamlTypeConverter()) .WithTypeConverter(new PSObjectYamlTypeConverter()) .WithNodeTypeResolver(new PSOptionYamlTypeResolver()) @@ -346,16 +357,16 @@ private static PSRuleOption FromEnvironment(PSRuleOption option) option ??= new PSRuleOption(); // Start loading matching values - var env = EnvironmentHelper.Default; - option.Convention.Load(env); - option.Execution.Load(env); - option.Include.Load(env); - option.Input.Load(env); - option.Logging.Load(env); - option.Output.Load(env); - option.Repository.Load(env); - option.Requires.Load(env); - BaselineOption.Load(option, env); + option.Baseline.Load(); + option.Convention.Load(); + option.Execution.Load(); + option.Include.Load(); + option.Input.Load(); + option.Logging.Load(); + option.Output.Load(); + option.Repository.Load(); + option.Requires.Load(); + BaselineOption.Load(option); return option; } @@ -375,6 +386,7 @@ public static PSRuleOption FromHashtable(Hashtable hashtable) // Start loading matching values var index = BuildIndex(hashtable); + option.Baseline.Load(index); option.Convention.Load(index); option.Execution.Load(index); option.Include.Load(index); @@ -493,6 +505,7 @@ public override bool Equals(object obj) public bool Equals(PSRuleOption other) { return other != null && + Baseline == other.Baseline && Binding == other.Binding && Configuration == other.Configuration && Convention == other.Convention && @@ -513,6 +526,7 @@ public override int GetHashCode() unchecked // Overflow is fine { var hash = 17; + hash = hash * 23 + (Baseline != null ? Baseline.GetHashCode() : 0); hash = hash * 23 + (Binding != null ? Binding.GetHashCode() : 0); hash = hash * 23 + (Configuration != null ? Configuration.GetHashCode() : 0); hash = hash * 23 + (Convention != null ? Convention.GetHashCode() : 0); @@ -637,6 +651,7 @@ private string GetYaml() var s = new SerializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .WithTypeConverter(new FieldMapYamlTypeConverter()) + .WithTypeConverter(new StringArrayMapConverter()) .Build(); return s.Serialize(this); } diff --git a/src/PSRule/Configuration/RepositoryOption.cs b/src/PSRule/Configuration/RepositoryOption.cs index c9a5d0d1c1..caf0ef35e8 100644 --- a/src/PSRule/Configuration/RepositoryOption.cs +++ b/src/PSRule/Configuration/RepositoryOption.cs @@ -96,12 +96,12 @@ internal static RepositoryOption Combine(RepositoryOption o1, RepositoryOption o /// Load options from environment variables into repository option. /// Options that appear in both will replaced by environment variable values. /// - internal void Load(EnvironmentHelper env) + internal void Load() { - if (env.TryString("PSRULE_REPOSITORY_BASEREF", out var baseRef)) + if (Environment.TryString("PSRULE_REPOSITORY_BASEREF", out var baseRef)) BaseRef = baseRef; - if (env.TryString("PSRULE_REPOSITORY_URL", out var url)) + if (Environment.TryString("PSRULE_REPOSITORY_URL", out var url)) Url = url; } diff --git a/src/PSRule/Configuration/RequiresOption.cs b/src/PSRule/Configuration/RequiresOption.cs index b26c46b7e3..6bae994c8f 100644 --- a/src/PSRule/Configuration/RequiresOption.cs +++ b/src/PSRule/Configuration/RequiresOption.cs @@ -48,9 +48,9 @@ public ModuleConstraint[] ToArray() /// /// Load Requires option from environment variables. /// - internal void Load(EnvironmentHelper env) + internal void Load() { - Load(ENVIRONMENT_PREFIX, env, ConvertUnderscore); + Load(ENVIRONMENT_PREFIX, ConvertUnderscore); } /// diff --git a/src/PSRule/Definitions/Expressions/LanguageExpressions.cs b/src/PSRule/Definitions/Expressions/LanguageExpressions.cs index 7dce3d47d8..3e1c6a999f 100644 --- a/src/PSRule/Definitions/Expressions/LanguageExpressions.cs +++ b/src/PSRule/Definitions/Expressions/LanguageExpressions.cs @@ -318,7 +318,7 @@ private LanguageExpressionOuterFn Operator(string path, LanguageOperator express /// /// Returns a quantifier function if set for the expression. /// - private Func GetQuantifier(LanguageOperator expression) + private static Func GetQuantifier(LanguageOperator expression) { if (expression.Property.TryGetLong(GREATEROREQUAL, out var q)) return (number) => number >= q.Value; @@ -339,7 +339,7 @@ private Func GetQuantifier(LanguageOperator expression) } [DebuggerStepThrough] - private string Value(ExpressionContext context, object v) + private static string Value(ExpressionContext context, object v) { return v as string; } diff --git a/src/PSRule/Definitions/Resource.cs b/src/PSRule/Definitions/Resource.cs index 6da6ed1953..6c7af13ba3 100644 --- a/src/PSRule/Definitions/Resource.cs +++ b/src/PSRule/Definitions/Resource.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.IO; using System.Text; +using PSRule.Converters.Yaml; using PSRule.Definitions.Rules; using PSRule.Pipeline; using PSRule.Runtime; @@ -195,7 +196,8 @@ internal ResourceBuilder() .IgnoreUnmatchedProperties() .WithNamingConvention(CamelCaseNamingConvention.Instance) .WithTypeConverter(new FieldMapYamlTypeConverter()) - .WithTypeConverter(new StringArrayYamlTypeConverter()) + .WithTypeConverter(new StringArrayMapConverter()) + .WithTypeConverter(new StringArrayConverter()) .WithNodeDeserializer( inner => new ResourceNodeDeserializer(new LanguageExpressionDeserializer(inner)), s => s.InsteadOf()) @@ -380,7 +382,7 @@ public string ToViewString() foreach (var kv in this) { if (i > 0) - sb.Append(Environment.NewLine); + sb.Append(System.Environment.NewLine); sb.Append(kv.Key); sb.Append('='); diff --git a/src/PSRule/Help/HelpLexer.cs b/src/PSRule/Help/HelpLexer.cs index 98616cb609..3bfdd752c1 100644 --- a/src/PSRule/Help/HelpLexer.cs +++ b/src/PSRule/Help/HelpLexer.cs @@ -169,9 +169,9 @@ private static void AppendEnding(StringBuilder stringBuilder, MarkdownToken toke preserveEnding = true; if (token.IsDoubleLineEnding()) - stringBuilder.Append(preserveEnding ? string.Concat(Environment.NewLine, Environment.NewLine) : Environment.NewLine); + stringBuilder.Append(preserveEnding ? string.Concat(System.Environment.NewLine, System.Environment.NewLine) : System.Environment.NewLine); else if (token.IsSingleLineEnding()) - stringBuilder.Append(preserveEnding ? Environment.NewLine : Space); + stringBuilder.Append(preserveEnding ? System.Environment.NewLine : Space); } } } diff --git a/src/PSRule/Help/MarkdownStream.cs b/src/PSRule/Help/MarkdownStream.cs index b2f62f0813..ba52edcc89 100644 --- a/src/PSRule/Help/MarkdownStream.cs +++ b/src/PSRule/Help/MarkdownStream.cs @@ -470,7 +470,7 @@ public string CaptureUntil(CharacterMatchDelegate match, bool ignoreEscaping = f private string Substring(int start, int length, bool ignoreEscaping = false) { - var newLine = Environment.NewLine.ToCharArray(); + var newLine = System.Environment.NewLine.ToCharArray(); var position = start; var i = 0; diff --git a/src/PSRule/Host/Host.cs b/src/PSRule/Host/Host.cs index 5e54d9b242..2db27a6738 100644 --- a/src/PSRule/Host/Host.cs +++ b/src/PSRule/Host/Host.cs @@ -93,7 +93,7 @@ public TargetObjectVariable() } - public override object Value => RunspaceContext.CurrentThread.TargetObject.Value; + public override object Value => RunspaceContext.CurrentThread.TargetObject?.Value; } internal sealed class ConfigurationVariable : PSVariable @@ -169,7 +169,7 @@ public static InitialSessionState CreateSessionState(Configuration.SessionState private static void SetExecutionPolicy(InitialSessionState state, Microsoft.PowerShell.ExecutionPolicy executionPolicy) { // Only set execution policy on Windows - if (Environment.OSVersion.Platform == PlatformID.Win32NT) + if (System.Environment.OSVersion.Platform == PlatformID.Win32NT) state.ExecutionPolicy = executionPolicy; } } diff --git a/src/PSRule/Host/HostHelper.cs b/src/PSRule/Host/HostHelper.cs index f7ff1de213..ee1ebf6798 100644 --- a/src/PSRule/Host/HostHelper.cs +++ b/src/PSRule/Host/HostHelper.cs @@ -10,6 +10,7 @@ using System.Management.Automation; using Newtonsoft.Json; using PSRule.Annotations; +using PSRule.Converters.Yaml; using PSRule.Definitions; using PSRule.Definitions.Baselines; using PSRule.Definitions.Conventions; @@ -252,7 +253,8 @@ private static ILanguageBlock[] GetYamlLanguageBlocks(Source[] sources, Runspace .IgnoreUnmatchedProperties() .WithNamingConvention(CamelCaseNamingConvention.Instance) .WithTypeConverter(new FieldMapYamlTypeConverter()) - .WithTypeConverter(new StringArrayYamlTypeConverter()) + .WithTypeConverter(new StringArrayMapConverter()) + .WithTypeConverter(new Converters.Yaml.StringArrayConverter()) .WithTypeConverter(new PSObjectYamlTypeConverter()) .WithNodeTypeResolver(new PSOptionYamlTypeResolver()) .WithNodeDeserializer( diff --git a/src/PSRule/PSRule.psm1 b/src/PSRule/PSRule.psm1 index 55f844e86c..c4d35a9f6f 100644 --- a/src/PSRule/PSRule.psm1 +++ b/src/PSRule/PSRule.psm1 @@ -708,7 +708,7 @@ function Get-PSRule { # Check that some matching script files were found if ($Null -eq $sourceFiles) { - Write-Verbose -Message "[Get-PSRule] -- Could not find any .Rule.ps1 script files in the path"; + Write-Verbose -Message "[Get-PSRule] -- Could not find any .Rule.ps1 script files in the path."; return; # continue causes issues with Pester } @@ -737,8 +737,6 @@ function Get-PSRule { $builder.IncludeDependencies(); } - # $builder.UseCommandRuntime($PSCmdlet); - # $builder.UseExecutionContext($ExecutionContext); try { $pipeline = $builder.Build(); if ($Null -ne $pipeline) { @@ -828,7 +826,7 @@ function Get-PSRuleBaseline { # Check that some matching script files were found if ($Null -eq $sourceFiles) { - Write-Verbose -Message "[Get-PSRuleBaseline] -- Could not find any .Rule.ps1 script files in the path"; + Write-Verbose -Message "[Get-PSRuleBaseline] -- Could not find any .Rule.ps1 script files in the path."; return; # continue causes issues with Pester } @@ -935,7 +933,7 @@ function Export-PSRuleBaseline { # Check that some matching script files were found if ($Null -eq $sourceFiles) { - Write-Verbose -Message "[Export-PSRuleBaseline] -- Could not find any .Rule.ps1 script files in the path"; + Write-Verbose -Message "[Export-PSRuleBaseline] -- Could not find any .Rule.ps1 script files in the path."; return; # continue causes issues with Pester } @@ -1043,7 +1041,7 @@ function Get-PSRuleHelp { # Check that some matching script files were found if ($Null -eq $sourceFiles) { - Write-Verbose -Message "[Get-PSRuleHelp] -- Could not find any .Rule.ps1 script files in the path"; + Write-Verbose -Message "[Get-PSRuleHelp] -- Could not find any .Rule.ps1 script files in the path."; return; # continue causes issues with Pester } @@ -1129,6 +1127,10 @@ function New-PSRuleOption { # Options + # Sets the Baseline.Group option + [Parameter(Mandatory = $False)] + [Hashtable]$BaselineGroup, + # Sets the Binding.IgnoreCase option [Parameter(Mandatory = $False)] [System.Boolean]$BindingIgnoreCase = $True, @@ -1455,6 +1457,10 @@ function Set-PSRuleOption { # Options + # Sets the Baseline.Group option + [Parameter(Mandatory = $False)] + [Hashtable]$BaselineGroup, + # Sets the Binding.IgnoreCase option [Parameter(Mandatory = $False)] [System.Boolean]$BindingIgnoreCase = $True, @@ -2228,6 +2234,10 @@ function SetOptions { # Options + # Sets the Baseline.Group option + [Parameter(Mandatory = $False)] + [Hashtable]$BaselineGroup, + # Sets the Binding.IgnoreCase option [Parameter(Mandatory = $False)] [System.Boolean]$BindingIgnoreCase = $True, @@ -2459,6 +2469,11 @@ function SetOptions { process { # Options + # Sets option Baseline.Group + if ($PSBoundParameters.ContainsKey('BaselineGroup')) { + $Option.Baseline.Group = $BaselineGroup; + } + # Sets option Binding.IgnoreCase if ($PSBoundParameters.ContainsKey('BindingIgnoreCase')) { $Option.Binding.IgnoreCase = $BindingIgnoreCase; diff --git a/src/PSRule/Pipeline/ExportBaselinePipelineBuilder.cs b/src/PSRule/Pipeline/ExportBaselinePipelineBuilder.cs index 59a84db420..f4bd2e29d6 100644 --- a/src/PSRule/Pipeline/ExportBaselinePipelineBuilder.cs +++ b/src/PSRule/Pipeline/ExportBaselinePipelineBuilder.cs @@ -29,20 +29,19 @@ public override IPipelineBuilder Configure(PSRuleOption option) if (option == null) return this; + Option.Baseline = new Options.BaselineOption(option.Baseline); Option.Output.As = ResultFormat.Detail; Option.Output.Culture = GetCulture(option.Output.Culture); Option.Output.Format = option.Output.Format ?? OutputOption.Default.Format; Option.Output.Encoding = option.Output.Encoding ?? OutputOption.Default.Encoding; Option.Output.Path = option.Output.Path ?? OutputOption.Default.Path; Option.Output.JsonIndent = NormalizeJsonIndentRange(option.Output.JsonIndent); - return this; } public override IPipeline Build(IPipelineWriter writer = null) { var filter = new BaselineFilter(_Name); - return new GetBaselinePipeline( pipeline: PrepareContext( bindTargetName: null, @@ -56,4 +55,4 @@ public override IPipeline Build(IPipelineWriter writer = null) ); } } -} \ No newline at end of file +} diff --git a/src/PSRule/Pipeline/Formatters/AssertFormatter.cs b/src/PSRule/Pipeline/Formatters/AssertFormatter.cs index 5f515e08ba..9aa711c942 100644 --- a/src/PSRule/Pipeline/Formatters/AssertFormatter.cs +++ b/src/PSRule/Pipeline/Formatters/AssertFormatter.cs @@ -325,7 +325,7 @@ private void Banner() if (!Option.Output.Banner.GetValueOrDefault(BannerFormat.Default).HasFlag(BannerFormat.Title)) return; - WriteLine(FormatterStrings.Banner.Replace("\\n", Environment.NewLine)); + WriteLine(FormatterStrings.Banner.Replace("\\n", System.Environment.NewLine)); LineBreak(); } @@ -551,7 +551,7 @@ protected void WriteLines(string message, string prefix = null, ConsoleColor? fo if (string.IsNullOrEmpty(message)) return; - var lines = message.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); + var lines = message.Split(new string[] { System.Environment.NewLine }, StringSplitOptions.None); for (var i = 0; i < lines.Length; i++) WriteLine(lines[i], prefix, forgroundColor); } diff --git a/src/PSRule/Pipeline/GetBaselinePipelineBuilder.cs b/src/PSRule/Pipeline/GetBaselinePipelineBuilder.cs index 2464b6830b..765b5e6fc7 100644 --- a/src/PSRule/Pipeline/GetBaselinePipelineBuilder.cs +++ b/src/PSRule/Pipeline/GetBaselinePipelineBuilder.cs @@ -29,18 +29,17 @@ public override IPipelineBuilder Configure(PSRuleOption option) if (option == null) return this; + Option.Baseline = new Options.BaselineOption(option.Baseline); Option.Output.As = ResultFormat.Detail; Option.Output.Culture = GetCulture(option.Output.Culture); Option.Output.Format = SuppressFormat(option.Output.Format); Option.Output.JsonIndent = NormalizeJsonIndentRange(option.Output.JsonIndent); - return this; } public override IPipeline Build(IPipelineWriter writer = null) { - var filter = new BaselineFilter(_Name); - + var filter = new BaselineFilter(ResolveBaselineGroup(_Name)); return new GetBaselinePipeline( pipeline: PrepareContext( bindTargetName: null, @@ -61,4 +60,4 @@ private static OutputFormat SuppressFormat(OutputFormat? format) format == OutputFormat.Json) ? OutputFormat.None : format.Value; } } -} \ No newline at end of file +} diff --git a/src/PSRule/Pipeline/GetRulePipelineBuiler.cs b/src/PSRule/Pipeline/GetRulePipelineBuilder.cs similarity index 97% rename from src/PSRule/Pipeline/GetRulePipelineBuiler.cs rename to src/PSRule/Pipeline/GetRulePipelineBuilder.cs index e3981aae8b..5ac72e6430 100644 --- a/src/PSRule/Pipeline/GetRulePipelineBuiler.cs +++ b/src/PSRule/Pipeline/GetRulePipelineBuilder.cs @@ -32,12 +32,12 @@ public override IPipelineBuilder Configure(PSRuleOption option) if (option == null) return this; + Option.Baseline = new Options.BaselineOption(option.Baseline); 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); Option.Output.JsonIndent = NormalizeJsonIndentRange(option.Output.JsonIndent); - if (option.Rule != null) Option.Rule = new RuleOption(option.Rule); diff --git a/src/PSRule/Pipeline/InvokePipelineBuilder.cs b/src/PSRule/Pipeline/InvokePipelineBuilder.cs index ef0bdc2569..be2cb2a9f0 100644 --- a/src/PSRule/Pipeline/InvokePipelineBuilder.cs +++ b/src/PSRule/Pipeline/InvokePipelineBuilder.cs @@ -71,7 +71,7 @@ public void ResultVariable(string variableName) public void UnblockPublisher(string publisher) { - if (Environment.OSVersion.Platform != PlatformID.Win32NT) + if (System.Environment.OSVersion.Platform != PlatformID.Win32NT) return; _TrustedPublishers ??= new List(); @@ -124,7 +124,7 @@ protected void Unblock(IPipelineWriter writer) { if (Source == null || Source.Length == 0 || _TrustedPublishers == null || _TrustedPublishers.Count == 0 || - Environment.OSVersion.Platform != PlatformID.Win32NT) + System.Environment.OSVersion.Platform != PlatformID.Win32NT) return; var files = new List(); diff --git a/src/PSRule/Pipeline/Output/CsvOutputWriter.cs b/src/PSRule/Pipeline/Output/CsvOutputWriter.cs index c04b49c03a..c14856d094 100644 --- a/src/PSRule/Pipeline/Output/CsvOutputWriter.cs +++ b/src/PSRule/Pipeline/Output/CsvOutputWriter.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Text; using PSRule.Configuration; using PSRule.Definitions; @@ -79,7 +78,7 @@ private void WriteHeader() _Builder.Append(ViewStrings.Synopsis); _Builder.Append(COMMA); _Builder.Append(ViewStrings.Recommendation); - _Builder.Append(Environment.NewLine); + _Builder.Append(System.Environment.NewLine); } private void VisitRecord(RuleRecord record) @@ -100,7 +99,7 @@ private void VisitRecord(RuleRecord record) WriteColumn(record.Info.Synopsis); _Builder.Append(COMMA); WriteColumn(record.Info.Recommendation); - _Builder.Append(Environment.NewLine); + _Builder.Append(System.Environment.NewLine); } private void VisitSummaryRecord(RuleSummaryRecord record) @@ -119,7 +118,7 @@ private void VisitSummaryRecord(RuleSummaryRecord record) WriteColumn(record.Info.Synopsis); _Builder.Append(COMMA); WriteColumn(record.Info.Recommendation); - _Builder.Append(Environment.NewLine); + _Builder.Append(System.Environment.NewLine); } private void WriteColumn(string value) diff --git a/src/PSRule/Pipeline/Output/NUnit3OutputWriter.cs b/src/PSRule/Pipeline/Output/NUnit3OutputWriter.cs index 63b2ec75e7..6b154a4333 100644 --- a/src/PSRule/Pipeline/Output/NUnit3OutputWriter.cs +++ b/src/PSRule/Pipeline/Output/NUnit3OutputWriter.cs @@ -56,14 +56,14 @@ protected override string Serialize(InvokeResult[] o) xml.WriteAttributeString("time", TimeSpan.FromMilliseconds(time).ToString()); xml.WriteStartElement("environment"); - xml.WriteAttributeString("user", Environment.UserName); - xml.WriteAttributeString("machine-name", Environment.MachineName); + xml.WriteAttributeString("user", System.Environment.UserName); + xml.WriteAttributeString("machine-name", System.Environment.MachineName); xml.WriteAttributeString("cwd", PSRuleOption.GetWorkingPath()); - xml.WriteAttributeString("user-domain", Environment.UserDomainName); - xml.WriteAttributeString("platform", Environment.OSVersion.Platform.ToString()); + xml.WriteAttributeString("user-domain", System.Environment.UserDomainName); + xml.WriteAttributeString("platform", System.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.WriteAttributeString("os-version", System.Environment.OSVersion.Version.ToString()); + xml.WriteAttributeString("clr-version", System.Environment.Version.ToString()); xml.WriteEndElement(); xml.WriteStartElement("culture-info"); diff --git a/src/PSRule/Pipeline/PipelineBuilder.cs b/src/PSRule/Pipeline/PipelineBuilder.cs index 672b0dca9e..248237aa4e 100644 --- a/src/PSRule/Pipeline/PipelineBuilder.cs +++ b/src/PSRule/Pipeline/PipelineBuilder.cs @@ -5,7 +5,6 @@ using System.Collections; using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Management.Automation; using System.Threading; using PSRule.Configuration; @@ -348,6 +347,7 @@ public virtual IPipelineBuilder Configure(PSRuleOption option) if (option == null) return this; + Option.Baseline = new Options.BaselineOption(option.Baseline); Option.Binding = new BindingOption(option.Binding); Option.Convention = new ConventionOption(option.Convention); Option.Execution = GetExecutionOption(option.Execution); @@ -441,16 +441,7 @@ protected PipelineContext PrepareContext(BindTargetMethod bindTargetName, BindTa { var unresolved = new List(); if (_Baseline is BaselineOption.BaselineRef baselineRef) - unresolved.Add(new BaselineRef(baselineRef.Name, OptionContext.ScopeType.Explicit)); - - for (var i = 0; Source != null && i < Source.Length; i++) - { - if (Source[i].Module != null && Source[i].Module.Baseline != null && !unresolved.Any(u => ResourceIdEqualityComparer.IdEquals(u.Id, Source[i].Module.Baseline))) - { - unresolved.Add(new BaselineRef(Source[i].Module.Baseline, OptionContext.ScopeType.Module)); - PrepareWriter().WarnModuleManifestBaseline(Source[i].Module.Name); - } - } + unresolved.Add(new BaselineRef(ResolveBaselineGroup(baselineRef.Name), OptionContext.ScopeType.Explicit)); return PipelineContext.New( option: Option, @@ -464,6 +455,30 @@ protected PipelineContext PrepareContext(BindTargetMethod bindTargetName, BindTa ); } + protected string[] ResolveBaselineGroup(string[] name) + { + for (var i = 0; name != null && i < name.Length; i++) + name[i] = ResolveBaselineGroup(name[i]); + + return name; + } + + protected string ResolveBaselineGroup(string name) + { + if (name == null || name.Length < 2 || !name.StartsWith("@") || + Option == null || Option.Baseline == null || Option.Baseline.Group == null || + Option.Baseline.Group.Count == 0) + return name; + + var key = name.Substring(1); + if (!Option.Baseline.Group.TryGetValue(key, out var baselines) || baselines.Length == 0) + throw new PipelineConfigurationException("Baseline.Group", string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.PSR0003, key)); + + var writer = PrepareWriter(); + writer.WriteVerbose($"Using baseline group '{key}': {baselines[0]}"); + return baselines[0]; + } + protected virtual PipelineReader PrepareReader() { return new PipelineReader(null, null, GetInputObjectSourceFilter()); @@ -676,13 +691,13 @@ protected static OutputStyle GetStyle(OutputStyle style) if (style != OutputStyle.Detect) return style; - if (EnvironmentHelper.Default.IsAzurePipelines()) + if (Environment.IsAzurePipelines()) return OutputStyle.AzurePipelines; - if (EnvironmentHelper.Default.IsGitHubActions()) + if (Environment.IsGitHubActions()) return OutputStyle.GitHubActions; - return EnvironmentHelper.Default.IsVisualStudioCode() ? + return Environment.IsVisualStudioCode() ? OutputStyle.VisualStudioCode : OutputStyle.Client; } diff --git a/src/PSRule/Pipeline/PipelineContext.cs b/src/PSRule/Pipeline/PipelineContext.cs index 68937bb7a6..ffa6412fc6 100644 --- a/src/PSRule/Pipeline/PipelineContext.cs +++ b/src/PSRule/Pipeline/PipelineContext.cs @@ -17,6 +17,7 @@ using PSRule.Definitions.Selectors; using PSRule.Definitions.SuppressionGroups; using PSRule.Host; +using PSRule.Resources; using PSRule.Runtime; using PSRule.Runtime.ObjectPath; @@ -89,7 +90,7 @@ private PipelineContext(PSRuleOption option, IHostContext hostContext, PipelineR Baseline = baseline; _Unresolved = unresolved ?? new List(); _TrackedIssues = new List(); - RunId = EnvironmentHelper.Default.GetRunId() ?? ObjectHashAlgorithm.GetDigest(Guid.NewGuid().ToByteArray()); + RunId = Environment.GetRunId() ?? ObjectHashAlgorithm.GetDigest(Guid.NewGuid().ToByteArray()); RunTime = Stopwatch.StartNew(); } @@ -171,7 +172,7 @@ internal void Import(RunspaceContext context, IResource resource) TrackIssue(resource); if (TryBaseline(resource, out var baseline) && TryBaselineRef(resource.Id, out var baselineRef)) { - _Unresolved.Remove(baselineRef); + RemoveBaselineRef(resource.Id); 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) @@ -222,6 +223,15 @@ private bool TryBaselineRef(ResourceId resourceId, out BaselineRef baselineRef) return true; } + private void RemoveBaselineRef(ResourceId resourceId) + { + foreach (var r in _Unresolved.ToArray()) + { + if (ResourceIdEqualityComparer.IdEquals(r.Id, resourceId.Value)) + _Unresolved.Remove(r); + } + } + private static bool TryBaseline(IResource resource, out Baseline baseline) { baseline = null; @@ -249,10 +259,20 @@ private static bool TryModuleConfig(IResource resource, out ModuleConfigV1 modul internal void Begin(RunspaceContext runspaceContext) { + ReportUnresolved(runspaceContext); ReportIssue(runspaceContext); Baseline.CheckObsolete(runspaceContext); } + private void ReportUnresolved(RunspaceContext runspaceContext) + { + foreach (var unresolved in _Unresolved) + runspaceContext.ErrorResourceUnresolved(unresolved.Kind, unresolved.Id); + + if (_Unresolved.Count > 0) + throw new PipelineBuilderException(PSRuleResources.ErrorPipelineException); + } + /// /// Report any tracked issues. /// diff --git a/src/PSRule/Pipeline/PipelineWriterExtensions.cs b/src/PSRule/Pipeline/PipelineWriterExtensions.cs index 4b3dcdc327..96263c715e 100644 --- a/src/PSRule/Pipeline/PipelineWriterExtensions.cs +++ b/src/PSRule/Pipeline/PipelineWriterExtensions.cs @@ -40,14 +40,6 @@ internal static void WarnRulePathNotFound(this IPipelineWriter writer) writer.WriteWarning(PSRuleResources.RulePathNotFound); } - internal static void WarnModuleManifestBaseline(this IPipelineWriter writer, string moduleName) - { - if (writer == null || !writer.ShouldWriteWarning()) - return; - - writer.WriteWarning(PSRuleResources.ModuleManifestBaseline, moduleName); - } - internal static void WriteWarning(this IPipelineWriter writer, string message, params object[] args) { if (writer == null || !writer.ShouldWriteWarning() || string.IsNullOrEmpty(message)) diff --git a/src/PSRule/Pipeline/Source.cs b/src/PSRule/Pipeline/Source.cs index 2be5ec15d9..ac7d809f6c 100644 --- a/src/PSRule/Pipeline/Source.cs +++ b/src/PSRule/Pipeline/Source.cs @@ -140,7 +140,6 @@ internal sealed class ModuleInfo public readonly string Path; public readonly string Name; - public readonly string Baseline; public readonly string Version; public readonly string ProjectUri; public readonly string Guid; diff --git a/src/PSRule/Pipeline/SourcePipeline.cs b/src/PSRule/Pipeline/SourcePipeline.cs index 3b2c8abdc0..0495322996 100644 --- a/src/PSRule/Pipeline/SourcePipeline.cs +++ b/src/PSRule/Pipeline/SourcePipeline.cs @@ -258,7 +258,7 @@ private bool TryPackagedModule(string name, out string path) private bool TryInstalledModule(string name, out string path) { path = null; - if (!EnvironmentHelper.Default.TryPathEnvironmentVariable("PSModulePath", out var searchPaths)) + if (!Environment.TryPathEnvironmentVariable("PSModulePath", out var searchPaths)) return false; var unsorted = new List(); diff --git a/src/PSRule/Resources/PSRuleResources.Designer.cs b/src/PSRule/Resources/PSRuleResources.Designer.cs index 48f47895db..f31ce2c785 100644 --- a/src/PSRule/Resources/PSRuleResources.Designer.cs +++ b/src/PSRule/Resources/PSRuleResources.Designer.cs @@ -429,15 +429,6 @@ internal static string MatchTrue { } } - /// - /// 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 { - return ResourceManager.GetString("ModuleManifestBaseline", resourceCulture); - } - } - /// /// Looks up a localized string similar to No valid module can be found with that name.. /// @@ -582,6 +573,24 @@ internal static string PSR0002 { } } + /// + /// Looks up a localized string similar to PSR0003: The specified baseline group '{0}' is not known.. + /// + internal static string PSR0003 { + get { + return ResourceManager.GetString("PSR0003", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PSR0004: The specified {0} resource '{1}' is not known.. + /// + internal static string PSR0004 { + get { + return ResourceManager.GetString("PSR0004", resourceCulture); + } + } + /// /// Looks up a localized string similar to Failed to deserialize the file '{0}': {1}. /// diff --git a/src/PSRule/Resources/PSRuleResources.resx b/src/PSRule/Resources/PSRuleResources.resx index 55a97d3820..78e1c273fd 100644 --- a/src/PSRule/Resources/PSRuleResources.resx +++ b/src/PSRule/Resources/PSRuleResources.resx @@ -226,9 +226,6 @@ Matches: {0} - - 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. - Target object '{0}' has not been processed because no matching rules were found. @@ -412,4 +409,10 @@ Failed to read the path '{0}': {1} + + PSR0003: The specified baseline group '{0}' is not known. + + + PSR0004: The specified {0} resource '{1}' is not known. + \ No newline at end of file diff --git a/src/PSRule/Rules/RuleHelpInfo.cs b/src/PSRule/Rules/RuleHelpInfo.cs index d238b35bf2..9b86a6c049 100644 --- a/src/PSRule/Rules/RuleHelpInfo.cs +++ b/src/PSRule/Rules/RuleHelpInfo.cs @@ -217,7 +217,7 @@ public string GetLinkString() sb.Append(": "); sb.Append(Links[i].Uri); } - sb.Append(Environment.NewLine); + sb.Append(System.Environment.NewLine); } return sb.ToString(); } diff --git a/src/PSRule/Rules/RuleRecord.cs b/src/PSRule/Rules/RuleRecord.cs index 5a47c23c6d..60293eb1ed 100644 --- a/src/PSRule/Rules/RuleRecord.cs +++ b/src/PSRule/Rules/RuleRecord.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Collections; using System.ComponentModel; using System.Diagnostics; @@ -214,7 +213,7 @@ public bool IsProcessed() /// public string GetReasonViewString() { - return Reason == null || Reason.Length == 0 ? string.Empty : string.Join(Environment.NewLine, Reason); + return Reason == null || Reason.Length == 0 ? string.Empty : string.Join(System.Environment.NewLine, Reason); } internal bool HasSource() diff --git a/src/PSRule/Runtime/ILogger.cs b/src/PSRule/Runtime/ILogger.cs index b1f99e98d5..ed41b4bc8e 100644 --- a/src/PSRule/Runtime/ILogger.cs +++ b/src/PSRule/Runtime/ILogger.cs @@ -42,5 +42,12 @@ internal interface ILogger /// The warning message write. /// Any arguments to format the string with. void Warning(string message, params object[] args); + + /// + /// Write an error from an exception. + /// + /// The exception to write. + /// A string identififer for the error. + void Error(Exception exception, string errorId = null); } } diff --git a/src/PSRule/Runtime/RunspaceContext.cs b/src/PSRule/Runtime/RunspaceContext.cs index 8a3302013e..9f7560eb1f 100644 --- a/src/PSRule/Runtime/RunspaceContext.cs +++ b/src/PSRule/Runtime/RunspaceContext.cs @@ -527,7 +527,7 @@ private string GetStackTrace(ErrorRecord record) ? record.ScriptStackTrace : string.Concat( record.ScriptStackTrace, - Environment.NewLine, + System.Environment.NewLine, string.Format( Thread.CurrentThread.CurrentCulture, PSRuleResources.RuleStackTrace, @@ -578,7 +578,7 @@ private static int GetPositionMessageOffset(string positionMessage) if (string.IsNullOrEmpty(positionMessage)) return 0; - var lines = positionMessage.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + var lines = positionMessage.Split(new string[] { System.Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); return lines.Length != 3 ? 0 : lines[2].LastIndexOf('~') - 1; } @@ -931,6 +931,15 @@ public void Warning(string message, params object[] args) Writer.WriteWarning(message, args); } + /// + public void Error(Exception exception, string errorId = null) + { + if (Writer == null || exception == null) + return; + + Writer.WriteError(new ErrorRecord(exception, errorId, ErrorCategory.InvalidOperation, null)); + } + #endregion ILogger #region IDisposable diff --git a/tests/PSRule.Tests/OutputWriterTests.cs b/tests/PSRule.Tests/OutputWriterTests.cs index 570d5e1365..990df8ff65 100644 --- a/tests/PSRule.Tests/OutputWriterTests.cs +++ b/tests/PSRule.Tests/OutputWriterTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Collections; using System.IO; using System.Linq; @@ -302,7 +301,7 @@ public void NUnit3() 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"); + var xml = doc["test-results"]["test-suite"].OuterXml.Replace(System.Environment.NewLine, "\r\n"); Assert.Equal("", xml); } @@ -325,7 +324,7 @@ public void JobSummary() stream.Seek(0, SeekOrigin.Begin); using var reader = new StreamReader(stream); - var s = reader.ReadToEnd().Replace(Environment.NewLine, "\r\n"); + var s = reader.ReadToEnd().Replace(System.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 {context.RunTime.Elapsed}.\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); } diff --git a/tests/PSRule.Tests/PSRule.Baseline.Tests.ps1 b/tests/PSRule.Tests/PSRule.Baseline.Tests.ps1 index b67ad6b8f6..70c78cd174 100644 --- a/tests/PSRule.Tests/PSRule.Baseline.Tests.ps1 +++ b/tests/PSRule.Tests/PSRule.Baseline.Tests.ps1 @@ -380,6 +380,16 @@ Describe 'Get-PSRuleBaseline' -Tag 'Baseline','Get-PSRuleBaseline' { $result | Should -Not -BeNullOrEmpty; $result.Length | Should -Be 1; $result[0].Name | Should -Be 'TestBaseline1'; + + # Use a baseline group by name + $result = @(Get-PSRuleBaseline -Path $baselineFilePath -Name '@latest' -Option @{ + 'Baseline.Group' = @{ + latest = 'TestBaseline1' + } + }); + $result | Should -Not -BeNullOrEmpty; + $result.Length | Should -Be 1; + $result[0].Name | Should -Be 'TestBaseline1'; } It 'With -Module' { @@ -696,6 +706,20 @@ Describe 'Baseline' -Tag 'Baseline' { $result[0].TargetName | Should -Be 'TestObject1'; $result[0].TargetType | Should -Be 'TestObjectType'; $result[0].Field.kind | Should -Be 'TestObjectType'; + + # Use a baseline group by name + $result = @($testObject | Invoke-PSRule -Path $ruleFilePath,$baselineFilePath -Baseline '@latest' -Option @{ + 'Baseline.Group' = @{ + latest = 'TestBaseline1' + } + }); + $result | Should -Not -BeNullOrEmpty; + $result.Length | Should -Be 1; + $result[0].RuleName | Should -Be 'WithBaseline'; + $result[0].Outcome | Should -Be 'Pass'; + $result[0].TargetName | Should -Be 'TestObject1'; + $result[0].TargetType | Should -Be 'TestObjectType'; + $result[0].Field.kind | Should -Be 'TestObjectType'; } It 'With -Module' { diff --git a/tests/PSRule.Tests/PSRule.Options.Tests.ps1 b/tests/PSRule.Tests/PSRule.Options.Tests.ps1 index 17452206e1..0ba4396a01 100644 --- a/tests/PSRule.Tests/PSRule.Options.Tests.ps1 +++ b/tests/PSRule.Tests/PSRule.Options.Tests.ps1 @@ -255,6 +255,47 @@ Describe 'New-PSRuleOption' -Tag 'Option','New-PSRuleOption' { } } + Context 'Read Baseline.Group' { + It 'from default' { + $option = New-PSRuleOption -Default; + $option.Baseline.Group | Should -BeNullOrEmpty; + } + + It 'from Hashtable' { + $option = New-PSRuleOption -Option @{ 'Baseline.Group' = @{ 'test' = 'Test123' } }; + $option.Baseline.Group.Count | Should -Be 1; + $option.Baseline.Group['test'] | Should -Not -BeNullOrEmpty; + $option.Baseline.Group['test'][0] | Should -Be 'Test123'; + } + + It 'from YAML' { + $option = New-PSRuleOption -Option (Join-Path -Path $here -ChildPath 'PSRule.Tests.yml'); + $option.Baseline.Group.Count | Should -Be 1; + $option.Baseline.Group['latest'] | Should -Not -BeNullOrEmpty; + $option.Baseline.Group['latest'][0] | Should -Be '.\TestBaseline1'; + } + + It 'from Environment' { + try { + $Env:PSRULE_BASELINE_GROUP = 'latest=YourBaseline'; + $option = New-PSRuleOption; + $option.Baseline.Group.Count | Should -Be 1; + $option.Baseline.Group['latest'] | Should -Not -BeNullOrEmpty; + $option.Baseline.Group['latest'][0] | Should -Be 'YourBaseline'; + } + finally { + Remove-Item 'Env:PSRULE_BASELINE_GROUP' -Force; + } + } + + It 'from parameter' { + $option = New-PSRuleOption -BaselineGroup @{ test = 'Test123' } -Path $emptyOptionsFilePath; + $option.Baseline.Group.Count | Should -Be 1; + $option.Baseline.Group['test'] | Should -Not -BeNullOrEmpty; + $option.Baseline.Group['test'][0] | Should -Be 'Test123'; + } + } + Context 'Read Binding.Field' { It 'from default' { $option = New-PSRuleOption -Default; diff --git a/tests/PSRule.Tests/PSRule.Tests.csproj b/tests/PSRule.Tests/PSRule.Tests.csproj index 327ed15505..39f19b3f75 100644 --- a/tests/PSRule.Tests/PSRule.Tests.csproj +++ b/tests/PSRule.Tests/PSRule.Tests.csproj @@ -63,6 +63,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/tests/PSRule.Tests/PSRule.Tests.yml b/tests/PSRule.Tests/PSRule.Tests.yml index 16a83ec8e3..543ad61d1a 100644 --- a/tests/PSRule.Tests/PSRule.Tests.yml +++ b/tests/PSRule.Tests/PSRule.Tests.yml @@ -4,6 +4,10 @@ repository: url: 'https://github.com/microsoft/PSRule.UnitTest' baseRef: dev +baseline: + group: + latest: .\TestBaseline1 + # Configure baseline rule: include: diff --git a/tests/PSRule.Tests/PSRuleOptionTests.cs b/tests/PSRule.Tests/PSRuleOptionTests.cs index 5c6ba4d2c5..f3ff57b7cd 100644 --- a/tests/PSRule.Tests/PSRuleOptionTests.cs +++ b/tests/PSRule.Tests/PSRuleOptionTests.cs @@ -74,6 +74,17 @@ public void GetObjectArrayFromYaml() Assert.Equal("East US", pso.PropertyValue("location")); } + [Fact] + public void GetBaselineGroupFromYaml() + { + var option = GetOption(); + var actual = option.Baseline.Group; + Assert.NotNull(actual); + Assert.Single(actual); + Assert.True(actual.TryGetValue("latest", out var latest)); + Assert.Equal(new string[] { ".\\TestBaseline1" }, latest); + } + #region Helper methods private static Runtime.Configuration GetConfigurationHelper(PSRuleOption option) @@ -94,12 +105,7 @@ private static Source[] GetSource() private static PSRuleOption GetOption() { - var loaded = PSRuleOption.FromFile(GetSourcePath("PSRule.Tests.yml")); - var option = new PSRuleOption - { - Configuration = loaded.Configuration - }; - return option; + return PSRuleOption.FromFile(GetSourcePath("PSRule.Tests.yml")); } private static string GetSourcePath(string fileName) diff --git a/tests/PSRule.Tests/PipelineTests.cs b/tests/PSRule.Tests/PipelineTests.cs index 5f9b385a74..c7bb4e0d1a 100644 --- a/tests/PSRule.Tests/PipelineTests.cs +++ b/tests/PSRule.Tests/PipelineTests.cs @@ -150,6 +150,31 @@ public void BuildGetPipeline() Assert.NotNull(builder.Build()); } + [Fact] + public void GetRuleWithBaseline() + { + var option = PSRuleOption.FromDefault(); + option.Baseline.Group = new Data.StringArrayMap(); + option.Baseline.Group["test"] = new string[] { ".\\TestBaseline1" }; + option.Execution.InvariantCulture = ExecutionActionPreference.Ignore; + Assert.Single(option.Baseline.Group); + + var builder = PipelineBuilder.Get(GetSource(new string[] + { + "Baseline.Rule.yaml", + "FromFileBaseline.Rule.ps1" + }), option, null); + builder.Baseline(BaselineOption.FromString("@test")); + var writer = new TestWriter(option); + var pipeline = builder.Build(writer); + + pipeline.Begin(); + pipeline.Process(null); + pipeline.End(); + + Assert.Single(writer.Output); + } + [Fact] public void PipelineWithInvariantCulture() { @@ -309,10 +334,13 @@ public void PipelineWithFileFormat() #region Helper methods - private static Source[] GetSource() + private static Source[] GetSource(string[] files = null) { var builder = new SourcePipelineBuilder(null, null); builder.Directory(GetSourcePath("FromFile.Rule.ps1")); + for (var i = 0; files != null && i < files.Length; i++) + builder.Directory(GetSourcePath(files[i])); + return builder.Build(); } diff --git a/tests/PSRule.Tests/SelectorTests.cs b/tests/PSRule.Tests/SelectorTests.cs index 2282da2529..25fd0cb04c 100644 --- a/tests/PSRule.Tests/SelectorTests.cs +++ b/tests/PSRule.Tests/SelectorTests.cs @@ -1673,7 +1673,7 @@ public void AllOf(string type, string path) GetObject((name: "name", value: "log1")), GetObject((name: "name", value: "log2")) })))); - actual3 = GetObject((name: "Name", value: "TargetObject1"), (name: "properties", value: GetObject((name: "logs", value: new object[] { })))); + actual3 = GetObject((name: "Name", value: "TargetObject1"), (name: "properties", value: GetObject((name: "logs", value: Array.Empty())))); Assert.True(allOf.Match(actual1)); Assert.True(allOf.Match(actual2));