Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions global.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
"msbuild-sdks": {
"Microsoft.Build.NoTargets": "3.7.56"
},
"sdk": {
"allowPrerelease": false,
"version": "8.0.400",
"rollForward": "latestPatch"
"sdk": {
"allowPrerelease": false,
"version": "8.0.117",
"rollForward": "latestPatch"
}
}
12 changes: 12 additions & 0 deletions src/PSRule/Common/Engine.g.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// This file is auto-generated by the build system.

Check failure

Code scanning / PSRule

Consider adding standard license header to code files. Error

Consider adding standard license header to code files.

Check failure

Code 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";
}
164 changes: 164 additions & 0 deletions src/PSRule/Definitions/Rules/BaselineRuleFilter.cs
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

View workflow job for this annotation

GitHub Actions / Build extension

Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 10 in src/PSRule/Definitions/Rules/BaselineRuleFilter.cs

View workflow job for this annotation

GitHub Actions / Build extension

Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 10 in src/PSRule/Definitions/Rules/BaselineRuleFilter.cs

View workflow job for this annotation

GitHub Actions / Build extension

Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 10 in src/PSRule/Definitions/Rules/BaselineRuleFilter.cs

View workflow job for this annotation

GitHub Actions / Build extension

Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 10 in src/PSRule/Definitions/Rules/BaselineRuleFilter.cs

View workflow job for this annotation

GitHub Actions / Build module

Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 10 in src/PSRule/Definitions/Rules/BaselineRuleFilter.cs

View workflow job for this annotation

GitHub Actions / Build module

Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 10 in src/PSRule/Definitions/Rules/BaselineRuleFilter.cs

View workflow job for this annotation

GitHub Actions / Build module

Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 10 in src/PSRule/Definitions/Rules/BaselineRuleFilter.cs

View workflow job for this annotation

GitHub Actions / Build module

Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 10 in src/PSRule/Definitions/Rules/BaselineRuleFilter.cs

View workflow job for this annotation

GitHub Actions / Build module

Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 10 in src/PSRule/Definitions/Rules/BaselineRuleFilter.cs

View workflow job for this annotation

GitHub Actions / Build module

Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 10 in src/PSRule/Definitions/Rules/BaselineRuleFilter.cs

View workflow job for this annotation

GitHub Actions / Build module

Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 10 in src/PSRule/Definitions/Rules/BaselineRuleFilter.cs

View workflow job for this annotation

GitHub Actions / Build module

Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 10 in src/PSRule/Definitions/Rules/BaselineRuleFilter.cs

View workflow job for this annotation

GitHub Actions / Build module

Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 10 in src/PSRule/Definitions/Rules/BaselineRuleFilter.cs

View workflow job for this annotation

GitHub Actions / Build module

Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

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 notice

Code 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);
}
}
62 changes: 62 additions & 0 deletions tests/PSRule.Tests/BaselineSelector.Rule.yaml
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']
66 changes: 66 additions & 0 deletions tests/PSRule.Tests/BaselineSelectorTests.cs
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();
}
}
Loading