diff --git a/src/PSRule/Host/HostHelper.cs b/src/PSRule/Host/HostHelper.cs index 4628bb292f..f6dc945b6f 100644 --- a/src/PSRule/Host/HostHelper.cs +++ b/src/PSRule/Host/HostHelper.cs @@ -20,6 +20,11 @@ public static IEnumerable GetRule(RuleSource[] source, RuleFilter filter) return ToRule(GetLanguageBlock(sources: source), filter); } + public static RuleHelpInfo GetRuleHelp(RuleSource[] source, RuleFilter filter) + { + return ToRuleHelp(GetLanguageBlock(sources: source), filter); + } + public static DependencyGraph GetRuleBlockGraph(RuleSource[] source, RuleFilter filter) { var builder = new DependencyGraphBuilder(); @@ -206,5 +211,33 @@ private static Rule[] ToRule(IEnumerable blocks, RuleFilter filt return results.Values.ToArray(); } + + private static RuleHelpInfo ToRuleHelp(IEnumerable blocks, RuleFilter filter) + { + // Index deployments by environment/name + var results = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var block in blocks.OfType()) + { + // Ignore deployment blocks that don't match + if (filter != null && !filter.Match(block)) + { + continue; + } + + if (!results.ContainsKey(block.RuleId)) + { + results[block.RuleId] = new RuleHelpInfo + { + Name = block.RuleName, + Synopsis = block.Description, + Recommendation = block.Recommendation, + Annotations = block.Annotations.ToHashtable() + }; + } + } + + return results.Values.FirstOrDefault(); + } } } diff --git a/src/PSRule/PSRule.Format.ps1xml b/src/PSRule/PSRule.Format.ps1xml index d6ba527ddc..5089ce964f 100644 --- a/src/PSRule/PSRule.Format.ps1xml +++ b/src/PSRule/PSRule.Format.ps1xml @@ -1,5 +1,73 @@ + + + Help-Name + + + + + + + + 4 + + + NAME + + + + + + + + + + + Help-Synopsis + + + + + + + + 4 + + + Synopsis + + + + + + + + + + + Help-Recommendation + + + + + + + + 4 + + + Recommendation + + + + + + + + + + PSRule.Rules.Rule @@ -9,14 +77,14 @@ - + - + - + @@ -47,15 +115,15 @@ - + - + - + @@ -83,19 +151,19 @@ - + - + - + - + @@ -118,5 +186,38 @@ + + PSRule.Rules.RuleHelpInfo + + PSRule.Rules.RuleHelpInfo + + + + + + + Help-Name + + + + + + Help-Synopsis + + + + + + Help-Recommendation + + + + + + + + + + diff --git a/src/PSRule/PSRule.psd1 b/src/PSRule/PSRule.psd1 index 4c6d71e5ce..3616807c15 100644 --- a/src/PSRule/PSRule.psd1 +++ b/src/PSRule/PSRule.psd1 @@ -76,6 +76,7 @@ FunctionsToExport = @( 'Invoke-PSRule' 'Test-PSRuleTarget' 'Get-PSRule' + 'Get-PSRuleHelp' 'New-PSRuleOption' 'Set-PSRuleOption' 'AllOf' diff --git a/src/PSRule/PSRule.psm1 b/src/PSRule/PSRule.psm1 index 39bc17863a..d107a0c286 100644 --- a/src/PSRule/PSRule.psm1 +++ b/src/PSRule/PSRule.psm1 @@ -436,6 +436,119 @@ function Get-PSRule { } } +# .ExternalHelp PSRule-Help.xml +function Get-PSRuleHelp { + [CmdletBinding()] + [OutputType([PSRule.Rules.RuleHelpInfo])] + param ( + # A list of paths to check for rule definitions + [Parameter(Position = 0, Mandatory = $False)] + [Alias('p')] + [String]$Path = $PWD, + + # Filter to rules with the following names + [Parameter(Mandatory = $True)] + [Alias('n')] + [String]$Name, + + [Parameter(Mandatory = $False)] + [String]$Module, + + [Parameter(Mandatory = $False)] + [String]$Culture, + + [Parameter(Mandatory = $False)] + [Switch]$Online = $False + ) + + begin { + Write-Verbose -Message "[Get-PSRuleHelp]::BEGIN"; + + # Get parameter options, which will override options from other sources + $optionParams = @{ }; + + if ($PSBoundParameters.ContainsKey('Option')) { + $optionParams['Option'] = $Option; + } + + # Get an options object + $Option = New-PSRuleOption @optionParams; + + # Discover scripts in the specified paths + $sourceParams = @{ }; + + if ($PSBoundParameters.ContainsKey('Path')) { + $sourceParams['Path'] = $Path; + } + if ($PSBoundParameters.ContainsKey('Module')) { + $sourceParams['Module'] = $Module; + } + if ($sourceParams.Count -eq 0) { + $sourceParams['Path'] = $Path; + } + if ($PSBoundParameters.ContainsKey('Culture')) { + $sourceParams['Culture'] = $Culture; + } + [PSRule.Rules.RuleSource[]]$sourceFiles = GetRuleScriptPath @sourceParams -Verbose:$VerbosePreference; + + # Check that some matching script files were found + if ($Null -eq $sourceFiles) { + Write-Verbose -Message "[Get-PSRuleHelp] -- Could not find any .Rule.ps1 script files in the path"; + return; # continue causes issues with Pester + } + + Write-Verbose -Message "[Get-PSRuleHelp] -- Found $($sourceFiles.Length) script(s)"; + + $isDeviceGuard = IsDeviceGuardEnabled; + + # If DeviceGuard is enabled, force a contrained execution environment + if ($isDeviceGuard) { + $Option.Execution.LanguageMode = [PSRule.Configuration.LanguageMode]::ConstrainedLanguage; + } + + if ($PSBoundParameters.ContainsKey('Name')) { + $Option.Baseline.RuleName = $Name; + } + + $builder = [PSRule.Pipeline.PipelineBuilder]::GetHelp().Configure($Option); + $builder.Source($sourceFiles); + $builder.UseCommandRuntime($PSCmdlet.CommandRuntime); + $builder.UseLoggingPreferences($ErrorActionPreference, $WarningPreference, $VerbosePreference, $InformationPreference); + $pipeline = $builder.Build(); + } + + process { + if ($Null -ne (Get-Variable -Name pipeline -ErrorAction SilentlyContinue)) { + try { + # Get matching rule help + $result = $pipeline.Process(); + + if ($Null -ne $result -and $Online) { + $launchUri = $result.GetOnlineHelpUri(); + + if ($Null -ne $launchUri) { + LaunchOnlineHelp -Uri $launchUri -Verbose:$VerbosePreference; + } + } + else { + $result; + } + } + catch { + $pipeline.Dispose(); + throw; + } + } + } + + end { + if ($Null -ne (Get-Variable -Name pipeline -ErrorAction SilentlyContinue)) { + $pipeline.Dispose(); + } + Write-Verbose -Message "[Get-PSRuleHelp]::END"; + } +} + # .ExternalHelp PSRule-Help.xml function New-PSRuleOption { [CmdletBinding()] @@ -1180,12 +1293,9 @@ function YamlContainsComments { } function IsDeviceGuardEnabled { - [CmdletBinding()] [OutputType([System.Boolean])] - param ( - - ) + param () process { @@ -1202,13 +1312,26 @@ function IsDeviceGuardEnabled { } } -function InitEditorServices { - +function LaunchOnlineHelp { [CmdletBinding()] + [OutputType([void])] param ( - + [Parameter(Mandatory = $True)] + [System.Uri]$Uri ) + process { + $launchProcess = New-Object -TypeName System.Diagnostics.Process; + $launchProcess.StartInfo.FileName = $Uri.OriginalString; + $launchProcess.StartInfo.UseShellExecute = $True; + $Null = $launchProcess.Start(); + } +} + +function InitEditorServices { + [CmdletBinding()] + param () + process { if ($Null -ne (Get-Variable -Name psEditor -ErrorAction Ignore)) { # Export keywords @@ -1255,6 +1378,7 @@ Export-ModuleMember -Function @( 'Invoke-PSRule' 'Test-PSRuleTarget' 'Get-PSRule' + 'Get-PSRuleHelp' 'New-PSRuleOption' 'Set-PSRuleOption' ) diff --git a/src/PSRule/Pipeline/GetRuleHelpPipeline.cs b/src/PSRule/Pipeline/GetRuleHelpPipeline.cs new file mode 100644 index 0000000000..1ea08b3d83 --- /dev/null +++ b/src/PSRule/Pipeline/GetRuleHelpPipeline.cs @@ -0,0 +1,20 @@ +using PSRule.Configuration; +using PSRule.Host; +using PSRule.Rules; + +namespace PSRule.Pipeline +{ + public sealed class GetRuleHelpPipeline : RulePipeline + { + internal GetRuleHelpPipeline(PSRuleOption option, RuleSource[] source, RuleFilter filter, PipelineContext context) + : base(context, option, source, filter) + { + // Do nothing + } + + public RuleHelpInfo Process() + { + return HostHelper.GetRuleHelp(source: _Source, filter: _Filter); + } + } +} diff --git a/src/PSRule/Pipeline/GetRuleHelpPipelineBuilder.cs b/src/PSRule/Pipeline/GetRuleHelpPipelineBuilder.cs new file mode 100644 index 0000000000..92275411db --- /dev/null +++ b/src/PSRule/Pipeline/GetRuleHelpPipelineBuilder.cs @@ -0,0 +1,70 @@ +using PSRule.Configuration; +using PSRule.Rules; +using System.Management.Automation; + +namespace PSRule.Pipeline +{ + public sealed class GetRuleHelpPipelineBuilder + { + private readonly PSRuleOption _Option; + private readonly PipelineLogger _Logger; + + private RuleSource[] _Source; + private bool _LogError; + private bool _LogWarning; + private bool _LogVerbose; + private bool _LogInformation; + + internal GetRuleHelpPipelineBuilder() + { + _Logger = new PipelineLogger(); + _Option = new PSRuleOption(); + } + + public void Source(RuleSource[] source) + { + _Source = source; + } + + public GetRuleHelpPipelineBuilder Configure(PSRuleOption option) + { + if (option == null) + { + return this; + } + + _Option.Execution.LanguageMode = option.Execution.LanguageMode ?? ExecutionOption.Default.LanguageMode; + + if (option.Baseline != null) + { + _Option.Baseline.RuleName = option.Baseline.RuleName; + _Option.Baseline.Exclude = option.Baseline.Exclude; + } + + return this; + } + + public void UseCommandRuntime(ICommandRuntime2 commandRuntime) + { + _Logger.OnWriteVerbose = commandRuntime.WriteVerbose; + _Logger.OnWriteWarning = commandRuntime.WriteWarning; + _Logger.OnWriteError = commandRuntime.WriteError; + _Logger.OnWriteInformation = commandRuntime.WriteInformation; + } + + public void UseLoggingPreferences(ActionPreference error, ActionPreference warning, ActionPreference verbose, ActionPreference information) + { + _LogError = (error != ActionPreference.Ignore); + _LogWarning = (warning != ActionPreference.Ignore); + _LogVerbose = !(verbose == ActionPreference.Ignore || verbose == ActionPreference.SilentlyContinue); + _LogInformation = !(information == ActionPreference.Ignore || information == ActionPreference.SilentlyContinue); + } + + public GetRuleHelpPipeline Build() + { + var filter = new RuleFilter(ruleName: _Option.Baseline.RuleName, tag: null, exclude: _Option.Baseline.Exclude); + var context = PipelineContext.New(logger: _Logger, option: _Option, bindTargetName: null, bindTargetType: null, logError: _LogError, logWarning: _LogWarning, logVerbose: _LogVerbose, logInformation: _LogInformation); + return new GetRuleHelpPipeline(option: _Option, source: _Source, filter: filter, context: context); + } + } +} diff --git a/src/PSRule/Pipeline/PipelineBuilder.cs b/src/PSRule/Pipeline/PipelineBuilder.cs index 8906dffd3f..761995f3b5 100644 --- a/src/PSRule/Pipeline/PipelineBuilder.cs +++ b/src/PSRule/Pipeline/PipelineBuilder.cs @@ -11,5 +11,10 @@ public static GetRulePipelineBuilder Get() { return new GetRulePipelineBuilder(); } + + public static GetRuleHelpPipelineBuilder GetHelp() + { + return new GetRuleHelpPipelineBuilder(); + } } } diff --git a/src/PSRule/Resources/FormatResources.Designer.cs b/src/PSRule/Resources/FormatResources.Designer.cs index 36ef9aecb6..d08fda95dc 100644 --- a/src/PSRule/Resources/FormatResources.Designer.cs +++ b/src/PSRule/Resources/FormatResources.Designer.cs @@ -60,6 +60,24 @@ internal FormatResources() { } } + /// + /// Looks up a localized string similar to Description. + /// + internal static string Description { + get { + return ResourceManager.GetString("Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fail. + /// + internal static string Fail { + get { + return ResourceManager.GetString("Fail", resourceCulture); + } + } + /// /// Looks up a localized string similar to RELATED LINKS. /// @@ -69,6 +87,24 @@ internal static string Links { } } + /// + /// Looks up a localized string similar to Message. + /// + internal static string Message { + get { + return ResourceManager.GetString("Message", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ModuleName. + /// + internal static string ModuleName { + get { + return ResourceManager.GetString("ModuleName", resourceCulture); + } + } + /// /// Looks up a localized string similar to NAME. /// @@ -87,6 +123,24 @@ internal static string Notes { } } + /// + /// Looks up a localized string similar to Outcome. + /// + internal static string Outcome { + get { + return ResourceManager.GetString("Outcome", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pass. + /// + internal static string Pass { + get { + return ResourceManager.GetString("Pass", resourceCulture); + } + } + /// /// Looks up a localized string similar to RECOMMENDATION. /// @@ -96,6 +150,15 @@ internal static string Recommendation { } } + /// + /// Looks up a localized string similar to RuleName. + /// + internal static string RuleName { + get { + return ResourceManager.GetString("RuleName", resourceCulture); + } + } + /// /// Looks up a localized string similar to SYNOPSIS. /// diff --git a/src/PSRule/Resources/FormatResources.resx b/src/PSRule/Resources/FormatResources.resx index 378ce1a053..8e451edec7 100644 --- a/src/PSRule/Resources/FormatResources.resx +++ b/src/PSRule/Resources/FormatResources.resx @@ -117,18 +117,39 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Description + + + Fail + RELATED LINKS + + Message + + + ModuleName + NAME NOTES + + Outcome + + + Pass + RECOMMENDATION + + RuleName + SYNOPSIS diff --git a/src/PSRule/Rules/RuleHelpInfo.cs b/src/PSRule/Rules/RuleHelpInfo.cs index 68fcd4d87c..c9ab83e010 100644 --- a/src/PSRule/Rules/RuleHelpInfo.cs +++ b/src/PSRule/Rules/RuleHelpInfo.cs @@ -1,10 +1,15 @@ -namespace PSRule.Rules +using System; +using System.Collections; + +namespace PSRule.Rules { /// /// Output view helper class for rule help. /// public sealed class RuleHelpInfo { + private const string ONLINE_HELP_LINK_ANNOTATION = "online version"; + /// /// The name of the rule. /// @@ -24,5 +29,29 @@ public sealed class RuleHelpInfo /// Additional notes for the rule. /// public string Notes { get; set; } + + /// + /// Metadata annotations for the rule. + /// + public Hashtable Annotations { get; set; } + + /// + /// Get the URI for the online version of the documentation. + /// + /// A URI when a valid link is set. Otherwise null is returned. + public Uri GetOnlineHelpUri() + { + if (Annotations == null || !Annotations.ContainsKey(ONLINE_HELP_LINK_ANNOTATION)) + { + return null; + } + + if (Uri.TryCreate(Annotations[ONLINE_HELP_LINK_ANNOTATION].ToString(), UriKind.Absolute, out Uri result)) + { + return result; + } + + return null; + } } }