-
Couldn't load subscription status.
- Fork 61
Add baseline selector infrastructure for advanced rule filtering #2976
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
4e1b243
22c5bdc
80184c6
8b120ff
bd558cc
f7f3232
463c82f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| // This file is auto-generated by the build system. | ||
Check failureCode scanning / PSRule Check for license in code files Error
Check for license in code files
|
||
| // Do not edit this file directly. | ||
|
|
||
| namespace PSRule; | ||
|
|
||
| /// <summary> | ||
| /// The PSRule engine (generated). | ||
| /// </summary> | ||
| public static partial class Engine | ||
| { | ||
| private static readonly string _Version = "0.0.1-dev"; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System.Collections; | ||
| using System.Management.Automation; | ||
| using PSRule.Configuration; | ||
| using PSRule.Data; | ||
| using PSRule.Definitions.Expressions; | ||
| using PSRule.Pipeline; | ||
| using PSRule.Runtime; | ||
|
Check failure on line 10 in src/PSRule/Definitions/Rules/BaselineRuleFilter.cs
|
||
|
|
||
| namespace PSRule.Definitions.Rules; | ||
|
|
||
| /// <summary> | ||
| /// An enhanced rule filter that supports both traditional filtering and selector-based filtering. | ||
| /// This filter is used specifically for baseline rule selection. | ||
| /// </summary> | ||
| internal sealed class BaselineRuleFilter : IResourceFilter | ||
| { | ||
| private readonly RuleFilter _baseFilter; | ||
| private readonly LanguageIf? _selector; | ||
| private readonly IExpressionContext? _context; | ||
|
|
||
| /// <summary> | ||
| /// Create a baseline rule filter that combines traditional filtering with selector support. | ||
| /// </summary> | ||
| /// <param name="include">Only accept these rules by name.</param> | ||
| /// <param name="tag">Only accept rules that have these tags.</param> | ||
| /// <param name="exclude">Rule that are always excluded by name.</param> | ||
| /// <param name="includeLocal">Determine if local rules are automatically included.</param> | ||
| /// <param name="labels">Only accept rules that have these labels.</param> | ||
| /// <param name="selector">An optional selector expression to dynamically filter rules.</param> | ||
| /// <param name="context">Expression context for selector evaluation.</param> | ||
| public BaselineRuleFilter(string[] include, Hashtable tag, string[] exclude, bool? includeLocal, ResourceLabels labels, LanguageIf? selector, IExpressionContext? context) | ||
| { | ||
| _baseFilter = new RuleFilter(include, tag, exclude, includeLocal, labels); | ||
| _selector = selector; | ||
| _context = context; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Create a baseline rule filter from existing RuleOption and optional selector. | ||
| /// </summary> | ||
| public static BaselineRuleFilter FromRuleOption(RuleOption ruleOption, LanguageIf? selector, IExpressionContext? context) | ||
| { | ||
| return new BaselineRuleFilter( | ||
| ruleOption.Include, | ||
| ruleOption.Tag, | ||
| ruleOption.Exclude, | ||
| ruleOption.IncludeLocal, | ||
| ruleOption.Labels, | ||
| selector, | ||
| context); | ||
| } | ||
|
|
||
| ResourceKind IResourceFilter.Kind => ResourceKind.Rule; | ||
|
|
||
| /// <summary> | ||
| /// Matches if the rule passes both traditional filters and selector evaluation. | ||
| /// </summary> | ||
| /// <returns>Return true if rule is matched, otherwise false.</returns> | ||
| public bool Match(IResource resource) | ||
| { | ||
| // First apply traditional filtering | ||
| if (!_baseFilter.Match(resource)) | ||
| return false; | ||
|
|
||
| // If no selector is defined, we're done | ||
| if (_selector == null || _context == null) | ||
| return true; | ||
|
|
||
| // Evaluate the selector against the rule | ||
| return EvaluateSelector(resource); | ||
| } | ||
|
|
||
| private bool EvaluateSelector(IResource resource) | ||
| { | ||
| try | ||
| { | ||
| // Convert the rule to a target object for selector evaluation | ||
| var targetObject = CreateRuleTargetObject(resource); | ||
|
|
||
| // Build and evaluate the expression directly | ||
| var builder = new LanguageExpressionBuilder(new ResourceId("", "BaselineSelector", ResourceIdKind.Id)); | ||
| var fn = builder.Build(_selector); | ||
|
|
||
| return fn != null && fn(_context, targetObject) == true; | ||
| } | ||
| catch | ||
| { | ||
| // If selector evaluation fails, default to excluding the rule | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Create a target object from a rule resource that can be evaluated by selectors. | ||
| /// This exposes rule properties as fields that can be queried by expressions. | ||
| /// </summary> | ||
| private static ITargetObject CreateRuleTargetObject(IResource resource) | ||
| { | ||
| var properties = new PSObject(); | ||
|
|
||
| // Add basic rule properties | ||
| properties.Properties.Add(new PSNoteProperty("Name", resource.Name)); | ||
| properties.Properties.Add(new PSNoteProperty("Module", resource.Source.Module)); | ||
| properties.Properties.Add(new PSNoteProperty("Kind", resource.Kind.ToString())); | ||
| properties.Properties.Add(new PSNoteProperty("ApiVersion", resource.ApiVersion)); | ||
|
|
||
| // Add help information | ||
| if (resource.Info != null) | ||
| { | ||
| properties.Properties.Add(new PSNoteProperty("Synopsis", resource.Info.Synopsis?.Text)); | ||
| properties.Properties.Add(new PSNoteProperty("Description", resource.Info.Description?.Text)); | ||
| properties.Properties.Add(new PSNoteProperty("DisplayName", resource.Info.DisplayName)); | ||
| } | ||
|
|
||
| // Add tags if available | ||
| if (resource.Tags != null) | ||
| { | ||
| var tags = new PSObject(); | ||
| foreach (var tag in resource.Tags) | ||
| { | ||
| tags.Properties.Add(new PSNoteProperty(tag.Key.ToString(), tag.Value)); | ||
| } | ||
| properties.Properties.Add(new PSNoteProperty("Tags", tags)); | ||
| } | ||
|
|
||
| // Add labels if available | ||
| if (resource.Labels != null) | ||
| { | ||
| var labels = new PSObject(); | ||
| foreach (var label in resource.Labels) | ||
| { | ||
| labels.Properties.Add(new PSNoteProperty(label.Key, label.Value)); | ||
| } | ||
| properties.Properties.Add(new PSNoteProperty("Labels", labels)); | ||
| } | ||
|
|
||
| // Add rule-specific properties if this is a rule | ||
| if (resource is IRuleV1 rule) | ||
| { | ||
| properties.Properties.Add(new PSNoteProperty("Level", rule.Level.ToString())); | ||
| properties.Properties.Add(new PSNoteProperty("Recommendation", rule.Recommendation?.Text)); | ||
|
|
||
| // Add severity as both Level and Severity for compatibility | ||
| properties.Properties.Add(new PSNoteProperty("Severity", rule.Level.ToString())); | ||
| } | ||
|
|
||
| // TODO: Add annotations from metadata if available | ||
Check noticeCode scanning / devskim A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note
Suspicious comment
|
||
| // The metadata access pattern needs to be determined for the baseline filter | ||
| // if (resource is Resource<TSpec> resourceWithMetadata && resourceWithMetadata.Metadata?.Annotations != null) | ||
| // { | ||
| // var annotations = new PSObject(); | ||
| // foreach (var annotation in resourceWithMetadata.Metadata.Annotations) | ||
| // { | ||
| // annotations.Properties.Add(new PSNoteProperty(annotation.Key, annotation.Value)); | ||
| // } | ||
| // properties.Properties.Add(new PSNoteProperty("Annotations", annotations)); | ||
| // } | ||
|
|
||
| return new TargetObject(properties); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| # Copyright (c) Microsoft Corporation. | ||
| # Licensed under the MIT License. | ||
|
|
||
| # Test baselines with selector support | ||
|
|
||
| --- | ||
| # Synopsis: Example baseline that selects high severity rules | ||
| apiVersion: github.com/microsoft/PSRule/v1 | ||
| kind: Baseline | ||
| metadata: | ||
| name: HighSeverityBaseline | ||
| spec: | ||
| rule: | ||
| selector: | ||
| if: | ||
| field: 'Level' | ||
| equals: 'Error' | ||
|
|
||
| --- | ||
| # Synopsis: Example baseline that selects rules with names starting with prefix | ||
| apiVersion: github.com/microsoft/PSRule/v1 | ||
| kind: Baseline | ||
| metadata: | ||
| name: PrefixBaseline | ||
| spec: | ||
| rule: | ||
| selector: | ||
| if: | ||
| field: 'Name' | ||
| startsWith: 'Azure.' | ||
|
|
||
| --- | ||
| # Synopsis: Example baseline combining multiple criteria | ||
| apiVersion: github.com/microsoft/PSRule/v1 | ||
| kind: Baseline | ||
| metadata: | ||
| name: ComplexBaseline | ||
| spec: | ||
| rule: | ||
| selector: | ||
| if: | ||
| anyOf: | ||
| - field: 'Level' | ||
| in: ['Error', 'Warning'] | ||
| - allOf: | ||
| - field: 'Name' | ||
| startsWith: 'Security.' | ||
| - field: 'Tags.category' | ||
| equals: 'Security' | ||
|
|
||
| --- | ||
| # Synopsis: Example baseline using annotation-based selection | ||
| apiVersion: github.com/microsoft/PSRule/v1 | ||
| kind: Baseline | ||
| metadata: | ||
| name: AnnotationBaseline | ||
| spec: | ||
| rule: | ||
| selector: | ||
| if: | ||
| field: 'Annotations.severity' | ||
| in: ['high', 'critical'] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System.Linq; | ||
| using PSRule.Definitions; | ||
| using PSRule.Definitions.Baselines; | ||
| using PSRule.Definitions.Rules; | ||
|
|
||
| namespace PSRule; | ||
|
|
||
| /// <summary> | ||
| /// Tests for baseline selector functionality. | ||
| /// </summary> | ||
| public sealed class BaselineSelectorTests : ContextBaseTests | ||
| { | ||
| private const string BaselineSelectorFileName = "BaselineSelector.Rule.yaml"; | ||
|
|
||
| [Fact] | ||
| public void BaselineRuleFilter_WithSelector() | ||
| { | ||
| // Test that BaselineRuleFilter can filter rules using selectors | ||
| var context = GetContext(); | ||
| var rules = GetTestRules(); | ||
|
|
||
| // Create a simple selector that filters for rules with "Error" level | ||
| var filter = new BaselineRuleFilter( | ||
| include: null, | ||
| tag: null, | ||
| exclude: null, | ||
| includeLocal: true, | ||
| labels: null, | ||
| selector: null, // Will implement selector parsing later | ||
| context: context | ||
| ); | ||
|
|
||
| var filteredRules = rules.Where(rule => filter.Match(rule)).ToArray(); | ||
|
|
||
| // For now, should behave like regular RuleFilter (no selector) | ||
| Assert.NotEmpty(filteredRules); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ReadBaseline_WithSelector() | ||
| { | ||
| // Test reading baseline YAML with selector | ||
| var baselines = GetBaselines(GetSource(BaselineSelectorFileName)); | ||
| Assert.NotNull(baselines); | ||
| Assert.Equal(4, baselines.Length); | ||
|
|
||
| // Validate that baselines were read correctly | ||
| Assert.Equal("HighSeverityBaseline", baselines[0].Name); | ||
| Assert.Equal("PrefixBaseline", baselines[1].Name); | ||
| Assert.Equal("ComplexBaseline", baselines[2].Name); | ||
| Assert.Equal("AnnotationBaseline", baselines[3].Name); | ||
| } | ||
|
|
||
| private IResource[] GetTestRules() | ||
| { | ||
| // Create mock rules for testing | ||
| var rules = new List<IResource>(); | ||
|
|
||
| // This will be expanded once we have the filtering working | ||
| // For now, return empty array | ||
| return rules.ToArray(); | ||
| } | ||
| } |
Check failure
Code scanning / PSRule
Consider adding standard license header to code files. Error