diff --git a/README.md b/README.md
index 304ee9514c..8f9033d7de 100644
--- a/README.md
+++ b/README.md
@@ -329,6 +329,7 @@ The following conceptual topics exist in the `PSRule` module:
- [Output.Path](https://aka.ms/ps-rule/options#outputpath)
- [Output.SarifProblemsOnly](https://aka.ms/ps-rule/options#outputsarifproblemsonly)
- [Output.Style](https://aka.ms/ps-rule/options#outputstyle)
+ - [Override.Level](https://aka.ms/ps-rule/options#overridelevel)
- [Repository.BaseRef](https://aka.ms/ps-rule/options#repositorybaseref)
- [Repository.Url](https://aka.ms/ps-rule/options#repositoryurl)
- [Requires](https://aka.ms/ps-rule/options#requires)
diff --git a/docs/CHANGELOG-v3.md b/docs/CHANGELOG-v3.md
index 9b5d5aef3b..f2ee238fb1 100644
--- a/docs/CHANGELOG-v3.md
+++ b/docs/CHANGELOG-v3.md
@@ -27,6 +27,12 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers
## Unreleased
+- New features:
+ - Added support for overriding rule severity level by @BernieWhite.
+ [#1180](https://github.com/microsoft/PSRule/issues/1180)
+ - Baselines now accept a new `spec.overrides.level` property which configures severity level overrides.
+ - Options now accept a new `overrides.level` properties which configures severity level overrides.
+ - For example, a rule that generates an `Error` can be overridden to `Warning`.
- General improvements:
- Automatically restore missing modules when running CLI by @BernieWhite.
[#2552](https://github.com/microsoft/PSRule/issues/2552)
diff --git a/docs/concepts/PSRule/en-US/about_PSRule_Baseline.md b/docs/concepts/PSRule/en-US/about_PSRule_Baseline.md
index 266a6dc8d5..b5d5f5266e 100644
--- a/docs/concepts/PSRule/en-US/about_PSRule_Baseline.md
+++ b/docs/concepts/PSRule/en-US/about_PSRule_Baseline.md
@@ -14,6 +14,7 @@ A baseline includes a set of rule and configuration options that are used for ev
The following baseline options can be configured:
- [Configuration](about_PSRule_Options.md#configuration)
+- [Override.Level](about_PSRule_Options.md#overridelevel)
- [Rule.Include](about_PSRule_Options.md#ruleinclude)
- [Rule.IncludeLocal](about_PSRule_Options.md#ruleincludelocal)
- [Rule.Exclude](about_PSRule_Options.md#ruleexclude)
@@ -48,8 +49,9 @@ metadata:
annotations: { }
spec:
# One or more baseline options
- rule: { }
configuration: { }
+ override: {}
+ rule: { }
```
For example:
@@ -100,8 +102,9 @@ To define a JSON baseline spec use the following structure:
"annotations": {}
},
"spec": {
+ "configuration": {},
+ "override": {},
"rule": {},
- "configuration": {}
}
}
]
diff --git a/docs/concepts/PSRule/en-US/about_PSRule_Options.md b/docs/concepts/PSRule/en-US/about_PSRule_Options.md
index 0aabf41c42..3a1284419c 100644
--- a/docs/concepts/PSRule/en-US/about_PSRule_Options.md
+++ b/docs/concepts/PSRule/en-US/about_PSRule_Options.md
@@ -68,6 +68,7 @@ The following workspace options are available for use:
Additionally the following baseline options can be included:
- [Configuration](#configuration)
+- [Override.Level](#overridelevel)
- [Rule.Baseline](#rulebaseline)
- [Rule.Include](#ruleinclude)
- [Rule.IncludeLocal](#ruleincludelocal)
@@ -3012,6 +3013,52 @@ variables:
value: 2
```
+### Override.Level
+
+This option is used to override the severity level of one or more rules.
+When specified, the severity level of the rule will be set to the value specified.
+Use this option to change the severity level of a rule to be different then originally defined by the author.
+
+The following severity levels are available:
+
+- `Error` - A serious problem that must be addressed before going forward.
+- `Warning` - A problem that should be addressed.
+- `Information` - A minor problem or an opportunity to improve the code.
+
+This option can be specified using:
+
+```powershell
+# PowerShell: Using the OverrideLevel parameter
+$option = New-PSRuleOption -OverrideLevel @{ 'rule1' = 'Information' };
+```
+
+```powershell
+# PowerShell: Using the OVerride.Level hashtable key
+$option = New-PSRuleOption -Option @{ 'Override.Level.rule1' = 'Information' };
+```
+
+```powershell
+# PowerShell: Using the OverrideLevel parameter to set YAML
+Set-PSRuleOption -OverrideLevel @{ 'rule1' = 'Information' };
+```
+
+```yaml
+# YAML: Using the override/level property
+override:
+ level:
+ rule1: Information
+```
+
+```bash
+# Bash: Using environment variable
+export PSRULE_OVERRIDE_LEVEL_RULE1='Information'
+```
+
+```powershell
+# PowerShell: Using environment variable
+$env:PSRULE_OVERRIDE_LEVEL_RULE1 = 'Information';
+```
+
### Repository.BaseRef
This option is used for specify the base branch for pull requests.
@@ -3464,6 +3511,12 @@ output:
sarifProblemsOnly: false
style: GitHubActions
+# Overrides the severity level for rules
+override:
+ level:
+ Rule1: Error
+ Rule2: Warning
+
# Configure rule suppression
suppression:
storageAccounts.UseHttps:
@@ -3571,6 +3624,9 @@ output:
sarifProblemsOnly: true
style: Detect
+override:
+ level: { }
+
# Configure rule suppression
suppression: { }
diff --git a/docs/concepts/options.md b/docs/concepts/options.md
new file mode 100644
index 0000000000..b74bb49e89
--- /dev/null
+++ b/docs/concepts/options.md
@@ -0,0 +1,26 @@
+# Options
+
+Options are used to customize how rules are evaluated and the resulting output.
+You can set options in multiple ways, including:
+
+- Parameters
+- Environment variables
+- Configuration files
+
+Rules or modules could also have a defaults configured by the rule or module author.
+
+## Option precedence
+
+When setting options, you may have a situation where an option is set to different values.
+For example, you may set an option in a configuration file and also set the same option as a parameter.
+
+When this happens, PSRule will use the option with the highest precedence.
+
+Option precedence is as follows:
+
+1. Parameters
+2. Explicit baselines
+3. Environment variables
+4. Configuration files
+5. Default baseline
+6. Module defaults
diff --git a/docs/concepts/sarif-format.md b/docs/concepts/sarif-format.md
new file mode 100644
index 0000000000..df39912577
--- /dev/null
+++ b/docs/concepts/sarif-format.md
@@ -0,0 +1,19 @@
+# SARIF Output
+
+PSRule uses a JSON structured output format called the
+
+SARIF format to report results.
+The SARIF format is a standard format for the output of static analysis tools.
+The format is designed to be easily consumed by other tools and services.
+
+## Runs
+
+When running PSRule executed a run will be generated in `runs` containing details about PSRule and configuration details.
+
+## Invocation
+
+The `invocation` property reports runtime information about how the run started.
+
+### RuleConfigurationOverrides
+
+When a rule has been overridden in configuration this invocation property will contain any level overrides.
diff --git a/schemas/PSRule-language.schema.json b/schemas/PSRule-language.schema.json
index bbc416d993..8c12b6fba6 100644
--- a/schemas/PSRule-language.schema.json
+++ b/schemas/PSRule-language.schema.json
@@ -209,6 +209,39 @@
}
},
"additionalProperties": false
+ },
+ "override": {
+ "type": "object",
+ "title": "Overrides",
+ "description": "Specifies additional rule overrides.",
+ "properties": {
+ "level": {
+ "type": "object",
+ "title": "Override severity level",
+ "description": "Overrides the severity level defined by the rule to the level specified. A rule can be configured with one of the following: Error, Warning, Information.",
+ "markdownDescription": "Overrides the severity level defined by the rule to the level specified.\n\nA rule can be configured with one of the following: `Error`, `Warning`, `Information`.\n\n[See help](https://microsoft.github.io/PSRule/v3/concepts/PSRule/en-US/about_PSRule_Options/#overridelevel)",
+ "additionalProperties": {
+ "type": "string",
+ "title": "Override level by rule",
+ "description": "Specify the new severity level of the rule. Choose one of the following supported values: Error, Warning, Information.",
+ "markdownDescription": "Specify the new severity level of the rule.\n\nChoose one of the following supported values: `Error`, `Warning`, `Information`.\n\n[See help](https://microsoft.github.io/PSRule/v3/concepts/PSRule/en-US/about_PSRule_Options/#overridelevel)",
+ "enum": [
+ "Error",
+ "Warning",
+ "Information"
+ ]
+ },
+ "defaultSnippets": [
+ {
+ "label": "Override level",
+ "body": {
+ "${1:Rule}": "Error"
+ }
+ }
+ ]
+ }
+ },
+ "additionalProperties": false
}
},
"additionalProperties": false
diff --git a/schemas/PSRule-options.schema.json b/schemas/PSRule-options.schema.json
index 1d93d78e5b..3cfabb99a3 100644
--- a/schemas/PSRule-options.schema.json
+++ b/schemas/PSRule-options.schema.json
@@ -928,6 +928,39 @@
},
"additionalProperties": false
},
+ "override-option": {
+ "type": "object",
+ "title": "Overrides",
+ "description": "Specifies additional rule overrides.",
+ "properties": {
+ "level": {
+ "type": "object",
+ "title": "Override severity level",
+ "description": "Overrides the severity level defined by the rule to the level specified. A rule can be configured with one of the following: Error, Warning, Information.",
+ "markdownDescription": "Overrides the severity level defined by the rule to the level specified.\n\nA rule can be configured with one of the following: `Error`, `Warning`, `Information`.\n\n[See help](https://microsoft.github.io/PSRule/v3/concepts/PSRule/en-US/about_PSRule_Options/#overridelevel)",
+ "additionalProperties": {
+ "type": "string",
+ "title": "Override level by rule",
+ "description": "Specify the new severity level of the rule. Choose one of the following supported values: Error, Warning, Information.",
+ "markdownDescription": "Specify the new severity level of the rule.\n\nChoose one of the following supported values: `Error`, `Warning`, `Information`.\n\n[See help](https://microsoft.github.io/PSRule/v3/concepts/PSRule/en-US/about_PSRule_Options/#overridelevel)",
+ "enum": [
+ "Error",
+ "Warning",
+ "Information"
+ ]
+ },
+ "defaultSnippets": [
+ {
+ "label": "Override level",
+ "body": {
+ "${1:Rule}": "Error"
+ }
+ }
+ ]
+ }
+ },
+ "additionalProperties": false
+ },
"options": {
"properties": {
"baseline": {
@@ -982,6 +1015,10 @@
"type": "object",
"$ref": "#/definitions/output-option"
},
+ "override": {
+ "type": "object",
+ "$ref": "#/definitions/override-option"
+ },
"repository": {
"type": "object",
"$ref": "#/definitions/repository-option"
diff --git a/src/PSRule.Types/Converters/TypeConverter.cs b/src/PSRule.Types/Converters/TypeConverter.cs
index 91c9c679d4..f88f044043 100644
--- a/src/PSRule.Types/Converters/TypeConverter.cs
+++ b/src/PSRule.Types/Converters/TypeConverter.cs
@@ -265,6 +265,20 @@ public static bool TryDouble(object o, bool convert, out double value)
return false;
}
+ ///
+ /// Try to get the environment variable as a enum of type .
+ ///
+ public static bool TryEnum(object o, bool convert, out T? value) where T : struct, Enum
+ {
+ if (o is T t || convert && o is string s && Enum.TryParse(s, ignoreCase: true, out t))
+ {
+ value = t;
+ return true;
+ }
+ value = null;
+ return false;
+ }
+
private static bool TryGetValue(object o, string propertyName, out object? value)
{
value = null;
diff --git a/src/PSRule.Types/Data/EnumMap.cs b/src/PSRule.Types/Data/EnumMap.cs
new file mode 100644
index 0000000000..0dcf0af149
--- /dev/null
+++ b/src/PSRule.Types/Data/EnumMap.cs
@@ -0,0 +1,72 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Collections;
+using PSRule.Converters;
+
+namespace PSRule.Data;
+
+///
+/// A mapping of string to string arrays.
+///
+public sealed class EnumMap : KeyMap where T : struct, Enum
+{
+ ///
+ /// Create an empty instance.
+ ///
+ public EnumMap()
+ : base() { }
+
+ ///
+ /// Create an instance by copying an existing .
+ ///
+ internal EnumMap(EnumMap map)
+ : base(map) { }
+
+ ///
+ /// Create an instance by copying mapped keys from a string dictionary.
+ ///
+ internal EnumMap(IDictionary map)
+ : base(map) { }
+
+ ///
+ /// Create an instance by copying mapped keys from a .
+ ///
+ ///
+ internal EnumMap(Hashtable map)
+ : base(map) { }
+
+ ///
+ ///
+ ///
+ ///
+ public static implicit operator EnumMap(Hashtable hashtable)
+ {
+ return new EnumMap(hashtable);
+ }
+
+ ///
+ /// Convert a hashtable into a instance.
+ ///
+ public static EnumMap FromHashtable(Hashtable hashtable)
+ {
+ return new EnumMap(hashtable);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ protected override bool TryConvertValue(object o, out T value)
+ {
+ value = default;
+ if (TypeConverter.TryEnum(o, convert: true, out var result) && result != null)
+ {
+ value = result.Value;
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/PSRule.Types/Data/KeyMap.cs b/src/PSRule.Types/Data/KeyMap.cs
new file mode 100644
index 0000000000..63f2fff546
--- /dev/null
+++ b/src/PSRule.Types/Data/KeyMap.cs
@@ -0,0 +1,186 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Collections;
+
+namespace PSRule.Data;
+
+///
+/// A mapping of string to string arrays.
+///
+public abstract class KeyMap : IEnumerable>
+{
+ private readonly Dictionary _Map;
+
+ ///
+ /// Create an empty instance.
+ ///
+ protected KeyMap()
+ {
+ _Map = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Create an instance by copying an existing .
+ ///
+ protected KeyMap(KeyMap map)
+ {
+ _Map = new Dictionary(map._Map, StringComparer.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Create an instance by copying mapped keys from a string dictionary.
+ ///
+ protected KeyMap(IDictionary map)
+ {
+ _Map = new Dictionary(map, StringComparer.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Create an instance by copying mapped keys from a .
+ ///
+ ///
+ protected KeyMap(Hashtable map)
+ : this()
+ {
+ if (map != null) FromDictionary(map.IndexByString());
+ }
+
+ ///
+ /// The number of mapped keys.
+ ///
+ public int Count => _Map.Count;
+
+ ///
+ /// Get or set mapping for a specified key.
+ ///
+ public T? this[string key]
+ {
+ get
+ {
+ return !string.IsNullOrEmpty(key) && _Map.TryGetValue(key, out var value) ? value : GetValueDefault();
+ }
+ set
+ {
+ if (!string.IsNullOrEmpty(key) && value != null)
+ _Map[key] = value;
+ }
+ }
+
+ ///
+ /// 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 T value)
+ {
+ return _Map.TryGetValue(key, out value);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void Add(string key, T value)
+ {
+ _Map.Add(key, value);
+ }
+
+ ///
+ public IEnumerator> GetEnumerator()
+ {
+ return ((IEnumerable>)_Map).GetEnumerator();
+ }
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ ///
+ /// Convert the instance into a dictionary.
+ ///
+ ///
+ public IDictionary ToDictionary()
+ {
+ return _Map;
+ }
+
+ ///
+ /// Get the default for the type.
+ ///
+ protected virtual T? GetValueDefault()
+ {
+ return default;
+ }
+
+ ///
+ /// Convert the type.
+ ///
+ protected virtual bool TryConvertValue(object o, out T? value)
+ {
+ value = default;
+ if (o is T t)
+ {
+ value = t;
+ return true;
+ }
+ return false;
+ }
+
+ ///
+ /// Load a key map from an existing dictionary.
+ ///
+ internal void FromDictionary(IDictionary dictionary, string? prefix = null, Func? format = null)
+ {
+ foreach (var kv in dictionary)
+ {
+ if (TryKeyPrefix(kv.Key, prefix, out var suffix) && TryConvertValue(kv.Value, out var value) && value != null)
+ {
+ if (format != null)
+ suffix = format(suffix);
+
+ _Map[suffix] = value;
+ }
+ }
+ }
+
+ ///
+ /// Load values from environment variables into the option.
+ /// Keys that appear in both will replaced by environment variable values.
+ ///
+ /// Is raised if the environment helper is null.
+ internal void FromEnvironment(string? prefix = null, Func? format = null)
+ {
+ foreach (var kv in Environment.GetByPrefix(prefix))
+ {
+ if (TryKeyPrefix(kv.Key, prefix, out var suffix) && TryConvertValue(kv.Value, out var value) && value != null)
+ {
+ if (format != null)
+ suffix = format(suffix);
+
+ _Map[suffix] = value;
+ }
+ }
+ }
+
+ ///
+ /// Try a key prefix.
+ ///
+ private static bool TryKeyPrefix(string key, string? prefix, out string suffix)
+ {
+ suffix = key;
+ if (prefix == null || prefix.Length == 0)
+ return true;
+
+ if (key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ suffix = key.Substring(prefix.Length);
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/PSRule.Types/Data/WildcardMap.cs b/src/PSRule.Types/Data/WildcardMap.cs
new file mode 100644
index 0000000000..ba189a4b8d
--- /dev/null
+++ b/src/PSRule.Types/Data/WildcardMap.cs
@@ -0,0 +1,84 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace PSRule.Data;
+
+///
+///
+///
+///
+public sealed class WildcardMap
+{
+ private readonly Dictionary _Map;
+ private List>? _Wildcard;
+
+ ///
+ ///
+ ///
+ public WildcardMap()
+ {
+ _Map = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public WildcardMap(IEnumerable> values)
+ {
+ _Map = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ Load(values);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public bool TryGetValue(string key, out T? value)
+ {
+ value = default;
+ if (string.IsNullOrEmpty(key)) return false;
+ if (_Map.TryGetValue(key, out value)) return true;
+
+ for (var i = 0; _Wildcard != null && i < _Wildcard.Count; i++)
+ {
+ if (key.Length > _Wildcard[i].Key.Length && key.StartsWith(_Wildcard[i].Key))
+ {
+ value = _Wildcard[i].Value;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void Load(IEnumerable> dictionary)
+ {
+ Dictionary? wildcardIndex = null;
+ foreach (var kv in dictionary)
+ {
+ var index = kv.Key.IndexOf('*');
+
+ // Simple keys
+ if (index < 0)
+ {
+ _Map[kv.Key] = kv.Value;
+ }
+ // Wildcard keys
+ else
+ {
+ var key = kv.Key.Substring(0, index);
+ wildcardIndex ??= new Dictionary(StringComparer.OrdinalIgnoreCase);
+ wildcardIndex[key] = kv.Value;
+ }
+ }
+
+ if (wildcardIndex != null)
+ {
+ _Wildcard = new List>();
+ foreach (var kv in wildcardIndex.OrderByDescending(s => s.Key))
+ _Wildcard.Add(kv);
+ }
+ }
+}
diff --git a/src/PSRule.Types/Definitions/Rules/SeverityLevel.cs b/src/PSRule.Types/Definitions/Rules/SeverityLevel.cs
index bf55cc6b4f..d117662e39 100644
--- a/src/PSRule.Types/Definitions/Rules/SeverityLevel.cs
+++ b/src/PSRule.Types/Definitions/Rules/SeverityLevel.cs
@@ -1,11 +1,15 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
namespace PSRule.Definitions.Rules;
///
/// If the rule fails, how serious is the result.
///
+[JsonConverter(typeof(StringEnumConverter))]
public enum SeverityLevel
{
///
diff --git a/src/PSRule.Types/Environment.cs b/src/PSRule.Types/Environment.cs
index 07a0cd2641..a6c3e2bc08 100644
--- a/src/PSRule.Types/Environment.cs
+++ b/src/PSRule.Types/Environment.cs
@@ -312,14 +312,14 @@ public static string CombineEnvironmentVariable(params string[] args)
///
/// Try to get any environment variable with a specific prefix.
///
- public static IEnumerable> GetByPrefix(string 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))
+ if (prefix == null || key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
yield return new KeyValuePair(key, enumerator.Value);
}
}
diff --git a/src/PSRule.Types/Options/IOverrideOption.cs b/src/PSRule.Types/Options/IOverrideOption.cs
new file mode 100644
index 0000000000..54b00abc17
--- /dev/null
+++ b/src/PSRule.Types/Options/IOverrideOption.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using PSRule.Data;
+using PSRule.Definitions.Rules;
+
+namespace PSRule.Options;
+
+///
+/// Options that configure additional rule overrides.
+///
+///
+/// See .
+///
+public interface IOverrideOption
+{
+ ///
+ /// A mapping of rule severity levels to override.
+ ///
+ EnumMap? Level { get; }
+}
diff --git a/src/PSRule.Types/Options/OverrideOption.cs b/src/PSRule.Types/Options/OverrideOption.cs
new file mode 100644
index 0000000000..3a83c203b2
--- /dev/null
+++ b/src/PSRule.Types/Options/OverrideOption.cs
@@ -0,0 +1,106 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.ComponentModel;
+using PSRule.Data;
+using PSRule.Definitions.Rules;
+
+namespace PSRule.Options;
+
+///
+/// Options that configure additional rule overrides.
+///
+///
+/// See .
+///
+public sealed class OverrideOption : IEquatable, IOption
+{
+ private const string ENVIRONMENT_LEVEL_KEY_PREFIX = "PSRULE_OVERRIDE_LEVEL_";
+ private const string DICTIONARY_LEVEL_KEY_PREFIX = "Override.Level.";
+
+ internal static readonly OverrideOption Default = new()
+ {
+ };
+
+ ///
+ /// Create an option instance.
+ ///
+ public OverrideOption()
+ {
+ Level = null;
+ }
+
+ ///
+ /// Create an option instance based on an existing object.
+ ///
+ /// The existing object to copy.
+ public OverrideOption(OverrideOption option)
+ {
+ if (option == null)
+ return;
+
+ Level = option.Level;
+ }
+
+ ///
+ public override bool Equals(object obj)
+ {
+ return obj is OverrideOption option && Equals(option);
+ }
+
+ ///
+ public bool Equals(OverrideOption other)
+ {
+ return other != null &&
+ Level == other.Level;
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ unchecked // Overflow is fine
+ {
+ var hash = 17;
+ hash = hash * 23 + (Level != null ? Level.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 OverrideOption Combine(OverrideOption o1, OverrideOption o2)
+ {
+ var result = new OverrideOption(o1)
+ {
+ Level = o1?.Level ?? o2?.Level,
+ };
+ return result;
+ }
+
+ ///
+ [DefaultValue(null)]
+ public EnumMap? Level { get; set; }
+
+ ///
+ /// Load from environment variables.
+ ///
+ internal void Load()
+ {
+ var level = Level ?? [];
+ level.FromEnvironment(prefix: ENVIRONMENT_LEVEL_KEY_PREFIX);
+ Level = level.Count > 0 ? level : null;
+ }
+
+ ///
+ /// Load from a dictionary.
+ ///
+ public void Import(IDictionary index)
+ {
+ var level = Level ?? [];
+ level.FromDictionary(index, prefix: DICTIONARY_LEVEL_KEY_PREFIX);
+ Level = level.Count > 0 ? level : null;
+ }
+}
diff --git a/src/PSRule/Commands/NewRuleDefinitionCommand.cs b/src/PSRule/Commands/NewRuleDefinitionCommand.cs
index ae8e97ba22..1d1238a87c 100644
--- a/src/PSRule/Commands/NewRuleDefinitionCommand.cs
+++ b/src/PSRule/Commands/NewRuleDefinitionCommand.cs
@@ -131,13 +131,18 @@ protected override void ProcessRecord()
displayName: Name,
moduleName: source.Module
);
+ context.LanguageScope.TryGetOverride(id, out var propertyOverride);
#pragma warning disable CA2000 // Dispose objects before losing scope, needs to be passed to pipeline
var block = new RuleBlock(
source: source,
id: id,
@ref: ResourceHelper.GetIdNullable(source.Module, Ref, ResourceIdKind.Ref),
- level: level,
+ @default: new RuleProperties
+ {
+ Level = level
+ },
+ @override: propertyOverride,
info: info,
condition: ps,
tag: tag,
diff --git a/src/PSRule/Common/BaselineJsonSerializationMapper.cs b/src/PSRule/Common/BaselineJsonSerializationMapper.cs
index d12441baed..416d120a18 100644
--- a/src/PSRule/Common/BaselineJsonSerializationMapper.cs
+++ b/src/PSRule/Common/BaselineJsonSerializationMapper.cs
@@ -6,6 +6,7 @@
using PSRule.Configuration;
using PSRule.Definitions;
using PSRule.Definitions.Baselines;
+using PSRule.Options;
namespace PSRule;
@@ -48,7 +49,7 @@ private static void MapBaselineSpec(JsonWriter writer, JsonSerializer serializer
MapPropertyName(writer, propertyName);
writer.WriteStartObject();
MapProperty(writer, serializer, nameof(baselineSpec.Configuration), baselineSpec.Configuration);
- MapProperty(writer, nameof(baselineSpec.Convention), baselineSpec.Convention);
+ MapProperty(writer, serializer, nameof(baselineSpec.Override), baselineSpec.Override);
MapProperty(writer, serializer, nameof(baselineSpec.Rule), baselineSpec.Rule);
writer.WriteEndObject();
}
@@ -152,6 +153,20 @@ private static void MapProperty(JsonWriter writer, string propertyName, Conventi
writer.WriteEndObject();
}
+ ///
+ /// Map a OverrideOption property.
+ ///
+ private static void MapProperty(JsonWriter writer, JsonSerializer serializer, string propertyName, OverrideOption value)
+ {
+ if (value == null)
+ return;
+
+ MapPropertyName(writer, propertyName);
+ writer.WriteStartObject();
+ MapProperty(writer, serializer, nameof(value.Level), value.Level?.ToDictionary());
+ writer.WriteEndObject();
+ }
+
///
/// Map a RuleOption property.
///
diff --git a/src/PSRule/Common/BaselineYamlSerializationMapper.cs b/src/PSRule/Common/BaselineYamlSerializationMapper.cs
index 6e19c27c8b..643ee524b5 100644
--- a/src/PSRule/Common/BaselineYamlSerializationMapper.cs
+++ b/src/PSRule/Common/BaselineYamlSerializationMapper.cs
@@ -5,6 +5,7 @@
using PSRule.Configuration;
using PSRule.Definitions;
using PSRule.Definitions.Baselines;
+using PSRule.Options;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
@@ -51,7 +52,7 @@ private static void MapBaselineSpec(IEmitter emitter, string propertyName, Basel
MapPropertyName(emitter, propertyName);
emitter.Emit(new MappingStart());
MapProperty(emitter, nameof(baselineSpec.Configuration), baselineSpec.Configuration);
- MapProperty(emitter, nameof(baselineSpec.Convention), baselineSpec.Convention);
+ MapProperty(emitter, nameof(baselineSpec.Override), baselineSpec.Override);
MapProperty(emitter, nameof(baselineSpec.Rule), baselineSpec.Rule);
emitter.Emit(new MappingEnd());
}
@@ -155,6 +156,20 @@ private static void MapProperty(IEmitter emitter, string propertyName, Conventio
emitter.Emit(new MappingEnd());
}
+ ///
+ /// Map a OverrideOption property.
+ ///
+ private static void MapProperty(IEmitter emitter, string propertyName, OverrideOption value)
+ {
+ if (value == null)
+ return;
+
+ MapPropertyName(emitter, propertyName);
+ emitter.Emit(new MappingStart());
+ MapProperty(emitter, nameof(value.Level), value.Level?.ToDictionary());
+ emitter.Emit(new MappingEnd());
+ }
+
///
/// Map a RuleOption property.
///
diff --git a/src/PSRule/Common/JsonConverters.cs b/src/PSRule/Common/JsonConverters.cs
index 820bc4c374..6b51012fe1 100644
--- a/src/PSRule/Common/JsonConverters.cs
+++ b/src/PSRule/Common/JsonConverters.cs
@@ -6,6 +6,7 @@
using Newtonsoft.Json.Serialization;
using PSRule.Annotations;
using PSRule.Configuration;
+using PSRule.Converters;
using PSRule.Data;
using PSRule.Definitions;
using PSRule.Definitions.Baselines;
@@ -976,6 +977,53 @@ public override void WriteJson(JsonWriter writer, ResourceId value, JsonSerializ
}
}
+///
+/// A converter for converting to/ from JSON.
+///
+internal sealed class EnumMapJsonConverter : JsonConverter where T : struct, Enum
+{
+ public override bool CanRead => true;
+
+ public override bool CanWrite => false;
+
+ public override bool CanConvert(Type objectType)
+ {
+ return typeof(EnumMap).IsAssignableFrom(objectType);
+ }
+
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ var map = existingValue as EnumMap ?? new EnumMap();
+ ReadMap(map, reader);
+ return map;
+ }
+
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ throw new NotImplementedException();
+ }
+
+ private static void ReadMap(EnumMap map, JsonReader reader)
+ {
+ if (reader.TokenType != JsonToken.StartObject || !reader.Read())
+ throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed);
+
+ string propertyName = null;
+ while (reader.TokenType != JsonToken.EndObject)
+ {
+ if (reader.TokenType == JsonToken.PropertyName)
+ {
+ propertyName = reader.Value.ToString();
+ }
+ else if (reader.TokenType == JsonToken.String && TypeConverter.TryEnum(reader.Value, convert: true, out var value) && value != null)
+ {
+ map.Add(propertyName, value.Value);
+ }
+ reader.Read();
+ }
+ }
+}
+
///
/// A converter for converting to/ from JSON.
///
diff --git a/src/PSRule/Common/YamlConverters.cs b/src/PSRule/Common/YamlConverters.cs
index 888fcaa4b1..2bee8a8464 100644
--- a/src/PSRule/Common/YamlConverters.cs
+++ b/src/PSRule/Common/YamlConverters.cs
@@ -6,6 +6,7 @@
using System.Reflection;
using PSRule.Annotations;
using PSRule.Configuration;
+using PSRule.Converters;
using PSRule.Data;
using PSRule.Definitions;
using PSRule.Definitions.Expressions;
@@ -1023,4 +1024,55 @@ public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializ
}
}
+///
+/// A converter for converting to/ from YAML.
+///
+internal sealed class EnumMapYamlTypeConverter : IYamlTypeConverter where T : struct, Enum
+{
+ public bool Accepts(Type type)
+ {
+ return typeof(EnumMap).IsAssignableFrom(type);
+ }
+
+ public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer)
+ {
+ var map = new EnumMap();
+ if (parser.TryConsume(out _))
+ {
+ while (parser.TryConsume(out var s1))
+ {
+ var propertyName = s1.Value;
+ if (parser.TryConsume(out var s2) && TypeConverter.TryEnum(s2.Value, convert: true, out var value) && value != null)
+ map.Add(propertyName, value.Value);
+ }
+ parser.Require();
+ parser.MoveNext();
+ }
+ else
+ {
+ parser.SkipThisAndNestedEvents();
+ }
+ return map;
+ }
+
+ public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer)
+ {
+ if (type == typeof(EnumMap) && value == null)
+ {
+ emitter.Emit(new MappingStart());
+ emitter.Emit(new MappingEnd());
+ }
+ if (value is not EnumMap map)
+ return;
+
+ emitter.Emit(new MappingStart());
+ foreach (var kv in map)
+ {
+ emitter.Emit(new Scalar(kv.Key));
+ emitter.Emit(new Scalar(kv.Value.ToString()));
+ }
+ emitter.Emit(new MappingEnd());
+ }
+}
+
#nullable restore
diff --git a/src/PSRule/Configuration/BaselineOption.cs b/src/PSRule/Configuration/BaselineOption.cs
index 1ca945dc34..0bb077b001 100644
--- a/src/PSRule/Configuration/BaselineOption.cs
+++ b/src/PSRule/Configuration/BaselineOption.cs
@@ -3,6 +3,7 @@
using System.Collections;
using PSRule.Definitions.Baselines;
+using PSRule.Options;
namespace PSRule.Configuration;
@@ -32,7 +33,7 @@ public BaselineInline()
public ConfigurationOption Configuration { get; set; }
- public ConventionOption Convention { get; set; }
+ public OverrideOption Override { get; set; }
public RuleOption Rule { get; set; }
}
@@ -104,6 +105,7 @@ internal static void Load(IBaselineV1Spec option)
// Rule.Tag - currently not supported
// Process configuration values
+ option.Override.Load();
option.Configuration.Load();
}
@@ -128,6 +130,7 @@ internal static void Load(IBaselineV1Spec option, Dictionary pro
option.Rule.Tag = tag;
// Process configuration values
+ option.Override.Import(properties);
option.Configuration.Load(properties);
}
}
diff --git a/src/PSRule/Configuration/PSRuleOption.cs b/src/PSRule/Configuration/PSRuleOption.cs
index 479832390e..19543ad0d0 100644
--- a/src/PSRule/Configuration/PSRuleOption.cs
+++ b/src/PSRule/Configuration/PSRuleOption.cs
@@ -6,6 +6,8 @@
using System.Management.Automation;
using PSRule.Converters.Yaml;
using PSRule.Definitions.Baselines;
+using PSRule.Definitions.Rules;
+using PSRule.Options;
using PSRule.Pipeline;
using PSRule.Resources;
using YamlDotNet.Core;
@@ -34,11 +36,12 @@ public sealed class PSRuleOption : IEquatable, IBaselineV1Spec
Baseline = Options.BaselineOption.Default,
Binding = BindingOption.Default,
Convention = ConventionOption.Default,
- Execution = Options.ExecutionOption.Default,
+ Execution = ExecutionOption.Default,
Include = IncludeOption.Default,
Input = InputOption.Default,
Logging = LoggingOption.Default,
Output = OutputOption.Default,
+ Override = OverrideOption.Default,
Rule = RuleOption.Default,
};
@@ -52,11 +55,12 @@ public PSRuleOption()
Binding = new BindingOption();
Configuration = new ConfigurationOption();
Convention = new ConventionOption();
- Execution = new Options.ExecutionOption();
+ Execution = new ExecutionOption();
Include = new IncludeOption();
Input = new InputOption();
Logging = new LoggingOption();
Output = new OutputOption();
+ Override = new OverrideOption();
Repository = new RepositoryOption();
Requires = new RequiresOption();
Rule = new RuleOption();
@@ -72,11 +76,12 @@ private PSRuleOption(string sourcePath, PSRuleOption option)
Binding = new BindingOption(option?.Binding);
Configuration = new ConfigurationOption(option?.Configuration);
Convention = new ConventionOption(option?.Convention);
- Execution = new Options.ExecutionOption(option?.Execution);
+ Execution = new ExecutionOption(option?.Execution);
Include = new IncludeOption(option?.Include);
Input = new InputOption(option?.Input);
Logging = new LoggingOption(option?.Logging);
Output = new OutputOption(option?.Output);
+ Override = new OverrideOption(option?.Override);
Repository = new RepositoryOption(option?.Repository);
Requires = new RequiresOption(option?.Requires);
Rule = new RuleOption(option?.Rule);
@@ -106,7 +111,7 @@ private PSRuleOption(string sourcePath, PSRuleOption option)
///
/// Options that configure the execution sandbox.
///
- public Options.ExecutionOption Execution { get; set; }
+ public ExecutionOption Execution { get; set; }
///
/// Options that affect source locations imported for execution.
@@ -128,6 +133,11 @@ private PSRuleOption(string sourcePath, PSRuleOption option)
///
public OutputOption Output { get; set; }
+ ///
+ /// Options that configure additional rule overrides.
+ ///
+ public OverrideOption Override { get; set; }
+
///
/// Options for repository properties that are used by PSRule.
///
@@ -199,16 +209,17 @@ 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);
- result.Execution = Options.ExecutionOption.Combine(result.Execution, o2?.Execution);
- result.Include = IncludeOption.Combine(result.Include, o2?.Include);
- result.Input = InputOption.Combine(result.Input, o2?.Input);
- result.Logging = LoggingOption.Combine(result.Logging, o2?.Logging);
- result.Repository = RepositoryOption.Combine(result.Repository, o2?.Repository);
- result.Output = OutputOption.Combine(result.Output, o2?.Output);
+ 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);
+ result.Execution = ExecutionOption.Combine(result?.Execution, o2?.Execution);
+ result.Include = IncludeOption.Combine(result?.Include, o2?.Include);
+ result.Input = InputOption.Combine(result?.Input, o2?.Input);
+ result.Logging = LoggingOption.Combine(result?.Logging, o2?.Logging);
+ result.Output = OutputOption.Combine(result?.Output, o2?.Output);
+ result.Override = OverrideOption.Combine(result?.Override, o2?.Override);
+ result.Repository = RepositoryOption.Combine(result?.Repository, o2?.Repository);
return result;
}
@@ -299,6 +310,7 @@ private static PSRuleOption FromYaml(string path, string yaml)
.IgnoreUnmatchedProperties()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.WithTypeConverter(new FieldMapYamlTypeConverter())
+ .WithTypeConverter(new EnumMapYamlTypeConverter())
.WithTypeConverter(new StringArrayMapConverter())
.WithTypeConverter(new SuppressionRuleYamlTypeConverter())
.WithTypeConverter(new PSObjectYamlTypeConverter())
@@ -336,6 +348,7 @@ private static PSRuleOption FromEnvironment(PSRuleOption option)
option.Input.Load();
option.Logging.Load();
option.Output.Load();
+ option.Override.Load();
option.Repository.Load();
option.Requires.Load();
BaselineOption.Load(option);
@@ -366,6 +379,7 @@ public static PSRuleOption FromHashtable(Hashtable hashtable)
option.Input.Load(index);
option.Logging.Load(index);
option.Output.Load(index);
+ option.Override.Import(index);
option.Repository.Load(index);
option.Requires.Load(index);
BaselineOption.Load(option, index);
@@ -431,6 +445,7 @@ public bool Equals(PSRuleOption other)
Input == other.Input &&
Logging == other.Logging &&
Output == other.Output &&
+ Override == other.Override &&
Suppression == other.Suppression &&
Repository == other.Repository &&
Rule == other.Rule;
@@ -451,6 +466,7 @@ public override int GetHashCode()
hash = hash * 23 + (Input != null ? Input.GetHashCode() : 0);
hash = hash * 23 + (Logging != null ? Logging.GetHashCode() : 0);
hash = hash * 23 + (Output != null ? Output.GetHashCode() : 0);
+ hash = hash * 23 + (Override != null ? Override.GetHashCode() : 0);
hash = hash * 23 + (Suppression != null ? Suppression.GetHashCode() : 0);
hash = hash * 23 + (Repository != null ? Repository.GetHashCode() : 0);
hash = hash * 23 + (Rule != null ? Rule.GetHashCode() : 0);
diff --git a/src/PSRule/Definitions/Baselines/BaselineSpec.cs b/src/PSRule/Definitions/Baselines/BaselineSpec.cs
index 6753ee90d8..c1f6325373 100644
--- a/src/PSRule/Definitions/Baselines/BaselineSpec.cs
+++ b/src/PSRule/Definitions/Baselines/BaselineSpec.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT License.
using PSRule.Configuration;
+using PSRule.Options;
namespace PSRule.Definitions.Baselines;
@@ -18,4 +19,7 @@ public sealed class BaselineSpec : Spec, IBaselineV1Spec
///
public RuleOption Rule { get; set; }
+
+ ///
+ public OverrideOption Override { get; set; }
}
diff --git a/src/PSRule/Definitions/Baselines/IBaselineV1Spec.cs b/src/PSRule/Definitions/Baselines/IBaselineV1Spec.cs
index 7024cdf25b..b5f592897d 100644
--- a/src/PSRule/Definitions/Baselines/IBaselineV1Spec.cs
+++ b/src/PSRule/Definitions/Baselines/IBaselineV1Spec.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT License.
using PSRule.Configuration;
+using PSRule.Options;
namespace PSRule.Definitions.Baselines;
@@ -16,12 +17,12 @@ internal interface IBaselineV1Spec
ConfigurationOption Configuration { get; set; }
///
- /// Options that configure conventions.
+ /// Options for that affect which rules are executed by including and filtering discovered rules.
///
- ConventionOption Convention { get; set; }
+ RuleOption Rule { get; set; }
///
- /// Options for that affect which rules are executed by including and filtering discovered rules.
+ /// Options that configure additional rule overrides.
///
- RuleOption Rule { get; set; }
+ OverrideOption Override { get; set; }
}
diff --git a/src/PSRule/Definitions/Rules/RuleOverride.cs b/src/PSRule/Definitions/Rules/RuleOverride.cs
new file mode 100644
index 0000000000..357e2cdd57
--- /dev/null
+++ b/src/PSRule/Definitions/Rules/RuleOverride.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace PSRule.Definitions.Rules;
+
+///
+/// Any overrides for the rule.
+///
+public sealed class RuleOverride
+{
+ ///
+ /// If the rule fails, how serious is the result.
+ ///
+ public SeverityLevel? Level { get; set; }
+}
diff --git a/src/PSRule/Definitions/Rules/RuleProperties.cs b/src/PSRule/Definitions/Rules/RuleProperties.cs
new file mode 100644
index 0000000000..81447924b1
--- /dev/null
+++ b/src/PSRule/Definitions/Rules/RuleProperties.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace PSRule.Definitions.Rules;
+
+///
+/// Any rule properties.
+///
+public sealed class RuleProperties
+{
+ ///
+ /// If the rule fails, how serious is the result.
+ ///
+ public SeverityLevel Level { get; set; }
+}
diff --git a/src/PSRule/Common/SeverityLevelExtensions.cs b/src/PSRule/Definitions/Rules/SeverityLevelExtensions.cs
similarity index 91%
rename from src/PSRule/Common/SeverityLevelExtensions.cs
rename to src/PSRule/Definitions/Rules/SeverityLevelExtensions.cs
index b86cd4be91..84cc7ae613 100644
--- a/src/PSRule/Common/SeverityLevelExtensions.cs
+++ b/src/PSRule/Definitions/Rules/SeverityLevelExtensions.cs
@@ -1,9 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-using PSRule.Definitions.Rules;
-
-namespace PSRule;
+namespace PSRule.Definitions.Rules;
internal static class SeverityLevelExtensions
{
diff --git a/src/PSRule/Host/HostHelper.cs b/src/PSRule/Host/HostHelper.cs
index 35c63ea733..f2358f8553 100644
--- a/src/PSRule/Host/HostHelper.cs
+++ b/src/PSRule/Host/HostHelper.cs
@@ -261,6 +261,7 @@ private static ILanguageBlock[] GetYamlLanguageBlocks(Source[] sources, IResourc
.WithTypeConverter(new StringArrayMapConverter())
.WithTypeConverter(new StringArrayConverter())
.WithTypeConverter(new PSObjectYamlTypeConverter())
+ .WithTypeConverter(new EnumMapYamlTypeConverter())
.WithNodeTypeResolver(new PSOptionYamlTypeResolver())
.WithNodeDeserializer(
inner => new ResourceNodeDeserializer(context, new LanguageExpressionDeserializer(context, inner)),
@@ -327,6 +328,7 @@ private static ILanguageBlock[] GetJsonLanguageBlocks(Source[] sources, IResourc
deserializer.Converters.Add(new FieldMapJsonConverter());
deserializer.Converters.Add(new StringArrayJsonConverter());
deserializer.Converters.Add(new LanguageExpressionJsonConverter(context));
+ deserializer.Converters.Add(new EnumMapJsonConverter());
try
{
@@ -518,6 +520,7 @@ private static DependencyTargetCollection ToRuleBlockV1(ILanguageBloc
var knownRuleNames = new HashSet(StringComparer.OrdinalIgnoreCase);
var knownRuleIds = new HashSet(ResourceIdEqualityComparer.Default);
+ // Process from PowerShell
foreach (var block in blocks.OfType())
{
if (knownRuleIds.ContainsIds(block.Id, block.Ref, block.Alias, out var duplicateId))
@@ -537,6 +540,7 @@ private static DependencyTargetCollection ToRuleBlockV1(ILanguageBloc
knownRuleIds.AddIds(block.Id, block.Ref, block.Alias);
}
+ // Process from YAML/ JSON
foreach (var block in blocks.OfType())
{
var ruleName = block.Name;
@@ -553,6 +557,7 @@ private static DependencyTargetCollection ToRuleBlockV1(ILanguageBloc
}
context.EnterLanguageScope(block.Source);
+ context.LanguageScope.TryGetOverride(block.Id, out var propertyOverride);
try
{
var info = GetRuleHelpInfo(context, block) ?? new RuleHelpInfo(
@@ -568,7 +573,11 @@ private static DependencyTargetCollection ToRuleBlockV1(ILanguageBloc
source: block.Source,
id: block.Id,
@ref: block.Ref,
- level: block.Level,
+ @default: new RuleProperties
+ {
+ Level = block.Level
+ },
+ @override: propertyOverride,
info: info,
condition: new RuleVisitor(context, block.Id, block.Source, block.Spec),
alias: block.Alias,
diff --git a/src/PSRule/PSRule.psm1 b/src/PSRule/PSRule.psm1
index efc8092666..dcd8f9fe33 100644
--- a/src/PSRule/PSRule.psm1
+++ b/src/PSRule/PSRule.psm1
@@ -1326,6 +1326,10 @@ function New-PSRuleOption {
[ValidateSet('Client', 'Plain', 'AzurePipelines', 'GitHubActions', 'VisualStudioCode', 'Detect')]
[PSRule.Configuration.OutputStyle]$OutputStyle = [PSRule.Configuration.OutputStyle]::Detect,
+ # Sets the OverrideLevel option
+ [Parameter(Mandatory = $False)]
+ [Hashtable]$OverrideLevel,
+
# Sets the Repository.BaseRef option
[Parameter(Mandatory = $False)]
[String]$RepositoryBaseRef,
@@ -1629,6 +1633,10 @@ function Set-PSRuleOption {
[ValidateSet('Client', 'Plain', 'AzurePipelines', 'GitHubActions', 'VisualStudioCode', 'Detect')]
[PSRule.Configuration.OutputStyle]$OutputStyle = [PSRule.Configuration.OutputStyle]::Detect,
+ # Sets the OverrideLevel option
+ [Parameter(Mandatory = $False)]
+ [Hashtable]$OverrideLevel,
+
# Sets the Repository.BaseRef option
[Parameter(Mandatory = $False)]
[String]$RepositoryBaseRef,
@@ -2393,6 +2401,10 @@ function SetOptions {
[ValidateSet('Client', 'Plain', 'AzurePipelines', 'GitHubActions', 'VisualStudioCode', 'Detect')]
[PSRule.Configuration.OutputStyle]$OutputStyle = [PSRule.Configuration.OutputStyle]::Detect,
+ # Sets the OverrideLevel option
+ [Parameter(Mandatory = $False)]
+ [Hashtable]$OverrideLevel,
+
# Sets the Repository.BaseRef option
[Parameter(Mandatory = $False)]
[String]$RepositoryBaseRef,
@@ -2643,6 +2655,11 @@ function SetOptions {
$Option.Output.Style = $OutputStyle;
}
+ # Sets option Override.Level
+ if ($PSBoundParameters.ContainsKey('OverrideLevel')) {
+ $Option.Override.Level = $OverrideLevel;
+ }
+
# Sets option Repository.BaseRef
if ($PSBoundParameters.ContainsKey('RepositoryBaseRef')) {
$Option.Repository.BaseRef = $RepositoryBaseRef;
diff --git a/src/PSRule/Pipeline/Formatters/AzurePipelinesFormatter.cs b/src/PSRule/Pipeline/Formatters/AzurePipelinesFormatter.cs
index 0a7dc1ee1b..c6b1a17379 100644
--- a/src/PSRule/Pipeline/Formatters/AzurePipelinesFormatter.cs
+++ b/src/PSRule/Pipeline/Formatters/AzurePipelinesFormatter.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT License.
using PSRule.Configuration;
-using PSRule.Definitions.Rules;
using PSRule.Rules;
namespace PSRule.Pipeline.Formatters;
@@ -51,13 +50,13 @@ protected override void FailDetail(RuleRecord record)
{
base.FailDetail(record);
var message = GetFailMessage(record);
- if (record.Level == SeverityLevel.Error)
+ if (record.Level == Definitions.Rules.SeverityLevel.Error)
Error(message);
- if (record.Level == SeverityLevel.Warning)
+ if (record.Level == Definitions.Rules.SeverityLevel.Warning)
Warning(message);
- if (record.Level != SeverityLevel.Information)
+ if (record.Level != Definitions.Rules.SeverityLevel.Information)
LineBreak();
}
}
diff --git a/src/PSRule/Pipeline/Formatters/GitHubActionsFormatter.cs b/src/PSRule/Pipeline/Formatters/GitHubActionsFormatter.cs
index 3c57d3fa7c..e1baf01fa9 100644
--- a/src/PSRule/Pipeline/Formatters/GitHubActionsFormatter.cs
+++ b/src/PSRule/Pipeline/Formatters/GitHubActionsFormatter.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT License.
using PSRule.Configuration;
-using PSRule.Definitions.Rules;
using PSRule.Rules;
namespace PSRule.Pipeline.Formatters;
@@ -53,13 +52,13 @@ protected override void FailDetail(RuleRecord record)
{
base.FailDetail(record);
var message = GetFailMessage(record);
- if (record.Level == SeverityLevel.Error)
+ if (record.Level == Definitions.Rules.SeverityLevel.Error)
Error(message);
- if (record.Level == SeverityLevel.Warning)
+ if (record.Level == Definitions.Rules.SeverityLevel.Warning)
Warning(message);
- if (record.Level == SeverityLevel.Information)
+ if (record.Level == Definitions.Rules.SeverityLevel.Information)
Information(message);
LineBreak();
diff --git a/src/PSRule/Pipeline/OptionContext.cs b/src/PSRule/Pipeline/OptionContext.cs
index 91a9a6466d..db7e529820 100644
--- a/src/PSRule/Pipeline/OptionContext.cs
+++ b/src/PSRule/Pipeline/OptionContext.cs
@@ -51,6 +51,8 @@ public ConventionOption Convention
public OutputOption Output { get; set; }
+ public OverrideOption Override { get; set; }
+
public RepositoryOption Repository { get; set; }
public RequiresOption Requires { get; set; }
diff --git a/src/PSRule/Pipeline/OptionContextBuilder.cs b/src/PSRule/Pipeline/OptionContextBuilder.cs
index f37c94b506..7d576ac6b1 100644
--- a/src/PSRule/Pipeline/OptionContextBuilder.cs
+++ b/src/PSRule/Pipeline/OptionContextBuilder.cs
@@ -150,6 +150,7 @@ private static void Combine(OptionContext context, OptionScope optionScope)
context.Input = InputOption.Combine(context.Input, optionScope.Input);
context.Logging = LoggingOption.Combine(context.Logging, optionScope.Logging);
context.Output = OutputOption.Combine(context.Output, optionScope.Output);
+ context.Override = OverrideOption.Combine(context.Override, optionScope.Override);
context.Repository = RepositoryOption.Combine(context.Repository, optionScope.Repository);
context.Requires = RequiresOption.Combine(context.Requires, optionScope.Requires);
context.Rule = RuleOption.Combine(context.Rule, optionScope.Rule);
diff --git a/src/PSRule/Pipeline/OptionScope.cs b/src/PSRule/Pipeline/OptionScope.cs
index 2494db4f07..c41e7f12f1 100644
--- a/src/PSRule/Pipeline/OptionScope.cs
+++ b/src/PSRule/Pipeline/OptionScope.cs
@@ -69,6 +69,8 @@ private OptionScope(ScopeType type, string languageScope)
public OutputOption Output { get; set; }
+ public OverrideOption Override { get; set; }
+
public RepositoryOption Repository { get; set; }
public RequiresOption Requires { get; set; }
@@ -109,6 +111,7 @@ public static OptionScope FromWorkspace(PSRuleOption option)
Input = option.Input,
Logging = option.Logging,
Output = option.Output,
+ Override = option.Override,
Repository = option.Repository,
Requires = option.Requires,
Rule = option.Rule,
@@ -146,7 +149,8 @@ internal static OptionScope FromBaseline(ScopeType type, string baselineId, stri
Tag = spec.Rule?.Tag,
Labels = spec.Rule?.Labels,
IncludeLocal = type == ScopeType.Explicit ? false : null
- }
+ },
+ Override = spec.Override
};
}
diff --git a/src/PSRule/Pipeline/Output/SarifBuilder.cs b/src/PSRule/Pipeline/Output/SarifBuilder.cs
index 187aadb002..e51d5b1dd1 100644
--- a/src/PSRule/Pipeline/Output/SarifBuilder.cs
+++ b/src/PSRule/Pipeline/Output/SarifBuilder.cs
@@ -41,13 +41,13 @@ internal sealed class SarifBuilder
public SarifBuilder(Source[] source, PSRuleOption option)
{
_Option = option;
- _Rules = new Dictionary();
- _Extensions = new Dictionary();
- _Artifacts = new Dictionary();
+ _Rules = [];
+ _Extensions = [];
+ _Artifacts = [];
_Run = new Run
{
Tool = GetTool(source),
- Results = new List(),
+ Results = [],
Invocations = GetInvocation(),
AutomationDetails = GetAutomationDetails(),
OriginalUriBaseIds = GetBaseIds(),
@@ -279,8 +279,8 @@ private ReportingDescriptorReference AddRule(RuleRecord record, string id)
DefaultConfiguration = new ReportingConfiguration
{
Enabled = true,
- Level = GetLevel(record),
- }
+ Level = GetLevel(record.Default.Level),
+ },
};
toolComponent.Rules.Add(descriptor);
@@ -294,9 +294,27 @@ private ReportingDescriptorReference AddRule(RuleRecord record, string id)
Guid = toolComponent.Guid,
Name = toolComponent.Name,
Index = _Run.Tool.Extensions == null ? -1 : _Run.Tool.Extensions.IndexOf(toolComponent),
- }
+ },
};
+
_Rules.Add(id, descriptorReference);
+
+ // Create a configuration override if applicable.
+ if (record.Override != null && record.Override.Level.HasValue && record.Override.Level.Value != SeverityLevel.None && record.Override.Level != record.Default.Level)
+ {
+ if (_Run.Invocations[0].RuleConfigurationOverrides == null)
+ _Run.Invocations[0].RuleConfigurationOverrides = [];
+
+ _Run.Invocations[0].RuleConfigurationOverrides.Add(new ConfigurationOverride
+ {
+ Descriptor = descriptorReference,
+ Configuration = new ReportingConfiguration
+ {
+ Level = GetLevel(record.Override.Level.Value),
+ }
+ });
+ }
+
return descriptorReference;
}
@@ -417,10 +435,24 @@ private static FailureLevel GetLevel(RuleRecord record)
if (record.Outcome != RuleOutcome.Fail)
return FailureLevel.None;
- if (record.Level == SeverityLevel.Error)
- return FailureLevel.Error;
+ return record.Level switch
+ {
+ SeverityLevel.Error => FailureLevel.Error,
+ SeverityLevel.Warning => FailureLevel.Warning,
+ SeverityLevel.Information => FailureLevel.Note,
+ _ => FailureLevel.None,
+ };
+ }
- return record.Level == SeverityLevel.Warning ? FailureLevel.Warning : FailureLevel.Note;
+ private static FailureLevel GetLevel(SeverityLevel level)
+ {
+ return level switch
+ {
+ SeverityLevel.Error => FailureLevel.Error,
+ SeverityLevel.Warning => FailureLevel.Warning,
+ SeverityLevel.Information => FailureLevel.Note,
+ _ => FailureLevel.None,
+ };
}
private Tool GetTool(Source[] source)
diff --git a/src/PSRule/Pipeline/Output/YamlOutputWriter.cs b/src/PSRule/Pipeline/Output/YamlOutputWriter.cs
index e6106444f9..84fadb7e87 100644
--- a/src/PSRule/Pipeline/Output/YamlOutputWriter.cs
+++ b/src/PSRule/Pipeline/Output/YamlOutputWriter.cs
@@ -3,6 +3,7 @@
using PSRule.Configuration;
using PSRule.Definitions.Baselines;
+using PSRule.Definitions.Rules;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.Serialization;
@@ -29,6 +30,7 @@ internal static string ToYaml(object[] o)
.WithTypeConverter(new PSObjectYamlTypeConverter())
.WithTypeConverter(new FieldMapYamlTypeConverter())
.WithTypeConverter(new InfoStringYamlTypeConverter())
+ .WithTypeConverter(new EnumMapYamlTypeConverter())
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults)
.Build();
diff --git a/src/PSRule/Pipeline/PipelineBuilderBase.cs b/src/PSRule/Pipeline/PipelineBuilderBase.cs
index 29713f10f0..f4c85a17d3 100644
--- a/src/PSRule/Pipeline/PipelineBuilderBase.cs
+++ b/src/PSRule/Pipeline/PipelineBuilderBase.cs
@@ -93,6 +93,7 @@ public virtual IPipelineBuilder Configure(PSRuleOption option)
Option.Output.Outcome ??= OutputOption.Default.Outcome;
Option.Output.Banner ??= OutputOption.Default.Banner;
Option.Output.Style = GetStyle(option.Output.Style ?? OutputOption.Default.Style.Value);
+ Option.Override = new OverrideOption(option.Override);
Option.Repository = GetRepository(option.Repository);
return this;
}
diff --git a/src/PSRule/Rules/RuleBlock.cs b/src/PSRule/Rules/RuleBlock.cs
index 22cbf088c5..ebe46daff8 100644
--- a/src/PSRule/Rules/RuleBlock.cs
+++ b/src/PSRule/Rules/RuleBlock.cs
@@ -23,7 +23,7 @@ namespace PSRule.Rules;
[DebuggerDisplay("{Id} @{Source.Path}")]
internal sealed class RuleBlock : ILanguageBlock, IDependencyTarget, IDisposable, IResource, IRuleV1
{
- internal RuleBlock(ISourceFile source, ResourceId id, ResourceId? @ref, SeverityLevel level, RuleHelpInfo info, ICondition condition, IResourceTags tag, ResourceId[] alias, ResourceId[] dependsOn, Hashtable configuration, ISourceExtent extent, ResourceFlags flags, IResourceLabels labels)
+ internal RuleBlock(ISourceFile source, ResourceId id, ResourceId? @ref, RuleProperties @default, RuleOverride @override, RuleHelpInfo info, ICondition condition, IResourceTags tag, ResourceId[] alias, ResourceId[] dependsOn, Hashtable configuration, ISourceExtent extent, ResourceFlags flags, IResourceLabels labels)
{
Source = source;
Name = id.Name;
@@ -33,7 +33,9 @@ internal RuleBlock(ISourceFile source, ResourceId id, ResourceId? @ref, Severity
Ref = @ref;
Alias = alias;
- Level = level;
+ Default = @default;
+ Override = @override;
+ Level = Override != null && Override.Level.HasValue && Override.Level != SeverityLevel.None ? Override.Level.Value : Default.Level;
Info = info;
Condition = condition;
Tag = tag;
@@ -106,6 +108,14 @@ internal RuleBlock(ISourceFile source, ResourceId id, ResourceId? @ref, Severity
[YamlIgnore]
public ResourceFlags Flags { get; }
+ [JsonIgnore]
+ [YamlIgnore]
+ public RuleProperties Default { get; }
+
+ [JsonIgnore]
+ [YamlIgnore]
+ public RuleOverride Override { get; }
+
ResourceId[] IDependencyTarget.DependsOn => DependsOn;
bool IDependencyTarget.Dependency => Source.IsDependency();
diff --git a/src/PSRule/Rules/RuleRecord.cs b/src/PSRule/Rules/RuleRecord.cs
index 6a71cc53f9..bd994ef462 100644
--- a/src/PSRule/Rules/RuleRecord.cs
+++ b/src/PSRule/Rules/RuleRecord.cs
@@ -14,6 +14,8 @@
namespace PSRule.Rules;
+#nullable enable
+
///
/// A detailed format for rule results.
///
@@ -25,7 +27,7 @@ public sealed class RuleRecord : IDetailedRuleResultV2
internal readonly ResultDetail _Detail;
- internal RuleRecord(string runId, ResourceId ruleId, string @ref, TargetObject targetObject, string targetName, string targetType, IResourceTags tag, RuleHelpInfo info, Hashtable field, SeverityLevel level, ISourceExtent extent, RuleOutcome outcome = RuleOutcome.None, RuleOutcomeReason reason = RuleOutcomeReason.None)
+ internal RuleRecord(string runId, ResourceId ruleId, string @ref, TargetObject targetObject, string targetName, string targetType, IResourceTags tag, RuleHelpInfo info, Hashtable field, RuleProperties @default, ISourceExtent extent, RuleOutcome outcome = RuleOutcome.None, RuleOutcomeReason reason = RuleOutcomeReason.None, RuleOverride? @override = null)
{
_TargetObject = targetObject;
RunId = runId;
@@ -39,8 +41,11 @@ internal RuleRecord(string runId, ResourceId ruleId, string @ref, TargetObject t
OutcomeReason = reason;
Info = info;
Source = targetObject.Source.GetSourceInfo();
- Level = level;
Extent = extent;
+ Default = @default;
+ Override = @override;
+ Level = Override?.Level ?? Default.Level;
+
_Detail = new ResultDetail();
if (tag != null)
Tag = tag.ToHashtable();
@@ -59,7 +64,7 @@ internal RuleRecord(string runId, ResourceId ruleId, string @ref, TargetObject t
/// A unique identifier for the rule.
///
///
- /// An additional opaque identifer may also be provided by by .
+ /// An additional opaque identifier may also be provided by by .
///
[JsonIgnore]
[YamlIgnore]
@@ -106,14 +111,14 @@ internal RuleRecord(string runId, ResourceId ruleId, string @ref, TargetObject t
///
[JsonIgnore]
[YamlIgnore]
- public string Recommendation => Info.Recommendation?.Text ?? Info.Synopsis?.Text;
+ public string? Recommendation => Info.Recommendation?.Text ?? Info.Synopsis?.Text;
///
/// The reason for the failed condition.
///
[DefaultValue(null)]
[JsonProperty(PropertyName = "reason")]
- public string[] Reason => _Detail.Count > 0 ? _Detail.GetReasonStrings() : null;
+ public string[]? Reason => _Detail.Count > 0 ? _Detail.GetReasonStrings() : null;
///
/// A name to identify the target object.
@@ -189,6 +194,20 @@ internal RuleRecord(string runId, ResourceId ruleId, string @ref, TargetObject t
[YamlMember()]
public IResultDetailV2 Detail => _Detail;
+ ///
+ /// Any default properties for the rule.
+ ///
+ [JsonIgnore]
+ [YamlIgnore]
+ public RuleProperties Default { get; set; }
+
+ ///
+ /// Any overrides for the rule.
+ ///
+ [JsonIgnore]
+ [YamlIgnore]
+ public RuleOverride? Override { get; }
+
///
/// Determine if the rule is successful or skipped.
///
@@ -221,3 +240,5 @@ internal bool HasSource()
return Source != null && Source.Length > 0;
}
}
+
+#nullable restore
diff --git a/src/PSRule/Runtime/ILanguageScope.cs b/src/PSRule/Runtime/ILanguageScope.cs
index f0caba0788..b2e144073e 100644
--- a/src/PSRule/Runtime/ILanguageScope.cs
+++ b/src/PSRule/Runtime/ILanguageScope.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT License.
using PSRule.Definitions;
+using PSRule.Definitions.Rules;
using PSRule.Pipeline;
using PSRule.Runtime.Binding;
@@ -67,6 +68,11 @@ internal interface ILanguageScope : IDisposable
/// Try to bind the name of the object.
///
bool TryGetName(object o, out string? name, out string? path);
+
+ ///
+ /// Try to get a rule override by resource ID.
+ ///
+ bool TryGetOverride(ResourceId id, out RuleOverride? propertyOverride);
}
#nullable restore
diff --git a/src/PSRule/Runtime/LanguageScope.cs b/src/PSRule/Runtime/LanguageScope.cs
index 5a3bfbdd72..9484721700 100644
--- a/src/PSRule/Runtime/LanguageScope.cs
+++ b/src/PSRule/Runtime/LanguageScope.cs
@@ -3,7 +3,10 @@
using System.Diagnostics;
using PSRule.Configuration;
+using PSRule.Data;
using PSRule.Definitions;
+using PSRule.Definitions.Rules;
+using PSRule.Options;
using PSRule.Pipeline;
using PSRule.Runtime.Binding;
@@ -15,6 +18,7 @@ namespace PSRule.Runtime;
internal sealed class LanguageScope : ILanguageScope
{
private IDictionary? _Configuration;
+ private WildcardMap? _Override;
private readonly Dictionary _Service;
private readonly Dictionary _Filter;
private ITargetBinder? _TargetBinder;
@@ -59,6 +63,18 @@ public void Configure(OptionContext context)
var builder = new TargetBinderBuilder(context.BindTargetName, context.BindTargetType, context.BindField, context.InputTargetType);
_TargetBinder = builder.Build(context.Binding);
+ _Override = WithOverride(context.Override);
+ }
+
+ private static WildcardMap? WithOverride(OverrideOption option)
+ {
+ if (option == null || option.Level == null)
+ return default;
+
+ var overrides = option.Level
+ .Where(l => l.Value != SeverityLevel.None)
+ .Select(l => new KeyValuePair(l.Key, new RuleOverride { Level = l.Value }));
+ return new WildcardMap(overrides);
}
///
@@ -68,6 +84,17 @@ public bool TryConfigurationValue(string key, out object? value)
return !string.IsNullOrEmpty(key) && _Configuration != null && _Configuration.TryGetValue(key, out value);
}
+ ///
+ public bool TryGetOverride(ResourceId id, out RuleOverride? value)
+ {
+ value = default;
+ if (_Override == null)
+ return false;
+
+ return _Override.TryGetValue(id.Value, out value) ||
+ _Override.TryGetValue(id.Name, out value);
+ }
+
///
public void WithFilter(IResourceFilter resourceFilter)
{
diff --git a/src/PSRule/Runtime/RunspaceContext.cs b/src/PSRule/Runtime/RunspaceContext.cs
index 1adc772fc9..da6b553868 100644
--- a/src/PSRule/Runtime/RunspaceContext.cs
+++ b/src/PSRule/Runtime/RunspaceContext.cs
@@ -596,9 +596,10 @@ public RuleRecord EnterRuleBlock(RuleBlock ruleBlock)
targetType: Binding?.TargetType,
tag: ruleBlock.Tag,
info: ruleBlock.Info,
- field: Binding?.Field,
- level: ruleBlock.Level,
- extent: ruleBlock.Extent
+ field: Binding.Field,
+ @default: ruleBlock.Default,
+ extent: ruleBlock.Extent,
+ @override: ruleBlock.Override
);
Writer?.EnterScope(ruleBlock.Name);
diff --git a/tests/PSRule.Tests/AssertFormatterTests.cs b/tests/PSRule.Tests/AssertFormatterTests.cs
index 995c421abf..5a77712470 100644
--- a/tests/PSRule.Tests/AssertFormatterTests.cs
+++ b/tests/PSRule.Tests/AssertFormatterTests.cs
@@ -392,7 +392,10 @@ private static InvokeResult GetPassResult()
tag: new ResourceTags(),
info: new RuleHelpInfo("Test", "Test rule", null),
field: null,
- level: SeverityLevel.Error,
+ @default: new RuleProperties
+ {
+ Level = SeverityLevel.Error
+ },
extent: null,
outcome: RuleOutcome.Pass,
reason: RuleOutcomeReason.Processed
@@ -414,7 +417,10 @@ private static InvokeResult GetFailResult(SeverityLevel level = SeverityLevel.Er
tag: new ResourceTags(),
info: new RuleHelpInfo("Test1", "Test rule", null),
field: null,
- level: level,
+ @default: new RuleProperties
+ {
+ Level = level
+ },
extent: null,
outcome: RuleOutcome.Fail,
reason: RuleOutcomeReason.Processed
@@ -430,7 +436,10 @@ private static InvokeResult GetFailResult(SeverityLevel level = SeverityLevel.Er
tag: new ResourceTags(),
info: new RuleHelpInfo("Test2", "Test rule", null),
field: null,
- level: level,
+ @default: new RuleProperties
+ {
+ Level = level
+ },
extent: null,
outcome: RuleOutcome.Fail,
reason: RuleOutcomeReason.Processed
diff --git a/tests/PSRule.Tests/BaseTests.cs b/tests/PSRule.Tests/BaseTests.cs
index 97c7ee0e25..d344586d2a 100644
--- a/tests/PSRule.Tests/BaseTests.cs
+++ b/tests/PSRule.Tests/BaseTests.cs
@@ -10,6 +10,8 @@
namespace PSRule;
+#nullable enable
+
///
/// A base class for all tests.
///
@@ -22,7 +24,12 @@ protected virtual PSRuleOption GetOption()
return new PSRuleOption();
}
- internal TestWriter GetTestWriter(PSRuleOption option = default)
+ protected virtual PSRuleOption GetOption(string path)
+ {
+ return PSRuleOption.FromFile(path);
+ }
+
+ internal TestWriter GetTestWriter(PSRuleOption? option = default)
{
return new TestWriter(option ?? GetOption());
}
@@ -63,3 +70,5 @@ protected static PSObject GetObject(params (string name, object value)[] propert
#endregion Helper methods
}
+
+#nullable restore
diff --git a/tests/PSRule.Tests/Baseline.Rule.jsonc b/tests/PSRule.Tests/Baseline.Rule.jsonc
index af3a3401cb..72ef0b8d3d 100644
--- a/tests/PSRule.Tests/Baseline.Rule.jsonc
+++ b/tests/PSRule.Tests/Baseline.Rule.jsonc
@@ -79,6 +79,11 @@
"low"
]
}
+ },
+ "override": {
+ "level": {
+ "rule1": "Warning"
+ }
}
}
},
diff --git a/tests/PSRule.Tests/Baseline.Rule.yaml b/tests/PSRule.Tests/Baseline.Rule.yaml
index a31b10d658..4ab0fc4afd 100644
--- a/tests/PSRule.Tests/Baseline.Rule.yaml
+++ b/tests/PSRule.Tests/Baseline.Rule.yaml
@@ -1,4 +1,3 @@
-
---
# Synopsis: This is an example baseline
apiVersion: github.com/microsoft/PSRule/v1
@@ -11,14 +10,14 @@ spec:
# Additional comment
rule:
include:
- # Additional comment
- - 'WithBaseline'
- # Additional comment
+ # Additional comment
+ - 'WithBaseline'
+ # Additional comment
configuration:
key1: value1
key2:
- - value1: abc
- - value2: def
+ - value1: abc
+ - value2: def
---
# Synopsis: This is an example baseline
@@ -29,7 +28,7 @@ metadata:
spec:
rule:
include:
- - ''
+ - ''
configuration:
key1: value1
@@ -54,9 +53,11 @@ spec:
rule:
tag:
severity:
- - 'high'
- - 'low'
-
+ - 'high'
+ - 'low'
+ override:
+ level:
+ rule1: Warning
---
# Synopsis: This is an example obsolete baseline
apiVersion: github.com/microsoft/PSRule/v1
@@ -65,7 +66,7 @@ metadata:
name: TestBaseline5
annotations:
obsolete: true
-spec: { }
+spec: {}
---
# Synopsis: An example of a baseline with labels defined
@@ -76,4 +77,4 @@ metadata:
spec:
rule:
labels:
- framework.v1/control: [ 'c-1', 'c-2' ]
+ framework.v1/control: ['c-1', 'c-2']
diff --git a/tests/PSRule.Tests/BaselineTests.cs b/tests/PSRule.Tests/BaselineTests.cs
index 9dcd941d49..6c25569eeb 100644
--- a/tests/PSRule.Tests/BaselineTests.cs
+++ b/tests/PSRule.Tests/BaselineTests.cs
@@ -8,6 +8,7 @@
using Newtonsoft.Json.Linq;
using PSRule.Definitions;
using PSRule.Definitions.Baselines;
+using PSRule.Definitions.Rules;
using PSRule.Host;
using PSRule.Pipeline;
using PSRule.Pipeline.Output;
@@ -49,6 +50,7 @@ public void ReadBaselineYaml()
// TestBaseline4
Assert.Equal("TestBaseline4", baseline[3].Name);
Assert.Null(baseline[3].Info.Synopsis.Text);
+ Assert.Equal(SeverityLevel.Warning, baseline[3].Spec.Override.Level["rule1"]);
// TestBaseline5
Assert.Equal("TestBaseline5", baseline[4].Name);
@@ -90,6 +92,7 @@ public void ReadBaselineJson()
// TestBaseline4
Assert.Equal("TestBaseline4", baseline[3].Name);
Assert.Null(baseline[3].Info.Synopsis.Text);
+ Assert.Equal(SeverityLevel.Warning, baseline[3].Spec.Override.Level["rule1"]);
// TestBaseline5
Assert.Equal("TestBaseline5", baseline[4].Name);
@@ -153,6 +156,13 @@ public void BaselineAsYaml()
Assert.Equal("value1", actual[0]["spec"]["configuration"]["key1"]);
Assert.Equal("abc", actual[0]["spec"]["configuration"]["key2"][0]["value1"]);
Assert.Equal("def", actual[0]["spec"]["configuration"]["key2"][1]["value2"]);
+
+ // TestBaseline4
+ Assert.Equal("github.com/microsoft/PSRule/v1", actual[3]["apiVersion"]);
+ Assert.Equal("Baseline", actual[3]["kind"]);
+ Assert.Equal("TestBaseline4", actual[3]["metadata"]["name"]);
+ Assert.NotNull(actual[3]["spec"]);
+ Assert.Equal("Warning", actual[3]["spec"]["override"]["level"]["rule1"]);
}
[Fact]
@@ -171,6 +181,13 @@ public void BaselineAsJson()
Assert.Equal("value1", actual?[0]["spec"]?["configuration"]?["key1"]);
Assert.Equal("abc", actual?[0]["spec"]?["configuration"]?["key2"]?[0]?["value1"]);
Assert.Equal("def", actual?[0]["spec"]?["configuration"]?["key2"]?[1]?["value2"]);
+
+ // TestBaseline4
+ Assert.Equal("github.com/microsoft/PSRule/v1", actual?[3]["apiVersion"]);
+ Assert.Equal("Baseline", actual?[3]["kind"]);
+ Assert.Equal("TestBaseline4", actual?[3]["metadata"]?["name"]);
+ Assert.NotNull(actual?[3]["spec"]);
+ Assert.Equal("Warning", actual?[3]["spec"]?["override"]?["level"]?["rule1"]);
}
#region Helper methods
diff --git a/tests/PSRule.Tests/MockLanguageScope.cs b/tests/PSRule.Tests/MockLanguageScope.cs
index 05c18d73c4..72277e6acd 100644
--- a/tests/PSRule.Tests/MockLanguageScope.cs
+++ b/tests/PSRule.Tests/MockLanguageScope.cs
@@ -76,6 +76,16 @@ public bool TryGetName(object o, out string name, out string path)
throw new NotImplementedException();
}
+ public bool TryGetOverride(ResourceId id, out RuleOverride propertyOverride)
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool TryGetScope(object o, out string[] scope)
+ {
+ throw new NotImplementedException();
+ }
+
public bool TryGetType(object o, out string type, out string path)
{
throw new NotImplementedException();
diff --git a/tests/PSRule.Tests/OutputWriterTests.cs b/tests/PSRule.Tests/OutputWriterTests.cs
index 1ac916986f..0d3948b529 100644
--- a/tests/PSRule.Tests/OutputWriterTests.cs
+++ b/tests/PSRule.Tests/OutputWriterTests.cs
@@ -88,7 +88,7 @@ public void SarifProblemsOnly()
Assert.Equal("rid-002", actual["runs"][0]["results"][0]["ruleId"].Value());
Assert.Equal("error", actual["runs"][0]["results"][0]["level"].Value());
- // Fail with warning
+ // Fail with warning (default value is omitted)
Assert.Equal("rid-003", actual["runs"][0]["results"][1]["ruleId"].Value());
Assert.Null(actual["runs"][0]["results"][1]["level"]);
@@ -97,6 +97,51 @@ public void SarifProblemsOnly()
Assert.Equal("note", actual["runs"][0]["results"][2]["level"].Value());
}
+ [Fact]
+ public void Output_WhenRuleLevelIsOverridden_ShouldReturnOverrideInSarifFormat()
+ {
+ var option = GetOption();
+ var output = new TestWriter(option);
+ var result = new InvokeResult();
+ result.Add(GetPass());
+ result.Add(GetFail());
+ result.Add(GetFail("rid-003", SeverityLevel.Warning, overrideLevel: SeverityLevel.Information));
+ result.Add(GetFail("rid-004", SeverityLevel.Information, overrideLevel: SeverityLevel.Warning));
+ var writer = new SarifOutputWriter(null, output, option, null);
+ writer.Begin();
+ writer.WriteObject(result, false);
+ writer.End(new DefaultPipelineResult(null, Options.BreakLevel.None));
+
+ var doc = JsonConvert.DeserializeObject(output.Output.OfType().FirstOrDefault());
+ Assert.NotNull(doc);
+ Assert.Equal("PSRule", doc["runs"][0]["tool"]["driver"]["name"].Value());
+ Assert.Equal("0.0.1", doc["runs"][0]["tool"]["driver"]["semanticVersion"].Value().Split('+')[0]);
+
+ // Fail with error
+ var actual = doc["runs"][0]["results"].Where(r => r["ruleId"].Value() == "rid-002").FirstOrDefault();
+ Assert.Equal("error", actual["level"].Value());
+
+ // Fail with note
+ actual = doc["runs"][0]["results"].Where(r => r["ruleId"].Value() == "rid-003").FirstOrDefault();
+ Assert.Equal("note", actual["level"].Value());
+
+ var ruleDefault = doc["runs"][0]["tool"]["driver"]["rules"].Where(r => r["id"].Value() == "rid-003").FirstOrDefault();
+ Assert.Null(ruleDefault["defaultConfiguration"]);
+
+ var ruleOverride = doc["runs"][0]["invocations"][0]["ruleConfigurationOverrides"].Where(r => r["descriptor"]["id"].Value() == "rid-003").FirstOrDefault();
+ Assert.Equal("note", actual["level"].Value());
+
+ // Fail with warning (default value is omitted)
+ actual = doc["runs"][0]["results"].Where(r => r["ruleId"].Value() == "rid-004").FirstOrDefault();
+ Assert.Null(actual["level"]);
+
+ ruleDefault = doc["runs"][0]["tool"]["driver"]["rules"].Where(r => r["id"].Value() == "rid-004").FirstOrDefault();
+ Assert.Equal("note", ruleDefault["defaultConfiguration"]["level"].Value());
+
+ ruleOverride = doc["runs"][0]["invocations"][0]["ruleConfigurationOverrides"].Where(r => r["descriptor"]["id"].Value() == "rid-004").FirstOrDefault();
+ Assert.Null(actual["level"]);
+ }
+
[Fact]
public void Yaml()
{
@@ -326,7 +371,7 @@ public void NUnit3()
result.Add(GetPass());
result.Add(GetFail());
result.Add(GetFail("rid-003", SeverityLevel.Warning));
- result.Add(GetFail("rid-004", SeverityLevel.Information, "Synopsis \"with quotes\"."));
+ result.Add(GetFail("rid-004", SeverityLevel.Information, synopsis: "Synopsis \"with quotes\"."));
var writer = new NUnit3OutputWriter(output, option, null);
writer.Begin();
writer.WriteObject(result, false);
@@ -413,7 +458,10 @@ private static RuleRecord GetPass()
recommendation: new InfoString("Recommendation for rule 001\r\nover two lines.")
),
field: new Hashtable(),
- level: SeverityLevel.Error,
+ @default: new RuleProperties
+ {
+ Level = SeverityLevel.Error
+ },
extent: null,
outcome: RuleOutcome.Pass,
reason: RuleOutcomeReason.Processed
@@ -423,7 +471,7 @@ private static RuleRecord GetPass()
};
}
- private static RuleRecord GetFail(string ruleRef = "rid-002", SeverityLevel level = SeverityLevel.Error, string synopsis = "This is rule 002.", string ruleId = "TestModule\\rule-002")
+ private static RuleRecord GetFail(string ruleRef = "rid-002", SeverityLevel level = SeverityLevel.Error, SeverityLevel? overrideLevel = null, string synopsis = "This is rule 002.", string ruleId = "TestModule\\rule-002")
{
var info = new RuleHelpInfo(
"rule-002",
@@ -432,10 +480,17 @@ private static RuleRecord GetFail(string ruleRef = "rid-002", SeverityLevel leve
synopsis: new InfoString(synopsis),
recommendation: new InfoString("Recommendation for rule 002")
);
+
info.Annotations = new Hashtable
{
["annotation-data"] = "Custom annotation"
};
+
+ var ruleOverride = overrideLevel == null ? null : new RuleOverride
+ {
+ Level = overrideLevel,
+ };
+
return new RuleRecord(
runId: "run-001",
ruleId: ResourceId.Parse(ruleId),
@@ -449,7 +504,11 @@ private static RuleRecord GetFail(string ruleRef = "rid-002", SeverityLevel leve
{
["field-data"] = "Custom field data"
},
- level: level,
+ @default: new RuleProperties
+ {
+ Level = level
+ },
+ @override: ruleOverride,
extent: null,
outcome: RuleOutcome.Fail,
reason: RuleOutcomeReason.Processed
diff --git a/tests/PSRule.Tests/PSRule.Options.Tests.ps1 b/tests/PSRule.Tests/PSRule.Options.Tests.ps1
index a53d030cb3..8bf770cf50 100644
--- a/tests/PSRule.Tests/PSRule.Options.Tests.ps1
+++ b/tests/PSRule.Tests/PSRule.Options.Tests.ps1
@@ -2145,6 +2145,39 @@ Describe 'New-PSRuleOption' -Tag 'Option','New-PSRuleOption' {
}
}
+ Context 'Read Override.Level' {
+ It 'from default' {
+ $option = New-PSRuleOption -Default;
+ $option.Override.Level | Should -BeNullOrEmpty;
+ }
+
+ It 'from Hashtable' {
+ $option = New-PSRuleOption -Option @{ 'Override.Level.rule1' = 'Information' };
+ $option.Override.Level['rule1'] | Should -Be 'Information';
+ }
+
+ It 'from YAML' {
+ $option = New-PSRuleOption -Option (Join-Path -Path $here -ChildPath 'PSRule.Tests.yml');
+ $option.Override.Level['rule1'] | Should -Be 'Information';
+ }
+
+ It 'from Environment' {
+ try {
+ $Env:PSRULE_OVERRIDE_LEVEL_RULE1 = 'Information';
+ $option = New-PSRuleOption;
+ $option.Override.Level['rule1'] | Should -Be 'Information';
+ }
+ finally {
+ Remove-Item 'Env:PSRULE_OVERRIDE_LEVEL_RULE1' -Force;
+ }
+ }
+
+ It 'from parameter' {
+ $option = New-PSRuleOption -OverrideLevel @{ rule1 = 'Information' } -Path $emptyOptionsFilePath;
+ $option.Override.Level['rule1'] | Should -Be 'Information';
+ }
+ }
+
Context 'Read Repository.BaseRef' {
It 'from default' {
$option = New-PSRuleOption -Default;
diff --git a/tests/PSRule.Tests/PSRule.Tests.csproj b/tests/PSRule.Tests/PSRule.Tests.csproj
index c5d3066847..9ddb5424fd 100644
--- a/tests/PSRule.Tests/PSRule.Tests.csproj
+++ b/tests/PSRule.Tests/PSRule.Tests.csproj
@@ -149,6 +149,9 @@
PreserveNewest
+
+ PreserveNewest
+
PreserveNewest
diff --git a/tests/PSRule.Tests/PSRule.Tests.yml b/tests/PSRule.Tests/PSRule.Tests.yml
index 2a882313ab..9d36a8ae0e 100644
--- a/tests/PSRule.Tests/PSRule.Tests.yml
+++ b/tests/PSRule.Tests/PSRule.Tests.yml
@@ -22,6 +22,11 @@ emitter:
include:
- .csproj
+override:
+ level:
+ rule1: Information
+ Group.*: Error
+
# Configure baseline
rule:
include:
diff --git a/tests/PSRule.Tests/PSRule.Tests14.yml b/tests/PSRule.Tests/PSRule.Tests14.yml
index 5df7c52d1a..ce50036c9c 100644
--- a/tests/PSRule.Tests/PSRule.Tests14.yml
+++ b/tests/PSRule.Tests/PSRule.Tests14.yml
@@ -2,14 +2,14 @@
suppression:
YAML.RuleWithAlias1:
- - 'TestObject1'
+ - 'TestObject1'
PSRZZ.0001:
- - 'TestObject1'
+ - 'TestObject1'
JSON.AlternativeName:
- - 'TestObject2'
+ - 'TestObject2'
YAML.AlternativeName:
- - 'TestObject2'
+ - 'TestObject2'
.\PSRZZ.0003:
- - 'TestObject3'
+ - 'TestObject3'
'.\PS.AlternativeName':
- - 'TestObject3'
+ - 'TestObject3'
diff --git a/tests/PSRule.Tests/PSRule.Tests17.yml b/tests/PSRule.Tests/PSRule.Tests17.yml
new file mode 100644
index 0000000000..5df42b9b23
--- /dev/null
+++ b/tests/PSRule.Tests/PSRule.Tests17.yml
@@ -0,0 +1,4 @@
+# These are options for unit tests
+
+override:
+ level:
diff --git a/tests/PSRule.Tests/PSRule.Tests2.yml b/tests/PSRule.Tests/PSRule.Tests2.yml
index 5298583a4a..b2dc669e26 100644
--- a/tests/PSRule.Tests/PSRule.Tests2.yml
+++ b/tests/PSRule.Tests/PSRule.Tests2.yml
@@ -3,39 +3,41 @@
# Configure binding
binding:
targetName:
- - ResourceName
- - AlternateName
+ - ResourceName
+ - AlternateName
targetType:
- - ResourceType
- - kind
+ - ResourceType
+ - kind
# Configure conventions
convention:
include:
- - 'Convention1'
- - 'Convention2'
+ - 'Convention1'
+ - 'Convention2'
# Configure input
input:
targetType:
- - virtualMachine
- - virtualNetwork
+ - virtualMachine
+ - virtualNetwork
# Configure output
output:
banner: 1
culture:
- - 'en-CC'
- - 'en-DD'
+ - 'en-CC'
+ - 'en-DD'
+
+override: {}
# Configure required module versions
-requires: { }
+requires: {}
# Configure baseline
rule:
include:
- - rule1
- - rule2
+ - rule1
+ - rule2
exclude:
- - rule3
- - rule4
+ - rule3
+ - rule4
diff --git a/tests/PSRule.Tests/PSRuleOptionTests.cs b/tests/PSRule.Tests/PSRuleOptionTests.cs
index f388ae2dce..54ab2696c9 100644
--- a/tests/PSRule.Tests/PSRuleOptionTests.cs
+++ b/tests/PSRule.Tests/PSRuleOptionTests.cs
@@ -5,6 +5,7 @@
using System.IO;
using System.Management.Automation;
using PSRule.Configuration;
+using PSRule.Definitions.Rules;
using PSRule.Pipeline;
namespace PSRule;
@@ -87,6 +88,28 @@ public void GetBaselineGroupFromYaml()
Assert.Equal(new string[] { ".\\TestBaseline1" }, latest);
}
+ [Fact]
+ public void FromFile_WhenOverrideIsDefined_ShouldDeserializeLevel()
+ {
+ var option = GetOption();
+ var actual = option.Override.Level;
+ Assert.True(actual.TryGetValue("rule1", out var level));
+ Assert.Equal(SeverityLevel.Information, level);
+
+ Assert.True(actual.TryGetValue("Group.*", out level));
+ Assert.Equal(SeverityLevel.Error, level);
+ }
+
+ [Theory]
+ [InlineData("PSRule.Tests2.yml")]
+ [InlineData("PSRule.Tests17.yml")]
+ public void FromFile_WhenOverrideIsPartiallyDefined_ShouldDeserializeWithoutError(string path)
+ {
+ var option = GetOption(GetSourcePath(path));
+ var actual = option.Override.Level;
+ Assert.Null(actual);
+ }
+
#region Helper methods
private Runtime.Configuration GetConfigurationHelper(PSRuleOption option)
@@ -108,7 +131,7 @@ private static Source[] GetSource()
protected sealed override PSRuleOption GetOption()
{
- return PSRuleOption.FromFile(GetSourcePath("PSRule.Tests.yml"));
+ return GetOption(GetSourcePath("PSRule.Tests.yml"));
}
#endregion Helper methods
diff --git a/tests/PSRule.Tests/PipelineTests.cs b/tests/PSRule.Tests/PipelineTests.cs
index 9c891b4e58..ba4988e035 100644
--- a/tests/PSRule.Tests/PipelineTests.cs
+++ b/tests/PSRule.Tests/PipelineTests.cs
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-using System;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -9,6 +8,7 @@
using System.Threading;
using Newtonsoft.Json.Linq;
using PSRule.Configuration;
+using PSRule.Definitions.Rules;
using PSRule.Options;
using PSRule.Pipeline;
using PSRule.Resources;
@@ -80,9 +80,15 @@ public void InvokePipelineWithJObject()
var actual = (writer.Output[0] as InvokeResult).AsRecord().FirstOrDefault();
Assert.Equal(RuleOutcome.Pass, actual.Outcome);
+ Assert.Equal(SeverityLevel.Error, actual.Default.Level);
+ Assert.Equal(SeverityLevel.Warning, actual.Override.Level);
+ Assert.Equal(SeverityLevel.Warning, actual.Level);
actual = (writer.Output[1] as InvokeResult).AsRecord().FirstOrDefault();
Assert.Equal(RuleOutcome.Fail, actual.Outcome);
+ Assert.Equal(SeverityLevel.Error, actual.Default.Level);
+ Assert.Equal(SeverityLevel.Warning, actual.Override.Level);
+ Assert.Equal(SeverityLevel.Warning, actual.Level);
Assert.Equal("Name", actual.Detail.Reason.First().Path);
Assert.Equal("resources[1].Name", actual.Detail.Reason.First().FullPath);
}
@@ -351,14 +357,11 @@ private static PSRuleOption GetOption(string path = null, ExecutionActionPrefere
var option = path == null ? new PSRuleOption() : PSRuleOption.FromFile(path);
option.Rule.IncludeLocal = false;
option.Execution.RuleExcluded = ruleExcludedAction;
+ option.Override.Level ??= [];
+ option.Override.Level.Add("ScriptReasonTest", SeverityLevel.Warning);
return option;
}
- private static string GetSourcePath(string fileName)
- {
- return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
- }
-
private static PSObject GetTestObject()
{
var info = new PSObject();
diff --git a/tests/PSRule.Tests/ResourceMapTests.cs b/tests/PSRule.Tests/ResourceMapTests.cs
new file mode 100644
index 0000000000..8a21670435
--- /dev/null
+++ b/tests/PSRule.Tests/ResourceMapTests.cs
@@ -0,0 +1,43 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Collections.Generic;
+using PSRule.Data;
+using PSRule.Definitions.Rules;
+
+namespace PSRule;
+
+public sealed class ResourceMapTests
+{
+ [Fact]
+ public void Get()
+ {
+ var map = new WildcardMap();
+ Assert.False(map.TryGetValue("Rule1", out _));
+
+ map = new WildcardMap(new Dictionary
+ {
+ { "Rule1", SeverityLevel.Warning },
+ { "Rule2", SeverityLevel.Error },
+ { "Rules.1", SeverityLevel.Error },
+ { "Rules.*", SeverityLevel.Information },
+ { "Rules.z*", SeverityLevel.Warning },
+ { "Rules.2", SeverityLevel.Error },
+ });
+
+ Assert.True(map.TryGetValue("Rule1", out var level));
+ Assert.Equal(SeverityLevel.Warning, level);
+ Assert.True(map.TryGetValue("Rule2", out level));
+ Assert.Equal(SeverityLevel.Error, level);
+ Assert.True(map.TryGetValue("Rules.1", out level));
+ Assert.Equal(SeverityLevel.Error, level);
+ Assert.True(map.TryGetValue("Rules.2", out level));
+ Assert.Equal(SeverityLevel.Error, level);
+ Assert.True(map.TryGetValue("Rules.3", out level));
+ Assert.Equal(SeverityLevel.Information, level);
+ Assert.True(map.TryGetValue("Rules.zzzz", out level));
+ Assert.Equal(SeverityLevel.Warning, level);
+ Assert.False(map.TryGetValue("Rules.", out _));
+ Assert.False(map.TryGetValue("Rules", out _));
+ }
+}