diff --git a/CHANGELOG.md b/CHANGELOG.md index aa38447a0e..300e5e17d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Fixed multiple value tag filtering. [#346](https://github.com/Microsoft/PSRule/issues/346) +- Added filtering for rules against a baseline with `Get-PSRule`. [#345](https://github.com/Microsoft/PSRule/issues/345) + ## v0.12.0-B1912002 (pre-release) - Fixed TargetType fall back to type name. [#339](https://github.com/Microsoft/PSRule/issues/339) diff --git a/docs/commands/PSRule/en-US/Get-PSRule.md b/docs/commands/PSRule/en-US/Get-PSRule.md index f8d532ccc0..209c2fe632 100644 --- a/docs/commands/PSRule/en-US/Get-PSRule.md +++ b/docs/commands/PSRule/en-US/Get-PSRule.md @@ -14,9 +14,9 @@ Get a list of rule definitions. ## SYNTAX ```text -Get-PSRule [-Module ] [-ListAvailable] [-OutputFormat ] [[-Path] ] - [-Name ] [-Tag ] [-Option ] [-Culture ] [-IncludeDependencies] - [] +Get-PSRule [-Module ] [-ListAvailable] [-OutputFormat ] [-Baseline ] + [[-Path] ] [-Name ] [-Tag ] [-Option ] [-Culture ] + [-IncludeDependencies] [] ``` ## DESCRIPTION @@ -263,6 +263,22 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -Baseline + +When specified, rules are filtered so that only rules that are included in the baselines are returned. + +```yaml +Type: BaselineOption +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### CommonParameters This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). diff --git a/docs/concepts/PSRule/en-US/about_PSRule_Options.md b/docs/concepts/PSRule/en-US/about_PSRule_Options.md index 2973ba9d27..fac1240659 100644 --- a/docs/concepts/PSRule/en-US/about_PSRule_Options.md +++ b/docs/concepts/PSRule/en-US/about_PSRule_Options.md @@ -951,22 +951,36 @@ rule: A set of required key value pairs (tags) that rules must have applied to them to be included. +Multiple values can be specified for the same tag. +When multiple values are used, only one must match. + This option can be overridden at runtime by using the `-Tag` cmdlet parameter. This option can be specified using: ```powershell # PowerShell: Using the Rule.Tag hashtable key -# $option = New-PSRuleOption -Option @{ 'Rule.Tag' = 'Rule3','Rule4' }; +$option = New-PSRuleOption -Option @{ 'Rule.Tag' = @{ severity = 'Critical','Warning' } }; ``` ```yaml # YAML: Using the rule/tag property rule: tag: - key1: value1 + severity: Critical +``` + +```yaml +# YAML: Using the rule/tag property, with multiple values +rule: + tag: + severity: + - Critical + - Warning ``` +In the example above, rules must have a tag of `severity` set to either `Critical` or `Warning` to be included. + ### Suppression In certain circumstances it may be necessary to exclude or suppress rules from processing objects that are in a known failed state. @@ -1099,6 +1113,10 @@ rule: exclude: - rule3 - rule4 + tag: + severity: + - Critical + - Warning ``` ### Default PSRule.yml @@ -1153,6 +1171,7 @@ configuration: { } rule: include: [ ] exclude: [ ] + tag: { } ``` ## NOTE diff --git a/schemas/PSRule-language.schema.json b/schemas/PSRule-language.schema.json index e6db038045..2594720009 100644 --- a/schemas/PSRule-language.schema.json +++ b/schemas/PSRule-language.schema.json @@ -127,9 +127,22 @@ "tag": { "type": "object", "title": "Tags", - "description": "Rules to include by tag in the baseline.", + "description": "Require rules to have the following tags.", "additionalProperties": { - "type": "string" + "oneOf": [ + { + "type": "string", + "description": "A required tag." + }, + { + "type": "array", + "description": "A required tag.", + "items": { + "type": "string" + }, + "uniqueItems": true + } + ] } } }, diff --git a/schemas/PSRule-options.schema.json b/schemas/PSRule-options.schema.json index d3785fc089..7bbc413aeb 100644 --- a/schemas/PSRule-options.schema.json +++ b/schemas/PSRule-options.schema.json @@ -362,9 +362,22 @@ "tag": { "type": "object", "title": "Tags", - "description": "Rules to include by tag in the baseline.", + "description": "Require rules to have the following tags.", "additionalProperties": { - "type": "string" + "oneOf": [ + { + "type": "string", + "description": "A required tag." + }, + { + "type": "array", + "description": "A required tag.", + "items": { + "type": "string" + }, + "uniqueItems": true + } + ] } } }, diff --git a/src/PSRule/Configuration/BaselineOption.cs b/src/PSRule/Configuration/BaselineOption.cs index b4a2d956bd..09f0197b34 100644 --- a/src/PSRule/Configuration/BaselineOption.cs +++ b/src/PSRule/Configuration/BaselineOption.cs @@ -93,6 +93,8 @@ internal static void Load(IBaselineSpec option, Dictionary prope else option.Rule.Exclude = new string[] { value.ToString() }; } + if (properties.TryPopValue("rule.tag" , out value) && value is Hashtable tag) + option.Rule.Tag = tag; // Process configuration values if (properties.Count > 0) diff --git a/src/PSRule/PSRule.psm1 b/src/PSRule/PSRule.psm1 index 75f71307f2..7049773f1b 100644 --- a/src/PSRule/PSRule.psm1 +++ b/src/PSRule/PSRule.psm1 @@ -538,6 +538,9 @@ function Get-PSRule { [ValidateSet('None', 'Wide')] [PSRule.Configuration.OutputFormat]$OutputFormat, + [Parameter(Mandatory = $False)] + [PSRule.Configuration.BaselineOption]$Baseline, + # A list of paths to check for rule definitions [Parameter(Mandatory = $False, Position = 0)] [Alias('p')] @@ -619,6 +622,7 @@ function Get-PSRule { $builder = [PSRule.Pipeline.PipelineBuilder]::Get($sourceFiles, $Option); $builder.Name($Name); $builder.Tag($Tag); + $builder.UseBaseline($Baseline); if ($IncludeDependencies) { $builder.IncludeDependencies(); diff --git a/src/PSRule/Pipeline/GetRulePipeline.cs b/src/PSRule/Pipeline/GetRulePipeline.cs index e416d9e4f4..c70bbbc618 100644 --- a/src/PSRule/Pipeline/GetRulePipeline.cs +++ b/src/PSRule/Pipeline/GetRulePipeline.cs @@ -64,6 +64,7 @@ internal sealed class GetRulePipeline : RulePipeline, IPipeline internal GetRulePipeline(PipelineContext context, Source[] source, PipelineReader reader, PipelineWriter writer, bool includeDependencies) : base(context, source, reader, writer) { + HostHelper.ImportResource(source: Source, context: context); _IncludeDependencies = includeDependencies; } diff --git a/src/PSRule/Rules/RuleFilter.cs b/src/PSRule/Rules/RuleFilter.cs index 019458b7e7..914e0a55c6 100644 --- a/src/PSRule/Rules/RuleFilter.cs +++ b/src/PSRule/Rules/RuleFilter.cs @@ -61,9 +61,7 @@ public bool Match(string name, TagSet tag) foreach (DictionaryEntry entry in _Tag) { if (!tag.Contains(entry.Key, entry.Value)) - { return false; - } } return true; } diff --git a/src/PSRule/Rules/TagSet.cs b/src/PSRule/Rules/TagSet.cs index 200e4f3f1a..84f28363f2 100644 --- a/src/PSRule/Rules/TagSet.cs +++ b/src/PSRule/Rules/TagSet.cs @@ -35,11 +35,11 @@ public bool Contains(object key, object value) if (key == null || value == null || !(key is string k) || !_Tag.ContainsKey(k)) return false; - if (value is object[] oValues) + if (TryArray(value, out string[] values)) { - for (var i = 0; i < oValues.Length; i++) + for (var i = 0; i < values.Length; i++) { - if (_ValueComparer.Equals(oValues[i].ToString(), _Tag[k])) + if (_ValueComparer.Equals(values[i], _Tag[k])) return true; } return false; @@ -110,5 +110,25 @@ public override bool TryGetMember(GetMemberBinder binder, out object result) result = value; return found; } + + private static bool TryArray(object o, out string[] values) + { + values = null; + if (o is string[] sArray) + { + values = sArray; + return true; + } + if (o is IEnumerable oValues) + { + var result = new List(); + foreach (var obj in oValues) + result.Add(obj.ToString()); + + values = result.ToArray(); + return true; + } + return false; + } } } diff --git a/tests/PSRule.Tests/Baseline.Rule.yaml b/tests/PSRule.Tests/Baseline.Rule.yaml index f6029985f5..b67813a776 100644 --- a/tests/PSRule.Tests/Baseline.Rule.yaml +++ b/tests/PSRule.Tests/Baseline.Rule.yaml @@ -49,6 +49,28 @@ spec: configuration: key1: value1 +--- +# Synopsis: This is an example baseline +kind: Baseline +metadata: + name: TestBaseline3 +spec: + rule: + tag: + category: group2 + +--- +# Synopsis: This is an example baseline +kind: Baseline +metadata: + name: TestBaseline4 +spec: + rule: + tag: + severity: + - 'high' + - 'low' + --- kind: ObjectSelector metadata: diff --git a/tests/PSRule.Tests/FromFileBaseline.Rule.ps1 b/tests/PSRule.Tests/FromFileBaseline.Rule.ps1 index 5eac2a11e2..f706c29668 100644 --- a/tests/PSRule.Tests/FromFileBaseline.Rule.ps1 +++ b/tests/PSRule.Tests/FromFileBaseline.Rule.ps1 @@ -6,13 +6,13 @@ # # Synopsis: Test for baseline -Rule 'WithBaseline' { +Rule 'WithBaseline' -Tag @{ category = 'group2'; severity = 'high' } { $Rule.TargetName -eq 'TestObject1' $Rule.TargetType -eq 'TestObjectType' $PSRule.Field.kind -eq 'TestObjectType' } # Synopsis: Test for baseline -Rule 'NotInBaseline' { +Rule 'NotInBaseline' -Tag @{ category = 'group2'; severity = 'low' } { $False; } diff --git a/tests/PSRule.Tests/PSRule.Baseline.Tests.ps1 b/tests/PSRule.Tests/PSRule.Baseline.Tests.ps1 index b2b1bef2ec..cb3493eaa2 100644 --- a/tests/PSRule.Tests/PSRule.Baseline.Tests.ps1 +++ b/tests/PSRule.Tests/PSRule.Baseline.Tests.ps1 @@ -37,9 +37,9 @@ Describe 'Get-PSRuleBaseline' -Tag 'Baseline','Get-PSRuleBaseline' { It 'With defaults' { $result = @(Get-PSRuleBaseline -Path $baselineFilePath); $result | Should -Not -BeNullOrEmpty; - $result.Length | Should -Be 2; + $result.Length | Should -Be 4; $result[0].Name | Should -Be 'TestBaseline1'; - $result[1].Name | Should -Be 'TestBaseline2'; + $result[3].Name | Should -Be 'TestBaseline4';; } It 'With -Name' { @@ -136,6 +136,27 @@ Describe 'Baseline' -Tag 'Baseline' { $result[1].Outcome | Should -Be 'Pass'; } } + + Context 'Get-PSRule' { + It 'With -Baseline' { + $result = @(Get-PSRule -Path $ruleFilePath,$baselineFilePath -Baseline 'TestBaseline1'); + $result | Should -Not -BeNullOrEmpty; + $result.Length | Should -Be 1; + $result[0].RuleName | Should -Be 'WithBaseline'; + + $result = @(Get-PSRule -Path $ruleFilePath,$baselineFilePath -Baseline 'TestBaseline3'); + $result | Should -Not -BeNullOrEmpty; + $result.Length | Should -Be 2; + $result[0].RuleName | Should -Be 'WithBaseline'; + $result[1].RuleName | Should -Be 'NotInBaseline'; + + $result = @(Get-PSRule -Path $ruleFilePath,$baselineFilePath -Baseline 'TestBaseline4'); + $result | Should -Not -BeNullOrEmpty; + $result.Length | Should -Be 2; + $result[0].RuleName | Should -Be 'WithBaseline'; + $result[1].RuleName | Should -Be 'NotInBaseline'; + } + } } #endregion Baseline diff --git a/tests/PSRule.Tests/PSRule.Options.Tests.ps1 b/tests/PSRule.Tests/PSRule.Options.Tests.ps1 index acae587873..4bbb38c613 100644 --- a/tests/PSRule.Tests/PSRule.Options.Tests.ps1 +++ b/tests/PSRule.Tests/PSRule.Options.Tests.ps1 @@ -125,12 +125,15 @@ Describe 'New-PSRuleOption' -Tag 'Option','New-PSRuleOption' { $option.Rule.Tag | Should -BeNullOrEmpty; } - # It 'from Hashtable' { - # $option = New-PSRuleOption -BaselineConfiguration @{ 'option1' = 'option'; 'option2' = 2; option3 = 'option3a', 'option3b' }; - # $option.Configuration.option1 | Should -BeIn 'option'; - # $option.Configuration.option2 | Should -Be 2; - # $option.Configuration.option3 | Should -BeIn 'option3a', 'option3b'; - # } + It 'from Hashtable' { + # With single item + $option = New-PSRuleOption -Option @{ 'Rule.Tag' = @{ key1 = 'rule3' } }; + $option.Rule.Tag.key1 | Should -Be 'rule3'; + + # With array + $option = New-PSRuleOption -Option @{ 'Rule.Tag' = @{ key1 = 'rule3', 'rule4' } }; + $option.Rule.Tag.key1 | Should -BeIn 'rule3', 'rule4'; + } It 'from YAML' { $option = New-PSRuleOption -Option (Join-Path -Path $here -ChildPath 'PSRule.Tests.yml'); diff --git a/tests/PSRule.Tests/RuleFilterTests.cs b/tests/PSRule.Tests/RuleFilterTests.cs new file mode 100644 index 0000000000..0b3720e266 --- /dev/null +++ b/tests/PSRule.Tests/RuleFilterTests.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using PSRule.Rules; +using System.Collections; +using Xunit; + +namespace PSRule +{ + public sealed class RuleFilterTests + { + [Fact] + public void MatchInclude() + { + var filter = new RuleFilter(new string[] { "rule1", "rule2" }, null, null); + Assert.True(filter.Match("rule1", null)); + Assert.True(filter.Match("Rule2", null)); + Assert.False(filter.Match("rule3", null)); + } + + [Fact] + public void MatchExclude() + { + var filter = new RuleFilter(null, null, new string[] { "rule3" }); + Assert.True(filter.Match("rule1", null)); + Assert.True(filter.Match("rule2", null)); + Assert.False(filter.Match("Rule3", null)); + } + + [Fact] + public void MatchTag() + { + var tag = new Hashtable(); + tag["category"] = new string[] { "group1", "group2" }; + var filter = new RuleFilter(null, tag, null); + + var ruleTags = new Hashtable(); + ruleTags["category"] = "group2"; + Assert.True(filter.Match("rule1", TagSet.FromHashtable(ruleTags))); + ruleTags["category"] = "group1"; + Assert.True(filter.Match("rule2", TagSet.FromHashtable(ruleTags))); + ruleTags["category"] = "group3"; + Assert.False(filter.Match("rule3", TagSet.FromHashtable(ruleTags))); + Assert.False(filter.Match("rule4", null)); + } + } +}