Skip to content

Commit

Permalink
Fixed multiple value tag filtering #346 #345 (#347)
Browse files Browse the repository at this point in the history
  • Loading branch information
BernieWhite authored Dec 4, 2019
1 parent cc99411 commit e5c367d
Show file tree
Hide file tree
Showing 15 changed files with 206 additions and 24 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
22 changes: 19 additions & 3 deletions docs/commands/PSRule/en-US/Get-PSRule.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ Get a list of rule definitions.
## SYNTAX

```text
Get-PSRule [-Module <String[]>] [-ListAvailable] [-OutputFormat <OutputFormat>] [[-Path] <String[]>]
[-Name <String[]>] [-Tag <Hashtable>] [-Option <PSRuleOption>] [-Culture <String>] [-IncludeDependencies]
[<CommonParameters>]
Get-PSRule [-Module <String[]>] [-ListAvailable] [-OutputFormat <OutputFormat>] [-Baseline <BaselineOption>]
[[-Path] <String[]>] [-Name <String[]>] [-Tag <Hashtable>] [-Option <PSRuleOption>] [-Culture <String>]
[-IncludeDependencies] [<CommonParameters>]
```

## DESCRIPTION
Expand Down Expand Up @@ -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).
Expand Down
23 changes: 21 additions & 2 deletions docs/concepts/PSRule/en-US/about_PSRule_Options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -1099,6 +1113,10 @@ rule:
exclude:
- rule3
- rule4
tag:
severity:
- Critical
- Warning
```

### Default PSRule.yml
Expand Down Expand Up @@ -1153,6 +1171,7 @@ configuration: { }
rule:
include: [ ]
exclude: [ ]
tag: { }
```

## NOTE
Expand Down
17 changes: 15 additions & 2 deletions schemas/PSRule-language.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]
}
}
},
Expand Down
17 changes: 15 additions & 2 deletions schemas/PSRule-options.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]
}
}
},
Expand Down
2 changes: 2 additions & 0 deletions src/PSRule/Configuration/BaselineOption.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ internal static void Load(IBaselineSpec option, Dictionary<string, object> 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)
Expand Down
4 changes: 4 additions & 0 deletions src/PSRule/PSRule.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -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')]
Expand Down Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions src/PSRule/Pipeline/GetRulePipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
2 changes: 0 additions & 2 deletions src/PSRule/Rules/RuleFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
26 changes: 23 additions & 3 deletions src/PSRule/Rules/TagSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<object> oValues)
{
var result = new List<string>();
foreach (var obj in oValues)
result.Add(obj.ToString());

values = result.ToArray();
return true;
}
return false;
}
}
}
22 changes: 22 additions & 0 deletions tests/PSRule.Tests/Baseline.Rule.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions tests/PSRule.Tests/FromFileBaseline.Rule.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
25 changes: 23 additions & 2 deletions tests/PSRule.Tests/PSRule.Baseline.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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' {
Expand Down Expand Up @@ -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
15 changes: 9 additions & 6 deletions tests/PSRule.Tests/PSRule.Options.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
47 changes: 47 additions & 0 deletions tests/PSRule.Tests/RuleFilterTests.cs
Original file line number Diff line number Diff line change
@@ -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));
}
}
}

0 comments on commit e5c367d

Please sign in to comment.