From a55918f18c870e177d66a7d3b1d3e3457be726a2 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Mon, 5 May 2025 02:17:32 +1000 Subject: [PATCH 1/3] Refactoring logging --- README.md | 4 - docs/changelog.md | 4 + .../commands/PSRule/en-US/New-PSRuleOption.md | 77 +------ .../commands/PSRule/en-US/Set-PSRuleOption.md | 69 +----- .../PSRule/en-US/about_PSRule_Options.md | 164 ------------- docs/deprecations.md | 9 + .../validation-pipeline.md | 2 - schemas/PSRule-options.schema.json | 62 ----- src/PSRule.CommandLine/ClientHost.cs | 139 +++++------ .../Models/SessionContext.cs | 44 ++-- .../Handlers/RunAnalysisCommandHandler.cs | 6 +- .../UpgradeDependencyCommandHandler.cs | 6 +- .../Hosting/ServerHostContext.cs | 71 +++--- src/PSRule.Types/Emitters/BaseEmitter.cs | 2 +- src/PSRule.Types/PipelineException.cs | 21 +- .../PipelineSerializationException.cs | 5 +- src/PSRule.Types/Runtime/EventId.cs | 6 + src/PSRule.Types/Runtime/ILogger.cs | 2 +- src/PSRule.Types/Runtime/LoggerExtensions.cs | 12 + .../Commands/ExportConventionCommand.cs | 2 +- .../Commands/InvokeConventionCommand.cs | 3 +- src/PSRule/Commands/InvokeRuleBlockCommand.cs | 21 +- .../Commands/NewRuleDefinitionCommand.cs | 2 +- src/PSRule/Common/HashSetExtensions.cs | 8 +- .../RunspaceContextDiagnosticExtensions.cs | 12 +- src/PSRule/Configuration/LoggingOption.cs | 145 ------------ src/PSRule/Configuration/OutcomeLogStream.cs | 34 --- src/PSRule/Configuration/PSRuleOption.cs | 13 -- .../Expressions/ExpressionContext.cs | 5 +- .../Expressions/LanguageExpressions.cs | 14 +- .../Definitions/IResourceDiscoveryContext.cs | 3 +- src/PSRule/Definitions/ResourceValidator.cs | 43 +--- src/PSRule/Definitions/ResultReason.cs | 2 +- src/PSRule/Host/HostHelper.cs | 45 ++-- src/PSRule/Host/HostState.cs | 12 +- src/PSRule/Host/RuleLanguageAst.cs | 54 ++--- src/PSRule/PSRule.psm1 | 68 ------ src/PSRule/Pipeline/AssertPipelineBuilder.cs | 142 ------------ src/PSRule/Pipeline/AssertWriter.cs | 155 +++++++++++++ src/PSRule/Pipeline/Exceptions.cs | 81 +------ .../Formatters/AssertFormatterBase.cs | 7 +- src/PSRule/Pipeline/HostContext.cs | 83 ++++--- src/PSRule/Pipeline/IHostContext.cs | 52 ++--- src/PSRule/Pipeline/IPipelineWriter.cs | 74 ++---- .../Pipeline/InvokePipelineBuilderBase.cs | 5 - src/PSRule/Pipeline/InvokeRulePipeline.cs | 2 - src/PSRule/Pipeline/OptionContext.cs | 2 - src/PSRule/Pipeline/OptionContextBuilder.cs | 1 - src/PSRule/Pipeline/OptionScope.cs | 3 - .../Pipeline/Output/FileOutputWriter.cs | 3 +- .../Pipeline/Output/HostPipelineWriter.cs | 190 +++------------ .../Pipeline/Output/WideOutputWriter.cs | 1 + src/PSRule/Pipeline/PSHostContext.cs | 120 +++++++--- src/PSRule/Pipeline/PathBuilder.cs | 4 +- src/PSRule/Pipeline/PipelineBuilderBase.cs | 4 +- src/PSRule/Pipeline/PipelineContext.cs | 4 +- src/PSRule/Pipeline/PipelineInputStream.cs | 6 +- src/PSRule/Pipeline/PipelineLogger.cs | 6 +- src/PSRule/Pipeline/PipelineLoggerBase.cs | 21 +- src/PSRule/Pipeline/PipelineWriter.cs | 218 +++--------------- .../Pipeline/PipelineWriterExtensions.cs | 71 +----- src/PSRule/Pipeline/ResourceCache.cs | 4 +- .../Pipeline/ResourceCacheDiscoveryContext.cs | 8 +- src/PSRule/Pipeline/ResourceIssue.cs | 27 ++- src/PSRule/Pipeline/ResourceIssueType.cs | 6 +- src/PSRule/Pipeline/RuntimeScopeException.cs | 56 +++++ src/PSRule/Pipeline/SourcePipelineBuilder.cs | 16 +- .../Resources/PSRuleResources.Designer.cs | 17 +- src/PSRule/Resources/PSRuleResources.resx | 11 +- src/PSRule/Runtime/ILanguageScopeSet.cs | 2 +- src/PSRule/Runtime/LegacyRunspaceContext.cs | 183 ++++----------- src/PSRule/Runtime/LoggerExtensions.cs | 134 ++++++++++- .../SuppressionGroupTests.cs | 12 +- tests/PSRule.Tests/LanguageVisitorTests.cs | 19 +- tests/PSRule.Tests/PSRule.Common.Tests.ps1 | 161 ++----------- tests/PSRule.Tests/PSRule.EndToEnd.Tests.ps1 | 40 ---- tests/PSRule.Tests/PSRule.Options.Tests.ps1 | 116 ---------- tests/PSRule.Tests/PSRule.Tests.csproj | 3 + .../Pipeline/InputPathBuilderTests.cs | 2 +- .../Output/HostPipelineWriterTests.cs | 39 ++++ tests/PSRule.Tests/Pipeline/PipelineTests.cs | 44 +++- tests/PSRule.Tests/ResourceValidatorTests.cs | 2 +- tests/PSRule.Tests/RuleLanguageAstTests.cs | 5 +- .../SourcePipelineBuilderTests.cs | 2 +- tests/PSRule.Tests/TestWriter.cs | 61 +++-- 85 files changed, 1164 insertions(+), 2252 deletions(-) delete mode 100644 src/PSRule/Configuration/LoggingOption.cs delete mode 100644 src/PSRule/Configuration/OutcomeLogStream.cs create mode 100644 src/PSRule/Pipeline/AssertWriter.cs create mode 100644 src/PSRule/Pipeline/RuntimeScopeException.cs create mode 100644 tests/PSRule.Tests/Pipeline/Output/HostPipelineWriterTests.cs diff --git a/README.md b/README.md index a2021d9187..ca75dab9d1 100644 --- a/README.md +++ b/README.md @@ -318,10 +318,6 @@ The following conceptual topics exist in the `PSRule` module: - [Input.ObjectPath](https://aka.ms/ps-rule/options#inputobjectpath) - [Input.PathIgnore](https://aka.ms/ps-rule/options#inputpathignore) - [Input.TargetType](https://aka.ms/ps-rule/options#inputtargettype) - - [Logging.LimitDebug](https://aka.ms/ps-rule/options#logginglimitdebug) - - [Logging.LimitVerbose](https://aka.ms/ps-rule/options#logginglimitverbose) - - [Logging.RuleFail](https://aka.ms/ps-rule/options#loggingrulefail) - - [Logging.RulePass](https://aka.ms/ps-rule/options#loggingrulepass) - [Output.As](https://aka.ms/ps-rule/options#outputas) - [Output.Banner](https://aka.ms/ps-rule/options#outputbanner) - [Output.Culture](https://aka.ms/ps-rule/options#outputculture) diff --git a/docs/changelog.md b/docs/changelog.md index 9d162c7bca..2a4c65c39d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -45,6 +45,10 @@ What's changed since pre-release v3.0.0-B0453: What's changed since pre-release v3.0.0-B0416: - Engineering: + - **Important change**: Remove legacy log scopes by @BernieWhite. + [#2891](https://github.com/microsoft/PSRule/issues/2891) + - **Important change**: Remove pass fail streams by @BernieWhite. + [#2892](https://github.com/microsoft/PSRule/issues/2892) - Bump System.Drawing.Common to v9.0.3. [#2808](https://github.com/microsoft/PSRule/pull/2808) - Bump NuGet.Protocol to v6.13.2. diff --git a/docs/commands/PSRule/en-US/New-PSRuleOption.md b/docs/commands/PSRule/en-US/New-PSRuleOption.md index 7fc1be9ad8..6b91e072ce 100644 --- a/docs/commands/PSRule/en-US/New-PSRuleOption.md +++ b/docs/commands/PSRule/en-US/New-PSRuleOption.md @@ -30,8 +30,7 @@ New-PSRuleOption [[-Path] ] [-Configuration ] [-IncludePath ] [-InputFileObjects ] [-InputStringFormat ] [-InputIgnoreGitPath ] [-InputIgnoreRepositoryCommon ] [-InputIgnoreObjectSource ] [-InputIgnoreUnchangedPath ] [-ObjectPath ] [-InputTargetType ] - [-InputPathIgnore ] [-LoggingLimitDebug ] [-LoggingLimitVerbose ] - [-LoggingRuleFail ] [-LoggingRulePass ] [-OutputAs ] + [-InputPathIgnore ] [-OutputAs ] [-OutputBanner ] [-OutputCulture ] [-OutputEncoding ] [-OutputFooter ] [-OutputFormat ] [-OutputJobSummaryPath ] [-OutputJsonIndent ] [-OutputOutcome ] [-OutputPath ] @@ -57,8 +56,7 @@ New-PSRuleOption [-Option] [-Configuration ] [-IncludePath ] [-InputFileObjects ] [-InputStringFormat ] [-InputIgnoreGitPath ] [-InputIgnoreRepositoryCommon ] [-InputIgnoreObjectSource ] [-InputIgnoreUnchangedPath ] [-ObjectPath ] [-InputTargetType ] - [-InputPathIgnore ] [-LoggingLimitDebug ] [-LoggingLimitVerbose ] - [-LoggingRuleFail ] [-LoggingRulePass ] [-OutputAs ] + [-InputPathIgnore ] [-OutputAs ] [-OutputBanner ] [-OutputCulture ] [-OutputEncoding ] [-OutputFooter ] [-OutputFormat ] [-OutputJobSummaryPath ] [-OutputJsonIndent ] [-OutputOutcome ] [-OutputPath ] @@ -84,8 +82,7 @@ New-PSRuleOption [-Default] [-Configuration ] [-SuppressTar [-IncludePath ] [-InputFileObjects ] [-InputStringFormat ] [-InputIgnoreGitPath ] [-InputIgnoreRepositoryCommon ] [-InputIgnoreObjectSource ] [-InputIgnoreUnchangedPath ] [-ObjectPath ] [-InputTargetType ] - [-InputPathIgnore ] [-LoggingLimitDebug ] [-LoggingLimitVerbose ] - [-LoggingRuleFail ] [-LoggingRulePass ] [-OutputAs ] + [-InputPathIgnore ] [-OutputAs ] [-OutputBanner ] [-OutputCulture ] [-OutputEncoding ] [-OutputFooter ] [-OutputFormat ] [-OutputJobSummaryPath ] [-OutputJsonIndent ] [-OutputOutcome ] [-OutputPath ] @@ -606,74 +603,6 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -LoggingLimitDebug - -Sets the `Logging.LimitDebug` option to limit debug messages to a list of named debug scopes. -See about_PSRule_Options for more information. - -```yaml -Type: String[] -Parameter Sets: (All) -Aliases: - -Required: False -Position: Named -Default value: None -Accept pipeline input: False -Accept wildcard characters: False -``` - -### -LoggingLimitVerbose - -Sets the `Logging.LimitVerbose` option to limit verbose messages to a list of named verbose scopes. -See about_PSRule_Options for more information. - -```yaml -Type: String[] -Parameter Sets: (All) -Aliases: - -Required: False -Position: Named -Default value: None -Accept pipeline input: False -Accept wildcard characters: False -``` - -### -LoggingRuleFail - -Sets the `Logging.RuleFail` option to generate an informational message for each rule fail. -See about_PSRule_Options for more information. - -```yaml -Type: OutcomeLogStream -Parameter Sets: (All) -Aliases: - -Required: False -Position: Named -Default value: None -Accept pipeline input: False -Accept wildcard characters: False -``` - -### -LoggingRulePass - -Sets the `Logging.RulePass` option to generate an informational message for each rule pass. -See about_PSRule_Options for more information. - -```yaml -Type: OutcomeLogStream -Parameter Sets: (All) -Aliases: - -Required: False -Position: Named -Default value: None -Accept pipeline input: False -Accept wildcard characters: False -``` - ### -OutputAs Sets the option `Output.As`. diff --git a/docs/commands/PSRule/en-US/Set-PSRuleOption.md b/docs/commands/PSRule/en-US/Set-PSRuleOption.md index 5e5f66303d..bb8124820a 100644 --- a/docs/commands/PSRule/en-US/Set-PSRuleOption.md +++ b/docs/commands/PSRule/en-US/Set-PSRuleOption.md @@ -28,8 +28,7 @@ Set-PSRuleOption [[-Path] ] [-Option ] [-PassThru] [-Force [-IncludePath ] [-InputFileObjects ] [-InputStringFormat ] [-InputIgnoreGitPath ] [-InputIgnoreObjectSource ] [-InputIgnoreRepositoryCommon ] [-InputIgnoreUnchangedPath ] [-ObjectPath ] [-InputPathIgnore ] - [-InputTargetType ] [-LoggingLimitDebug ] [-LoggingLimitVerbose ] - [-LoggingRuleFail ] [-LoggingRulePass ] [-OutputAs ] + [-InputTargetType ] [-OutputAs ] [-OutputBanner ] [-OutputCulture ] [-OutputEncoding ] [-OutputFooter ] [-OutputFormat ] [-OutputJobSummaryPath ] [-OutputJsonIndent ] [-OutputOutcome ] [-OutputPath ] @@ -542,72 +541,6 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -LoggingLimitDebug - -Sets the `Logging.LimitDebug` option to limit debug messages to a list of named debug scopes. - -```yaml -Type: String[] -Parameter Sets: (All) -Aliases: - -Required: False -Position: Named -Default value: None -Accept pipeline input: False -Accept wildcard characters: False -``` - -### -LoggingLimitVerbose - -Sets the `Logging.LimitVerbose` option to limit verbose messages to a list of named verbose scopes. - -```yaml -Type: String[] -Parameter Sets: (All) -Aliases: - -Required: False -Position: Named -Default value: None -Accept pipeline input: False -Accept wildcard characters: False -``` - -### -LoggingRuleFail - -Sets the `Logging.RuleFail` option to generate an informational message for each rule fail. - -```yaml -Type: OutcomeLogStream -Parameter Sets: (All) -Aliases: -Accepted values: None, Error, Warning, Information - -Required: False -Position: Named -Default value: None -Accept pipeline input: False -Accept wildcard characters: False -``` - -### -LoggingRulePass - -Sets the `Logging.RulePass` option to generate an informational message for each rule pass. - -```yaml -Type: OutcomeLogStream -Parameter Sets: (All) -Aliases: -Accepted values: None, Error, Warning, Information - -Required: False -Position: Named -Default value: None -Accept pipeline input: False -Accept wildcard characters: False -``` - ### -OutputAs Sets the option `Output.As`. diff --git a/docs/concepts/PSRule/en-US/about_PSRule_Options.md b/docs/concepts/PSRule/en-US/about_PSRule_Options.md index 98fe5614da..5c3c929a72 100644 --- a/docs/concepts/PSRule/en-US/about_PSRule_Options.md +++ b/docs/concepts/PSRule/en-US/about_PSRule_Options.md @@ -48,10 +48,6 @@ The following workspace options are available for use: - [Input.ObjectPath](#inputobjectpath) - [Input.PathIgnore](#inputpathignore) - [Input.TargetType](#inputtargettype) -- [Logging.LimitDebug](#logginglimitdebug) -- [Logging.LimitVerbose](#logginglimitverbose) -- [Logging.RuleFail](#loggingrulefail) -- [Logging.RulePass](#loggingrulepass) - [Output.As](#outputas) - [Output.Banner](#outputbanner) - [Output.Culture](#outputculture) @@ -2369,166 +2365,6 @@ variables: value: virtualMachine;virtualNetwork ``` -### Logging.LimitDebug - -Limits debug messages to a list of named debug scopes. - -When using the `-Debug` switch or preference variable, by default PSRule cmdlets log all debug output. -When using debug output for debugging a specific rule, it may be helpful to limit debug message to a specific rule. - -To identify a rule to include in debug output use the rule name. - -The following built-in scopes exist in addition to rule names: - -- `[Discovery.Source]` - Discovery messages for `.Rule.ps1` files and rule modules. -- `[Discovery.Rule]` - Discovery messages for individual rules within `.Rule.ps1` files. - -This option can be specified using: - -```powershell -# PowerShell: Using the LoggingLimitDebug parameter -$option = New-PSRuleOption -LoggingLimitDebug Rule1, Rule2; -``` - -```powershell -# PowerShell: Using the Logging.LimitDebug hashtable key -$option = New-PSRuleOption -Option @{ 'Logging.LimitDebug' = Rule1, Rule2 }; -``` - -```powershell -# PowerShell: Using the LoggingLimitDebug parameter to set YAML -Set-PSRuleOption -LoggingLimitDebug Rule1, Rule2; -``` - -```yaml -# YAML: Using the logging/limitDebug property -logging: - limitDebug: - - Rule1 - - Rule2 -``` - -### Logging.LimitVerbose - -Limits verbose messages to a list of named verbose scopes. - -When using the `-Verbose` switch or preference variable, by default PSRule cmdlets log all verbose output. -When using verbose output for troubleshooting a specific rule, -it may be helpful to limit verbose messages to a specific rule. - -To identify a rule to include in verbose output use the rule name. - -The following built-in scopes exist in addition to rule names: - -- `[Discovery.Source]` - Discovery messages for `.Rule.ps1` files and rule modules. -- `[Discovery.Rule]` - Discovery messages for individual rules within `.Rule.ps1` files. - -This option can be specified using: - -```powershell -# PowerShell: Using the LoggingLimitVerbose parameter -$option = New-PSRuleOption -LoggingLimitVerbose Rule1, Rule2; -``` - -```powershell -# PowerShell: Using the Logging.LimitVerbose hashtable key -$option = New-PSRuleOption -Option @{ 'Logging.LimitVerbose' = Rule1, Rule2 }; -``` - -```powershell -# PowerShell: Using the LoggingLimitVerbose parameter to set YAML -Set-PSRuleOption -LoggingLimitVerbose Rule1, Rule2; -``` - -```yaml -# YAML: Using the logging/limitVerbose property -logging: - limitVerbose: - - Rule1 - - Rule2 -``` - -### Logging.RuleFail - -When an object fails a rule condition the results are written to output as a structured object marked with the outcome of _Fail_. -If the rule executed successfully regardless of outcome no other informational messages are shown by default. - -In some circumstances such as a continuous integration (CI) pipeline, -it may be preferable to see informational messages or abort the CI process if one or more _Fail_ outcomes are returned. - -By settings this option, error, warning or information messages will be generated for each rule _fail_ outcome in addition to structured output. -By default, outcomes are not logged to an informational stream (i.e. None). - -The following streams available: - -- None -- Error -- Warning -- Information - -This option can be specified using: - -```powershell -# PowerShell: Using the LoggingRuleFail parameter -$option = New-PSRuleOption -LoggingRuleFail Error; -``` - -```powershell -# PowerShell: Using the Logging.RuleFail hashtable key -$option = New-PSRuleOption -Option @{ 'Logging.RuleFail' = 'Error' }; -``` - -```powershell -# PowerShell: Using the LoggingRuleFail parameter to set YAML -Set-PSRuleOption -LoggingRuleFail Error; -``` - -```yaml -# YAML: Using the logging/ruleFail property -logging: - ruleFail: Error -``` - -### Logging.RulePass - -When an object passes a rule condition the results are written to output as a structured object marked with the outcome of _Pass_. -If the rule executed successfully regardless of outcome no other informational messages are shown by default. - -In some circumstances such as a continuous integration (CI) pipeline, it may be preferable to see informational messages. - -By settings this option, error, warning or information messages will be generated for each rule _pass_ outcome in addition to structured output. -By default, outcomes are not logged to an informational stream (i.e. None). - -The following streams available: - -- None -- Error -- Warning -- Information - -This option can be specified using: - -```powershell -# PowerShell: Using the LoggingRulePass parameter -$option = New-PSRuleOption -LoggingRulePass Information; -``` - -```powershell -# PowerShell: Using the Logging.RulePass hashtable key -$option = New-PSRuleOption -Option @{ 'Logging.RulePass' = 'Information' }; -``` - -```powershell -# PowerShell: Using the LoggingRulePass parameter to set YAML -Set-PSRuleOption -LoggingRulePass Information; -``` - -```yaml -# YAML: Using the logging/rulePass property -logging: - rulePass: Information -``` - ### Output.As Configures the type of results to produce. diff --git a/docs/deprecations.md b/docs/deprecations.md index 513238559a..ec75d031e6 100644 --- a/docs/deprecations.md +++ b/docs/deprecations.md @@ -214,6 +214,15 @@ The following execution options have been deprecated and will be removed from _v You do not need to configure both options. If you have the deprecated option configured, switch to the new option. +### Logging options + +The following legacy logging options have been removed because they are no longer effective for their intended purpose: + +- `Logging.RuleFail` +- `Logging.RulePass` +- `Logging.LimitDebug` +- `Logging.LimitVerbose` + ## Changes to API ### Rule output object diff --git a/docs/scenarios/validation-pipeline/validation-pipeline.md b/docs/scenarios/validation-pipeline/validation-pipeline.md index c2ea6d9593..6c8d380aa8 100644 --- a/docs/scenarios/validation-pipeline/validation-pipeline.md +++ b/docs/scenarios/validation-pipeline/validation-pipeline.md @@ -295,8 +295,6 @@ By using `-If` or `-Type` pre-conditions, rules can dynamically provide validati When calling PSRule from Pester use `Invoke-PSRule` instead of `Assert-PSRule`. `Invoke-PSRule` returns validation result objects that can be tested by Pester `Should` conditions. -Additionally, the `Logging.RuleFail` option can be included to generate an error message for each failing rule. - For example: ```powershell diff --git a/schemas/PSRule-options.schema.json b/schemas/PSRule-options.schema.json index 0a5bbe1d56..ebedb88e81 100644 --- a/schemas/PSRule-options.schema.json +++ b/schemas/PSRule-options.schema.json @@ -842,60 +842,6 @@ }, "additionalProperties": false }, - "logging-option": { - "type": "object", - "title": "Logging options", - "description": "Options for configuring information logging.", - "properties": { - "limitDebug": { - "type": "array", - "title": "Scopes for debug messages", - "description": "Limits debug messages to a list of named debug scopes. No scopes are set by default.", - "markdownDescription": "Limits debug messages to a list of named debug scopes. No scopes are set by default. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#logginglimitdebug)", - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "limitVerbose": { - "type": "array", - "title": "Scopes for verbose messages", - "description": "Limits verbose messages to a list of named verbose scopes. No scopes are set by default.", - "markdownDescription": "Limits verbose messages to a list of named verbose scopes. No scopes are set by default. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#logginglimitverbose)", - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "ruleFail": { - "type": "string", - "title": "Report fail to stream", - "description": "Log fail outcomes for each rule to a specific informational stream. The default is None.", - "markdownDescription": "Log fail outcomes for each rule to a specific informational stream. The default is `None`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#loggingrulefail)", - "enum": [ - "None", - "Error", - "Warning", - "Information" - ], - "default": "None" - }, - "rulePass": { - "type": "string", - "title": "Report pass to stream", - "description": "Log pass outcomes for each rule to a specific informational stream. The default is None.", - "markdownDescription": "Log pass outcomes for each rule to a specific informational stream. The default is `None`. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Options/#loggingrulepass)", - "enum": [ - "None", - "Error", - "Warning", - "Information" - ], - "default": "None" - } - }, - "additionalProperties": false - }, "run-option": { "type": "object", "title": "Run", @@ -1230,14 +1176,6 @@ "type": "object", "$ref": "#/definitions/input-option" }, - "logging": { - "type": "object", - "oneOf": [ - { - "$ref": "#/definitions/logging-option" - } - ] - }, "output": { "type": "object", "$ref": "#/definitions/output-option" diff --git a/src/PSRule.CommandLine/ClientHost.cs b/src/PSRule.CommandLine/ClientHost.cs index 886b37edf7..44842fd67e 100644 --- a/src/PSRule.CommandLine/ClientHost.cs +++ b/src/PSRule.CommandLine/ClientHost.cs @@ -5,11 +5,12 @@ using System.CommandLine.IO; using System.Management.Automation; using PSRule.Pipeline; +using PSRule.Runtime; namespace PSRule.CommandLine; /// -/// +/// A host context for .NET processes. /// public sealed class ClientHost : HostContext { @@ -33,7 +34,7 @@ public ClientHost(ClientContext context, bool verbose, bool debug) _BackgroundColor = Console.BackgroundColor; _ForegroundColor = Console.ForegroundColor; - Verbose($"[PSRule] -- Using working path: {Directory.GetCurrentDirectory()}"); + _Context.LogVerbose($"[PSRule] -- Using working path: {Directory.GetCurrentDirectory()}"); } /// @@ -43,40 +44,15 @@ public ClientHost(ClientContext context, bool verbose, bool debug) /// public override ActionPreference GetPreferenceVariable(string variableName) { - if (variableName == "VerbosePreference") + if (variableName == VerbosePreference) return _Verbose ? ActionPreference.Continue : ActionPreference.SilentlyContinue; - if (variableName == "DebugPreference") + if (variableName == DebugPreference) return _Debug ? ActionPreference.Continue : ActionPreference.SilentlyContinue; return base.GetPreferenceVariable(variableName); } - /// - /// - /// - /// - public override void Error(ErrorRecord errorRecord) - { - if (errorRecord.Exception is PipelineException pipelineException) - { - // If the error is a pipeline exception, set the last error code. - _Context.SetLastErrorCode(pipelineException.EventId); - } - - _Context.LogError(errorRecord.Exception.Message); - base.Error(errorRecord); - } - - /// - /// - /// - /// - public override void Warning(string text) - { - _Context.Invocation.Console.WriteLine(text); - } - /// /// /// @@ -88,28 +64,18 @@ public override bool ShouldProcess(string target, string action) return true; } - /// - /// - /// - /// - public override void Information(InformationRecord informationRecord) + /// + public override void WriteHost(string message, ConsoleColor? backgroundColor = null, ConsoleColor? foregroundColor = null, bool? noNewLine = null) { - if (informationRecord?.MessageData is HostInformationMessage info) - { - SetConsole(info); - if (info.NoNewLine.GetValueOrDefault(false)) - _Context.Invocation.Console.Write(info.Message); - else - _Context.Invocation.Console.WriteLine(info.Message); + Console.BackgroundColor = backgroundColor.GetValueOrDefault(_BackgroundColor); + Console.ForegroundColor = foregroundColor.GetValueOrDefault(_ForegroundColor); - RevertConsole(); - } - } + if (noNewLine.GetValueOrDefault(false)) + _Context.Invocation.Console.Write(message); + else + _Context.Invocation.Console.WriteLine(message); - private void SetConsole(HostInformationMessage info) - { - Console.BackgroundColor = info.BackgroundColor.GetValueOrDefault(_BackgroundColor); - Console.ForegroundColor = info.ForegroundColor.GetValueOrDefault(_ForegroundColor); + RevertConsole(); } private void RevertConsole() @@ -121,36 +87,73 @@ private void RevertConsole() /// /// /// - /// - public override void Verbose(string text) + /// + public override string GetWorkingPath() { - if (!_Verbose) - return; - - _Context.LogVerbose(text); + return _Context.WorkingPath == null ? base.GetWorkingPath() : _Context.WorkingPath; } + /// + public override string? CachePath => _Context.CachePath; + + /// - /// + /// Determine if a log level is enabled. + /// All log levels are enabled by default except Trace and Debug. + /// Trace and Debug are enabled if the verbose or debug arguments are set in the constructor. /// - /// - public override void Debug(string text) + public override bool IsEnabled(Runtime.LogLevel logLevel) { - if (!_Debug) - return; - - _Context.Invocation.Console.WriteLine(text); + switch (logLevel) + { + case LogLevel.Trace: + return _Verbose; + case LogLevel.Debug: + return _Debug; + } + return true; } - /// - /// - /// - /// - public override string GetWorkingPath() + /// + public override void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { - return _Context.WorkingPath == null ? base.GetWorkingPath() : _Context.WorkingPath; + if (!IsEnabled(logLevel)) + return; + + switch (logLevel) + { + case LogLevel.Trace: + _Context.LogVerbose(formatter(state, exception)); + break; + case LogLevel.Debug: + _Context.Invocation.Console.WriteLine(formatter(state, exception)); + break; + case LogLevel.Information: + _Context.Invocation.Console.WriteLine(formatter(state, exception)); + break; + case LogLevel.Warning: + _Context.Invocation.Console.WriteLine(formatter(state, exception)); + break; + case LogLevel.Error: + case LogLevel.Critical: + if (exception is PipelineException pipelineException) + { + // If the error is a pipeline exception, set the last error code. + _Context.SetLastErrorCode(pipelineException.EventId); + } + + _Context.LogError(formatter(state, exception)); + base.Log(logLevel, eventId, state, exception, formatter); + break; + } } /// - public override string? CachePath => _Context.CachePath; + public override void SetExitCode(int exitCode) + { + if (exitCode == 0) return; + + _Context.SetLastErrorCode(exitCode); + base.SetExitCode(exitCode); + } } diff --git a/src/PSRule.CommandLine/Models/SessionContext.cs b/src/PSRule.CommandLine/Models/SessionContext.cs index f219d9c0ea..1cd06936ac 100644 --- a/src/PSRule.CommandLine/Models/SessionContext.cs +++ b/src/PSRule.CommandLine/Models/SessionContext.cs @@ -3,6 +3,7 @@ using System.Management.Automation; using PSRule.Pipeline; +using PSRule.Runtime; namespace PSRule.CommandLine.Models; @@ -13,26 +14,18 @@ internal sealed class SessionContext(IHostContext parent) : IHostContext { private readonly IHostContext _Parent = parent; + public string? CachePath => _Parent.CachePath; + + public int ExitCode => _Parent.ExitCode; + public bool InSession => _Parent.InSession; public bool HadErrors => _Parent.HadErrors; - public string? CachePath => _Parent.CachePath; - public string? WorkingPath { get; set; } public Action? GetResultOutput { get; set; } - public void Debug(string text) - { - _Parent.Debug(text); - } - - public void Error(ErrorRecord errorRecord) - { - _Parent.Error(errorRecord); - } - public ActionPreference GetPreferenceVariable(string variableName) { return _Parent.GetPreferenceVariable(variableName); @@ -48,15 +41,10 @@ public string GetWorkingPath() return WorkingPath ?? _Parent.GetWorkingPath(); } - public void Information(InformationRecord informationRecord) - { - _Parent.Information(informationRecord); - } - - public void Object(object sendToPipeline, bool enumerateCollection) + public void WriteObject(object sendToPipeline, bool enumerateCollection) { GetResultOutput?.Invoke(sendToPipeline); - _Parent.Object(sendToPipeline, enumerateCollection); + _Parent.WriteObject(sendToPipeline, enumerateCollection); } public void SetVariable(string variableName, T value) @@ -69,13 +57,23 @@ public bool ShouldProcess(string target, string action) return _Parent.ShouldProcess(target, action); } - public void Verbose(string text) + public void WriteHost(string message, ConsoleColor? backgroundColor = null, ConsoleColor? foregroundColor = null, bool? noNewLine = null) + { + _Parent.WriteHost(message, backgroundColor, foregroundColor, noNewLine); + } + + public bool IsEnabled(LogLevel logLevel) + { + return _Parent.IsEnabled(logLevel); + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { - _Parent.Verbose(text); + _Parent.Log(logLevel, eventId, state, exception, formatter); } - public void Warning(string text) + public void SetExitCode(int exitCode) { - _Parent.Warning(text); + _Parent.SetExitCode(exitCode); } } diff --git a/src/PSRule.EditorServices/Handlers/RunAnalysisCommandHandler.cs b/src/PSRule.EditorServices/Handlers/RunAnalysisCommandHandler.cs index 6873d8df09..9af4fdb9fa 100644 --- a/src/PSRule.EditorServices/Handlers/RunAnalysisCommandHandler.cs +++ b/src/PSRule.EditorServices/Handlers/RunAnalysisCommandHandler.cs @@ -37,8 +37,8 @@ public override async Task Handle(RunAnalysisCo BreakLevel = Options.BreakLevel.Never, }; - _Logger.LogInformation(new EventId(0), "Running tests in: {0}", input.WorkspacePath); - _Logger.LogInformation(new EventId(0), "Input path: {0}", string.Join(", ", input.InputPath ?? [])); + _Logger.LogInformation(EventId.None, "Running tests in: {0}", input.WorkspacePath); + _Logger.LogInformation(EventId.None, "Input path: {0}", string.Join(", ", input.InputPath ?? [])); try { @@ -47,7 +47,7 @@ public override async Task Handle(RunAnalysisCo } catch (Exception ex) { - _Logger.LogError(new EventId(0), ex, ex.Message); + _Logger.LogError(EventId.None, ex, ex.Message); } return new RunAnalysisCommandHandlerOutput(1); diff --git a/src/PSRule.EditorServices/Handlers/UpgradeDependencyCommandHandler.cs b/src/PSRule.EditorServices/Handlers/UpgradeDependencyCommandHandler.cs index 96e3a339bf..3c7f8dba36 100644 --- a/src/PSRule.EditorServices/Handlers/UpgradeDependencyCommandHandler.cs +++ b/src/PSRule.EditorServices/Handlers/UpgradeDependencyCommandHandler.cs @@ -42,11 +42,11 @@ public override async Task Handle(Upgrade if (all) { - _Logger.LogInformation(new EventId(0), "Checking for upgrades of all modules in: {0}", input.Path); + _Logger.LogInformation(EventId.None, "Checking for upgrades of all modules in: {0}", input.Path); } else { - _Logger.LogInformation(new EventId(0), "Checking for upgrades of module {0} in: {1}", input.Module, input.Path); + _Logger.LogInformation(EventId.None, "Checking for upgrades of module {0} in: {1}", input.Module, input.Path); } try @@ -59,7 +59,7 @@ public override async Task Handle(Upgrade } catch (Exception ex) { - _Logger.LogError(new EventId(0), ex, ex.Message); + _Logger.LogError(EventId.None, ex, ex.Message); } return new UpgradeDependencyCommandHandlerOutput(); diff --git a/src/PSRule.EditorServices/Hosting/ServerHostContext.cs b/src/PSRule.EditorServices/Hosting/ServerHostContext.cs index 97d9070cf4..c7d3e9f23e 100644 --- a/src/PSRule.EditorServices/Hosting/ServerHostContext.cs +++ b/src/PSRule.EditorServices/Hosting/ServerHostContext.cs @@ -6,6 +6,7 @@ using System.CommandLine.IO; using System.Management.Automation; using PSRule.Pipeline; +using PSRule.Runtime; namespace PSRule.EditorServices.Hosting; @@ -24,55 +25,69 @@ public ServerHostContext(InvocationContext invocation, bool verbose, bool debug) _Verbose = verbose; _Debug = debug; - Verbose($"Using working path: {Directory.GetCurrentDirectory()}"); + if (_Verbose) + { + _Invocation.Console.WriteLine($"Using working path: {Directory.GetCurrentDirectory()}"); + } } public override ActionPreference GetPreferenceVariable(string variableName) { - if (variableName == "VerbosePreference") + if (variableName == VerbosePreference) return _Verbose ? ActionPreference.Continue : ActionPreference.SilentlyContinue; - if (variableName == "DebugPreference") + if (variableName == DebugPreference) return _Debug ? ActionPreference.Continue : ActionPreference.SilentlyContinue; return base.GetPreferenceVariable(variableName); } - public override void Error(ErrorRecord errorRecord) - { - _Invocation.Console.Error.WriteLine(errorRecord.Exception.Message); - base.Error(errorRecord); - } - - public override void Warning(string text) - { - _Invocation.Console.WriteLine(text); - } - public override bool ShouldProcess(string target, string action) { return true; } - public override void Information(InformationRecord informationRecord) + /// + /// Determine if a log level is enabled. + /// All log levels are enabled by default except Trace and Debug. + /// Trace and Debug are enabled if the verbose or debug arguments are set in the constructor. + /// + public override bool IsEnabled(Runtime.LogLevel logLevel) { - if (informationRecord?.MessageData is HostInformationMessage info) - _Invocation.Console.WriteLine(info.Message); - } - - public override void Verbose(string text) - { - if (!_Verbose) - return; - - _Invocation.Console.WriteLine(text); + switch (logLevel) + { + case LogLevel.Trace: + return _Verbose; + case LogLevel.Debug: + return _Debug; + } + return true; } - public override void Debug(string text) + public override void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { - if (!_Debug) + if (!IsEnabled(logLevel)) return; - _Invocation.Console.WriteLine(text); + switch (logLevel) + { + case LogLevel.Trace: + _Invocation.Console.WriteLine(formatter(state, exception)); + break; + case LogLevel.Debug: + _Invocation.Console.WriteLine(formatter(state, exception)); + break; + case LogLevel.Information: + _Invocation.Console.WriteLine(formatter(state, exception)); + break; + case LogLevel.Warning: + _Invocation.Console.WriteLine(formatter(state, exception)); + break; + case LogLevel.Error: + case LogLevel.Critical: + _Invocation.Console.WriteLine(formatter(state, exception)); + base.Log(logLevel, eventId, state, exception, formatter); + break; + } } } diff --git a/src/PSRule.Types/Emitters/BaseEmitter.cs b/src/PSRule.Types/Emitters/BaseEmitter.cs index 87f5c14ed9..dadc577150 100644 --- a/src/PSRule.Types/Emitters/BaseEmitter.cs +++ b/src/PSRule.Types/Emitters/BaseEmitter.cs @@ -28,7 +28,7 @@ protected virtual void Dispose(bool disposing) { if (disposing) { - // TODO: dispose managed state (managed objects) + // Do nothing here. } _Disposed = true; } diff --git a/src/PSRule.Types/PipelineException.cs b/src/PSRule.Types/PipelineException.cs index aa32a8da36..7f83301589 100644 --- a/src/PSRule.Types/PipelineException.cs +++ b/src/PSRule.Types/PipelineException.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Runtime.Serialization; +using System.Security.Permissions; using PSRule.Runtime; namespace PSRule; @@ -51,10 +52,28 @@ protected PipelineException(EventId eventId, string message, Exception innerExce /// Initialize a new instance of a PSRule exception. /// protected PipelineException(SerializationInfo info, StreamingContext context) - : base(info, context) { } + : base(info, context) + { + EventId = info.GetValue("EventId", typeof(EventId?)) as EventId?; + } /// /// The event identifier for the exception. /// public EventId? EventId { get; } + + /// + /// An associated unique identifier related to why the exception was thrown. + /// + public string? ErrorId => EventId?.Name; + + /// + [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) throw new ArgumentNullException(nameof(info)); + + info.AddValue("EventId", EventId); + base.GetObjectData(info, context); + } } diff --git a/src/PSRule.Types/PipelineSerializationException.cs b/src/PSRule.Types/PipelineSerializationException.cs index ce39f7e8c3..6ed25cede6 100644 --- a/src/PSRule.Types/PipelineSerializationException.cs +++ b/src/PSRule.Types/PipelineSerializationException.cs @@ -3,6 +3,7 @@ using System.Runtime.Serialization; using System.Security.Permissions; +using PSRule.Runtime; namespace PSRule; @@ -22,8 +23,8 @@ public PipelineSerializationException() /// /// Creates a serialization exception. /// - internal PipelineSerializationException(string message, string path, Exception innerException) - : this(message, innerException) + internal PipelineSerializationException(EventId eventId, string message, string path, Exception innerException) + : base(eventId, message, innerException) { Path = path; } diff --git a/src/PSRule.Types/Runtime/EventId.cs b/src/PSRule.Types/Runtime/EventId.cs index f24cf518bc..ec06e3c1d8 100644 --- a/src/PSRule.Types/Runtime/EventId.cs +++ b/src/PSRule.Types/Runtime/EventId.cs @@ -7,8 +7,14 @@ namespace PSRule.Runtime; /// Identifies a logging event. /// The primary identifier is the "Id" property, with the "Name" property providing a short description of this type of event. /// +[Serializable] public readonly struct EventId : IEquatable { + /// + /// The default event id for an unknown event. + /// + public static readonly EventId None = new(0); + /// /// Implicitly creates an EventId from the given . /// diff --git a/src/PSRule.Types/Runtime/ILogger.cs b/src/PSRule.Types/Runtime/ILogger.cs index 6eb39bf5d4..2f01b00490 100644 --- a/src/PSRule.Types/Runtime/ILogger.cs +++ b/src/PSRule.Types/Runtime/ILogger.cs @@ -23,6 +23,6 @@ public interface ILogger /// An event identifier for the diagnostic message. /// Additional information that describes the diagnostic state to log. /// An optional exception which the diagnostic message is related to. - /// A function to format the diagnostic message for the outpuWt stream. + /// A function to format the diagnostic message for the output stream. public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter); } diff --git a/src/PSRule.Types/Runtime/LoggerExtensions.cs b/src/PSRule.Types/Runtime/LoggerExtensions.cs index 56f75fe555..37c6398ce1 100644 --- a/src/PSRule.Types/Runtime/LoggerExtensions.cs +++ b/src/PSRule.Types/Runtime/LoggerExtensions.cs @@ -36,6 +36,18 @@ public static void LogDebug(this ILogger logger, EventId eventId, string? messag logger.Log(LogLevel.Debug, eventId, default, message, args); } + /// + /// Log a verbose level message. + /// + /// A valid instance. + /// An event identifier for the warning. + /// The format message text. + /// Additional arguments to use within the format message. + public static void LogVerbose(this ILogger logger, EventId eventId, string? message, params object?[] args) + { + logger.Log(LogLevel.Trace, eventId, default, message, args); + } + /// /// Log a warning message. /// diff --git a/src/PSRule/Commands/ExportConventionCommand.cs b/src/PSRule/Commands/ExportConventionCommand.cs index ee4f17f00a..181646c121 100644 --- a/src/PSRule/Commands/ExportConventionCommand.cs +++ b/src/PSRule/Commands/ExportConventionCommand.cs @@ -83,7 +83,7 @@ protected override void ProcessRecord() position: MyInvocation.OffsetInLine ); - context.VerboseFoundResource(name: Name, moduleName: source.Module, scriptName: MyInvocation.ScriptName); + context.VerboseFoundResource(name: Name, scope: source.Module, scriptName: MyInvocation.ScriptName); var helpInfo = new ResourceHelpInfo(Name, Name, new InfoString(commentMetadata.Synopsis), new InfoString()); diff --git a/src/PSRule/Commands/InvokeConventionCommand.cs b/src/PSRule/Commands/InvokeConventionCommand.cs index fcfb8a8520..67d36dc889 100644 --- a/src/PSRule/Commands/InvokeConventionCommand.cs +++ b/src/PSRule/Commands/InvokeConventionCommand.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System.Management.Automation; -using PSRule.Pipeline; using PSRule.Resources; using PSRule.Runtime; @@ -36,7 +35,7 @@ protected override void ProcessRecord() var ifResult = RuleConditionHelper.Create(If.Invoke()); if (!ifResult.AllOf()) { - context.Writer.DebugMessage(PSRuleResources.DebugTargetIfMismatch); + context.Logger?.LogDebug(EventId.None, PSRuleResources.DebugTargetIfMismatch); return; } } diff --git a/src/PSRule/Commands/InvokeRuleBlockCommand.cs b/src/PSRule/Commands/InvokeRuleBlockCommand.cs index b501cb7158..0d004ed8a9 100644 --- a/src/PSRule/Commands/InvokeRuleBlockCommand.cs +++ b/src/PSRule/Commands/InvokeRuleBlockCommand.cs @@ -40,14 +40,14 @@ protected override void ProcessRecord() // Evaluate selector pre-condition if (!AcceptsWith()) { - context.Writer.DebugMessage(PSRuleResources.DebugTargetTypeMismatch); + context.Logger?.LogDebug(EventId.None, PSRuleResources.DebugTargetTypeMismatch); return; } // Evaluate type pre-condition if (!AcceptsType()) { - context.Writer.DebugMessage(PSRuleResources.DebugTargetTypeMismatch); + context.Logger?.LogDebug(EventId.None, PSRuleResources.DebugTargetTypeMismatch); return; } @@ -61,10 +61,15 @@ protected override void ProcessRecord() var ifResult = RuleConditionHelper.Create(If.Invoke()); if (!ifResult.AllOf()) { - context.Writer.DebugMessage(PSRuleResources.DebugTargetIfMismatch); + context.Logger?.LogDebug(EventId.None, PSRuleResources.DebugTargetIfMismatch); return; } } + catch (ActionPreferenceStopException ex) + { + context.Error(ex); + return; + } finally { context.PopScope(RunspaceScope.Precondition); @@ -79,15 +84,17 @@ protected override void ProcessRecord() var invokeResult = RuleConditionHelper.Create(Body.Invoke()); WriteObject(invokeResult); } + catch (ActionPreferenceStopException ex) + { + context.Error(ex); + WriteObject(new RuleConditionResult(0, 0, true)); + return; + } finally { context.PopScope(RunspaceScope.Rule); } } - catch (ActionPreferenceStopException ex) - { - context.Error(ex); - } catch (System.Management.Automation.RuntimeException ex) { if (ex.ErrorRecord.FullyQualifiedErrorId == "MethodInvocationNotSupportedInConstrainedLanguage") diff --git a/src/PSRule/Commands/NewRuleDefinitionCommand.cs b/src/PSRule/Commands/NewRuleDefinitionCommand.cs index 28c8006af1..ce16a1e91b 100644 --- a/src/PSRule/Commands/NewRuleDefinitionCommand.cs +++ b/src/PSRule/Commands/NewRuleDefinitionCommand.cs @@ -122,7 +122,7 @@ protected override void ProcessRecord() var id = new ResourceId(source.Module, Name, ResourceIdKind.Id); var labels = ResourceLabels.FromHashtable(Labels); - context.VerboseFoundResource(name: Name, moduleName: source.Module, scriptName: MyInvocation.ScriptName); + context.VerboseFoundResource(name: Name, scope: source.Module, scriptName: MyInvocation.ScriptName); CheckDependsOn(); var ps = GetCondition(context, id, source, errorPreference); diff --git a/src/PSRule/Common/HashSetExtensions.cs b/src/PSRule/Common/HashSetExtensions.cs index ab5f14e31a..2c50276d04 100644 --- a/src/PSRule/Common/HashSetExtensions.cs +++ b/src/PSRule/Common/HashSetExtensions.cs @@ -7,7 +7,7 @@ namespace PSRule; internal static class HashSetExtensions { - internal static bool ContainsIds(this HashSet set, ResourceId id, ResourceId? @ref, ResourceId[] aliases, out ResourceId? duplicate) + internal static bool ContainsIds(this HashSet set, ResourceId id, ResourceId? @ref, ResourceId[]? aliases, out ResourceId? duplicate) { duplicate = null; if (set == null || set.Count == 0) @@ -34,7 +34,7 @@ internal static bool ContainsIds(this HashSet set, ResourceId id, Re return false; } - internal static bool ContainsNames(this HashSet set, ResourceId id, ResourceId? @ref, ResourceId[] aliases, out string duplicate) + internal static bool ContainsNames(this HashSet set, ResourceId id, ResourceId? @ref, ResourceId[]? aliases, out string? duplicate) { duplicate = null; if (set == null || set.Count == 0) @@ -61,7 +61,7 @@ internal static bool ContainsNames(this HashSet set, ResourceId id, Reso return false; } - internal static void AddIds(this HashSet set, ResourceId id, ResourceId? @ref, ResourceId[] aliases) + internal static void AddIds(this HashSet set, ResourceId id, ResourceId? @ref, ResourceId[]? aliases) { if (set == null) return; @@ -77,7 +77,7 @@ internal static void AddIds(this HashSet set, ResourceId id, Resourc set.Add(aliases[i]); } - internal static void AddNames(this HashSet set, ResourceId id, ResourceId? @ref, ResourceId[] aliases) + internal static void AddNames(this HashSet set, ResourceId id, ResourceId? @ref, ResourceId[]? aliases) { if (set == null) return; diff --git a/src/PSRule/Common/RunspaceContextDiagnosticExtensions.cs b/src/PSRule/Common/RunspaceContextDiagnosticExtensions.cs index 7ada536f21..1804a3bd45 100644 --- a/src/PSRule/Common/RunspaceContextDiagnosticExtensions.cs +++ b/src/PSRule/Common/RunspaceContextDiagnosticExtensions.cs @@ -16,7 +16,7 @@ internal static class RunspaceContextDiagnosticExtensions internal static void WarnPropertyObsolete(this LegacyRunspaceContext context, string variableName, string propertyName) { context.DebugPropertyObsolete(variableName, propertyName); - if (context.Writer == null || !context.Writer.ShouldWriteWarning() || !context.ShouldWarnOnce(WARN_KEY_PROPERTY, variableName, propertyName)) + if (context.Writer == null || !context.Writer.IsEnabled(LogLevel.Warning) || !context.ShouldWarnOnce(WARN_KEY_PROPERTY, variableName, propertyName)) return; context.Writer.WriteWarning(PSRuleResources.PropertyObsolete, variableName, propertyName); @@ -27,7 +27,7 @@ internal static void WarnPropertyObsolete(this LegacyRunspaceContext context, st /// internal static void WarnDeprecatedOption(this LegacyRunspaceContext context, string option) { - if (context.Writer == null || !context.Writer.ShouldWriteWarning()) + if (context.Writer == null || !context.Writer.IsEnabled(LogLevel.Warning)) return; context.Writer.WriteWarning(PSRuleResources.DeprecatedOption, option); @@ -35,7 +35,7 @@ internal static void WarnDeprecatedOption(this LegacyRunspaceContext context, st internal static void WarnDuplicateRuleName(this LegacyRunspaceContext context, string ruleName) { - if (context == null || context.Writer == null || !context.Writer.ShouldWriteWarning()) + if (context == null || context.Writer == null || !context.Writer.IsEnabled(LogLevel.Warning)) return; context.Writer.WriteWarning(PSRuleResources.DuplicateRuleName, ruleName); @@ -76,16 +76,16 @@ internal static void Throw(this LegacyRunspaceContext context, ExecutionActionPr if (action == ExecutionActionPreference.Error) throw new RuleException(string.Format(Thread.CurrentThread.CurrentCulture, message, args)); - else if (action == ExecutionActionPreference.Warn && context.Writer != null && context.Writer.ShouldWriteWarning()) + else if (action == ExecutionActionPreference.Warn && context.Writer != null && context.Writer.IsEnabled(LogLevel.Warning)) context.Writer.WriteWarning(message, args); - else if (action == ExecutionActionPreference.Debug && context.Writer != null && context.Writer.ShouldWriteDebug()) + else if (action == ExecutionActionPreference.Debug && context.Writer != null && context.Writer.IsEnabled(LogLevel.Debug)) context.Writer.WriteDebug(message, args); } internal static void DebugPropertyObsolete(this LegacyRunspaceContext context, string variableName, string propertyName) { - if (context == null || context.Writer == null || !context.Writer.ShouldWriteDebug()) + if (context == null || context.Writer == null || !context.Writer.IsEnabled(LogLevel.Debug)) return; context.Writer.WriteDebug(PSRuleResources.DebugPropertyObsolete, context.RuleBlock.Name, variableName, propertyName); diff --git a/src/PSRule/Configuration/LoggingOption.cs b/src/PSRule/Configuration/LoggingOption.cs deleted file mode 100644 index 09f93720f0..0000000000 --- a/src/PSRule/Configuration/LoggingOption.cs +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.ComponentModel; - -namespace PSRule.Configuration; - -/// -/// Options for logging outcomes to a informational streams. -/// -public sealed class LoggingOption : IEquatable -{ - private const OutcomeLogStream DEFAULT_RULEFAIL = OutcomeLogStream.None; - private const OutcomeLogStream DEFAULT_RULEPASS = OutcomeLogStream.None; - - internal static readonly LoggingOption Default = new() - { - RuleFail = DEFAULT_RULEFAIL, - RulePass = DEFAULT_RULEPASS, - LimitVerbose = null, - LimitDebug = null - }; - - /// - /// Create an empty logging option. - /// - public LoggingOption() - { - LimitDebug = null; - LimitVerbose = null; - RuleFail = null; - RulePass = null; - } - - /// - /// Create an logging option by copying an existing instance. - /// - /// The option instance to copy. - public LoggingOption(LoggingOption option) - { - if (option == null) - return; - - LimitDebug = option.LimitDebug; - LimitVerbose = option.LimitVerbose; - RuleFail = option.RuleFail; - RulePass = option.RulePass; - } - - /// - public override bool Equals(object obj) - { - return obj is LoggingOption option && Equals(option); - } - - /// - public bool Equals(LoggingOption other) - { - return other != null && - LimitDebug == other.LimitDebug && - LimitVerbose == other.LimitVerbose && - RuleFail == other.RuleFail && - RulePass == other.RulePass; - } - - /// - public override int GetHashCode() - { - unchecked // Overflow is fine - { - var hash = 17; - hash = hash * 23 + (LimitDebug != null ? LimitDebug.GetHashCode() : 0); - hash = hash * 23 + (LimitVerbose != null ? LimitVerbose.GetHashCode() : 0); - hash = hash * 23 + (RuleFail.HasValue ? RuleFail.Value.GetHashCode() : 0); - hash = hash * 23 + (RulePass.HasValue ? RulePass.Value.GetHashCode() : 0); - return hash; - } - } - - internal static LoggingOption Combine(LoggingOption o1, LoggingOption o2) - { - var result = new LoggingOption(o1) - { - LimitDebug = o1?.LimitDebug ?? o2?.LimitDebug, - LimitVerbose = o1?.LimitVerbose ?? o2?.LimitVerbose, - RuleFail = o1?.RuleFail ?? o2?.RuleFail, - RulePass = o1?.RulePass ?? o2?.RulePass - }; - return result; - } - - /// - /// Limits debug messages to a list of named debug scopes. - /// - [DefaultValue(null)] - public string[] LimitDebug { get; set; } - - /// - /// Limits verbose messages to a list of named verbose scopes. - /// - [DefaultValue(null)] - public string[] LimitVerbose { get; set; } - - /// - /// Log fail outcomes for each rule to a specific informational stream. - /// - [DefaultValue(null)] - public OutcomeLogStream? RuleFail { get; set; } - - /// - /// Log pass outcomes for each rule to a specific informational stream. - /// - [DefaultValue(null)] - public OutcomeLogStream? RulePass { get; set; } - - internal void Load() - { - if (Environment.TryStringArray("PSRULE_LOGGING_LIMITDEBUG", out var limitDebug)) - LimitDebug = limitDebug; - - if (Environment.TryStringArray("PSRULE_LOGGING_LIMITVERBOSE", out var limitVerbose)) - LimitVerbose = limitVerbose; - - if (Environment.TryEnum("PSRULE_LOGGING_RULEFAIL", out OutcomeLogStream ruleFail)) - RuleFail = ruleFail; - - if (Environment.TryEnum("PSRULE_LOGGING_RULEPASS", out OutcomeLogStream rulePass)) - RulePass = rulePass; - } - - internal void Load(Dictionary index) - { - if (index.TryPopStringArray("Logging.LimitDebug", out var limitDebug)) - LimitDebug = limitDebug; - - if (index.TryPopStringArray("Logging.LimitVerbose", out var limitVerbose)) - LimitVerbose = limitVerbose; - - if (index.TryPopEnum("Logging.RuleFail", out OutcomeLogStream ruleFail)) - RuleFail = ruleFail; - - if (index.TryPopEnum("Logging.RulePass", out OutcomeLogStream rulePass)) - RulePass = rulePass; - } -} diff --git a/src/PSRule/Configuration/OutcomeLogStream.cs b/src/PSRule/Configuration/OutcomeLogStream.cs deleted file mode 100644 index ce027a8720..0000000000 --- a/src/PSRule/Configuration/OutcomeLogStream.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; - -namespace PSRule.Configuration; - -/// -/// The PowerShell informational stream to log specific outcomes to. -/// -[JsonConverter(typeof(StringEnumConverter))] -public enum OutcomeLogStream -{ - /// - /// Outcomes will not be logged to an informational stream. - /// - None = 0, - - /// - /// Log to Error stream. - /// - Error = 1, - - /// - /// Log to Warning stream. - /// - Warning = 2, - - /// - /// Log to Information stream. - /// - Information = 3 -} diff --git a/src/PSRule/Configuration/PSRuleOption.cs b/src/PSRule/Configuration/PSRuleOption.cs index 7469231ce3..b7d76ba41b 100644 --- a/src/PSRule/Configuration/PSRuleOption.cs +++ b/src/PSRule/Configuration/PSRuleOption.cs @@ -41,7 +41,6 @@ public sealed class PSRuleOption : IEquatable, IBaselineV1Spec Format = FormatOption.Default, Include = IncludeOption.Default, Input = InputOption.Default, - Logging = LoggingOption.Default, Output = OutputOption.Default, Override = OverrideOption.Default, Rule = RuleOption.Default, @@ -63,7 +62,6 @@ public PSRuleOption() Format = new FormatOption(); Include = new IncludeOption(); Input = new InputOption(); - Logging = new LoggingOption(); Output = new OutputOption(); Override = new OverrideOption(); Repository = new RepositoryOption(); @@ -87,7 +85,6 @@ private PSRuleOption(string sourcePath, PSRuleOption option) Format = new FormatOption(option?.Format); 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); @@ -142,11 +139,6 @@ private PSRuleOption(string sourcePath, PSRuleOption option) /// public InputOption Input { get; set; } - /// - /// Options for logging outcomes to a informational streams. - /// - public LoggingOption Logging { get; set; } - /// /// Options that affect how output is generated. /// @@ -242,7 +234,6 @@ private static PSRuleOption Combine(PSRuleOption o1, PSRuleOption o2) result.Format = FormatOption.Combine(result.Format, o2?.Format); 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); @@ -378,7 +369,6 @@ private static PSRuleOption FromEnvironment(PSRuleOption option) option.Format.Load(); option.Include.Load(); option.Input.Load(); - option.Logging.Load(); option.Output.Load(); option.Override.Load(); option.Repository.Load(); @@ -412,7 +402,6 @@ public static PSRuleOption FromHashtable(Hashtable hashtable) option.Format.Import(index); option.Include.Load(index); option.Input.Load(index); - option.Logging.Load(index); option.Output.Load(index); option.Override.Import(index); option.Repository.Load(index); @@ -481,7 +470,6 @@ public bool Equals(PSRuleOption other) Format == other.Format && Include == other.Include && Input == other.Input && - Logging == other.Logging && Output == other.Output && Override == other.Override && Suppression == other.Suppression && @@ -506,7 +494,6 @@ public override int GetHashCode() hash = hash * 23 + (Format != null ? Format.GetHashCode() : 0); hash = hash * 23 + (Include != null ? Include.GetHashCode() : 0); 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); diff --git a/src/PSRule/Definitions/Expressions/ExpressionContext.cs b/src/PSRule/Definitions/Expressions/ExpressionContext.cs index e7fb1e1a25..3b9e051e1f 100644 --- a/src/PSRule/Definitions/Expressions/ExpressionContext.cs +++ b/src/PSRule/Definitions/Expressions/ExpressionContext.cs @@ -52,10 +52,7 @@ bool IBindingContext.GetPathExpression(string path, out PathExpression expressio public void Debug(string message, params object[] args) { - if (Context.Writer == null) - return; - - Context.Writer.WriteDebug(message, args); + Context.Logger?.LogDebug(EventId.None, message, args); } public void PushScope(RunspaceScope scope) diff --git a/src/PSRule/Definitions/Expressions/LanguageExpressions.cs b/src/PSRule/Definitions/Expressions/LanguageExpressions.cs index d1e894b9f2..f6cd9fbc83 100644 --- a/src/PSRule/Definitions/Expressions/LanguageExpressions.cs +++ b/src/PSRule/Definitions/Expressions/LanguageExpressions.cs @@ -1275,7 +1275,7 @@ private static bool TryField(LanguageExpression.PropertyBag properties, out stri return properties.TryGetString(FIELD, out field); } - private static bool TryField(IExpressionContext context, LanguageExpression.PropertyBag properties, ITargetObject o, out IOperand operand) + private static bool TryField(IExpressionContext context, LanguageExpression.PropertyBag properties, ITargetObject o, out IOperand? operand) { operand = null; if (!properties.TryGetString(FIELD, out var field)) @@ -1287,7 +1287,7 @@ private static bool TryField(IExpressionContext context, LanguageExpression.Prop return operand != null || NotHasField(context, field); } - private static bool TryName(IExpressionContext context, LanguageExpression.PropertyBag properties, ITargetObject o, out IOperand operand) + private static bool TryName(IExpressionContext context, LanguageExpression.PropertyBag properties, ITargetObject o, out IOperand? operand) { operand = null; if (properties.TryGetString(NAME, out var svalue)) @@ -1304,7 +1304,7 @@ private static bool TryName(IExpressionContext context, LanguageExpression.Prope return operand != null; } - private static bool TryType(IExpressionContext context, LanguageExpression.PropertyBag properties, ITargetObject o, out IOperand operand) + private static bool TryType(IExpressionContext context, LanguageExpression.PropertyBag properties, ITargetObject o, out IOperand? operand) { operand = null; if (properties.TryGetString(TYPE, out var svalue)) @@ -1321,7 +1321,7 @@ private static bool TryType(IExpressionContext context, LanguageExpression.Prope return operand != null; } - private static bool TrySource(IExpressionContext context, LanguageExpression.PropertyBag properties, out IOperand operand) + private static bool TrySource(IExpressionContext context, LanguageExpression.PropertyBag properties, out IOperand? operand) { operand = null; if (properties.TryGetString(SOURCE, out var sourceValue)) @@ -1335,7 +1335,7 @@ private static bool TrySource(IExpressionContext context, LanguageExpression.Pro return operand != null; } - private static bool TryValue(IExpressionContext context, LanguageExpression.PropertyBag properties, out IOperand operand) + private static bool TryValue(IExpressionContext context, LanguageExpression.PropertyBag properties, out IOperand? operand) { operand = null; if (properties.TryGetValue(VALUE, out var value)) @@ -1346,7 +1346,7 @@ private static bool TryValue(IExpressionContext context, LanguageExpression.Prop return operand != null; } - private static bool TryScope(IExpressionContext context, LanguageExpression.PropertyBag properties, ITargetObject o, out IOperand operand) + private static bool TryScope(IExpressionContext context, LanguageExpression.PropertyBag properties, ITargetObject o, out IOperand? operand) { operand = null; if (properties.TryGetString(SCOPE, out var svalue)) @@ -1365,7 +1365,7 @@ private static bool TryScope(IExpressionContext context, LanguageExpression.Prop /// /// Unwrap a function delegate or a literal value. /// - private static object Value(IExpressionContext context, IOperand operand) + private static object? Value(IExpressionContext context, IOperand operand) { if (operand == null) return null; diff --git a/src/PSRule/Definitions/IResourceDiscoveryContext.cs b/src/PSRule/Definitions/IResourceDiscoveryContext.cs index 029c4feafd..77990b421b 100644 --- a/src/PSRule/Definitions/IResourceDiscoveryContext.cs +++ b/src/PSRule/Definitions/IResourceDiscoveryContext.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using PSRule.Pipeline; using PSRule.Runtime; namespace PSRule.Definitions; @@ -16,7 +15,7 @@ internal interface IResourceDiscoveryContext /// /// A writer to log messages. /// - IPipelineWriter Writer { get; } + ILogger Logger { get; } /// /// The current source file. diff --git a/src/PSRule/Definitions/ResourceValidator.cs b/src/PSRule/Definitions/ResourceValidator.cs index 4ae25d9b5c..89bc3c8539 100644 --- a/src/PSRule/Definitions/ResourceValidator.cs +++ b/src/PSRule/Definitions/ResourceValidator.cs @@ -1,28 +1,19 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Management.Automation; using System.Text.RegularExpressions; -using PSRule.Pipeline; -using PSRule.Resources; +using PSRule.Runtime; namespace PSRule.Definitions; /// /// A helper class to help validate a resource object. /// -internal sealed class ResourceValidator : IResourceValidator +internal sealed class ResourceValidator(ILogger? logger) : IResourceValidator { - private const string ERROR_ID_INVALID_RESOURCE_NAME = "PSRule.Parse.InvalidResourceName"; - private static readonly Regex ValidName = new("^[^<>:/\\\\|?*\"'`+@._\\-\x00-\x1F][^<>:/\\\\|?*\"'`+@\x00-\x1F]{1,126}[^<>:/\\\\|?*\"'`+@._\\-\x00-\x1F]$", RegexOptions.Compiled, TimeSpan.FromSeconds(5)); - private readonly IPipelineWriter _Writer; - - public ResourceValidator(IPipelineWriter writer) - { - _Writer = writer; - } + private readonly ILogger? _Logger = logger; internal static bool IsNameValid(string name) { @@ -41,7 +32,7 @@ private bool VisitName(IResource resource, string name) if (IsNameValid(name)) return true; - ReportError(ERROR_ID_INVALID_RESOURCE_NAME, PSRuleResources.InvalidResourceName, name, ReportExtent(resource.Extent)); + _Logger?.LogInvalidResourceName(name, ReportExtent(resource.Extent)); return false; } @@ -50,7 +41,7 @@ private bool VisitName(IResource resource, ResourceId? name) return !name.HasValue || VisitName(resource, name.Value.Name); } - private bool VisitName(IResource resource, ResourceId[] name) + private bool VisitName(IResource resource, ResourceId[]? name) { if (name == null || name.Length == 0) return true; @@ -66,28 +57,4 @@ private static string ReportExtent(ISourceExtent extent) { return string.Concat(extent.File, " line ", extent.Line); } - - private void ReportError(string errorId, string message, params object[] args) - { - if (_Writer == null) - return; - - ReportError(new Pipeline.ParseException( - message: string.Format(Thread.CurrentThread.CurrentCulture, message, args), - errorId: errorId - )); - } - - private void ReportError(Pipeline.ParseException exception) - { - if (_Writer == null) - return; - - _Writer.WriteError(new ErrorRecord( - exception: exception, - errorId: exception.ErrorId, - errorCategory: ErrorCategory.InvalidOperation, - targetObject: null - )); - } } diff --git a/src/PSRule/Definitions/ResultReason.cs b/src/PSRule/Definitions/ResultReason.cs index 7badc86089..eae09a1c61 100644 --- a/src/PSRule/Definitions/ResultReason.cs +++ b/src/PSRule/Definitions/ResultReason.cs @@ -10,7 +10,7 @@ namespace PSRule.Definitions; /// /// A reason for the rule result. /// -internal sealed class ResultReason : IResultReason +public sealed class ResultReason : IResultReason { private readonly string _ParentPath; diff --git a/src/PSRule/Host/HostHelper.cs b/src/PSRule/Host/HostHelper.cs index 181e1cc9bb..f46ec19560 100644 --- a/src/PSRule/Host/HostHelper.cs +++ b/src/PSRule/Host/HostHelper.cs @@ -134,7 +134,7 @@ internal static void UnblockFile(IPipelineWriter writer, string[] publisher, str if (ps.HadErrors) { foreach (var error in ps.Streams.Error) - writer.WriteError(error); + writer.LogError(error); } } finally @@ -152,12 +152,14 @@ private static ILanguageBlock[] GetPSLanguageBlocks(IScriptResourceDiscoveryCont if (context.GetExecutionOption().RestrictScriptSource == Options.RestrictScriptSource.DisablePowerShell) return []; + var ps = context.GetPowerShell() ?? throw new InvalidOperationException("PowerShell runspace is not available."); var results = new List(); - var ps = context.GetPowerShell(); + var logger = context.Logger; try { - context.Writer?.EnterScope("[Discovery.Rule]"); + // TODO: Add scope wrapping for discovery. + // logger.EnterScope("[Discovery.Rule]"); context.PushScope(RunspaceScope.Source); // Process scripts @@ -169,25 +171,22 @@ private static ILanguageBlock[] GetPSLanguageBlocks(IScriptResourceDiscoveryCont continue; ps.Commands.Clear(); - context.Writer?.VerboseRuleDiscovery(path: file.Path); + logger.VerboseRuleDiscovery(path: file.Path); context.EnterLanguageScope(file); try { var scriptAst = System.Management.Automation.Language.Parser.ParseFile(file.Path, out var tokens, out var errors); - var visitor = new RuleLanguageAst(); + var visitor = new RuleLanguageAst(logger); scriptAst.Visit(visitor); - if (visitor.Errors != null && visitor.Errors.Count > 0) + if (visitor.HadErrors) { - foreach (var record in visitor.Errors) - context.Writer?.WriteError(record); - continue; } if (errors != null && errors.Length > 0) { foreach (var error in errors) - context.Writer?.WriteError(error); + logger.LogError(error); continue; } @@ -215,7 +214,7 @@ private static ILanguageBlock[] GetPSLanguageBlocks(IScriptResourceDiscoveryCont } finally { - context.Writer?.ExitScope(); + // logger.ExitScope(); context.PopScope(RunspaceScope.Source); ps.Runspace = null; ps.Dispose(); @@ -228,8 +227,10 @@ private static ILanguageBlock[] GetPSLanguageBlocks(IScriptResourceDiscoveryCont /// private static ILanguageBlock[] GetYamlLanguageBlocks(Source[] sources, IResourceDiscoveryContext context) { + // TODO: Add scope wrapping for discovery. + var logger = context.Logger; var result = new Collection(); - var visitor = new ResourceValidator(context.Writer); + var visitor = new ResourceValidator(logger); var d = new DeserializerBuilder() .IgnoreUnmatchedProperties() .WithNamingConvention(CamelCaseNamingConvention.Instance) @@ -250,7 +251,7 @@ private static ILanguageBlock[] GetYamlLanguageBlocks(Source[] sources, IResourc try { - context.Writer?.EnterScope("[Discovery.Resource]"); + // logger.EnterScope("[Discovery.Resource]"); context.PushScope(RunspaceScope.Resource); foreach (var source in sources) { @@ -259,7 +260,7 @@ private static ILanguageBlock[] GetYamlLanguageBlocks(Source[] sources, IResourc if (file.Type != SourceType.Yaml) continue; - context.Writer?.VerboseRuleDiscovery(path: file.Path); + logger.VerboseRuleDiscovery(path: file.Path); context.EnterLanguageScope(file); try { @@ -275,7 +276,7 @@ private static ILanguageBlock[] GetYamlLanguageBlocks(Source[] sources, IResourc } else if (item != null && item.Block == null) { - context.Writer?.LogUnknownResourceKind(item.Kind, item.ApiVersion, file); + logger.LogUnknownResourceKind(item.Kind, item.ApiVersion, file); } } } @@ -288,7 +289,7 @@ private static ILanguageBlock[] GetYamlLanguageBlocks(Source[] sources, IResourc } finally { - context.Writer?.ExitScope(); + // logger.ExitScope(); context.PopScope(RunspaceScope.Resource); } return result.Count == 0 ? [] : [.. result]; @@ -299,8 +300,10 @@ private static ILanguageBlock[] GetYamlLanguageBlocks(Source[] sources, IResourc /// private static ILanguageBlock[] GetJsonLanguageBlocks(Source[] sources, IResourceDiscoveryContext context) { + // TODO: Add scope wrapping for discovery. + var logger = context.Logger; var result = new Collection(); - var visitor = new ResourceValidator(context.Writer); + var visitor = new ResourceValidator(logger); var deserializer = new JsonSerializer { DateTimeZoneHandling = DateTimeZoneHandling.Utc @@ -314,7 +317,7 @@ private static ILanguageBlock[] GetJsonLanguageBlocks(Source[] sources, IResourc try { - context.Writer?.EnterScope("[Discovery.Resource]"); + // logger.EnterScope("[Discovery.Resource]"); context.PushScope(RunspaceScope.Resource); foreach (var source in sources) @@ -324,7 +327,7 @@ private static ILanguageBlock[] GetJsonLanguageBlocks(Source[] sources, IResourc if (file.Type != SourceType.Json) continue; - context.Writer?.VerboseRuleDiscovery(file.Path); + logger.VerboseRuleDiscovery(file.Path); context.EnterLanguageScope(file); try { @@ -344,7 +347,7 @@ private static ILanguageBlock[] GetJsonLanguageBlocks(Source[] sources, IResourc } else if (item != null && item.Block == null) { - context.Writer?.LogUnknownResourceKind(item.Kind, item.ApiVersion, file); + logger.LogUnknownResourceKind(item.Kind, item.ApiVersion, file); } // Consume all end objects at the end of each resource @@ -361,7 +364,7 @@ private static ILanguageBlock[] GetJsonLanguageBlocks(Source[] sources, IResourc } finally { - context.Writer?.ExitScope(); + // logger.ExitScope(); context.PopScope(RunspaceScope.Resource); } return result.Count == 0 ? [] : [.. result]; diff --git a/src/PSRule/Host/HostState.cs b/src/PSRule/Host/HostState.cs index d5d5b4b0e1..4f07f0f8ad 100644 --- a/src/PSRule/Host/HostState.cs +++ b/src/PSRule/Host/HostState.cs @@ -12,8 +12,8 @@ internal static class HostState /// /// Define language commands. /// - private static readonly SessionStateCmdletEntry[] BuiltInCmdlets = new SessionStateCmdletEntry[] - { + private static readonly SessionStateCmdletEntry[] BuiltInCmdlets = + [ new("New-RuleDefinition", typeof(NewRuleDefinitionCommand), null), new("Export-PSRuleConvention", typeof(ExportConventionCommand), null), new("Write-Recommendation", typeof(WriteRecommendationCommand), null), @@ -24,13 +24,13 @@ internal static class HostState new("Assert-TypeOf", typeof(AssertTypeOfCommand), null), new("Assert-AllOf", typeof(AssertAllOfCommand), null), new("Assert-AnyOf", typeof(AssertAnyOfCommand), null), - }; + ]; /// /// Define language aliases. /// - private static readonly SessionStateAliasEntry[] BuiltInAliases = new SessionStateAliasEntry[] - { + private static readonly SessionStateAliasEntry[] BuiltInAliases = + [ new(LanguageKeywords.Rule, "New-RuleDefinition", string.Empty, ScopedItemOptions.ReadOnly), new(LanguageKeywords.Recommend, "Write-Recommendation", string.Empty, ScopedItemOptions.ReadOnly), new(LanguageKeywords.Reason, "Write-Reason", string.Empty, ScopedItemOptions.ReadOnly), @@ -40,7 +40,7 @@ internal static class HostState new(LanguageKeywords.TypeOf, "Assert-TypeOf", string.Empty, ScopedItemOptions.ReadOnly), new(LanguageKeywords.AllOf, "Assert-AllOf", string.Empty, ScopedItemOptions.ReadOnly), new(LanguageKeywords.AnyOf, "Assert-AnyOf", string.Empty, ScopedItemOptions.ReadOnly), - }; + ]; /// /// Create a default session state. diff --git a/src/PSRule/Host/RuleLanguageAst.cs b/src/PSRule/Host/RuleLanguageAst.cs index 57833bc8f1..1217f953f8 100644 --- a/src/PSRule/Host/RuleLanguageAst.cs +++ b/src/PSRule/Host/RuleLanguageAst.cs @@ -4,11 +4,11 @@ using System.Management.Automation; using System.Management.Automation.Language; using PSRule.Definitions; -using PSRule.Resources; +using PSRule.Runtime; namespace PSRule.Host; -internal sealed class RuleLanguageAst : AstVisitor +internal sealed class RuleLanguageAst(ILogger? logger) : AstVisitor { private const string PARAMETER_NAME = "Name"; private const string PARAMETER_REF = "Ref"; @@ -16,19 +16,11 @@ internal sealed class RuleLanguageAst : AstVisitor private const string PARAMETER_BODY = "Body"; private const string PARAMETER_ERRORACTION = "ErrorAction"; private const string RULE_KEYWORD = "Rule"; - private const string ERRORID_PARAMETERNOTFOUND = "PSRule.Parse.RuleParameterNotFound"; - private const string ERRORID_INVALIDRULENESTING = "PSRule.Parse.InvalidRuleNesting"; - private const string ERRORID_INVALIDERRORACTION = "PSRule.Parse.InvalidErrorAction"; - private const string ERRORID_INVALIDRESOURCENAME = "PSRule.Parse.InvalidResourceName"; - private readonly StringComparer _Comparer; + private readonly StringComparer _Comparer = StringComparer.OrdinalIgnoreCase; + private readonly ILogger? _Logger = logger; - internal List Errors; - - internal RuleLanguageAst() - { - _Comparer = StringComparer.OrdinalIgnoreCase; - } + public bool HadErrors { get; private set; } private sealed class ParameterBindResult { @@ -93,7 +85,8 @@ private bool VisitBodyParameter(CommandAst commandAst, ParameterBindResult bindR if (bindResult.Has(PARAMETER_BODY, 1, out ScriptBlockExpressionAst _)) return true; - ReportError(ERRORID_PARAMETERNOTFOUND, PSRuleResources.RuleParameterNotFound, PARAMETER_BODY, ReportExtent(commandAst.Extent)); + HadErrors = true; + _Logger?.LogRuleParameterNotFound(PARAMETER_BODY, ReportExtent(commandAst.Extent)); return false; } @@ -105,7 +98,8 @@ private bool VisitNameParameter(CommandAst commandAst, ParameterBindResult bindR if (bindResult.Has(PARAMETER_NAME, 0, out StringConstantExpressionAst value)) return IsNameValid(value); - ReportError(ERRORID_PARAMETERNOTFOUND, PSRuleResources.RuleParameterNotFound, PARAMETER_NAME, ReportExtent(commandAst.Extent)); + HadErrors = true; + _Logger?.LogRuleParameterNotFound(PARAMETER_NAME, ReportExtent(commandAst.Extent)); return false; } @@ -133,7 +127,8 @@ private bool IsNameValid(StringConstantExpressionAst name) if (ResourceValidator.IsNameValid(name.Value)) return true; - ReportError(ERRORID_INVALIDRESOURCENAME, PSRuleResources.InvalidResourceName, name.Value, ReportExtent(name.Extent)); + HadErrors = true; + _Logger?.LogInvalidResourceName(name.Value, ReportExtent(name.Extent)); return false; } @@ -154,7 +149,8 @@ private bool NotNested(CommandAst commandAst) if (GetParentBlock(commandAst)?.Parent == null) return true; - ReportError(ERRORID_INVALIDRULENESTING, PSRuleResources.InvalidRuleNesting, ReportExtent(commandAst.Extent)); + HadErrors = true; + _Logger?.LogInvalidRuleNesting(ReportExtent(commandAst.Extent)); return false; } @@ -169,7 +165,8 @@ private bool VisitErrorAction(CommandAst commandAst, ParameterBindResult bindRes if (!Enum.TryParse(value.Value, out ActionPreference result) || (result == ActionPreference.Ignore || result == ActionPreference.Stop)) return true; - ReportError(ERRORID_INVALIDERRORACTION, PSRuleResources.InvalidErrorAction, value.Value, ReportExtent(commandAst.Extent)); + HadErrors = true; + _Logger?.LogInvalidErrorAction(value.Value, ReportExtent(commandAst.Extent)); return false; } @@ -209,27 +206,6 @@ private static ParameterBindResult BindParameters(CommandAst commandAst) return result; } - private void ReportError(string errorId, string message, params object[] args) - { - ReportError(new Pipeline.ParseException( - message: string.Format(Thread.CurrentThread.CurrentCulture, message, args), - errorId: errorId - )); - } - - private void ReportError(Pipeline.ParseException exception) - { - if (Errors == null) - Errors = new List(); - - Errors.Add(new ErrorRecord( - exception: exception, - errorId: exception.ErrorId, - errorCategory: ErrorCategory.InvalidOperation, - targetObject: null - )); - } - private static string ReportExtent(IScriptExtent extent) { return string.Concat(extent.File, " line ", extent.StartLineNumber); diff --git a/src/PSRule/PSRule.psm1 b/src/PSRule/PSRule.psm1 index e9bcfd6be0..56823121d6 100644 --- a/src/PSRule/PSRule.psm1 +++ b/src/PSRule/PSRule.psm1 @@ -1262,22 +1262,6 @@ function New-PSRuleOption { [Parameter(Mandatory = $False)] [String[]]$InputPathIgnore = '', - # Sets the Logging.LimitDebug option - [Parameter(Mandatory = $False)] - [String[]]$LoggingLimitDebug = $Null, - - # Sets the Logging.LimitVerbose option - [Parameter(Mandatory = $False)] - [String[]]$LoggingLimitVerbose = $Null, - - # Sets the Logging.RuleFail option - [Parameter(Mandatory = $False)] - [PSRule.Configuration.OutcomeLogStream]$LoggingRuleFail = 'None', - - # Sets the Logging.RulePass option - [Parameter(Mandatory = $False)] - [PSRule.Configuration.OutcomeLogStream]$LoggingRulePass = 'None', - # Sets the Output.As option [Parameter(Mandatory = $False)] [ValidateSet('Detail', 'Summary')] @@ -1567,22 +1551,6 @@ function Set-PSRuleOption { [Parameter(Mandatory = $False)] [String[]]$InputTargetType, - # Sets the Logging.LimitDebug option - [Parameter(Mandatory = $False)] - [String[]]$LoggingLimitDebug = $Null, - - # Sets the Logging.LimitVerbose option - [Parameter(Mandatory = $False)] - [String[]]$LoggingLimitVerbose = $Null, - - # Sets the Logging.RuleFail option - [Parameter(Mandatory = $False)] - [PSRule.Configuration.OutcomeLogStream]$LoggingRuleFail = 'None', - - # Sets the Logging.RulePass option - [Parameter(Mandatory = $False)] - [PSRule.Configuration.OutcomeLogStream]$LoggingRulePass = 'None', - # Sets the Output.As option [Parameter(Mandatory = $False)] [ValidateSet('Detail', 'Summary')] @@ -2333,22 +2301,6 @@ function SetOptions { [Parameter(Mandatory = $False)] [String[]]$InputTargetType, - # Sets the Logging.LimitDebug option - [Parameter(Mandatory = $False)] - [String[]]$LoggingLimitDebug = $Null, - - # Sets the Logging.LimitVerbose option - [Parameter(Mandatory = $False)] - [String[]]$LoggingLimitVerbose = $Null, - - # Sets the Logging.RuleFail option - [Parameter(Mandatory = $False)] - [PSRule.Configuration.OutcomeLogStream]$LoggingRuleFail = 'None', - - # Sets the Logging.RulePass option - [Parameter(Mandatory = $False)] - [PSRule.Configuration.OutcomeLogStream]$LoggingRulePass = 'None', - # Sets the Output.As option [Parameter(Mandatory = $False)] [ValidateSet('Detail', 'Summary')] @@ -2581,26 +2533,6 @@ function SetOptions { $Option.Input.TargetType = $InputTargetType; } - # Sets option Logging.LimitDebug - if ($PSBoundParameters.ContainsKey('LoggingLimitDebug')) { - $Option.Logging.LimitDebug = $LoggingLimitDebug; - } - - # Sets option Logging.LimitVerbose - if ($PSBoundParameters.ContainsKey('LoggingLimitVerbose')) { - $Option.Logging.LimitVerbose = $LoggingLimitVerbose; - } - - # Sets option Logging.RuleFail - if ($PSBoundParameters.ContainsKey('LoggingRuleFail')) { - $Option.Logging.RuleFail = $LoggingRuleFail; - } - - # Sets option Logging.RulePass - if ($PSBoundParameters.ContainsKey('LoggingRulePass')) { - $Option.Logging.RulePass = $LoggingRulePass; - } - # Sets option Output.As if ($PSBoundParameters.ContainsKey('OutputAs')) { $Option.Output.As = $OutputAs; diff --git a/src/PSRule/Pipeline/AssertPipelineBuilder.cs b/src/PSRule/Pipeline/AssertPipelineBuilder.cs index 86a60a6455..4263aa742f 100644 --- a/src/PSRule/Pipeline/AssertPipelineBuilder.cs +++ b/src/PSRule/Pipeline/AssertPipelineBuilder.cs @@ -1,12 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Management.Automation; using PSRule.Configuration; -using PSRule.Definitions.Rules; -using PSRule.Pipeline.Formatters; using PSRule.Pipeline.Output; -using PSRule.Resources; using PSRule.Rules; namespace PSRule.Pipeline; @@ -21,144 +17,6 @@ internal sealed class AssertPipelineBuilder : InvokePipelineBuilderBase internal AssertPipelineBuilder(Source[] source, IHostContext hostContext) : base(source, hostContext) { } - /// - /// A writer for outputting assertions. - /// - private sealed class AssertWriter : PipelineWriter - { - internal readonly IAssertFormatter _Formatter; - private readonly PipelineWriter _InnerWriter; - private readonly string _ResultVariableName; - private readonly IHostContext _HostContext; - private readonly List _Results; - private int _ErrorCount; - private int _FailCount; - private int _TotalCount; - private bool _PSError; - private SeverityLevel _Level; - - internal AssertWriter(PSRuleOption option, Source[] source, PipelineWriter inner, PipelineWriter next, OutputStyle style, string resultVariableName, IHostContext hostContext) - : base(inner, option, hostContext.ShouldProcess) - { - _InnerWriter = next; - _ResultVariableName = resultVariableName; - _HostContext = hostContext; - if (!string.IsNullOrEmpty(resultVariableName)) - _Results = []; - - _Formatter = GetFormatter(style, source, inner, option); - } - - private static IAssertFormatter GetFormatter(OutputStyle style, Source[] source, PipelineWriter inner, PSRuleOption option) - { - if (style == OutputStyle.AzurePipelines) - return new AzurePipelinesFormatter(source, inner, option); - - if (style == OutputStyle.GitHubActions) - return new GitHubActionsFormatter(source, inner, option); - - if (style == OutputStyle.VisualStudioCode) - return new VisualStudioCodeFormatter(source, inner, option); - - return style == OutputStyle.Plain ? - new PlainFormatter(source, inner, option) : - new ClientFormatter(source, inner, option); - } - - public override void WriteObject(object sendToPipeline, bool enumerateCollection) - { - if (sendToPipeline is not InvokeResult result) - return; - - ProcessResult(result); - _InnerWriter?.WriteObject(sendToPipeline, enumerateCollection); - } - - public override void WriteWarning(string message) - { - var warningPreference = _HostContext.GetPreferenceVariable(WarningPreference); - if (warningPreference == ActionPreference.Ignore || warningPreference == ActionPreference.SilentlyContinue) - return; - - _Formatter.Warning(new WarningRecord(message)); - } - - public override void WriteError(ErrorRecord errorRecord) - { - HadErrors = true; - var errorPreference = _HostContext.GetPreferenceVariable(ErrorPreference); - if (errorPreference == ActionPreference.Ignore || errorPreference == ActionPreference.SilentlyContinue) - return; - - _PSError = true; - _Formatter.Error(errorRecord); - } - - public override void Begin() - { - base.Begin(); - _Formatter.Begin(); - } - - public override void End(IPipelineResult result) - { - _Formatter.End(_TotalCount, _FailCount, _ErrorCount); - base.End(result); - try - { - if (_ErrorCount > 0) - { - HadErrors = true; - base.WriteError(new ErrorRecord( - new FailPipelineException(PSRuleResources.RuleErrorPipelineException), - "PSRule.Error", - ErrorCategory.InvalidOperation, - null)); - } - else if (result.ShouldBreakFromFailure) - { - HadFailures = true; - base.WriteError(new ErrorRecord( - new FailPipelineException(PSRuleResources.RuleFailPipelineException), - "PSRule.Fail", - ErrorCategory.InvalidData, - null)); - } - else if (_PSError) - { - HadErrors = true; - base.WriteError(new ErrorRecord( - new FailPipelineException(PSRuleResources.ErrorPipelineException), - "PSRule.Error", - ErrorCategory.InvalidOperation, - null)); - } - else if (_FailCount > 0) - { - HadFailures = true; - base.WriteHost(new HostInformationMessage() { Message = PSRuleResources.RuleFailPipelineException }); - } - - if (_Results != null && _HostContext != null) - _HostContext.SetVariable(_ResultVariableName, _Results.ToArray()); - } - finally - { - _InnerWriter?.End(result); - } - } - - private void ProcessResult(InvokeResult result) - { - _Formatter.Result(result); - _FailCount += result.Fail; - _ErrorCount += result.Error; - _TotalCount += result.Total; - _Level = _Level.GetWorstCase(result.Level); - _Results?.AddRange(result.AsRecord()); - } - } - protected override PipelineWriter PrepareWriter() { return GetWriter(); diff --git a/src/PSRule/Pipeline/AssertWriter.cs b/src/PSRule/Pipeline/AssertWriter.cs new file mode 100644 index 0000000000..f60de65218 --- /dev/null +++ b/src/PSRule/Pipeline/AssertWriter.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; +using PSRule.Configuration; +using PSRule.Definitions.Rules; +using PSRule.Pipeline.Formatters; +using PSRule.Resources; +using PSRule.Rules; +using PSRule.Runtime; + +namespace PSRule.Pipeline; + +/// +/// A writer for an Assert pipeline that formats results as text. +/// +internal sealed class AssertWriter : PipelineWriter +{ + internal readonly IAssertFormatter _Formatter; + private readonly PipelineWriter _InnerWriter; + private readonly string _ResultVariableName; + private readonly IHostContext _HostContext; + private readonly List _Results; + private int _ErrorCount; + private int _FailCount; + private int _TotalCount; + private bool _HadErrors; + private bool _HadFailures; + private bool _PSError; + private SeverityLevel _Level; + + internal AssertWriter(PSRuleOption option, Source[] source, PipelineWriter inner, PipelineWriter next, OutputStyle style, string resultVariableName, IHostContext hostContext) + : base(inner, option, hostContext.ShouldProcess) + { + _InnerWriter = next; + _ResultVariableName = resultVariableName; + _HostContext = hostContext; + if (!string.IsNullOrEmpty(resultVariableName)) + _Results = []; + + _Formatter = GetFormatter(style, source, inner, option); + } + + public override bool HadErrors => _HadErrors || base.HadErrors; + + public override bool HadFailures => _HadFailures || base.HadFailures; + + private static IAssertFormatter GetFormatter(OutputStyle style, Source[] source, PipelineWriter inner, PSRuleOption option) + { + if (style == OutputStyle.AzurePipelines) + return new AzurePipelinesFormatter(source, inner, option); + + if (style == OutputStyle.GitHubActions) + return new GitHubActionsFormatter(source, inner, option); + + if (style == OutputStyle.VisualStudioCode) + return new VisualStudioCodeFormatter(source, inner, option); + + return style == OutputStyle.Plain ? + new PlainFormatter(source, inner, option) : + new ClientFormatter(source, inner, option); + } + + public override void WriteObject(object sendToPipeline, bool enumerateCollection) + { + if (sendToPipeline is not InvokeResult result) + return; + + ProcessResult(result); + _InnerWriter?.WriteObject(sendToPipeline, enumerateCollection); + } + + public override void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (logLevel == LogLevel.Error || logLevel == LogLevel.Critical) + _HadErrors = true; + + if (!IsEnabled(logLevel)) + return; + + switch (logLevel) + { + case LogLevel.Trace: + case LogLevel.Debug: + case LogLevel.Information: + base.Log(logLevel, eventId, state, exception, formatter); + break; + + case LogLevel.Warning: + _Formatter.Warning(new WarningRecord(formatter(state, exception))); + break; + + case LogLevel.Error: + case LogLevel.Critical: + _PSError = true; + var exitCode = eventId.Id == 0 ? 1 : eventId.Id; + SetExitCode(exitCode); + + _Formatter.Error(new ErrorRecord(exception, "PSRule.Error", ErrorCategory.InvalidOperation, null)); + break; + } + } + + public override void Begin() + { + base.Begin(); + _Formatter.Begin(); + } + + public override void End(IPipelineResult result) + { + _Formatter.End(_TotalCount, _FailCount, _ErrorCount); + base.End(result); + try + { + if (_ErrorCount > 0) + { + _HadErrors = true; + base.Log(LogLevel.Error, new EventId(0, "PSRule.Error"), ErrorCategory.InvalidOperation, new FailPipelineException(PSRuleResources.RuleErrorPipelineException), (s, e) => e!.Message); + } + else if (result.ShouldBreakFromFailure) + { + _HadFailures = true; + base.Log(LogLevel.Error, new EventId(0, "PSRule.Fail"), ErrorCategory.InvalidData, new FailPipelineException(PSRuleResources.RuleFailPipelineException), (s, e) => e!.Message); + } + else if (_PSError) + { + _HadErrors = true; + base.Log(LogLevel.Error, new EventId(0, "PSRule.Error"), ErrorCategory.InvalidOperation, new FailPipelineException(PSRuleResources.ErrorPipelineException), (s, e) => e!.Message); + } + else if (_FailCount > 0) + { + _HadFailures = true; + base.WriteHost(new HostInformationMessage() { Message = PSRuleResources.RuleFailPipelineException }); + } + + if (_Results != null && _HostContext != null) + _HostContext.SetVariable(_ResultVariableName, _Results.ToArray()); + } + finally + { + _InnerWriter?.End(result); + } + } + + private void ProcessResult(InvokeResult result) + { + _Formatter.Result(result); + _FailCount += result.Fail; + _ErrorCount += result.Error; + _TotalCount += result.Total; + _Level = _Level.GetWorstCase(result.Level); + _Results?.AddRange(result.AsRecord()); + } +} diff --git a/src/PSRule/Pipeline/Exceptions.cs b/src/PSRule/Pipeline/Exceptions.cs index 15bcd0aef6..28ce61052e 100644 --- a/src/PSRule/Pipeline/Exceptions.cs +++ b/src/PSRule/Pipeline/Exceptions.cs @@ -144,46 +144,20 @@ public ParseException(string message, Exception innerException) /// /// Creates a parser exception. /// - /// The detail of the exception. - /// A unique identifier related to the exception. - internal ParseException(string message, string errorId) : base(message) - { - ErrorId = errorId; - } + internal ParseException(EventId eventId, string message) + : base(eventId, message) { } /// /// Creates a parser exception. /// - /// The detail of the exception. - /// A unique identifier related to the exception. - /// A nested exception that caused the issue. - internal ParseException(string message, string errorId, Exception innerException) : base(message, innerException) - { - ErrorId = errorId; - } + internal ParseException(EventId eventId, string message, Exception innerException) + : base(eventId, message, innerException) { } /// /// Creates a parser exception. /// - private ParseException(SerializationInfo info, StreamingContext context) : base(info, context) - { - ErrorId = info.GetString("ErrorId"); - } - - /// - /// An associated identifier related to why the exception was thrown. - /// - public string ErrorId { get; } - - /// - [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - if (info == null) throw new ArgumentNullException(nameof(info)); - - info.AddValue("ErrorId", ErrorId); - base.GetObjectData(info, context); - } + private ParseException(SerializationInfo info, StreamingContext context) + : base(info, context) { } } /// @@ -232,14 +206,13 @@ private RuleException(SerializationInfo info, StreamingContext context) /// /// An associated error record related to the exception. /// - public ErrorRecord ErrorRecord { get; } + public ErrorRecord? ErrorRecord { get; } /// [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] public override void GetObjectData(SerializationInfo info, StreamingContext context) { - if (info == null) - throw new ArgumentNullException(nameof(info)); + if (info == null) throw new ArgumentNullException(nameof(info)); base.GetObjectData(info, context); } @@ -441,41 +414,3 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont base.GetObjectData(info, context); } } - -/// -/// An exception thrown by PSRule when a runtime property or method is used outside of the intended scope. -/// Avoid using runtime variables outside of a PSRule pipeline. -/// -[Serializable] -public sealed class RuntimeScopeException : PipelineException -{ - /// - public RuntimeScopeException() - { - } - - /// - public RuntimeScopeException(string message) : base(message) - { - } - - /// - public RuntimeScopeException(string message, Exception innerException) : base(message, innerException) - { - } - - /// - private RuntimeScopeException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } - - /// - [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - if (info == null) - throw new ArgumentNullException(nameof(info)); - - base.GetObjectData(info, context); - } -} diff --git a/src/PSRule/Pipeline/Formatters/AssertFormatterBase.cs b/src/PSRule/Pipeline/Formatters/AssertFormatterBase.cs index 0824ed3c5e..c5f993eec7 100644 --- a/src/PSRule/Pipeline/Formatters/AssertFormatterBase.cs +++ b/src/PSRule/Pipeline/Formatters/AssertFormatterBase.cs @@ -6,6 +6,7 @@ using PSRule.Definitions; using PSRule.Resources; using PSRule.Rules; +using PSRule.Runtime; namespace PSRule.Pipeline.Formatters; @@ -89,17 +90,17 @@ protected sealed override void DoWriteWarning(string message) protected sealed override void DoWriteVerbose(string message) { - Writer.WriteVerbose(message); + Writer.LogVerbose(EventId.None, message); } protected sealed override void DoWriteInformation(InformationRecord informationRecord) { - Writer.WriteInformation(informationRecord); + Writer.LogInformation(EventId.None, informationRecord.MessageData.ToString()); } protected sealed override void DoWriteDebug(DebugRecord debugRecord) { - Writer.WriteDebug(debugRecord); + Writer.LogDebug(EventId.None, debugRecord.Message); } protected sealed override void DoWriteObject(object sendToPipeline, bool enumerateCollection) diff --git a/src/PSRule/Pipeline/HostContext.cs b/src/PSRule/Pipeline/HostContext.cs index 0450e74b49..d4b2212b8f 100644 --- a/src/PSRule/Pipeline/HostContext.cs +++ b/src/PSRule/Pipeline/HostContext.cs @@ -3,36 +3,53 @@ using System.Management.Automation; using PSRule.Definitions; +using PSRule.Runtime; namespace PSRule.Pipeline; #nullable enable /// -/// A base class for custom host context instances. +/// A base class for host context instances, by default this is a no-op implementation. /// public abstract class HostContext : IHostContext { - private const string ErrorPreference = "ErrorActionPreference"; - private const string WarningPreference = "WarningPreference"; + /// + /// A preference variable for error handling action. + /// + protected const string ErrorPreference = "ErrorActionPreference"; - /// - public virtual bool InSession => false; + /// + /// A preference variable for warning handling action. + /// + protected const string WarningPreference = "WarningPreference"; + + /// + /// A preference variable for verbose handling action. + /// + protected const string VerbosePreference = "VerbosePreference"; + + /// + /// A preference variable for information handling action. + /// + protected const string InformationPreference = "InformationPreference"; + + /// + /// A preference variable for debug handling action. + /// + protected const string DebugPreference = "DebugPreference"; /// - public virtual bool HadErrors { get; protected set; } + public virtual string? CachePath => null; /// - public virtual void Debug(string text) - { + public int ExitCode { get; private set; } - } + /// + public virtual bool InSession => false; /// - public virtual void Error(ErrorRecord errorRecord) - { - HadErrors = true; - } + public virtual bool HadErrors { get; private set; } /// public virtual ActionPreference GetPreferenceVariable(string variableName) @@ -48,13 +65,7 @@ public virtual ActionPreference GetPreferenceVariable(string variableName) } /// - public virtual void Information(InformationRecord informationRecord) - { - - } - - /// - public virtual void Object(object sendToPipeline, bool enumerateCollection) + public virtual void WriteObject(object sendToPipeline, bool enumerateCollection) { if (sendToPipeline is IResultRecord record) Record(record); @@ -63,25 +74,19 @@ public virtual void Object(object sendToPipeline, bool enumerateCollection) } /// - public virtual void SetVariable(string variableName, T value) + public virtual void WriteHost(string message, ConsoleColor? backgroundColor = null, ConsoleColor? foregroundColor = null, bool? noNewLine = null) { } /// - public abstract bool ShouldProcess(string target, string action); - - /// - public virtual void Verbose(string text) + public virtual void SetVariable(string variableName, T value) { } /// - public virtual void Warning(string text) - { - - } + public abstract bool ShouldProcess(string target, string action); /// /// Handle record objects emitted from the pipeline. @@ -98,7 +103,25 @@ public virtual string GetWorkingPath() } /// - public virtual string? CachePath => null; + public virtual bool IsEnabled(LogLevel logLevel) + { + return false; + } + + /// + public virtual void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (logLevel == LogLevel.Error || logLevel == LogLevel.Critical) + HadErrors = true; + } + + /// + public virtual void SetExitCode(int exitCode) + { + if (exitCode == 0) return; + + ExitCode = exitCode; + } } #nullable restore diff --git a/src/PSRule/Pipeline/IHostContext.cs b/src/PSRule/Pipeline/IHostContext.cs index 40154e2225..86032653ed 100644 --- a/src/PSRule/Pipeline/IHostContext.cs +++ b/src/PSRule/Pipeline/IHostContext.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Management.Automation; +using PSRule.Runtime; namespace PSRule.Pipeline; @@ -10,8 +11,19 @@ namespace PSRule.Pipeline; /// /// A host context for handling input and output emitted from the pipeline. /// -public interface IHostContext +public interface IHostContext : ILogger { + /// + /// Configures the root path to use for caching artifacts including modules. + /// Each artifact is in a subdirectory of the root path. + /// + string? CachePath { get; } + + /// + /// Get the last exit code. + /// + int ExitCode { get; } + /// /// Determines if the pipeline is executing in a remote PowerShell session. /// @@ -40,34 +52,16 @@ public interface IHostContext void SetVariable(string variableName, T value); /// - /// Handle an error reported by the pipeline. - /// - void Error(ErrorRecord errorRecord); - - /// - /// Handle a warning reported by the pipeline. + /// Write an object to output. /// - void Warning(string text); + /// The object to write to output. + /// Determines when the object is enumerable if it should be enumerated as more then one object. + void WriteObject(object o, bool enumerateCollection); /// - /// Handle an informational record reported by the pipeline. + /// Write a message to the host. /// - void Information(InformationRecord informationRecord); - - /// - /// Handle a verbose message reported by the pipeline. - /// - void Verbose(string text); - - /// - /// Handle a debug message reported by the pipeline. - /// - void Debug(string text); - - /// - /// Handle an object emitted from the pipeline. - /// - void Object(object sendToPipeline, bool enumerateCollection); + void WriteHost(string message, ConsoleColor? backgroundColor = null, ConsoleColor? foregroundColor = null, bool? noNewLine = null); /// /// Determines if a destructive action such as overwriting a file should be processed. @@ -77,13 +71,13 @@ public interface IHostContext /// /// Get the current working path. /// - string GetWorkingPath(); + string GetWorkingPath(); /// - /// Configures the root path to use for caching artifacts including modules. - /// Each artifact is in a subdirectory of the root path. + /// Set the terminating exit code of the pipeline. /// - string? CachePath { get; } + /// The numerical exit code. + void SetExitCode(int exitCode); } #nullable restore diff --git a/src/PSRule/Pipeline/IPipelineWriter.cs b/src/PSRule/Pipeline/IPipelineWriter.cs index ae87a617d9..a311e898b0 100644 --- a/src/PSRule/Pipeline/IPipelineWriter.cs +++ b/src/PSRule/Pipeline/IPipelineWriter.cs @@ -11,6 +11,11 @@ namespace PSRule.Pipeline; /// public interface IPipelineWriter : IDisposable, ILogger { + /// + /// Get the last exit code. + /// + int ExitCode { get; } + /// /// Determines if any errors were reported. /// @@ -21,77 +26,22 @@ public interface IPipelineWriter : IDisposable, ILogger /// bool HadFailures { get; } - /// - /// Write a verbose message. - /// - void WriteVerbose(string message); - - /// - /// Determines if a verbose message should be written to output. - /// - bool ShouldWriteVerbose(); - - /// - /// Write a warning message. - /// - void WriteWarning(string message); - - /// - /// Determines if a warning message should be written to output. - /// - bool ShouldWriteWarning(); - - /// - /// Write an error message. - /// - void WriteError(ErrorRecord errorRecord); - - /// - /// Determines if an error message should be written to output. - /// - bool ShouldWriteError(); - - /// - /// Write an informational message. - /// - void WriteInformation(InformationRecord informationRecord); - /// /// Write a message to the host process. /// void WriteHost(HostInformationMessage info); - /// - /// Determines if an informational message should be written to output. - /// - bool ShouldWriteInformation(); - - /// - /// Write a debug message. - /// - void WriteDebug(string text, params object[] args); - - /// - /// Determines if a debug message should be written to output. - /// - bool ShouldWriteDebug(); - /// /// Write an object to output. /// - /// The object to write to the pipeline. + /// The object to write to output. /// Determines when the object is enumerable if it should be enumerated as more then one object. - void WriteObject(object sendToPipeline, bool enumerateCollection); + void WriteObject(object o, bool enumerateCollection); /// - /// Enter a logging scope. + /// Write a result to the pipeline. /// - void EnterScope(string scopeName); - - /// - /// Exit a logging scope. - /// - void ExitScope(); + void WriteResult(InvokeResult result); /// /// Start and initialize the writer. @@ -102,4 +52,10 @@ public interface IPipelineWriter : IDisposable, ILogger /// Stop and finalized the writer. /// void End(IPipelineResult result); + + /// + /// Set the terminating exit code of the pipeline. + /// + /// The numerical exit code. + void SetExitCode(int exitCode); } diff --git a/src/PSRule/Pipeline/InvokePipelineBuilderBase.cs b/src/PSRule/Pipeline/InvokePipelineBuilderBase.cs index b693263428..b78ffadab7 100644 --- a/src/PSRule/Pipeline/InvokePipelineBuilderBase.cs +++ b/src/PSRule/Pipeline/InvokePipelineBuilderBase.cs @@ -74,11 +74,6 @@ public override IPipelineBuilder Configure(PSRuleOption option) base.Configure(option); - Option.Logging.RuleFail = option.Logging.RuleFail ?? LoggingOption.Default.RuleFail; - Option.Logging.RulePass = option.Logging.RulePass ?? LoggingOption.Default.RulePass; - Option.Logging.LimitVerbose = option.Logging.LimitVerbose; - Option.Logging.LimitDebug = option.Logging.LimitDebug; - Option.Output.As = option.Output.As ?? OutputOption.Default.As; Option.Output.Culture = GetCulture(option.Output.Culture); Option.Output.Encoding = option.Output.Encoding ?? OutputOption.Default.Encoding; diff --git a/src/PSRule/Pipeline/InvokeRulePipeline.cs b/src/PSRule/Pipeline/InvokeRulePipeline.cs index 003e6492a2..6598d2ea55 100644 --- a/src/PSRule/Pipeline/InvokeRulePipeline.cs +++ b/src/PSRule/Pipeline/InvokeRulePipeline.cs @@ -187,13 +187,11 @@ private InvokeResult InvokeRun(IRun run, TargetObject targetObject) if (ruleRecord.Outcome == RuleOutcome.Pass) { ruleBlockTarget.Pass(); - Context.Pass(); } else if (ruleRecord.Outcome == RuleOutcome.Fail) { Result.Fail(ruleRecord.Level); ruleBlockTarget.Fail(); - Context.Fail(); } else if (ruleRecord.Outcome == RuleOutcome.Error) { diff --git a/src/PSRule/Pipeline/OptionContext.cs b/src/PSRule/Pipeline/OptionContext.cs index 44f1e25f74..c67b7241d9 100644 --- a/src/PSRule/Pipeline/OptionContext.cs +++ b/src/PSRule/Pipeline/OptionContext.cs @@ -39,8 +39,6 @@ public ConventionOption? Convention public InputOption? Input { get; set; } - public LoggingOption? Logging { get; set; } - public OutputOption? Output { get; set; } public OverrideOption? Override { get; set; } diff --git a/src/PSRule/Pipeline/OptionContextBuilder.cs b/src/PSRule/Pipeline/OptionContextBuilder.cs index 8f8b8677f5..d2d8870e42 100644 --- a/src/PSRule/Pipeline/OptionContextBuilder.cs +++ b/src/PSRule/Pipeline/OptionContextBuilder.cs @@ -155,7 +155,6 @@ private static void Combine(OptionContext context, OptionScope optionScope) context.Execution = ExecutionOption.Combine(context.Execution, optionScope.Execution); context.Include = IncludeOption.Combine(context.Include, optionScope.Include); 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); diff --git a/src/PSRule/Pipeline/OptionScope.cs b/src/PSRule/Pipeline/OptionScope.cs index a5abc4eb9b..0c00aca8ec 100644 --- a/src/PSRule/Pipeline/OptionScope.cs +++ b/src/PSRule/Pipeline/OptionScope.cs @@ -65,8 +65,6 @@ private OptionScope(ScopeType type, string languageScope) public InputOption Input { get; set; } - public LoggingOption Logging { get; set; } - public OutputOption Output { get; set; } public OverrideOption Override { get; set; } @@ -109,7 +107,6 @@ public static OptionScope FromWorkspace(PSRuleOption option) Execution = option.Execution, Include = option.Include, Input = option.Input, - Logging = option.Logging, Output = option.Output, Override = option.Override, Repository = option.Repository, diff --git a/src/PSRule/Pipeline/Output/FileOutputWriter.cs b/src/PSRule/Pipeline/Output/FileOutputWriter.cs index 91bf4f195f..5fb5dd6e83 100644 --- a/src/PSRule/Pipeline/Output/FileOutputWriter.cs +++ b/src/PSRule/Pipeline/Output/FileOutputWriter.cs @@ -5,6 +5,7 @@ using System.Text; using PSRule.Configuration; using PSRule.Resources; +using PSRule.Runtime; namespace PSRule.Pipeline.Output; @@ -57,7 +58,7 @@ private void InfoOutputPath(string rootedPath) } else { - WriteInformation(new InformationRecord(message, Source)); + this.LogInformation(EventId.None, message); } } } diff --git a/src/PSRule/Pipeline/Output/HostPipelineWriter.cs b/src/PSRule/Pipeline/Output/HostPipelineWriter.cs index 1640d23080..5552701714 100644 --- a/src/PSRule/Pipeline/Output/HostPipelineWriter.cs +++ b/src/PSRule/Pipeline/Output/HostPipelineWriter.cs @@ -13,139 +13,38 @@ namespace PSRule.Pipeline.Output; /// internal sealed class HostPipelineWriter : PipelineWriter { - private const string Source = "PSRule"; - private const string HostTag = "PSHOST"; - - private Action _OnWriteWarning; - private Action _OnWriteVerbose; - private Action _OnWriteError; - private Action _OnWriteInformation; - private Action _OnWriteDebug; - internal Action OnWriteObject; - - private bool _LogError; - private bool _LogWarning; - private bool _LogVerbose; - private bool _LogInformation; - private bool _LogDebug; + private readonly IHostContext _HostContext; - private HashSet _VerboseFilter; - private HashSet _DebugFilter; + internal Action OnWriteObject; - private string _ScopeName; + private bool _HadErrors; + private bool _HadFailures; internal HostPipelineWriter(IHostContext hostContext, PSRuleOption option, ShouldProcess shouldProcess) : base(null, option, shouldProcess) { + _HostContext = hostContext; if (hostContext != null) { UseCommandRuntime(hostContext); - UseExecutionContext(hostContext); } } - public override void Begin() - { - if (Option.Logging.LimitVerbose != null && Option.Logging.LimitVerbose.Length > 0) - _VerboseFilter = new HashSet(Option.Logging.LimitVerbose); + public override bool HadErrors => _HadErrors || _HostContext.HadErrors; - if (Option.Logging.LimitDebug != null && Option.Logging.LimitDebug.Length > 0) - _DebugFilter = new HashSet(Option.Logging.LimitDebug); - } + public override bool HadFailures => _HadFailures; - private void UseCommandRuntime(IHostContext hostContext) + public override void Begin() { - if (hostContext == null) - return; - - _OnWriteVerbose = hostContext.Verbose; - _OnWriteWarning = hostContext.Warning; - _OnWriteError = hostContext.Error; - _OnWriteInformation = hostContext.Information; - _OnWriteDebug = hostContext.Debug; - OnWriteObject = hostContext.Object; + // Do nothing. } - private void UseExecutionContext(IHostContext hostContext) + private void UseCommandRuntime(IHostContext hostContext) { if (hostContext == null) return; - _LogError = GetPreferenceVariable(hostContext, ErrorPreference); - _LogWarning = GetPreferenceVariable(hostContext, WarningPreference); - _LogVerbose = GetPreferenceVariable(hostContext, VerbosePreference); - _LogInformation = GetPreferenceVariable(hostContext, InformationPreference); - _LogDebug = GetPreferenceVariable(hostContext, DebugPreference); - } - - private static bool GetPreferenceVariable(IHostContext hostContext, string variableName) - { - var preference = hostContext.GetPreferenceVariable(variableName); - return preference != ActionPreference.Ignore && !(preference == ActionPreference.SilentlyContinue && ( - variableName == VerbosePreference || - variableName == DebugPreference) - ); - } - - #region Internal logging methods - - /// - /// Core methods to hand off to logger. - /// - /// A valid PowerShell error record. - public override void WriteError(ErrorRecord errorRecord) - { - if (_OnWriteError == null || !ShouldWriteError()) - return; - - _OnWriteError(errorRecord); - } - - /// - /// Core method to hand off verbose messages to logger. - /// - /// A message to log. - public override void WriteVerbose(string message) - { - if (_OnWriteVerbose == null || !ShouldWriteVerbose()) - return; - - _OnWriteVerbose(message); - } - - /// - /// Core method to hand off warning messages to logger. - /// - /// A message to log - public override void WriteWarning(string message) - { - if (_OnWriteWarning == null || !ShouldWriteWarning()) - return; - - _OnWriteWarning(message); - } - - /// - /// Core method to hand off information messages to logger. - /// - public override void WriteInformation(InformationRecord informationRecord) - { - if (_OnWriteInformation == null || !ShouldWriteInformation()) - return; - - _OnWriteInformation(informationRecord); - } - - /// - /// Core method to hand off debug messages to logger. - /// - public override void WriteDebug(string text, params object[] args) - { - if (_OnWriteDebug == null || string.IsNullOrEmpty(text) || !ShouldWriteDebug()) - return; - - text = args == null || args.Length == 0 ? text : string.Format(Thread.CurrentThread.CurrentCulture, text, args); - _OnWriteDebug(text); + OnWriteObject = hostContext.WriteObject; } public override void WriteObject(object sendToPipeline, bool enumerateCollection) @@ -159,76 +58,39 @@ public override void WriteObject(object sendToPipeline, bool enumerateCollection OnWriteObject(sendToPipeline, enumerateCollection); } - public override void WriteHost(HostInformationMessage info) + public override void WriteResult(InvokeResult result) { - if (_OnWriteInformation == null) - return; + if (result == null) return; - var record = new InformationRecord(info, Source); - record.Tags.Add(HostTag); - _OnWriteInformation(record); + ProcessRecord(result.AsRecord()); } - public override bool ShouldWriteVerbose() - { - return _LogVerbose && (_VerboseFilter == null || _ScopeName == null || _VerboseFilter.Contains(_ScopeName)); - } - - public override bool ShouldWriteInformation() - { - return _LogInformation; - } - - public override bool ShouldWriteDebug() + public override void WriteHost(HostInformationMessage info) { - return _LogDebug && (_DebugFilter == null || _ScopeName == null || _DebugFilter.Contains(_ScopeName)); + _HostContext?.WriteHost(info.Message, info.BackgroundColor, info.ForegroundColor, info.NoNewLine); } - public override bool ShouldWriteError() + public override bool IsEnabled(LogLevel logLevel) { - return _LogError; + return _HostContext?.IsEnabled(logLevel) ?? false; } - public override bool ShouldWriteWarning() + public override void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { - return _LogWarning; - } + if (logLevel == LogLevel.Error || logLevel == LogLevel.Critical) + _HadErrors = true; - public override void EnterScope(string scopeName) - { - _ScopeName = scopeName; - } + if (!IsEnabled(logLevel) || _HostContext == null) + return; - public override void ExitScope() - { - _ScopeName = null; + _HostContext.Log(logLevel, eventId, state, exception, formatter); } - public override void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + public override void SetExitCode(int exitCode) { - if (logLevel == LogLevel.Error || logLevel == LogLevel.Critical) - HadErrors = true; - - if (logLevel == LogLevel.Error || logLevel == LogLevel.Critical) - { - WriteError(new ErrorRecord(exception, eventId.Id.ToString(), ErrorCategory.InvalidOperation, null)); - } - else if (logLevel == LogLevel.Warning) - { - WriteWarning(formatter(state, exception)); - } - else if (logLevel == LogLevel.Information) - { - WriteInformation(new InformationRecord(formatter(state, exception), null)); - } - else if (logLevel == LogLevel.Debug || logLevel == LogLevel.Trace) - { - WriteDebug(formatter(state, exception)); - } + _HostContext?.SetExitCode(exitCode); } - #endregion Internal logging methods - private void ProcessRecord(RuleRecord[] records) { if (records == null || records.Length == 0) diff --git a/src/PSRule/Pipeline/Output/WideOutputWriter.cs b/src/PSRule/Pipeline/Output/WideOutputWriter.cs index a377b1bc68..6c6e361461 100644 --- a/src/PSRule/Pipeline/Output/WideOutputWriter.cs +++ b/src/PSRule/Pipeline/Output/WideOutputWriter.cs @@ -31,6 +31,7 @@ private void WriteWideObject(IEnumerable collection) var o = PSObject.AsPSObject(item); o.TypeNames.Insert(0, typeName); base.WriteObject(o, false); + if (item is RuleRecord record) WriteErrorInfo(record); } diff --git a/src/PSRule/Pipeline/PSHostContext.cs b/src/PSRule/Pipeline/PSHostContext.cs index 122b99aa23..9c2f7c050e 100644 --- a/src/PSRule/Pipeline/PSHostContext.cs +++ b/src/PSRule/Pipeline/PSHostContext.cs @@ -2,24 +2,28 @@ // Licensed under the MIT License. using System.Management.Automation; +using PSRule.Runtime; namespace PSRule.Pipeline; #nullable enable /// -/// The host context used for PowerShell-based pipelines. +/// The host context used to wrap the parent PowerShell runtime when executing PowerShell-based cmdlet. /// public sealed class PSHostContext : IHostContext { - internal readonly PSCmdlet CmdletContext; - internal readonly EngineIntrinsics ExecutionContext; + private const string Source = "PSRule"; + private const string HostTag = "PSHOST"; - /// - public bool InSession { get; } + private const string ErrorPreference = "ErrorActionPreference"; + private const string WarningPreference = "WarningPreference"; + private const string VerbosePreference = "VerbosePreference"; + private const string InformationPreference = "InformationPreference"; + private const string DebugPreference = "DebugPreference"; - /// - public bool HadErrors { get; private set; } + internal readonly PSCmdlet CmdletContext; + internal readonly EngineIntrinsics ExecutionContext; /// /// Create an instance of a PowerShell-based host context. @@ -32,6 +36,18 @@ public PSHostContext(PSCmdlet commandRuntime, EngineIntrinsics executionContext) InSession = executionContext != null && executionContext.SessionState.PSVariable.GetValue("PSSenderInfo") != null; } + /// + public string? CachePath { get; } + + /// + public int ExitCode { get; private set; } + + /// + public bool InSession { get; } + + /// + public bool HadErrors { get; private set; } + /// public ActionPreference GetPreferenceVariable(string variableName) { @@ -41,7 +57,7 @@ public ActionPreference GetPreferenceVariable(string variableName) } /// - public T GetVariable(string variableName) + public T? GetVariable(string variableName) { return ExecutionContext == null ? default : (T)ExecutionContext.SessionState.PSVariable.GetValue(variableName); } @@ -59,50 +75,96 @@ public bool ShouldProcess(string target, string action) } /// - public void Error(ErrorRecord errorRecord) + public void WriteObject(object sendToPipeline, bool enumerateCollection) { - CmdletContext.WriteError(errorRecord); - HadErrors = true; + CmdletContext.WriteObject(sendToPipeline, enumerateCollection); } /// - public void Warning(string text) + public void WriteHost(string message, ConsoleColor? backgroundColor = null, ConsoleColor? foregroundColor = null, bool? noNewLine = null) { - CmdletContext.WriteWarning(text); + var record = new InformationRecord(new HostInformationMessage + { + Message = message, + BackgroundColor = backgroundColor, + ForegroundColor = foregroundColor, + NoNewLine = noNewLine + }, Source); + record.Tags.Add(HostTag); + + CmdletContext.WriteInformation(record); } /// - public void Information(InformationRecord informationRecord) + public string GetWorkingPath() { - CmdletContext.WriteInformation(informationRecord); + return ExecutionContext.SessionState.Path.CurrentFileSystemLocation.Path; } /// - public void Verbose(string text) + public bool IsEnabled(LogLevel logLevel) { - CmdletContext.WriteVerbose(text); + if (logLevel == LogLevel.None) + return false; + + var preference = logLevel switch + { + LogLevel.Trace => GetPreferenceVariable(VerbosePreference), + LogLevel.Debug => GetPreferenceVariable(DebugPreference), + LogLevel.Information => GetPreferenceVariable(InformationPreference), + LogLevel.Warning => GetPreferenceVariable(WarningPreference), + LogLevel.Error => GetPreferenceVariable(ErrorPreference), + _ => ActionPreference.SilentlyContinue + }; + + return preference != ActionPreference.Ignore && !(preference == ActionPreference.SilentlyContinue && ( + logLevel == LogLevel.Trace || + logLevel == LogLevel.Debug) + ); } /// - public void Debug(string text) + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { - CmdletContext.WriteDebug(text); + if (logLevel == LogLevel.Error || logLevel == LogLevel.Critical) + HadErrors = true; + + if (!IsEnabled(logLevel)) + return; + + switch (logLevel) + { + case LogLevel.Trace: + CmdletContext.WriteVerbose(formatter(state, exception)); + break; + + case LogLevel.Debug: + CmdletContext.WriteDebug(formatter(state, exception)); + break; + + case LogLevel.Information: + CmdletContext.WriteInformation(new InformationRecord(formatter(state, exception), null)); + break; + + case LogLevel.Warning: + CmdletContext.WriteWarning(formatter(state, exception)); + break; + + case LogLevel.Error: + case LogLevel.Critical: + var errorRecord = new ErrorRecord(exception, eventId.Name, ErrorCategory.NotSpecified, state); + CmdletContext.WriteError(errorRecord); + break; + } } /// - public void Object(object sendToPipeline, bool enumerateCollection) + public void SetExitCode(int exitCode) { - CmdletContext.WriteObject(sendToPipeline, enumerateCollection); - } + if (exitCode == 0) return; - /// - public string GetWorkingPath() - { - return ExecutionContext.SessionState.Path.CurrentFileSystemLocation.Path; + ExitCode = exitCode; } - - /// - public string? CachePath { get; } } #nullable restore diff --git a/src/PSRule/Pipeline/PathBuilder.cs b/src/PSRule/Pipeline/PathBuilder.cs index 17f27c84de..90c377a0ae 100644 --- a/src/PSRule/Pipeline/PathBuilder.cs +++ b/src/PSRule/Pipeline/PathBuilder.cs @@ -78,7 +78,7 @@ private void LogFilesDiagnostic(List files) foreach (var file in _Files) { - _Logger.LogDebug(new EventId(0), "Included file path: {0}", file.Path); + _Logger.LogDebug(EventId.None, "Included file path: {0}", file.Path); } } @@ -144,7 +144,7 @@ private void ErrorNotFound(string path) if (_Logger == null) return; - _Logger.WriteError(new ErrorRecord(new FileNotFoundException(), "PSRule.PathBuilder.ErrorNotFound", ErrorCategory.ObjectNotFound, path)); + _Logger.LogError(new ErrorRecord(new FileNotFoundException(), "PSRule.PathBuilder.ErrorNotFound", ErrorCategory.ObjectNotFound, path)); } /// diff --git a/src/PSRule/Pipeline/PipelineBuilderBase.cs b/src/PSRule/Pipeline/PipelineBuilderBase.cs index e5735ffabe..8257655099 100644 --- a/src/PSRule/Pipeline/PipelineBuilderBase.cs +++ b/src/PSRule/Pipeline/PipelineBuilderBase.cs @@ -326,7 +326,7 @@ protected ILanguageScopeSet GetLanguageScopeSet() throw new PipelineConfigurationException("Baseline.Group", string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.PSR0003, key)); var writer = PrepareWriter(); - writer.WriteVerbose($"Using baseline group '{key}': {baselines[0]}"); + writer.LogVerbose(EventId.None, "Using baseline group '{0}': {1}", key, baselines[0]); return baselines[0]; } @@ -530,7 +530,7 @@ protected bool TryChangedFiles(out string[]? files) for (var i = 0; i < files.Length; i++) { - HostContext.Verbose(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.UsingChangedFile, files[i])); + HostContext.LogVerbose(EventId.None, PSRuleResources.UsingChangedFile, files[i]); } return true; } diff --git a/src/PSRule/Pipeline/PipelineContext.cs b/src/PSRule/Pipeline/PipelineContext.cs index 4d02783fbc..960c6e91d9 100644 --- a/src/PSRule/Pipeline/PipelineContext.cs +++ b/src/PSRule/Pipeline/PipelineContext.cs @@ -190,9 +190,9 @@ private void ReportIssue(LegacyRunspaceContext runspaceContext) { foreach (var issue in ResourceCache.Issues) { - if (issue.Issue == ResourceIssueType.SuppressionGroupExpired) + if (issue.Type == ResourceIssueType.SuppressionGroupExpired) { - runspaceContext.SuppressionGroupExpired(issue.Resource.Id); + runspaceContext.SuppressionGroupExpired(issue.ResourceId); } } } diff --git a/src/PSRule/Pipeline/PipelineInputStream.cs b/src/PSRule/Pipeline/PipelineInputStream.cs index 04947cb624..ac21d32a73 100644 --- a/src/PSRule/Pipeline/PipelineInputStream.cs +++ b/src/PSRule/Pipeline/PipelineInputStream.cs @@ -61,7 +61,7 @@ public bool TryDequeue(out ITargetObject sourceObject) /// public void Open() { - _Logger?.LogDebug(new EventId(0), "Opening input stream."); + _Logger?.LogDebug(EventId.None, "Opening input stream."); if (_InputPath == null || _InputPath.Count == 0) return; @@ -69,7 +69,7 @@ public void Open() var files = _InputPath.Build(); for (var i = 0; i < files.Length; i++) { - _Logger?.LogDebug(new EventId(0), "opening with: {0}", files[i].Path); + _Logger?.LogDebug(EventId.None, "opening with: {0}", files[i].Path); EnqueueFile(files[i]); } } @@ -100,7 +100,7 @@ public void Add(string path) path = Environment.GetRootedPath(path, normalize: true); var basePath = Environment.GetRootedBasePath(null, normalize: true); - _Logger?.Log(LogLevel.Debug, new EventId(0), null, PSRuleResources.InputAdded, path); + _Logger?.Log(LogLevel.Debug, EventId.None, null, PSRuleResources.InputAdded, path); _InputPath.Add(path, useGlobalFilter: false); } diff --git a/src/PSRule/Pipeline/PipelineLogger.cs b/src/PSRule/Pipeline/PipelineLogger.cs index 234299a307..9de8d77ca3 100644 --- a/src/PSRule/Pipeline/PipelineLogger.cs +++ b/src/PSRule/Pipeline/PipelineLogger.cs @@ -51,11 +51,7 @@ internal void UseExecutionContext(EngineIntrinsics executionContext) internal void Configure(PSRuleOption option) { - if (option.Logging.LimitVerbose != null && option.Logging.LimitVerbose.Length > 0) - _VerboseFilter = new HashSet(option.Logging.LimitVerbose); - - if (option.Logging.LimitDebug != null && option.Logging.LimitDebug.Length > 0) - _DebugFilter = new HashSet(option.Logging.LimitDebug); + // Do nothing. } private static bool GetPreferenceVariable(EngineIntrinsics executionContext, string variableName) diff --git a/src/PSRule/Pipeline/PipelineLoggerBase.cs b/src/PSRule/Pipeline/PipelineLoggerBase.cs index 35f2879df0..2672aecde8 100644 --- a/src/PSRule/Pipeline/PipelineLoggerBase.cs +++ b/src/PSRule/Pipeline/PipelineLoggerBase.cs @@ -17,6 +17,8 @@ internal abstract class PipelineLoggerBase : IPipelineWriter public bool HadFailures { get; private set; } + public int ExitCode { get; private set; } + #region Logging public void WriteError(ErrorRecord errorRecord) @@ -81,14 +83,9 @@ public void WriteObject(object sendToPipeline, bool enumerateCollection) DoWriteObject(sendToPipeline, enumerateCollection); } - public void EnterScope(string scopeName) + public void WriteResult(InvokeResult result) { - ScopeName = scopeName; - } - public void ExitScope() - { - ScopeName = null; } #endregion Logging @@ -156,8 +153,6 @@ public void Dispose() protected abstract void DoWriteObject(object sendToPipeline, bool enumerateCollection); - #region ILogger - public virtual bool IsEnabled(LogLevel logLevel) { switch (logLevel) @@ -179,7 +174,7 @@ public virtual bool IsEnabled(LogLevel logLevel) return false; } - public virtual void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + public virtual void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { if (logLevel == LogLevel.Error || logLevel == LogLevel.Critical) HadErrors = true; @@ -202,5 +197,11 @@ public virtual void Log(LogLevel logLevel, EventId eventId, TState state } } - #endregion ILogger + /// + public void SetExitCode(int exitCode) + { + if (exitCode == 0) return; + + ExitCode = exitCode; + } } diff --git a/src/PSRule/Pipeline/PipelineWriter.cs b/src/PSRule/Pipeline/PipelineWriter.cs index 312db001ad..a9bca93296 100644 --- a/src/PSRule/Pipeline/PipelineWriter.cs +++ b/src/PSRule/Pipeline/PipelineWriter.cs @@ -12,7 +12,7 @@ namespace PSRule.Pipeline; #nullable enable /// -/// A base class for writers. +/// A base class for pipeline writers that passes through to an inner writer. /// internal abstract class PipelineWriter(IPipelineWriter? inner, PSRuleOption option, ShouldProcess shouldProcess) : IPipelineWriter { @@ -22,173 +22,55 @@ internal abstract class PipelineWriter(IPipelineWriter? inner, PSRuleOption opti protected const string InformationPreference = "InformationPreference"; protected const string DebugPreference = "DebugPreference"; - private readonly IPipelineWriter? _Writer = inner; + private readonly IPipelineWriter? _Inner = inner; private readonly ShouldProcess _ShouldProcess = shouldProcess; - protected readonly PSRuleOption Option = option; private bool _IsDisposed; - private bool _HadErrors; - private bool _HadFailures; - - bool IPipelineWriter.HadErrors => HadErrors; - bool IPipelineWriter.HadFailures => HadFailures; + /// + public int ExitCode => _Inner?.ExitCode ?? 0; /// - public virtual bool HadErrors - { - get - { - return _HadErrors || (_Writer != null && _Writer.HadErrors); - } - set - { - _HadErrors = value; - } - } + public virtual bool HadErrors => _Inner?.HadErrors ?? false; /// - public virtual bool HadFailures - { - get - { - return _HadFailures || (_Writer != null && _Writer.HadFailures); - } - set - { - _HadFailures = value; - } - } + public virtual bool HadFailures => _Inner?.HadFailures ?? false; /// public virtual void Begin() { - if (_Writer == null) - return; - - _Writer.Begin(); + _Inner?.Begin(); } /// public virtual void WriteObject(object sendToPipeline, bool enumerateCollection) { - if (_Writer == null || sendToPipeline == null) - return; - - _Writer.WriteObject(sendToPipeline, enumerateCollection); - } - - /// - public virtual void End(IPipelineResult result) - { - if (_Writer == null) - return; - - _Writer.End(result); - } - - /// - public virtual void WriteVerbose(string message) - { - if (_Writer == null || string.IsNullOrEmpty(message)) - return; - - _Writer.WriteVerbose(message); - } - - /// - public virtual bool ShouldWriteVerbose() - { - return _Writer != null && _Writer.ShouldWriteVerbose(); - } - - /// - public virtual void WriteWarning(string message) - { - if (_Writer == null || string.IsNullOrEmpty(message)) - return; - - _Writer.WriteWarning(message); - } - - /// - public virtual bool ShouldWriteWarning() - { - return _Writer != null && _Writer.ShouldWriteWarning(); + _Inner?.WriteObject(sendToPipeline, enumerateCollection); } /// - public virtual void WriteError(ErrorRecord errorRecord) + public virtual void WriteResult(InvokeResult result) { - if (_Writer == null || errorRecord == null) - return; - - _Writer.WriteError(errorRecord); + _Inner?.WriteResult(result); } /// - public virtual bool ShouldWriteError() + public virtual void End(IPipelineResult result) { - return _Writer != null && _Writer.ShouldWriteError(); - } - - /// - public virtual void WriteInformation(InformationRecord informationRecord) - { - if (_Writer == null || informationRecord == null) - return; - - _Writer.WriteInformation(informationRecord); + _Inner?.End(result); } /// public virtual void WriteHost(HostInformationMessage info) { - if (_Writer == null) - return; - - _Writer.WriteHost(info); - } - - /// - public virtual bool ShouldWriteInformation() - { - return _Writer != null && _Writer.ShouldWriteInformation(); - } - - /// - public virtual void WriteDebug(string text, params object[] args) - { - if (_Writer == null || string.IsNullOrEmpty(text) || !ShouldWriteDebug()) - return; - - text = args == null || args.Length == 0 ? text : string.Format(Thread.CurrentThread.CurrentCulture, text, args); - _Writer.WriteDebug(text); - } - - /// - public virtual bool ShouldWriteDebug() - { - return _Writer != null && _Writer.ShouldWriteDebug(); - } - - /// - public virtual void EnterScope(string scopeName) - { - if (_Writer == null) - return; - - _Writer.EnterScope(scopeName); + _Inner?.WriteHost(info); } /// - public virtual void ExitScope() + public virtual void SetExitCode(int exitCode) { - if (_Writer == null) - return; - - _Writer.ExitScope(); + _Inner?.SetExitCode(exitCode); } #region IDisposable @@ -197,8 +79,8 @@ protected virtual void Dispose(bool disposing) { if (!_IsDisposed) { - if (disposing && _Writer != null) - _Writer.Dispose(); + if (disposing && _Inner != null) + _Inner.Dispose(); _IsDisposed = true; } @@ -234,23 +116,7 @@ protected void WriteErrorInfo(RuleRecord record) record.Error.ScriptExtent.StartLineNumber, record.Error.ScriptExtent.StartColumnNumber )); - WriteError(errorRecord); - } - - private bool ShouldProcess(string target, string action) - { - return _ShouldProcess == null || _ShouldProcess(target, action); - } - - private bool CreatePath(string path) - { - var parentPath = Directory.GetParent(path); - if (!parentPath.Exists && ShouldProcess(target: parentPath.FullName, action: PSRuleResources.ShouldCreatePath)) - { - Directory.CreateDirectory(path: parentPath.FullName); - return true; - } - return parentPath.Exists; + _Inner?.LogError(errorRecord); } protected bool CreateFile(string path) @@ -258,49 +124,35 @@ protected bool CreateFile(string path) return CreatePath(path) && ShouldProcess(target: path, action: PSRuleResources.ShouldWriteFile); } - /// - /// Get the value of a preference variable. - /// - protected static ActionPreference GetPreferenceVariable(SessionState sessionState, string variableName) - { - return (ActionPreference)sessionState.PSVariable.GetValue(variableName); - } - #region ILogger public virtual bool IsEnabled(LogLevel logLevel) { - switch (logLevel) - { - case LogLevel.Trace: - case LogLevel.Debug: - return ShouldWriteDebug(); - - case LogLevel.Information: - return ShouldWriteInformation(); - - case LogLevel.Warning: - return ShouldWriteWarning(); - - case LogLevel.Error: - case LogLevel.Critical: - return ShouldWriteError(); - } - return false; + return _Inner?.IsEnabled(logLevel) ?? false; } public virtual void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { - if (logLevel == LogLevel.Error || logLevel == LogLevel.Critical) - HadErrors = true; + _Inner?.Log(logLevel, eventId, state, exception, formatter); + } - if (_Writer == null) - return; + #endregion ILogger - _Writer.Log(logLevel, eventId, state, exception, formatter); + private bool ShouldProcess(string target, string action) + { + return _ShouldProcess == null || _ShouldProcess(target, action); } - #endregion ILogger + private bool CreatePath(string path) + { + var parentPath = Directory.GetParent(path); + if (!parentPath.Exists && ShouldProcess(target: parentPath.FullName, action: PSRuleResources.ShouldCreatePath)) + { + Directory.CreateDirectory(path: parentPath.FullName); + return true; + } + return parentPath.Exists; + } } #nullable restore diff --git a/src/PSRule/Pipeline/PipelineWriterExtensions.cs b/src/PSRule/Pipeline/PipelineWriterExtensions.cs index 512fe93677..6f64bfc1fe 100644 --- a/src/PSRule/Pipeline/PipelineWriterExtensions.cs +++ b/src/PSRule/Pipeline/PipelineWriterExtensions.cs @@ -4,6 +4,7 @@ using System.Management.Automation; using System.Management.Automation.Language; using PSRule.Resources; +using PSRule.Runtime; namespace PSRule.Pipeline; @@ -12,39 +13,14 @@ namespace PSRule.Pipeline; /// public static class PipelineWriterExtensions { - /// - /// Write a debug message. - /// - public static void WriteDebug(this IPipelineWriter writer, DebugRecord debugRecord) - { - if (debugRecord == null) - return; - - writer.WriteDebug(debugRecord.Message); - } - - internal static void DebugMessage(this IPipelineWriter logger, string message) - { - if (logger == null || !logger.ShouldWriteDebug()) - return; - - logger.WriteDebug(new DebugRecord(message)); - } - internal static void WriteWarning(this IPipelineWriter writer, string message, params object[] args) { - if (writer == null || !writer.ShouldWriteWarning() || string.IsNullOrEmpty(message)) - return; - - writer.WriteWarning(Format(message, args)); + writer?.Log(LogLevel.Warning, EventId.None, message, null, (s, e) => Format(message, args)); } internal static void ErrorRequiredVersionMismatch(this IPipelineWriter writer, string moduleName, string moduleVersion, string requiredVersion) { - if (writer == null || !writer.ShouldWriteError()) - return; - - writer.WriteError( + writer?.WriteError( new PipelineBuilderException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.RequiredVersionMismatch, moduleName, moduleVersion, requiredVersion)), "PSRule.RequiredVersionMismatch", ErrorCategory.InvalidOperation @@ -53,10 +29,7 @@ internal static void ErrorRequiredVersionMismatch(this IPipelineWriter writer, s internal static void ErrorReadFileFailed(this IPipelineWriter writer, string path, Exception innerException) { - if (writer == null || !writer.ShouldWriteError()) - return; - - writer.WriteError( + writer?.WriteError( new PipelineSerializationException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.ReadFileFailed, path, innerException.Message), path, innerException), "PSRule.ReadFileFailed", ErrorCategory.InvalidData @@ -65,11 +38,8 @@ internal static void ErrorReadFileFailed(this IPipelineWriter writer, string pat internal static void ErrorReadInputFailed(this IPipelineWriter writer, string path, Exception innerException) { - if (writer == null || !writer.ShouldWriteError()) - return; - - writer.WriteError( - new PipelineSerializationException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.ReadInputFailed, path, innerException.Message), path, innerException), + writer?.WriteError( + new PipelineSerializationException(new EventId(0, "PSRule.ReadInputFailed"), string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.ReadInputFailed, path, innerException.Message), path, innerException), "PSRule.ReadInputFailed", ErrorCategory.ReadError ); @@ -77,44 +47,25 @@ internal static void ErrorReadInputFailed(this IPipelineWriter writer, string pa internal static void WriteError(this IPipelineWriter writer, PipelineException exception, string errorId, ErrorCategory errorCategory) { - if (writer == null) - return; - - writer.WriteError(new ErrorRecord(exception, errorId, errorCategory, null)); + writer?.LogError(new ErrorRecord(exception, errorId, errorCategory, null)); } internal static void WriteError(this IPipelineWriter writer, ParseError error) { - if (writer == null || !writer.ShouldWriteError()) - return; - var record = new ErrorRecord ( - exception: new Pipeline.ParseException(message: error.Message, errorId: error.ErrorId), + // TODO: Fix event id 0 + exception: new ParseException(eventId: new EventId(0, error.ErrorId), message: error.Message), errorId: error.ErrorId, errorCategory: ErrorCategory.InvalidOperation, targetObject: null ); - writer.WriteError(errorRecord: record); + writer?.LogError(errorRecord: record); } internal static void WriteDebug(this IPipelineWriter writer, string message, params object[] args) { - if (writer == null || !writer.ShouldWriteDebug() || string.IsNullOrEmpty(message)) - return; - - writer.WriteDebug(new DebugRecord - ( - message: Format(message, args) - )); - } - - internal static void VerboseRuleDiscovery(this IPipelineWriter writer, string path) - { - if (writer == null || !writer.ShouldWriteVerbose() || string.IsNullOrEmpty(path)) - return; - - writer.WriteVerbose($"[PSRule][D] -- Discovering rules in: {path}"); + writer?.Log(LogLevel.Debug, EventId.None, message, null, (s, e) => Format(message, args)); } private static string Format(string message, params object[] args) diff --git a/src/PSRule/Pipeline/ResourceCache.cs b/src/PSRule/Pipeline/ResourceCache.cs index ebc7693d3e..c564f5a157 100644 --- a/src/PSRule/Pipeline/ResourceCache.cs +++ b/src/PSRule/Pipeline/ResourceCache.cs @@ -119,7 +119,7 @@ private bool TrackIssue(IResource resource) case ISuppressionGroupV1Spec v1: if (v1.ExpiresOn.HasValue && v1.ExpiresOn.Value <= DateTime.UtcNow) { - _TrackedIssues.Add(new ResourceIssue(resource, ResourceIssueType.SuppressionGroupExpired)); + _TrackedIssues.Add(new ResourceIssue(ResourceIssueType.SuppressionGroupExpired, resource.Id)); return true; } break; @@ -127,7 +127,7 @@ private bool TrackIssue(IResource resource) case ISuppressionGroupV2Spec v2: if (v2.ExpiresOn.HasValue && v2.ExpiresOn.Value <= DateTime.UtcNow) { - _TrackedIssues.Add(new ResourceIssue(resource, ResourceIssueType.SuppressionGroupExpired)); + _TrackedIssues.Add(new ResourceIssue(ResourceIssueType.SuppressionGroupExpired, resource.Id)); return true; } break; diff --git a/src/PSRule/Pipeline/ResourceCacheDiscoveryContext.cs b/src/PSRule/Pipeline/ResourceCacheDiscoveryContext.cs index d91e3b851f..2c0ac0cc90 100644 --- a/src/PSRule/Pipeline/ResourceCacheDiscoveryContext.cs +++ b/src/PSRule/Pipeline/ResourceCacheDiscoveryContext.cs @@ -13,13 +13,15 @@ namespace PSRule.Pipeline; /// /// Define a context used for early stage resource discovery. /// -internal sealed class ResourceCacheDiscoveryContext(IPipelineWriter? writer, ILanguageScopeSet languageScopeSet) : IResourceDiscoveryContext +internal sealed class ResourceCacheDiscoveryContext(ILogger? logger, ILanguageScopeSet languageScopeSet) : IResourceDiscoveryContext { + private static readonly EventId PSR0022 = new(22, "PSR0022"); + private readonly ILanguageScopeSet _LanguageScopeSet = languageScopeSet; private ILanguageScope? _CurrentLanguageScope; - public IPipelineWriter? Writer { get; } = writer; + public ILogger Logger { get; } = logger ?? NullLogger.Instance; public ISourceFile? Source { get; private set; } @@ -38,7 +40,7 @@ public void EnterLanguageScope(ISourceFile file) throw new FileNotFoundException(PSRuleResources.ScriptNotFound, file.Path); if (!_LanguageScopeSet.TryScope(file.Module, out var scope)) - throw new Exception("Language scope is unknown."); + throw new RuntimeScopeException(PSR0022, PSRuleResources.PSR0022); Source = file; _CurrentLanguageScope = scope; diff --git a/src/PSRule/Pipeline/ResourceIssue.cs b/src/PSRule/Pipeline/ResourceIssue.cs index 857c4218e2..f03e1dd3a7 100644 --- a/src/PSRule/Pipeline/ResourceIssue.cs +++ b/src/PSRule/Pipeline/ResourceIssue.cs @@ -5,15 +5,26 @@ namespace PSRule.Pipeline; -internal sealed class ResourceIssue +/// +/// Describes an issue with a resource. +/// +/// The type of issue. +/// The affected resource by ID. +/// Additional information based on the issue type. +internal sealed class ResourceIssue(ResourceIssueType type, ResourceId resourceId, params object[]? args) { - public ResourceIssue(IResource resource, ResourceIssueType issue) - { - Resource = resource; - Issue = issue; - } + /// + /// The affected resource by ID. + /// + public ResourceId ResourceId { get; } = resourceId; - public IResource Resource { get; } + /// + /// The type of issue. + /// + public ResourceIssueType Type { get; } = type; - public ResourceIssueType Issue { get; } + /// + /// Additional information based on the issue type. + /// + public object[]? Args { get; } = args; } diff --git a/src/PSRule/Pipeline/ResourceIssueType.cs b/src/PSRule/Pipeline/ResourceIssueType.cs index ba4ee36301..65890f7ccb 100644 --- a/src/PSRule/Pipeline/ResourceIssueType.cs +++ b/src/PSRule/Pipeline/ResourceIssueType.cs @@ -7,5 +7,9 @@ internal enum ResourceIssueType { Unknown, - SuppressionGroupExpired + SuppressionGroupExpired, + + DuplicateResourceId, + + DuplicateResourceName, } diff --git a/src/PSRule/Pipeline/RuntimeScopeException.cs b/src/PSRule/Pipeline/RuntimeScopeException.cs new file mode 100644 index 0000000000..23e154051d --- /dev/null +++ b/src/PSRule/Pipeline/RuntimeScopeException.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.Serialization; +using System.Security.Permissions; +using PSRule.Runtime; + +namespace PSRule.Pipeline; + +/// +/// An exception thrown by PSRule when a runtime property or method is used outside of the intended scope. +/// Avoid using runtime variables outside of a PSRule pipeline. +/// +[Serializable] +public sealed class RuntimeScopeException : PipelineException +{ + /// + public RuntimeScopeException() + { + } + + /// + public RuntimeScopeException(string message) : base(message) + { + } + + /// + public RuntimeScopeException(EventId eventId, string message) : base(eventId, message) + { + } + + /// + public RuntimeScopeException(string message, Exception innerException) : base(message, innerException) + { + } + + /// + public RuntimeScopeException(EventId eventId, string message, Exception innerException) : base(eventId, message, innerException) + { + } + + /// + private RuntimeScopeException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + /// + [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) + throw new ArgumentNullException(nameof(info)); + + base.GetObjectData(info, context); + } +} diff --git a/src/PSRule/Pipeline/SourcePipelineBuilder.cs b/src/PSRule/Pipeline/SourcePipelineBuilder.cs index 32279dfe1b..963861d99d 100644 --- a/src/PSRule/Pipeline/SourcePipelineBuilder.cs +++ b/src/PSRule/Pipeline/SourcePipelineBuilder.cs @@ -9,6 +9,7 @@ using PSRule.Options; using PSRule.Pipeline.Output; using PSRule.Resources; +using PSRule.Runtime; namespace PSRule.Pipeline; @@ -44,7 +45,7 @@ internal SourcePipelineBuilder(IHostContext hostContext, PSRuleOption option, st _Source = new Dictionary(StringComparer.OrdinalIgnoreCase); _HostContext = hostContext; _Writer = new HostPipelineWriter(hostContext, option, ShouldProcess); - _Writer.EnterScope("[Discovery.Source]"); + // _Writer.EnterScope("[Discovery.Source]"); _UseDefaultPath = option == null || option.Include == null || option.Include.Path == null; _CachePath = cachePath; _RestrictScriptSource = option?.Execution?.RestrictScriptSource ?? ExecutionOption.Default.RestrictScriptSource!.Value; @@ -85,10 +86,7 @@ public void VerboseScanModule(string moduleName) /// private void Log(string message, params object[] args) { - if (!_Writer.ShouldWriteVerbose()) - return; - - _Writer.WriteVerbose(string.Format(Thread.CurrentThread.CurrentCulture, message, args)); + _Writer.LogVerbose(EventId.None, message, args); } /// @@ -96,10 +94,7 @@ private void Log(string message, params object[] args) /// private void Debug(string message, params object[] args) { - if (!_Writer.ShouldWriteDebug()) - return; - - _Writer.WriteDebug(string.Format(Thread.CurrentThread.CurrentCulture, message, args)); + _Writer.LogVerbose(EventId.None, message, args); } #endregion Logging @@ -432,8 +427,7 @@ private static bool ShouldInclude(string path) private static SourceFile[]? IncludeFile(string path, string? helpPath, RestrictScriptSource restrictScriptSource, string workspacePath) { - if (!File.Exists(path)) - throw new FileNotFoundException(PSRuleResources.SourceNotFound, path); + if (!File.Exists(path)) throw new FileNotFoundException(PSRuleResources.SourceNotFound, path); var sourceType = GetSourceType(path); if (sourceType == SourceType.Script && IgnoreScript(path, restrictScriptSource, workspacePath)) diff --git a/src/PSRule/Resources/PSRuleResources.Designer.cs b/src/PSRule/Resources/PSRuleResources.Designer.cs index 05fddc66d7..346eaee139 100644 --- a/src/PSRule/Resources/PSRuleResources.Designer.cs +++ b/src/PSRule/Resources/PSRuleResources.Designer.cs @@ -574,7 +574,7 @@ internal static string PSR0001 { } /// - /// Looks up a localized string similar to PSR0002: Summary results are not supported with Job Summaries. See https://aka.ms/ps-rule/troubleshooting.. + /// Looks up a localized string similar to PSR0002: Summary results are not supported with Job Summaries. See https://aka.ms/ps-rule/troubleshooting-v3. /// internal static string PSR0002 { get { @@ -691,7 +691,7 @@ internal static string PSR0014 { } /// - /// Looks up a localized string similar to PSR0015: No valid sources were found. Please check your working path and configured options. See https://aka.ms/ps-rule/troubleshooting. + /// Looks up a localized string similar to PSR0015: No valid sources were found. Please check your working path and configured options. See https://aka.ms/ps-rule/troubleshooting-v3. /// internal static string PSR0015 { get { @@ -700,7 +700,7 @@ internal static string PSR0015 { } /// - /// Looks up a localized string similar to PSR0016: Could not find a matching rule. Please check that Path, Name, and Tag parameters are correct. See https://aka.ms/ps-rule/troubleshooting. + /// Looks up a localized string similar to PSR0016: Could not find a matching rule. Please check that Path, Name, and Tag parameters are correct. See https://aka.ms/ps-rule/troubleshooting-v3. /// internal static string PSR0016 { get { @@ -709,7 +709,7 @@ internal static string PSR0016 { } /// - /// Looks up a localized string similar to PSR0017: No valid input objects or files were found. Please check your working path and configured options. See https://aka.ms/ps-rule/troubleshooting. + /// Looks up a localized string similar to PSR0017: No valid input objects or files were found. Please check your working path and configured options. See https://aka.ms/ps-rule/troubleshooting-v3. /// internal static string PSR0017 { get { @@ -717,6 +717,15 @@ internal static string PSR0017 { } } + /// + /// Looks up a localized string similar to PSR0022: Language scope not found. See https://aka.ms/ps-rule/troubleshooting-v3. + /// + internal static string PSR0022 { + get { + return ResourceManager.GetString("PSR0022", resourceCulture); + } + } + /// /// Looks up a localized string similar to Failed to deserialize the file '{0}': {1}. /// diff --git a/src/PSRule/Resources/PSRuleResources.resx b/src/PSRule/Resources/PSRuleResources.resx index 1a4412045f..146fa55ec7 100644 --- a/src/PSRule/Resources/PSRuleResources.resx +++ b/src/PSRule/Resources/PSRuleResources.resx @@ -392,7 +392,7 @@ The option '{0}' is deprecated and will be removed with PSRule v3. See http://aka.ms/ps-rule/deprecations for more detail. - PSR0002: Summary results are not supported with Job Summaries. See https://aka.ms/ps-rule/troubleshooting. + PSR0002: Summary results are not supported with Job Summaries. See https://aka.ms/ps-rule/troubleshooting-v3 Rule '{0}' was excluded from run. @@ -437,12 +437,15 @@ PSR0014: Failed to create runtime factory '{0}' from '{1}'. {2} - PSR0016: Could not find a matching rule. Please check that Path, Name, and Tag parameters are correct. See https://aka.ms/ps-rule/troubleshooting + PSR0016: Could not find a matching rule. Please check that Path, Name, and Tag parameters are correct. See https://aka.ms/ps-rule/troubleshooting-v3 - PSR0015: No valid sources were found. Please check your working path and configured options. See https://aka.ms/ps-rule/troubleshooting + PSR0015: No valid sources were found. Please check your working path and configured options. See https://aka.ms/ps-rule/troubleshooting-v3 - PSR0017: No valid input objects or files were found. Please check your working path and configured options. See https://aka.ms/ps-rule/troubleshooting + PSR0017: No valid input objects or files were found. Please check your working path and configured options. See https://aka.ms/ps-rule/troubleshooting-v3 + + + PSR0022: Language scope not found. See https://aka.ms/ps-rule/troubleshooting-v3 \ No newline at end of file diff --git a/src/PSRule/Runtime/ILanguageScopeSet.cs b/src/PSRule/Runtime/ILanguageScopeSet.cs index cefe899e84..f0f4cd89f8 100644 --- a/src/PSRule/Runtime/ILanguageScopeSet.cs +++ b/src/PSRule/Runtime/ILanguageScopeSet.cs @@ -16,7 +16,7 @@ internal interface ILanguageScopeSet : IDisposable /// The name of the scope. /// The resulting scope instance. /// Returns true when the scope exists. Otherwise returns false. - bool TryScope(string name, out ILanguageScope? scope); + bool TryScope(string? name, out ILanguageScope? scope); /// /// Get all the language scopes configured in the collection. diff --git a/src/PSRule/Runtime/LegacyRunspaceContext.cs b/src/PSRule/Runtime/LegacyRunspaceContext.cs index 22fec8a971..cbc8593a8f 100644 --- a/src/PSRule/Runtime/LegacyRunspaceContext.cs +++ b/src/PSRule/Runtime/LegacyRunspaceContext.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.Management.Automation; using System.Management.Automation.Language; -using PSRule.Configuration; using PSRule.Definitions; using PSRule.Definitions.Conventions; using PSRule.Options; @@ -28,6 +27,8 @@ internal sealed class LegacyRunspaceContext : IDisposable, ILogger, IScriptResou private const string ERROR_ID_INVALID_RULE_RESULT = "PSRule.Runtime.InvalidRuleResult"; private const string WARN_KEY_SEPARATOR = "_"; + private static readonly EventId PSR0022 = new(22, "PSR0022"); + [ThreadStatic] internal static LegacyRunspaceContext? CurrentThread; @@ -42,8 +43,6 @@ internal sealed class LegacyRunspaceContext : IDisposable, ILogger, IScriptResou private readonly ExecutionActionPreference _UnprocessedObject; private readonly ExecutionActionPreference _RuleSuppressed; private readonly ExecutionActionPreference _InvariantCulture; - private readonly OutcomeLogStream _FailStream; - private readonly OutcomeLogStream _PassStream; /// /// Track the current runspace scope. @@ -79,8 +78,6 @@ internal LegacyRunspaceContext(PipelineContext pipeline) _RuleSuppressed = Pipeline.Option.Execution.RuleSuppressed.GetValueOrDefault(ExecutionOption.Default.RuleSuppressed!.Value); _InvariantCulture = Pipeline.Option.Execution.InvariantCulture.GetValueOrDefault(ExecutionOption.Default.InvariantCulture!.Value); - _FailStream = Pipeline.Option.Logging.RuleFail ?? LoggingOption.Default.RuleFail!.Value; - _PassStream = Pipeline.Option.Logging.RulePass ?? LoggingOption.Default.RulePass!.Value; _WarnOnce = []; _ObjectNumber = -1; @@ -93,6 +90,8 @@ internal LegacyRunspaceContext(PipelineContext pipeline) public IPipelineWriter Writer => Pipeline.Writer; + public ILogger? Logger => Pipeline.Writer; + internal IEnumerable? Output { get; private set; } internal TargetObject? TargetObject { get; private set; } @@ -105,7 +104,7 @@ internal LegacyRunspaceContext(PipelineContext pipeline) public ILanguageScope? LanguageScope { get; private set; } - internal bool IsScope(RunspaceScope scope) + public bool IsScope(RunspaceScope scope) { if (scope == RunspaceScope.None && (_Scope == null || _Scope.Count == 0)) return true; @@ -131,64 +130,6 @@ public void PopScope(RunspaceScope scope) _Scope.Pop(); } - public void Pass() - { - if (Writer == null || _PassStream == OutcomeLogStream.None || RuleRecord == null) - return; - - if (_PassStream == OutcomeLogStream.Warning && Writer.ShouldWriteWarning()) - Writer.WriteWarning(PSRuleResources.OutcomeRulePass, RuleRecord.RuleName, Binding?.TargetName); - - if (_PassStream == OutcomeLogStream.Error && Writer.ShouldWriteError()) - Writer.WriteError(new ErrorRecord( - new RuleException(string.Format( - Thread.CurrentThread.CurrentCulture, - PSRuleResources.OutcomeRulePass, - RuleRecord.RuleName, - Binding?.TargetName)), - SOURCE_OUTCOME_PASS, - ErrorCategory.InvalidData, - null)); - - if (_PassStream == OutcomeLogStream.Information && Writer.ShouldWriteInformation()) - Writer.WriteInformation(new InformationRecord( - messageData: string.Format( - Thread.CurrentThread.CurrentCulture, - PSRuleResources.OutcomeRulePass, - RuleRecord.RuleName, - Binding?.TargetName), - source: SOURCE_OUTCOME_PASS)); - } - - public void Fail() - { - if (Writer == null || _FailStream == OutcomeLogStream.None || RuleRecord == null) - return; - - if (_FailStream == OutcomeLogStream.Warning && Writer.ShouldWriteWarning()) - Writer.WriteWarning(PSRuleResources.OutcomeRuleFail, RuleRecord.RuleName, Binding?.TargetName); - - if (_FailStream == OutcomeLogStream.Error && Writer.ShouldWriteError()) - Writer.WriteError(new ErrorRecord( - new RuleException(string.Format( - Thread.CurrentThread.CurrentCulture, - PSRuleResources.OutcomeRuleFail, - RuleRecord.RuleName, - Binding?.TargetName)), - SOURCE_OUTCOME_FAIL, - ErrorCategory.InvalidData, - null)); - - if (_FailStream == OutcomeLogStream.Information && Writer.ShouldWriteInformation()) - Writer.WriteInformation(new InformationRecord( - messageData: string.Format( - Thread.CurrentThread.CurrentCulture, - PSRuleResources.OutcomeRuleFail, - RuleRecord.RuleName, - Binding?.TargetName), - source: SOURCE_OUTCOME_FAIL)); - } - public void WarnRuleInconclusive(string ruleId) { this.Throw(_RuleInconclusive, PSRuleResources.RuleInconclusive, ruleId, Binding?.TargetName); @@ -233,10 +174,10 @@ public void RuleSuppressionGroupCount(ISuppressionInfo suppression, int count) public void ErrorInvalidRuleResult() { - if (Writer == null || !Writer.ShouldWriteError()) + if (Logger == null || !Logger.IsEnabled(LogLevel.Error)) return; - Writer.WriteError(new ErrorRecord( + Logger.LogError(new ErrorRecord( exception: new RuleException(message: string.Format( Thread.CurrentThread.CurrentCulture, PSRuleResources.InvalidRuleResult, @@ -248,18 +189,18 @@ public void ErrorInvalidRuleResult() )); } - public void VerboseFoundResource(string name, string moduleName, string scriptName) + public void VerboseFoundResource(string name, string scope, string scriptName) { - if (Writer == null || !Writer.ShouldWriteVerbose()) + if (Writer == null || !Writer.IsEnabled(LogLevel.Trace)) return; - var m = string.IsNullOrEmpty(moduleName) ? "." : moduleName; - Writer.WriteVerbose($"[PSRule][D] -- Found {m}\\{name} in {scriptName}"); + scope = string.IsNullOrEmpty(scope) ? "." : scope; + Writer.LogVerbose(EventId.None, "[PSRule][D] -- Found {0}\\{1} in {2}", scope, name, scriptName); } public void LogObjectStart() { - if (Writer == null || !Writer.ShouldWriteDebug()) + if (Writer == null || !Writer.IsEnabled(LogLevel.Debug)) return; Writer.WriteDebug(string.Concat(GetLogPrefix(), " :: ", Binding?.TargetName)); @@ -267,10 +208,10 @@ public void LogObjectStart() public void VerboseConditionMessage(string condition, string message, params object[] args) { - if (Writer == null || !Writer.ShouldWriteVerbose()) + if (Writer == null || !Writer.IsEnabled(LogLevel.Trace)) return; - Writer.WriteVerbose(string.Concat( + Writer.LogVerbose(EventId.None, string.Concat( GetLogPrefix(), "[", condition, @@ -280,26 +221,26 @@ public void VerboseConditionMessage(string condition, string message, params obj public void VerboseConditionResult(string condition, int pass, int count, bool outcome) { - if (Writer == null || !Writer.ShouldWriteVerbose()) + if (Writer == null || !Writer.IsEnabled(LogLevel.Trace)) return; - Writer.WriteVerbose(string.Concat(GetLogPrefix(), "[", condition, "] -- [", pass, "/", count, "] [", outcome, "]")); + Writer.LogVerbose(EventId.None, string.Concat(GetLogPrefix(), "[", condition, "] -- [", pass, "/", count, "] [", outcome, "]")); } public void VerboseConditionResult(string condition, bool outcome) { - if (Writer == null || !Writer.ShouldWriteVerbose()) + if (Writer == null || !Writer.IsEnabled(LogLevel.Trace)) return; - Writer.WriteVerbose(string.Concat(GetLogPrefix(), "[", condition, "] -- [", outcome, "]")); + Writer.LogVerbose(EventId.None, string.Concat(GetLogPrefix(), "[", condition, "] -- [", outcome, "]")); } public void VerboseConditionResult(int pass, int count, RuleOutcome outcome) { - if (Writer == null || !Writer.ShouldWriteVerbose()) + if (Writer == null || !Writer.IsEnabled(LogLevel.Trace)) return; - Writer.WriteVerbose(string.Concat(GetLogPrefix(), " -- [", pass, "/", count, "] [", outcome, "]")); + Writer.LogVerbose(EventId.None, string.Concat(GetLogPrefix(), " -- [", pass, "/", count, "] [", outcome, "]")); } public ExecutionOption GetExecutionOption() @@ -326,50 +267,50 @@ private static void EnableLogging(PowerShell ps) private static void Debug_DataAdded(object sender, DataAddedEventArgs e) { - if (CurrentThread?.Writer == null) + if (CurrentThread?.Logger == null) return; if (sender is not PSDataCollection collection) return; var record = collection[e.Index]; - CurrentThread.Writer.WriteDebug(debugRecord: record); + CurrentThread.Logger.LogDebug(EventId.None, record.Message); } private static void Information_DataAdded(object sender, DataAddedEventArgs e) { - if (CurrentThread?.Writer == null) + if (CurrentThread?.Logger == null) return; if (sender is not PSDataCollection collection) return; var record = collection[e.Index]; - CurrentThread.Writer.WriteInformation(informationRecord: record); + CurrentThread.Logger.LogInformation(EventId.None, record.MessageData.ToString()); } private static void Verbose_DataAdded(object sender, DataAddedEventArgs e) { - if (CurrentThread?.Writer == null) + if (CurrentThread?.Logger == null) return; if (sender is not PSDataCollection collection) return; var record = collection[e.Index]; - CurrentThread.Writer.WriteVerbose(record.Message); + CurrentThread.Logger.LogVerbose(EventId.None, record.Message); } private static void Warning_DataAdded(object sender, DataAddedEventArgs e) { - if (CurrentThread?.Writer == null) + if (CurrentThread?.Logger == null) return; if (sender is not PSDataCollection collection) return; var record = collection[e.Index]; - CurrentThread.Writer.WriteWarning(message: record.Message); + CurrentThread.Logger.LogWarning(EventId.None, record.Message); } private static void Error_DataAdded(object sender, DataAddedEventArgs e) @@ -378,28 +319,26 @@ private static void Error_DataAdded(object sender, DataAddedEventArgs e) return; CurrentThread._RuleErrors++; - if (CurrentThread.Writer == null) + if (CurrentThread.Logger == null) return; if (sender is not PSDataCollection collection) return; var record = collection[e.Index]; - CurrentThread.Error(record); + CurrentThread.Logger.LogError(record); } public void Error(ActionPreferenceStopException ex) { - if (ex == null) - return; + if (ex == null) return; Error(ex.ErrorRecord); } public void Error(Exception ex) { - if (ex == null) - return; + if (ex == null) return; var errorRecord = ex is IContainsErrorRecord error ? error.ErrorRecord : null; var scriptStackTrace = errorRecord != null ? GetStackTrace(errorRecord) : null; @@ -407,7 +346,7 @@ public void Error(Exception ex) var errorId = errorRecord != null ? GetErrorId(errorRecord) : null; if (RuleRecord == null) { - Writer.WriteError(errorRecord); + CurrentThread.Logger?.LogError(errorRecord); return; } RuleRecord.Outcome = RuleOutcome.Error; @@ -424,11 +363,9 @@ public void Error(Exception ex) public void Error(ErrorRecord error) { - if (RuleRecord == null) - { - Writer.WriteError(error); - return; - } + Logger?.LogError(error); + if (RuleRecord == null) return; + RuleRecord.Outcome = RuleOutcome.Error; RuleRecord.Error = new ErrorInfo( message: error.Exception?.Message, @@ -516,7 +453,7 @@ public void EnterLanguageScope(ISourceFile file) throw new FileNotFoundException(PSRuleResources.ScriptNotFound, file.Path); if (!Pipeline.LanguageScope.TryScope(file.Module, out var scope)) - throw new Exception("Language scope is unknown."); + throw new RuntimeScopeException(PSR0022, PSRuleResources.PSR0022); LanguageScope = scope; @@ -597,7 +534,7 @@ public RuleRecord EnterRuleBlock(RuleBlock ruleBlock) @override: ruleBlock.Override ); - Writer?.EnterScope(ruleBlock.Name); + // Writer?.EnterScope(ruleBlock.Name); // Starts rule execution timer _RuleTimer.Restart(); @@ -622,7 +559,7 @@ public void ExitRuleBlock(RuleBlock ruleBlock) } } - Writer?.ExitScope(); + // Writer?.ExitScope(); _LogPrefix = null; RuleRecord = null; @@ -676,7 +613,7 @@ private void RunConventionEnd() _Conventions[i].End(this, null); } - internal void WriteReason(ResultReason[] reason) + public void WriteReason(ResultReason[] reason) { for (var i = 0; reason != null && i < reason.Length; i++) WriteReason(reason[i]); @@ -819,49 +756,13 @@ internal bool TryGetConfigurationValue(string name, out object? value) /// bool ILogger.IsEnabled(LogLevel logLevel) { - if (Writer == null) - return false; - - switch (logLevel) - { - case LogLevel.Trace: - case LogLevel.Debug: - return Writer.ShouldWriteDebug(); - - case LogLevel.Information: - return Writer.ShouldWriteInformation(); - - case LogLevel.Warning: - return Writer.ShouldWriteWarning(); - - case LogLevel.Error: - case LogLevel.Critical: - return Writer.ShouldWriteError(); - } - return false; + return Logger?.IsEnabled(logLevel) ?? false; } /// void ILogger.Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { - if (Writer == null) return; - - if (logLevel == LogLevel.Error || logLevel == LogLevel.Critical) - { - Writer.WriteError(new ErrorRecord(exception, eventId.Id.ToString(), ErrorCategory.InvalidOperation, null)); - } - else if (logLevel == LogLevel.Warning) - { - Writer.WriteWarning(formatter(state, exception)); - } - else if (logLevel == LogLevel.Information) - { - Writer.WriteInformation(new InformationRecord(formatter(state, exception), null)); - } - else if (logLevel == LogLevel.Debug || logLevel == LogLevel.Trace) - { - Writer.WriteDebug(formatter(state, exception)); - } + Logger?.Log(logLevel, eventId, state, exception, formatter); } #endregion ILogger diff --git a/src/PSRule/Runtime/LoggerExtensions.cs b/src/PSRule/Runtime/LoggerExtensions.cs index eec3e13eeb..1d3244f269 100644 --- a/src/PSRule/Runtime/LoggerExtensions.cs +++ b/src/PSRule/Runtime/LoggerExtensions.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Management.Automation; +using System.Management.Automation.Language; using PSRule.Definitions; using PSRule.Options; using PSRule.Pipeline; @@ -24,7 +26,10 @@ internal static class LoggerExtensions private static readonly EventId PSR0015 = new(15, "PSR0015"); private static readonly EventId PSR0016 = new(16, "PSR0016"); private static readonly EventId PSR0017 = new(17, "PSR0017"); - + private static readonly EventId PSR0018 = new(18, "PSR0018"); + private static readonly EventId PSR0019 = new(19, "PSR0019"); + private static readonly EventId PSR0020 = new(20, "PSR0020"); + private static readonly EventId PSR0021 = new(21, "PSR0021"); /// /// PSR0005: The {0} '{1}' is obsolete. @@ -236,4 +241,131 @@ internal static void LogNoValidInput(this ILogger logger, ExecutionActionPrefere PSRuleResources.PSR0017 ); } + + /// + /// The resource name '{0}' is not valid at {1}. Each resource name must be between 3-128 characters in length, must start and end with a letter or number, and only contain letters, numbers, hyphens, dots, or underscores. See https://aka.ms/ps-rule/naming for more information. + /// + internal static void LogInvalidResourceName(this ILogger logger, string name, string extent) + { + logger.Log + ( + LogLevel.Critical, + PSR0018, + new FormattedLogValues(PSRuleResources.InvalidResourceName, name, extent), + new Pipeline.ParseException( + eventId: PSR0018, + message: string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.InvalidResourceName, name, extent) + ), + (s, e) => s.ToString() + ); + } + + /// + /// Could not find required rule definition parameter '{0}' on rule at {1}. + /// + internal static void LogRuleParameterNotFound(this ILogger logger, string name, string extent) + { + logger.Log + ( + LogLevel.Critical, + PSR0019, + new FormattedLogValues(PSRuleResources.RuleParameterNotFound, name, extent), + new Pipeline.ParseException( + eventId: PSR0019, + message: string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.RuleParameterNotFound, name, extent) + ), + (s, e) => s.ToString() + ); + } + + /// + /// Rule nesting was detected for rule at {0}. Rules must not be nested. + /// + internal static void LogInvalidRuleNesting(this ILogger logger, string extent) + { + logger.Log + ( + LogLevel.Critical, + PSR0020, + new FormattedLogValues(PSRuleResources.InvalidRuleNesting, extent), + new Pipeline.ParseException( + eventId: PSR0020, + message: string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.InvalidRuleNesting, extent) + ), + (s, e) => s.ToString() + ); + } + + /// + /// An invalid ErrorAction ({0}) was specified for rule at {1}. Ignore | Stop are supported. + /// + internal static void LogInvalidErrorAction(this ILogger logger, string action, string extent) + { + logger.Log + ( + LogLevel.Critical, + PSR0021, + new FormattedLogValues(PSRuleResources.InvalidErrorAction, action, extent), + new Pipeline.ParseException( + eventId: PSR0021, + message: string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.InvalidErrorAction, action, extent) + ), + (s, e) => s.ToString() + ); + } + + internal static void VerboseRuleDiscovery(this ILogger logger, string path) + { + if (logger == null || !logger.IsEnabled(LogLevel.Trace) || string.IsNullOrEmpty(path)) + return; + + logger.LogVerbose(EventId.None, "[PSRule][D] -- Discovering rules in: {0}", path); + } + + internal static void LogError(this ILogger logger, ErrorRecord errorRecord) + { + if (logger == null || !logger.IsEnabled(LogLevel.Error)) + return; + + var eventId = EventId.None; + if (errorRecord.Exception is PipelineException pipelineException && pipelineException.EventId.HasValue) + { + eventId = pipelineException.EventId.Value; + } + + logger.LogError(eventId, errorRecord.Exception, errorRecord.Exception.Message); + } + + internal static void LogError(this ILogger logger, ParseError error) + { + if (logger == null || !logger.IsEnabled(LogLevel.Error)) + return; + + var exception = new Pipeline.ParseException(eventId: new EventId(0, error.ErrorId), message: error.Message); + logger.LogError(new EventId(0, error.ErrorId), exception, exception.Message); + } + + internal static void VerboseFoundResource(this ILogger logger, string name, string moduleName, string scriptName) + { + if (logger == null || !logger.IsEnabled(LogLevel.Trace)) + return; + + moduleName = string.IsNullOrEmpty(moduleName) ? "." : moduleName; + logger.LogVerbose(EventId.None, "[PSRule][D] -- Found {0}\\{1} in {2}", moduleName, name, scriptName); + } + + internal static void Throw(this ILogger logger, ExecutionActionPreference action, string message, params object[] args) + { + if (logger == null || action == ExecutionActionPreference.Ignore) + return; + + if (action == ExecutionActionPreference.Error) + throw new RuleException(string.Format(Thread.CurrentThread.CurrentCulture, message, args)); + + else if (action == ExecutionActionPreference.Warn && logger.IsEnabled(LogLevel.Warning)) + logger.LogWarning(EventId.None, message, args); + + else if (action == ExecutionActionPreference.Debug && logger.IsEnabled(LogLevel.Debug)) + logger.LogDebug(EventId.None, message, args); + } } diff --git a/tests/PSRule.Tests/Definitions/SuppressionGroups/SuppressionGroupTests.cs b/tests/PSRule.Tests/Definitions/SuppressionGroups/SuppressionGroupTests.cs index 756eef7dae..f57539ebd8 100644 --- a/tests/PSRule.Tests/Definitions/SuppressionGroups/SuppressionGroupTests.cs +++ b/tests/PSRule.Tests/Definitions/SuppressionGroups/SuppressionGroupTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Linq; using PSRule.Configuration; using PSRule.Pipeline; @@ -97,16 +96,13 @@ public void Import_WhenSuppressionGroupIsExpired_ShouldReturnIssue(string path) context.Initialize(sources); context.Begin(); - var suppressionGroup = resourcesCache.OfType().Where(g => g.Id.Equals(".\\SuppressWithExpiry")); + var resourceId = ".\\SuppressWithExpiry"; + + var suppressionGroup = resourcesCache.OfType().Where(g => g.Id.Equals(resourceId)); Assert.Empty(suppressionGroup); - var issues = resourcesCache.Issues.Where(issue => issue.Resource.Id.Equals(".\\SuppressWithExpiry")); + var issues = resourcesCache.Issues.Where(issue => issue.ResourceId.Equals(resourceId)); Assert.Single(issues); - - var actual = issues.FirstOrDefault().Resource as SuppressionGroupV1; - Assert.Equal("SuppressWithExpiry", actual.Name); - Assert.Equal("Suppress with expiry.", actual.Info.Synopsis.Text); - Assert.Equal(DateTime.Parse("2022-01-01T00:00:00Z").ToUniversalTime(), actual.Spec.ExpiresOn); } #region Helper methods diff --git a/tests/PSRule.Tests/LanguageVisitorTests.cs b/tests/PSRule.Tests/LanguageVisitorTests.cs index 751a8707bd..fdeb17990a 100644 --- a/tests/PSRule.Tests/LanguageVisitorTests.cs +++ b/tests/PSRule.Tests/LanguageVisitorTests.cs @@ -6,11 +6,12 @@ namespace PSRule; -public sealed class LanguageVisitorTests +public sealed class LanguageVisitorTests : ContextBaseTests { [Fact] public void NestedRule() { + var logger = GetTestWriter(); var content = @" # Header comment Rule 'Rule1' { @@ -18,11 +19,12 @@ public void NestedRule() } "; var scriptAst = ScriptBlock.Create(content).Ast; - var visitor = new RuleLanguageAst(); + var visitor = new RuleLanguageAst(logger); scriptAst.Visit(visitor); - Assert.Null(visitor.Errors); + Assert.Empty(logger.Errors); + logger = GetTestWriter(); content = @" # Header comment Rule 'Rule1' { @@ -32,15 +34,16 @@ public void NestedRule() } "; scriptAst = ScriptBlock.Create(content).Ast; - visitor = new RuleLanguageAst(); + visitor = new RuleLanguageAst(logger); scriptAst.Visit(visitor); - Assert.Single(visitor.Errors); + Assert.Single(logger.Errors); } [Fact] public void UnvalidRule() { + var logger = GetTestWriter(); var content = @" Rule '' { @@ -71,10 +74,10 @@ public void UnvalidRule() "; var scriptAst = ScriptBlock.Create(content).Ast; - var visitor = new RuleLanguageAst(); + var visitor = new RuleLanguageAst(logger); scriptAst.Visit(visitor); - Assert.NotNull(visitor.Errors); - Assert.Equal(4, visitor.Errors.Count); + Assert.NotNull(logger.Errors); + Assert.Equal(4, logger.Errors.Count); } } diff --git a/tests/PSRule.Tests/PSRule.Common.Tests.ps1 b/tests/PSRule.Tests/PSRule.Common.Tests.ps1 index cb4eadfc82..02a6548ec9 100644 --- a/tests/PSRule.Tests/PSRule.Common.Tests.ps1 +++ b/tests/PSRule.Tests/PSRule.Common.Tests.ps1 @@ -140,7 +140,7 @@ Describe 'Invoke-PSRule' -Tag 'Invoke-PSRule','Common' { # Errors $outErrors | Should -BeLike '*Rule error message*'; - $outErrors.FullyQualifiedErrorId | Should -BeLike '*,WithError,Invoke-PSRule'; + $outErrors.FullyQualifiedErrorId | Should -Be 'Invoke-PSRule'; # Information $informationMessages = $outInformation.ToArray(); @@ -156,7 +156,7 @@ Describe 'Invoke-PSRule' -Tag 'Invoke-PSRule','Common' { It 'Propagates PowerShell exceptions' { $Null = $testObject | Invoke-PSRule -Path (Join-Path -Path $here -ChildPath 'FromFileWithException.Rule.ps1') -ErrorVariable outErrors -ErrorAction SilentlyContinue -WarningAction SilentlyContinue; - $outErrors | Should -Be 'You cannot call a method on a null-valued expression.', 'PSR0016: Could not find a matching rule. Please check that Path, Name, and Tag parameters are correct. See https://aka.ms/ps-rule/troubleshooting'; + $outErrors | Should -Be 'You cannot call a method on a null-valued expression.', 'PSR0016: Could not find a matching rule. Please check that Path, Name, and Tag parameters are correct. See https://aka.ms/ps-rule/troubleshooting-v3'; } It 'Processes rule tags' { @@ -389,7 +389,7 @@ Describe 'Invoke-PSRule' -Tag 'Invoke-PSRule','Common' { $errorMessages = @($outErrors); $errorMessages.Length | Should -Be 1; $errorMessages[0] | Should -BeOfType [System.Management.Automation.ErrorRecord]; - $errorMessages[0].Exception.Message | Should -Be 'PSR0015: No valid sources were found. Please check your working path and configured options. See https://aka.ms/ps-rule/troubleshooting'; + $errorMessages[0].Exception.Message | Should -Be 'PSR0015: No valid sources were found. Please check your working path and configured options. See https://aka.ms/ps-rule/troubleshooting-v3'; } } @@ -933,7 +933,7 @@ Describe 'Invoke-PSRule' -Tag 'Invoke-PSRule','Common' { $result | Should -BeNullOrEmpty; $records = @($outErrors); $records | Should -Not -BeNullOrEmpty; - $records.CategoryInfo.Category | Should -BeIn 'ObjectNotFound'; + $records.CategoryInfo.Category | Should -BeIn 'NotSpecified'; $records.Length | Should -Be 1; # Multiple files @@ -945,7 +945,7 @@ Describe 'Invoke-PSRule' -Tag 'Invoke-PSRule','Common' { $result | Should -BeNullOrEmpty; $records = @($outErrors); $records | Should -Not -BeNullOrEmpty; - $records.CategoryInfo.Category | Should -BeIn 'ObjectNotFound'; + $records.CategoryInfo.Category | Should -BeIn 'NotSpecified'; $records.Length | Should -Be 2; } } @@ -1082,132 +1082,6 @@ Describe 'Invoke-PSRule' -Tag 'Invoke-PSRule','Common' { } } - Context 'Logging' { - It 'RuleFail' { - $testObject = [PSCustomObject]@{ - Name = 'LoggingTest' - } - - # Warning - $option = New-PSRuleOption -Option @{ 'Logging.RuleFail' = 'Warning'}; - $result = $testObject | Invoke-PSRule -Option $option -Path $ruleFilePath -Name 'FromFile2' -WarningVariable outWarning -WarningAction SilentlyContinue; - $messages = @($outwarning); - $result | Should -Not -BeNullOrEmpty; - $result.Outcome | Should -Be 'Fail'; - $messages | Should -Not -BeNullOrEmpty; - $messages | Should -Be "[FAIL] -- FromFile2:: Reported for 'LoggingTest'" - - # Error - $option = New-PSRuleOption -Option @{ 'Logging.RuleFail' = 'Error'}; - $result = $testObject | Invoke-PSRule -Option $option -Path $ruleFilePath -Name 'FromFile2' -ErrorVariable outError -ErrorAction SilentlyContinue; - $messages = @($outError); - $result | Should -Not -BeNullOrEmpty; - $result.Outcome | Should -Be 'Fail'; - $messages | Should -Not -BeNullOrEmpty; - $messages.Exception.Message | Should -Be "[FAIL] -- FromFile2:: Reported for 'LoggingTest'" - - # Information - $option = New-PSRuleOption -Option @{ 'Logging.RuleFail' = 'Information'}; - $result = $testObject | Invoke-PSRule -Option $option -Path $ruleFilePath -Name 'FromFile2' -InformationVariable outInformation; - $messages = @($outInformation); - $result | Should -Not -BeNullOrEmpty; - $result.Outcome | Should -Be 'Fail'; - $messages | Should -Not -BeNullOrEmpty; - $messages | Should -Be "[FAIL] -- FromFile2:: Reported for 'LoggingTest'" - } - - It 'RulePass' { - $testObject = [PSCustomObject]@{ - Name = 'LoggingTest' - } - - # Warning - $option = New-PSRuleOption -Option @{ 'Logging.RulePass' = 'Warning'}; - $result = $testObject | Invoke-PSRule -Option $option -Path $ruleFilePath -Name 'FromFile1' -WarningVariable outWarning -WarningAction SilentlyContinue; - $messages = @($outwarning); - $result | Should -Not -BeNullOrEmpty; - $result.Outcome | Should -Be 'Pass'; - $messages | Should -Not -BeNullOrEmpty; - $messages | Should -Be "[PASS] -- FromFile1:: Reported for 'LoggingTest'" - - # Error - $option = New-PSRuleOption -Option @{ 'Logging.RulePass' = 'Error'}; - $result = $testObject | Invoke-PSRule -Option $option -Path $ruleFilePath -Name 'FromFile1' -ErrorVariable outError -ErrorAction SilentlyContinue; - $messages = @($outError); - $result | Should -Not -BeNullOrEmpty; - $result.Outcome | Should -Be 'Pass'; - $messages | Should -Not -BeNullOrEmpty; - $messages.Exception.Message | Should -Be "[PASS] -- FromFile1:: Reported for 'LoggingTest'" - - # Information - $option = New-PSRuleOption -Option @{ 'Logging.RulePass' = 'Information'}; - $result = $testObject | Invoke-PSRule -Option $option -Path $ruleFilePath -Name 'FromFile1' -InformationVariable outInformation; - $messages = @($outInformation); - $result | Should -Not -BeNullOrEmpty; - $result.Outcome | Should -Be 'Pass'; - $messages | Should -Not -BeNullOrEmpty; - $messages | Should -Be "[PASS] -- FromFile1:: Reported for 'LoggingTest'" - } - - It 'LimitDebug' { - $withLoggingRulePath = (Join-Path -Path $here -ChildPath 'FromFileWithLogging.Rule.ps1'); - $loggingParams = @{ - Path = $withLoggingRulePath - Name = 'WithDebug', 'WithDebug2' - WarningAction = 'SilentlyContinue' - ErrorAction = 'SilentlyContinue' - InformationAction = 'SilentlyContinue' - } - $option = New-PSRuleOption -LoggingLimitDebug 'WithDebug2', '[Discovery.Rule]'; - $testObject = [PSCustomObject]@{ - Name = 'LoggingTest' - } - - $outDebug = @() - $originalDebugPreference = $DebugPreference; - - try { - $Global:DebugPreference = [System.Management.Automation.ActionPreference]::Continue; - $outDebug += ($testObject | Invoke-PSRule @loggingParams -Option $option 5>&1 | Where-Object { - $_ -like "* debug message*" - }); - } - finally { - $Global:DebugPreference = $originalDebugPreference; - } - - # Debug - $outDebug.Length | Should -Be 2; - $outDebug[0] | Should -Be 'Script debug message'; - $outDebug[1] | Should -Be 'Rule debug message 2'; - } - - It 'LimitVerbose' { - $withLoggingRulePath = (Join-Path -Path $here -ChildPath 'FromFileWithLogging.Rule.ps1'); - $loggingParams = @{ - Path = $withLoggingRulePath - Name = 'WithVerbose', 'WithVerbose2' - WarningAction = 'SilentlyContinue' - ErrorAction = 'SilentlyContinue' - InformationAction = 'SilentlyContinue' - } - $option = New-PSRuleOption -LoggingLimitVerbose 'WithVerbose2', '[Discovery.Rule]'; - $testObject = [PSCustomObject]@{ - Name = 'LoggingTest' - } - - $outVerbose = @($testObject | Invoke-PSRule @loggingParams -Option $option -Verbose 4>&1 | Where-Object { - $_ -is [System.Management.Automation.VerboseRecord] -and - $_.Message -like "* verbose message*" - }); - - # Verbose - $outVerbose.Length | Should -Be 2; - $outVerbose[0] | Should -Be 'Script verbose message'; - $outVerbose[1] | Should -Be 'Rule verbose message 2'; - } - } - Context 'Suppression output warnings' { BeforeAll { $testObject = @( @@ -1437,19 +1311,19 @@ Describe 'Test-PSRuleTarget' -Tag 'Test-PSRuleTarget','Common' { # Check result with no matching rules $result = $testObject | Test-PSRuleTarget -Path $ruleFilePath -Name 'NotARule' -ErrorVariable outErrors -ErrorAction SilentlyContinue; $result | Should -BeNullOrEmpty; - $outErrors | Should -Be 'PSR0016: Could not find a matching rule. Please check that Path, Name, and Tag parameters are correct. See https://aka.ms/ps-rule/troubleshooting'; + $outErrors | Should -Be 'PSR0016: Could not find a matching rule. Please check that Path, Name, and Tag parameters are correct. See https://aka.ms/ps-rule/troubleshooting-v3'; # Json $jsonRuleFilePath = Join-Path -Path $here -ChildPath 'FromFileEmpty.Rule.jsonc' $result = $testObject | Invoke-PSRule -Path $jsonRuleFilePath -ErrorVariable outErrors -ErrorAction SilentlyContinue; $result | Should -BeNullOrEmpty; - $outErrors | Should -Be 'PSR0016: Could not find a matching rule. Please check that Path, Name, and Tag parameters are correct. See https://aka.ms/ps-rule/troubleshooting'; + $outErrors | Should -Be 'PSR0016: Could not find a matching rule. Please check that Path, Name, and Tag parameters are correct. See https://aka.ms/ps-rule/troubleshooting-v3'; # Yaml $yamlRuleFilePath = Join-Path -Path $here -ChildPath 'FromFileEmpty.Rule.yaml' $result = $testObject | Invoke-PSRule -Path $yamlRuleFilePath -ErrorVariable outErrors -ErrorAction SilentlyContinue; $result | Should -BeNullOrEmpty; - $outErrors | Should -Be 'PSR0016: Could not find a matching rule. Please check that Path, Name, and Tag parameters are correct. See https://aka.ms/ps-rule/troubleshooting'; + $outErrors | Should -Be 'PSR0016: Could not find a matching rule. Please check that Path, Name, and Tag parameters are correct. See https://aka.ms/ps-rule/troubleshooting-v3'; } It 'Returns warning with empty path' { @@ -1461,7 +1335,7 @@ Describe 'Test-PSRuleTarget' -Tag 'Test-PSRuleTarget','Common' { $errorMessages = @($outErrors); $errorMessages.Length | Should -Be 1; $errorMessages[0] | Should -BeOfType [System.Management.Automation.ErrorRecord]; - $errorMessages[0].Exception.Message | Should -Be 'PSR0015: No valid sources were found. Please check your working path and configured options. See https://aka.ms/ps-rule/troubleshooting'; + $errorMessages[0].Exception.Message | Should -Be 'PSR0015: No valid sources were found. Please check your working path and configured options. See https://aka.ms/ps-rule/troubleshooting-v3'; } It 'Returns warning when not processed' { @@ -1692,7 +1566,7 @@ Describe 'Assert-PSRule' -Tag 'Assert-PSRule','Common' { $result = $testObject | Assert-PSRule @assertParams -WarningAction SilentlyContinue 6>&1 | Out-String; $result | Should -Not -BeNullOrEmpty; $result | Should -BeOfType System.String; - $result | Should -Not -Match "\[WARN\] This is a warning"; + $result | Should -Not -Contain "[WARN] This is a warning"; } It 'Writes output to file' { @@ -2343,11 +2217,6 @@ Describe 'Get-PSRule' -Tag 'Get-PSRule','Common' { } } - # Context 'Get rule with invalid path' { - # # TODO: Test with invalid path - # $result = Get-PSRule -Path (Join-Path -Path $here -ChildPath invalid); - # } - Context 'With constrained language' { BeforeAll { Mock -CommandName IsDeviceGuardEnabled -ModuleName PSRule -Verifiable -MockWith { @@ -2628,14 +2497,14 @@ Describe 'Rules' -Tag 'Common', 'Rules' { } It 'Error on nested rules' { - $filteredResult = @($messages | Where-Object { $_.Exception -is [PSRule.Pipeline.ParseException] -and $_.Exception.ErrorId -eq 'PSRule.Parse.InvalidRuleNesting' }); + $filteredResult = @($messages | Where-Object { $_.Exception -is [PSRule.Pipeline.ParseException] -and $_.Exception.ErrorId -eq 'PSR0020' }); $filteredResult.Length | Should -Be 1; $filteredResult[0].Exception | Should -BeOfType PSRule.Pipeline.ParseException; $filteredResult[0].Exception.Message | Should -BeLike 'Rule nesting was detected for rule at *'; } It 'Error on missing parameter' { - $filteredResult = @($messages | Where-Object { $_.Exception -is [PSRule.Pipeline.ParseException] -and $_.Exception.ErrorId -eq 'PSRule.Parse.RuleParameterNotFound' }); + $filteredResult = @($messages | Where-Object { $_.Exception -is [PSRule.Pipeline.ParseException] -and $_.Exception.ErrorId -eq 'PSR0019' }); $filteredResult.Length | Should -Be 2; $filteredResult.Exception | Should -BeOfType PSRule.Pipeline.ParseException; $filteredResult[0].Exception.Message | Should -BeLike 'Could not find required rule definition parameter ''Name'' on rule at * line *'; @@ -2643,14 +2512,14 @@ Describe 'Rules' -Tag 'Common', 'Rules' { } It 'Error on invalid ErrorAction' { - $filteredResult = @($messages | Where-Object { $_.Exception -is [PSRule.Pipeline.ParseException] -and $_.Exception.ErrorId -eq 'PSRule.Parse.InvalidErrorAction' }); + $filteredResult = @($messages | Where-Object { $_.Exception -is [PSRule.Pipeline.ParseException] -and $_.Exception.ErrorId -eq 'PSR0021' }); $filteredResult.Length | Should -Be 1; $filteredResult.Exception | Should -BeOfType PSRule.Pipeline.ParseException; $filteredResult[0].Exception.Message | Should -BeLike 'An invalid ErrorAction (*) was specified for rule at *'; } It 'Error on invalid name' { - $filteredResult = @($messages | Where-Object { $_.Exception -is [PSRule.Pipeline.ParseException] -and $_.Exception.ErrorId -eq 'PSRule.Parse.InvalidResourceName' }); + $filteredResult = @($messages | Where-Object { $_.Exception -is [PSRule.Pipeline.ParseException] -and $_.Exception.ErrorId -eq 'PSR0018' }); $filteredResult.Length | Should -Be 1; $filteredResult.Exception | Should -BeOfType PSRule.Pipeline.ParseException; $filteredResult[0].Exception.Message | Should -BeLike "The resource name '' is not valid at * line 16. Each resource name must be between 3-128 characters in length, must start and end with a letter or number, and only contain letters, numbers, hyphens, dots, or underscores. See https://aka.ms/ps-rule/naming for more information."; @@ -2680,7 +2549,7 @@ Describe 'Rules' -Tag 'Common', 'Rules' { # Errors $errorsOut.Length | Should -Be 1; $errorsOut[0] | Should -BeLike '*Some error 1*'; - $errorsOut[0].FullyQualifiedErrorId | Should -BeLike '*,WithRuleErrorActionDefault,Invoke-PSRule'; + $errorsOut[0].FullyQualifiedErrorId | Should -Be 'Invoke-PSRule'; } It 'Ignore handled exception' { diff --git a/tests/PSRule.Tests/PSRule.EndToEnd.Tests.ps1 b/tests/PSRule.Tests/PSRule.EndToEnd.Tests.ps1 index 1a0e61314d..3fe9e4e6c3 100644 --- a/tests/PSRule.Tests/PSRule.EndToEnd.Tests.ps1 +++ b/tests/PSRule.Tests/PSRule.EndToEnd.Tests.ps1 @@ -207,43 +207,3 @@ Describe 'Scenarios -- rule-module' -Tag 'EndToEnd', 'rule-module' { } } } - -# Describe 'Scenarios -- validation-pipeline' -Tag 'EndToEnd', 'validation-pipeline' { -# BeforeAll { -# $scenarioPath = Join-Path -Path $rootPath -ChildPath docs/scenarios/validation-pipeline; -# $sourcePath = Join-Path -Path $rootPath -ChildPath src/PSRule; -# $sourceFiles = Get-ChildItem -Path $sourcePath -Recurse -Include *.ps1,*.psm1,*.psd1; -# } - -# Context 'Invoke-PSRule' { -# BeforeAll { -# $option = New-PSRuleOption -Option @{ 'Logging.RuleFail' = 'Error'; }; -# $result = $sourceFiles | Invoke-PSRule -Path $scenarioPath -Option $option; -# } - -# It 'Module quality' { -# $fail = @($result | Where-Object -FilterScript { !$_.IsSuccess() }); -# $fail.Length | Should -Be 0; -# $pass = @($result | Where-Object -FilterScript { $_.IsSuccess() }); -# $pass.Length | Should -BeGreaterThan 0; -# } - -# It 'Use header' { -# $filteredResult = @($result | Where-Object -FilterScript { $_.RuleName -eq 'file.Header' }); -# $filteredResult | Should -Not -BeNullOrEmpty; -# } - -# It 'Use encoding' { -# $filteredResult = @($result | Where-Object -FilterScript { $_.RuleName -eq 'file.Encoding' }); -# $filteredResult | Should -Not -BeNullOrEmpty; -# } - -# It 'Use NUnit output' { -# $report = [Xml]($sourceFiles | Invoke-PSrule -Path $scenarioPath -Option $option -OutputFormat NUnit3); -# $report.Save((Join-Path -Path $reportPath -ChildPath 'rule.report.xml')); -# $items = @($report.DocumentElement.'test-suite'); -# Test-Path -Path (Join-Path -Path $reportPath -ChildPath 'rule.report.xml') | Should -be $True; -# $items.Length | Should -Be 5; -# } -# } -# } diff --git a/tests/PSRule.Tests/PSRule.Options.Tests.ps1 b/tests/PSRule.Tests/PSRule.Options.Tests.ps1 index 23abaafb71..41da53b08f 100644 --- a/tests/PSRule.Tests/PSRule.Options.Tests.ps1 +++ b/tests/PSRule.Tests/PSRule.Options.Tests.ps1 @@ -1840,94 +1840,6 @@ Describe 'New-PSRuleOption' -Tag 'Option','New-PSRuleOption' { } } - Context 'Read Logging.LimitDebug' { - It 'from default' { - $option = New-PSRuleOption -Default; - $option.Logging.LimitDebug | Should -BeNullOrEmpty; - } - - It 'from Hashtable' { - $option = New-PSRuleOption -Option @{ 'Logging.LimitDebug' = 'TestRule1' }; - $option.Logging.LimitDebug | Should -Be 'TestRule1'; - } - - It 'from YAML' { - $option = New-PSRuleOption -Option (Join-Path -Path $here -ChildPath 'PSRule.Tests.yml'); - $option.Logging.LimitDebug | Should -Be 'TestRule2'; - } - - It 'from parameter' { - $option = New-PSRuleOption -LoggingLimitDebug 'TestRule2' -Path $emptyOptionsFilePath; - $option.Logging.LimitDebug | Should -Be 'TestRule2'; - } - } - - Context 'Read Logging.LimitVerbose' { - It 'from default' { - $option = New-PSRuleOption -Default; - $option.Logging.LimitVerbose | Should -BeNullOrEmpty; - } - - It 'from Hashtable' { - $option = New-PSRuleOption -Option @{ 'Logging.LimitVerbose' = 'TestRule1' }; - $option.Logging.LimitVerbose | Should -Be 'TestRule1'; - } - - It 'from YAML' { - $option = New-PSRuleOption -Option (Join-Path -Path $here -ChildPath 'PSRule.Tests.yml'); - $option.Logging.LimitVerbose | Should -Be 'TestRule2'; - } - - It 'from parameter' { - $option = New-PSRuleOption -LoggingLimitVerbose 'TestRule2' -Path $emptyOptionsFilePath; - $option.Logging.LimitVerbose | Should -Be 'TestRule2'; - } - } - - Context 'Read Logging.RuleFail' { - It 'from default' { - $option = New-PSRuleOption -Default; - $option.Logging.RuleFail | Should -Be 'None'; - } - - It 'from Hashtable' { - $option = New-PSRuleOption -Option @{ 'Logging.RuleFail' = 'Error' }; - $option.Logging.RuleFail | Should -Be 'Error'; - } - - It 'from YAML' { - $option = New-PSRuleOption -Option (Join-Path -Path $here -ChildPath 'PSRule.Tests.yml'); - $option.Logging.RuleFail | Should -Be 'Warning'; - } - - It 'from parameter' { - $option = New-PSRuleOption -LoggingRuleFail 'Warning' -Path $emptyOptionsFilePath; - $option.Logging.RuleFail | Should -Be 'Warning'; - } - } - - Context 'Read Logging.RulePass' { - It 'from default' { - $option = New-PSRuleOption -Default; - $option.Logging.RulePass | Should -Be 'None'; - } - - It 'from Hashtable' { - $option = New-PSRuleOption -Option @{ 'Logging.RulePass' = 'Error' }; - $option.Logging.RulePass | Should -Be 'Error'; - } - - It 'from YAML' { - $option = New-PSRuleOption -Option (Join-Path -Path $here -ChildPath 'PSRule.Tests.yml'); - $option.Logging.RulePass | Should -Be 'Warning'; - } - - It 'from parameter' { - $option = New-PSRuleOption -LoggingRulePass 'Warning' -Path $emptyOptionsFilePath; - $option.Logging.RulePass | Should -Be 'Warning'; - } - } - Context 'Read Output.As' { It 'from default' { $option = New-PSRuleOption -Default; @@ -2761,34 +2673,6 @@ Describe 'Set-PSRuleOption' -Tag 'Option','Set-PSRuleOption' { } } - Context 'Read Logging.LimitDebug' { - It 'from parameter' { - $option = Set-PSRuleOption -LoggingLimitDebug 'TestRule2' @optionParams; - $option.Logging.LimitDebug | Should -Be 'TestRule2'; - } - } - - Context 'Read Logging.LimitVerbose' { - It 'from parameter' { - $option = Set-PSRuleOption -LoggingLimitVerbose 'TestRule2' @optionParams; - $option.Logging.LimitVerbose | Should -Be 'TestRule2'; - } - } - - Context 'Read Logging.RuleFail' { - It 'from parameter' { - $option = Set-PSRuleOption -LoggingRuleFail 'Warning' @optionParams; - $option.Logging.RuleFail | Should -Be 'Warning'; - } - } - - Context 'Read Logging.RulePass' { - It 'from parameter' { - $option = Set-PSRuleOption -LoggingRulePass 'Warning' @optionParams; - $option.Logging.RulePass | Should -Be 'Warning'; - } - } - Context 'Read Output.As' { It 'from parameter' { $option = Set-PSRuleOption -OutputAs 'Summary' @optionParams; diff --git a/tests/PSRule.Tests/PSRule.Tests.csproj b/tests/PSRule.Tests/PSRule.Tests.csproj index 0c408ee216..3ad3d01171 100644 --- a/tests/PSRule.Tests/PSRule.Tests.csproj +++ b/tests/PSRule.Tests/PSRule.Tests.csproj @@ -182,6 +182,9 @@ PreserveNewest + + PreserveNewest + diff --git a/tests/PSRule.Tests/Pipeline/InputPathBuilderTests.cs b/tests/PSRule.Tests/Pipeline/InputPathBuilderTests.cs index 02828a6fc6..a61191dc0a 100644 --- a/tests/PSRule.Tests/Pipeline/InputPathBuilderTests.cs +++ b/tests/PSRule.Tests/Pipeline/InputPathBuilderTests.cs @@ -66,7 +66,7 @@ public void Build_WithInvalidPathAdded_ShouldReturnEmpty() var actual = builder.Build(); Assert.Empty(actual); - Assert.True(writer.Errors.Count(r => r.FullyQualifiedErrorId == "PSRule.ReadInputFailed") == 1); + Assert.True(writer.Errors.Count(r => r.eventId.Name == "PSRule.ReadInputFailed") == 1); } [Fact] diff --git a/tests/PSRule.Tests/Pipeline/Output/HostPipelineWriterTests.cs b/tests/PSRule.Tests/Pipeline/Output/HostPipelineWriterTests.cs new file mode 100644 index 0000000000..72778f4a8d --- /dev/null +++ b/tests/PSRule.Tests/Pipeline/Output/HostPipelineWriterTests.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Pipeline.Output; + +/// +/// Unit tests for . +/// +public sealed class HostPipelineWriterTests : OutputWriterBaseTests +{ + [Fact] + public void WriteObject_WithSimpleObject_ShouldInvokeHostContextForObject() + { + var hostContext = new Mock(); + var writer = new HostPipelineWriter(hostContext.Object, GetOption(), null); + var expected = "TestObject"; + + writer.WriteObject(expected, false); + + hostContext.Verify(h => h.WriteObject(expected, false), Times.Once); + } + + [Fact] + public void WriteResult_WithResult_ShouldInvokeHostContextForEachRecord() + { + var hostContext = new Mock(); + var writer = new HostPipelineWriter(hostContext.Object, GetOption(), null); + var expected = new InvokeResult(GetRun()); + var pass = GetPass(); + var fail = GetFail(); + expected.Add(pass); + expected.Add(fail); + + writer.WriteResult(expected); + + hostContext.Verify(h => h.WriteObject(pass, false), Times.Once); + hostContext.Verify(h => h.WriteObject(fail, false), Times.Once); + } +} diff --git a/tests/PSRule.Tests/Pipeline/PipelineTests.cs b/tests/PSRule.Tests/Pipeline/PipelineTests.cs index 292031be8e..fe3a7d62fb 100644 --- a/tests/PSRule.Tests/Pipeline/PipelineTests.cs +++ b/tests/PSRule.Tests/Pipeline/PipelineTests.cs @@ -24,14 +24,18 @@ public void InvokePipeline() var option = GetOption(); option.Rule.Include = ["FromFile1"]; var builder = PipelineBuilder.Invoke(GetSource(), option, null); - var pipeline = builder.Build(); + var writer = GetTestWriter(option); + var pipeline = builder.Build(writer); Assert.NotNull(pipeline); pipeline.Begin(); for (var i = 0; i < 100; i++) + { pipeline.Process(PSObject.AsPSObject(testObject1)); - + } pipeline.End(); + + Assert.Equal(100, writer.Output.Count); } [Fact] @@ -54,7 +58,7 @@ public void InvokePipeline_WithJObject_ShouldRunSuccessfully() var option = GetOption(); option.Rule.Include = ["ScriptReasonTest"]; var builder = PipelineBuilder.Invoke(GetSource(), option, null); - var writer = new TestWriter(option); + var writer = GetTestWriter(option); var pipeline = builder.Build(writer); Assert.NotNull(pipeline); @@ -98,7 +102,7 @@ public void InvokePipeline_WithPathPrefix_ShouldRunSuccessfully() var option = GetOption(); option.Rule.Include = ["WithPathPrefix"]; var builder = PipelineBuilder.Invoke(GetSource(), option, null); - var writer = new TestWriter(option); + var writer = GetTestWriter(option); var pipeline = builder.Build(writer); Assert.NotNull(pipeline); @@ -237,7 +241,7 @@ public void PipelineWithSource() ]; // Default - var writer = new TestWriter(option); + var writer = GetTestWriter(option); var builder = PipelineBuilder.Invoke(GetSource(), option, null); builder.InputPath(["./**/ObjectFromFile*.json"]); var pipeline = builder.Build(writer); @@ -256,7 +260,7 @@ public void PipelineWithSource() // With reason full path option.Rule.Include = ["ScriptReasonTest"]; - writer = new TestWriter(option); + writer = GetTestWriter(option); builder = PipelineBuilder.Invoke(GetSource(), option, null); PipelineBuilder.Invoke(GetSource(), option, null); builder.InputPath(["./**/ObjectFromFile*.json"]); @@ -281,7 +285,7 @@ public void PipelineWithSource() // With IgnoreObjectSource option.Rule.Include = ["FromFile1"]; option.Input.IgnoreObjectSource = true; - writer = new TestWriter(option); + writer = GetTestWriter(option); builder = PipelineBuilder.Invoke(GetSource(), option, null); PipelineBuilder.Invoke(GetSource(), option, null); builder.InputPath(["./**/ObjectFromFile*.json"]); @@ -299,6 +303,32 @@ public void PipelineWithSource() //Assert.True(items[2].HasSource()); } + [Fact] + public void Invoke_WhenRuleLogs_ShouldReturnMessage() + { + var testObject1 = new TestObject { Name = "TestObject1" }; + var option = GetOption(); + var builder = PipelineBuilder.Invoke(GetSource("FromFileWithLogging.Rule.ps1"), option, null); + var writer = GetTestWriter(option); + var pipeline = builder.Build(writer); + + Assert.NotNull(pipeline); + pipeline.Begin(); + pipeline.Process(PSObject.AsPSObject(testObject1)); + pipeline.End(); + + Assert.Contains(writer.Warnings, s => s == "Script warning message"); + Assert.Contains(writer.Warnings, s => s == "Rule warning message"); + + Assert.Contains(writer.Information, s => s.ToString() == "Script information message"); + Assert.Contains(writer.Information, s => s.ToString() == "Rule information message"); + + Assert.Single(writer.Output); + var result = writer.Output[0] as InvokeResult; + + Assert.Equal(RuleOutcome.Error, result.Outcome); + } + ///// ///// An Invoke pipeline reading from an input file with File format. ///// diff --git a/tests/PSRule.Tests/ResourceValidatorTests.cs b/tests/PSRule.Tests/ResourceValidatorTests.cs index a92cce743e..084437e809 100644 --- a/tests/PSRule.Tests/ResourceValidatorTests.cs +++ b/tests/PSRule.Tests/ResourceValidatorTests.cs @@ -37,7 +37,7 @@ public void GetRule_WithInvalidResourceName_ShouldReturnError(string path) var rule = HostHelper.GetRule(context, includeDependencies: false); Assert.NotNull(rule); Assert.NotEmpty(writer.Errors); - Assert.Equal("PSRule.Parse.InvalidResourceName", writer.Errors[0].FullyQualifiedErrorId); + Assert.Equal("PSR0018", writer.Errors[0].eventId.Name); } [Fact] diff --git a/tests/PSRule.Tests/RuleLanguageAstTests.cs b/tests/PSRule.Tests/RuleLanguageAstTests.cs index 8cfd0467d0..5b70a65fdc 100644 --- a/tests/PSRule.Tests/RuleLanguageAstTests.cs +++ b/tests/PSRule.Tests/RuleLanguageAstTests.cs @@ -10,10 +10,11 @@ public sealed class RuleLanguageAstTests : BaseTests [Fact] public void RuleName() { + var logger = GetTestWriter(); var scriptAst = System.Management.Automation.Language.Parser.ParseFile(GetSourcePath("FromFileName.Rule.ps1"), out _, out _); - var visitor = new RuleLanguageAst(); + var visitor = new RuleLanguageAst(logger); scriptAst.Visit(visitor); - Assert.Equal("PSRule.Parse.InvalidResourceName", visitor.Errors[0].FullyQualifiedErrorId); + Assert.Equal("PSR0018", logger.Errors[0].eventId.Name); } } diff --git a/tests/PSRule.Tests/SourcePipelineBuilderTests.cs b/tests/PSRule.Tests/SourcePipelineBuilderTests.cs index 675c5dd440..5e7214da0b 100644 --- a/tests/PSRule.Tests/SourcePipelineBuilderTests.cs +++ b/tests/PSRule.Tests/SourcePipelineBuilderTests.cs @@ -30,7 +30,7 @@ public void Add_directory() var sources = builder.Build(); Assert.Single(sources); - Assert.Equal(25, sources[0].File.Length); + Assert.Equal(26, sources[0].File.Length); } [Fact] diff --git a/tests/PSRule.Tests/TestWriter.cs b/tests/PSRule.Tests/TestWriter.cs index 32ea5489a7..c912646727 100644 --- a/tests/PSRule.Tests/TestWriter.cs +++ b/tests/PSRule.Tests/TestWriter.cs @@ -1,42 +1,20 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Collections.Generic; -using System.Management.Automation; using PSRule.Configuration; using PSRule.Pipeline; +using PSRule.Runtime; namespace PSRule; -internal sealed class TestWriter : PipelineWriter +internal sealed class TestWriter(PSRuleOption option) : PipelineWriter(null, option, null) { - internal List Errors = new(); - internal List Warnings = new(); - internal List Information = new(); - internal List Output = new(); - - public TestWriter(PSRuleOption option) - : base(null, option, null) { } - - public override void WriteError(ErrorRecord errorRecord) - { - Errors.Add(errorRecord); - } - - public override void WriteWarning(string message) - { - Warnings.Add(message); - } - - public override bool ShouldWriteError() - { - return true; - } - - public override bool ShouldWriteWarning() - { - return true; - } + internal List<(EventId eventId, string message, Exception exception)> Errors = []; + internal List Warnings = []; + internal List Information = []; + internal List Output = []; public override void WriteObject(object sendToPipeline, bool enumerateCollection) { @@ -53,13 +31,30 @@ public override void WriteObject(object sendToPipeline, bool enumerateCollection } } - public override bool ShouldWriteInformation() + public override bool IsEnabled(LogLevel logLevel) { - return true; + return logLevel == LogLevel.Information || + logLevel == LogLevel.Warning || + logLevel == LogLevel.Error || + logLevel == LogLevel.Critical; } - public override void WriteInformation(InformationRecord informationRecord) + public override void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { - Information.Add(informationRecord.MessageData); + switch (logLevel) + { + case LogLevel.Information: + Information.Add(formatter(state, exception)); + break; + + case LogLevel.Warning: + Warnings.Add(formatter(state, exception)); + break; + + case LogLevel.Error: + case LogLevel.Critical: + Errors.Add(new(eventId, formatter(state, exception), exception)); + break; + } } } From dbceae60dba13c67c95ba9508cf3a599bd6059a7 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Mon, 5 May 2025 13:11:55 +1000 Subject: [PATCH 2/3] Fix --- src/PSRule/Pipeline/IHostContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PSRule/Pipeline/IHostContext.cs b/src/PSRule/Pipeline/IHostContext.cs index 86032653ed..9d6a7a28d8 100644 --- a/src/PSRule/Pipeline/IHostContext.cs +++ b/src/PSRule/Pipeline/IHostContext.cs @@ -71,7 +71,7 @@ public interface IHostContext : ILogger /// /// Get the current working path. /// - string GetWorkingPath(); + string GetWorkingPath(); /// /// Set the terminating exit code of the pipeline. From 0c36414b6eb3e4753b21de8390af7310284cfc89 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Mon, 5 May 2025 22:54:11 +1000 Subject: [PATCH 3/3] Fix --- tests/PSRule.Tests/SourcePipelineBuilderTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PSRule.Tests/SourcePipelineBuilderTests.cs b/tests/PSRule.Tests/SourcePipelineBuilderTests.cs index ad1a8f8c05..1e63152b05 100644 --- a/tests/PSRule.Tests/SourcePipelineBuilderTests.cs +++ b/tests/PSRule.Tests/SourcePipelineBuilderTests.cs @@ -80,7 +80,7 @@ public void Directory_WithScriptFileAndModuleOnly_ShouldNotFindAny(string path) } [Theory] - [InlineData("", 28)] + [InlineData("", 29)] [InlineData("John's Documents", 1)] public void Directory_WithDirectory_ShouldFindCount(string path, int count) {