diff --git a/src/PSRule/Configuration/BindingOption.cs b/src/PSRule/Configuration/BindingOption.cs
index 84fb7a72c6..fb8e0fad8c 100644
--- a/src/PSRule/Configuration/BindingOption.cs
+++ b/src/PSRule/Configuration/BindingOption.cs
@@ -7,8 +7,11 @@
namespace PSRule.Configuration;
///
-/// Options that affect property binding of TargetName and TargetType.
+/// Options that configure property binding.
///
+///
+/// See .
+///
public sealed class BindingOption : IEquatable, IBindingOption
{
private const bool DEFAULT_IGNORECASE = true;
diff --git a/src/PSRule/Configuration/IBindingOption.cs b/src/PSRule/Configuration/IBindingOption.cs
index 4591b27ae1..5c7c58ab3c 100644
--- a/src/PSRule/Configuration/IBindingOption.cs
+++ b/src/PSRule/Configuration/IBindingOption.cs
@@ -5,6 +5,67 @@
namespace PSRule.Configuration;
+///
+/// Options that configure property binding.
+///
+///
+/// See .
+///
public interface IBindingOption : IOption
{
+ ///
+ /// One or more custom fields to bind.
+ ///
+ ///
+ /// See .
+ ///
+ FieldMap Field { get; }
+
+ ///
+ /// Determines if custom binding uses ignores case when matching properties.
+ ///
+ ///
+ /// See .
+ ///
+ bool? IgnoreCase { get; }
+
+ ///
+ /// Configures the separator to use for building a qualified name.
+ ///
+ ///
+ /// See .
+ ///
+ string NameSeparator { get; }
+
+ ///
+ /// Determines if binding prefers target info provided by the object over custom configuration.
+ ///
+ ///
+ /// See .
+ ///
+ bool? PreferTargetInfo { get; }
+
+ ///
+ /// Property names to use to bind TargetName.
+ ///
+ ///
+ /// See .
+ ///
+ string[] TargetName { get; }
+
+ ///
+ /// Property names to use to bind TargetType.
+ ///
+ ///
+ /// See .
+ ///
+ string[] TargetType { get; }
+
+ ///
+ /// Determines if a qualified TargetName is used.
+ ///
+ ///
+ /// See .
+ ///
+ bool? UseQualifiedName { get; }
}
diff --git a/src/PSRule/Pipeline/GetTargetPipelineBuilder.cs b/src/PSRule/Pipeline/GetTargetPipelineBuilder.cs
index 9b2d93bbd6..382f1df9c5 100644
--- a/src/PSRule/Pipeline/GetTargetPipelineBuilder.cs
+++ b/src/PSRule/Pipeline/GetTargetPipelineBuilder.cs
@@ -63,42 +63,6 @@ public override IPipeline Build(IPipelineWriter writer = null)
///
protected override PipelineInputStream PrepareReader()
{
- if (!string.IsNullOrEmpty(Option.Input.ObjectPath))
- {
- AddVisitTargetObjectAction((sourceObject, next) =>
- {
- return PipelineReceiverActions.ReadObjectPath(sourceObject, next, Option.Input.ObjectPath, true);
- });
- }
-
- if (Option.Input.Format == InputFormat.Yaml)
- {
- AddVisitTargetObjectAction((sourceObject, next) =>
- {
- return PipelineReceiverActions.ConvertFromYaml(sourceObject, next);
- });
- }
- else if (Option.Input.Format == InputFormat.Json)
- {
- AddVisitTargetObjectAction((sourceObject, next) =>
- {
- return PipelineReceiverActions.ConvertFromJson(sourceObject, next);
- });
- }
- else if (Option.Input.Format == InputFormat.Markdown)
- {
- AddVisitTargetObjectAction((sourceObject, next) =>
- {
- return PipelineReceiverActions.ConvertFromMarkdown(sourceObject, next);
- });
- }
- else if (Option.Input.Format == InputFormat.PowerShellData)
- {
- AddVisitTargetObjectAction((sourceObject, next) =>
- {
- return PipelineReceiverActions.ConvertFromPowerShellData(sourceObject, next);
- });
- }
- return new PipelineInputStream(VisitTargetObject, _InputPath, GetInputObjectSourceFilter(), Option);
+ return new PipelineInputStream(_InputPath, GetInputObjectSourceFilter(), Option);
}
}
diff --git a/src/PSRule/Pipeline/IPipelineBuilder.cs b/src/PSRule/Pipeline/IPipelineBuilder.cs
new file mode 100644
index 0000000000..4c9612bd8b
--- /dev/null
+++ b/src/PSRule/Pipeline/IPipelineBuilder.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using PSRule.Configuration;
+
+namespace PSRule.Pipeline;
+
+///
+/// A helper to build a PSRule pipeline.
+///
+public interface IPipelineBuilder
+{
+ ///
+ /// Configure the pipeline with options.
+ ///
+ IPipelineBuilder Configure(PSRuleOption option);
+
+ ///
+ /// Configure the pipeline to use a specific baseline.
+ ///
+ /// A baseline option or the name of a baseline.
+ void Baseline(BaselineOption baseline);
+
+ ///
+ /// Build the pipeline.
+ ///
+ /// Optionally specify a custom writer which will handle output processing.
+ IPipeline Build(IPipelineWriter writer = null);
+}
diff --git a/src/PSRule/Pipeline/IPipelineReader.cs b/src/PSRule/Pipeline/IPipelineReader.cs
index 6f367c1ab3..3d6cb63fd1 100644
--- a/src/PSRule/Pipeline/IPipelineReader.cs
+++ b/src/PSRule/Pipeline/IPipelineReader.cs
@@ -5,6 +5,8 @@
namespace PSRule.Pipeline;
+#nullable enable
+
internal interface IPipelineReader
{
int Count { get; }
diff --git a/src/PSRule/Pipeline/InvokePipelineBuilderBase.cs b/src/PSRule/Pipeline/InvokePipelineBuilderBase.cs
index fc35064dcb..9d72d60609 100644
--- a/src/PSRule/Pipeline/InvokePipelineBuilderBase.cs
+++ b/src/PSRule/Pipeline/InvokePipelineBuilderBase.cs
@@ -131,42 +131,6 @@ private static bool IsBlocked(string path)
protected override PipelineInputStream PrepareReader()
{
- if (!string.IsNullOrEmpty(Option.Input.ObjectPath))
- {
- AddVisitTargetObjectAction((sourceObject, next) =>
- {
- return PipelineReceiverActions.ReadObjectPath(sourceObject, next, Option.Input.ObjectPath, true);
- });
- }
-
- if (Option.Input.Format == InputFormat.Yaml)
- {
- AddVisitTargetObjectAction((sourceObject, next) =>
- {
- return PipelineReceiverActions.ConvertFromYaml(sourceObject, next);
- });
- }
- else if (Option.Input.Format == InputFormat.Json)
- {
- AddVisitTargetObjectAction((sourceObject, next) =>
- {
- return PipelineReceiverActions.ConvertFromJson(sourceObject, next);
- });
- }
- else if (Option.Input.Format == InputFormat.Markdown)
- {
- AddVisitTargetObjectAction((sourceObject, next) =>
- {
- return PipelineReceiverActions.ConvertFromMarkdown(sourceObject, next);
- });
- }
- else if (Option.Input.Format == InputFormat.PowerShellData)
- {
- AddVisitTargetObjectAction((sourceObject, next) =>
- {
- return PipelineReceiverActions.ConvertFromPowerShellData(sourceObject, next);
- });
- }
- return new PipelineInputStream(VisitTargetObject, _InputPath, GetInputObjectSourceFilter(), Option);
+ return new PipelineInputStream(_InputPath, GetInputObjectSourceFilter(), Option);
}
}
diff --git a/src/PSRule/Pipeline/PipelineBuilder.cs b/src/PSRule/Pipeline/PipelineBuilder.cs
index 73d31c5aa9..9de9fae046 100644
--- a/src/PSRule/Pipeline/PipelineBuilder.cs
+++ b/src/PSRule/Pipeline/PipelineBuilder.cs
@@ -1,15 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-using System.Collections;
-using System.Globalization;
using PSRule.Configuration;
-using PSRule.Data;
-using PSRule.Definitions;
-using PSRule.Definitions.Baselines;
-using PSRule.Options;
-using PSRule.Pipeline.Output;
-using PSRule.Resources;
namespace PSRule.Pipeline;
@@ -164,453 +156,3 @@ public static IGetTargetPipelineBuilder GetTarget(PSRuleOption option, IHostCont
return pipeline;
}
}
-
-///
-/// A helper to build a PSRule pipeline.
-///
-public interface IPipelineBuilder
-{
- ///
- /// Configure the pipeline with options.
- ///
- IPipelineBuilder Configure(PSRuleOption option);
-
- ///
- /// Configure the pipeline to use a specific baseline.
- ///
- /// A baseline option or the name of a baseline.
- void Baseline(Configuration.BaselineOption baseline);
-
- ///
- /// Build the pipeline.
- ///
- /// Optionally specify a custom writer which will handle output processing.
- IPipeline Build(IPipelineWriter writer = null);
-}
-
-internal abstract class PipelineBuilderBase : IPipelineBuilder
-{
- private const string ENGINE_MODULE_NAME = "PSRule";
-
- protected readonly PSRuleOption Option;
- protected readonly Source[] Source;
- protected readonly IHostContext HostContext;
- protected BindTargetMethod BindTargetNameHook;
- protected BindTargetMethod BindTargetTypeHook;
- protected BindTargetMethod BindFieldHook;
- protected VisitTargetObject VisitTargetObject;
-
- private string[] _Include;
- private Hashtable _Tag;
- private Configuration.BaselineOption _Baseline;
- private string[] _Convention;
- private PathFilter _InputFilter;
- private PipelineWriter _Writer;
-
- private readonly HostPipelineWriter _Output;
-
- private const int MIN_JSON_INDENT = 0;
- private const int MAX_JSON_INDENT = 4;
-
- protected PipelineBuilderBase(Source[] source, IHostContext hostContext)
- {
- Option = new PSRuleOption();
- Source = source;
- _Output = new HostPipelineWriter(hostContext, Option, ShouldProcess);
- HostContext = hostContext;
- BindTargetNameHook = PipelineHookActions.BindTargetName;
- BindTargetTypeHook = PipelineHookActions.BindTargetType;
- BindFieldHook = PipelineHookActions.BindField;
- VisitTargetObject = PipelineReceiverActions.PassThru;
- }
-
- ///
- /// Determines if the pipeline is executing in a remote PowerShell session.
- ///
- public bool InSession => HostContext != null && HostContext.InSession;
-
- ///
- public void Name(string[] name)
- {
- if (name == null || name.Length == 0)
- return;
-
- _Include = name;
- }
-
- ///
- public void Tag(Hashtable tag)
- {
- if (tag == null || tag.Count == 0)
- return;
-
- _Tag = tag;
- }
-
- ///
- public void Convention(string[] convention)
- {
- if (convention == null || convention.Length == 0)
- return;
-
- _Convention = convention;
- }
-
- ///
- public virtual IPipelineBuilder Configure(PSRuleOption option)
- {
- if (option == null)
- return this;
-
- Option.Baseline = new Options.BaselineOption(option.Baseline);
- Option.Binding = new BindingOption(option.Binding);
- Option.Convention = new ConventionOption(option.Convention);
- Option.Execution = GetExecutionOption(option.Execution);
- Option.Input = new InputOption(option.Input);
- Option.Input.Format ??= InputOption.Default.Format;
- Option.Output = new OutputOption(option.Output);
- Option.Output.Outcome ??= OutputOption.Default.Outcome;
- Option.Output.Banner ??= OutputOption.Default.Banner;
- Option.Output.Style = GetStyle(option.Output.Style ?? OutputOption.Default.Style.Value);
- Option.Repository = GetRepository(option.Repository);
- return this;
- }
-
- ///
- public abstract IPipeline Build(IPipelineWriter writer = null);
-
- ///
- public void Baseline(Configuration.BaselineOption baseline)
- {
- if (baseline == null)
- return;
-
- _Baseline = baseline;
- }
-
- ///
- /// Require correct module versions for pipeline execution.
- ///
- protected bool RequireModules()
- {
- var result = true;
- if (Option.Requires.TryGetValue(ENGINE_MODULE_NAME, out var requiredVersion))
- {
- var engineVersion = Engine.GetVersion();
- if (GuardModuleVersion(ENGINE_MODULE_NAME, engineVersion, requiredVersion))
- result = false;
- }
- for (var i = 0; Source != null && i < Source.Length; i++)
- {
- if (Source[i].Module != null && Option.Requires.TryGetValue(Source[i].Module.Name, out requiredVersion))
- {
- if (GuardModuleVersion(Source[i].Module.Name, Source[i].Module.Version, requiredVersion))
- result = false;
- }
- }
- return result;
- }
-
- ///
- /// Require sources for pipeline execution.
- ///
- protected bool RequireSources()
- {
- if (Source == null || Source.Length == 0)
- {
- PrepareWriter().WarnRulePathNotFound();
- return false;
- }
- return true;
- }
-
- private bool GuardModuleVersion(string moduleName, string moduleVersion, string requiredVersion)
- {
- if (!TryModuleVersion(moduleVersion, requiredVersion))
- {
- var writer = PrepareWriter();
- writer.ErrorRequiredVersionMismatch(moduleName, moduleVersion, requiredVersion);
- writer.End(new DefaultPipelineResult(null, BreakLevel.None) { HadErrors = true });
- return true;
- }
- return false;
- }
-
- private static bool TryModuleVersion(string moduleVersion, string requiredVersion)
- {
- return SemanticVersion.TryParseVersion(moduleVersion, out var version) &&
- SemanticVersion.TryParseConstraint(requiredVersion, out var constraint) &&
- constraint.Equals(version);
- }
-
- protected PipelineContext PrepareContext(BindTargetMethod bindTargetName, BindTargetMethod bindTargetType, BindTargetMethod bindField)
- {
- var unresolved = new List();
- if (_Baseline is Configuration.BaselineOption.BaselineRef baselineRef)
- unresolved.Add(new BaselineRef(ResolveBaselineGroup(baselineRef.Name), ScopeType.Explicit));
-
- return PipelineContext.New(
- option: Option,
- hostContext: HostContext,
- reader: PrepareReader(),
- bindTargetName: bindTargetName,
- bindTargetType: bindTargetType,
- bindField: bindField,
- optionBuilder: GetOptionBuilder(),
- unresolved: unresolved
- );
- }
-
- protected string[] ResolveBaselineGroup(string[] name)
- {
- for (var i = 0; name != null && i < name.Length; i++)
- name[i] = ResolveBaselineGroup(name[i]);
-
- return name;
- }
-
- protected string ResolveBaselineGroup(string name)
- {
- if (name == null || name.Length < 2 || !name.StartsWith("@") ||
- Option == null || Option.Baseline == null || Option.Baseline.Group == null ||
- Option.Baseline.Group.Count == 0)
- return name;
-
- var key = name.Substring(1);
- if (!Option.Baseline.Group.TryGetValue(key, out var baselines) || baselines.Length == 0)
- throw new PipelineConfigurationException("Baseline.Group", string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.PSR0003, key));
-
- var writer = PrepareWriter();
- writer.WriteVerbose($"Using baseline group '{key}': {baselines[0]}");
- return baselines[0];
- }
-
- protected virtual PipelineInputStream PrepareReader()
- {
- return new PipelineInputStream(null, null, GetInputObjectSourceFilter(), Option);
- }
-
- protected virtual PipelineWriter PrepareWriter()
- {
- if (_Writer != null)
- return _Writer;
-
- var output = GetOutput();
- _Writer = Option.Output.Format switch
- {
- OutputFormat.Csv => new CsvOutputWriter(output, Option, ShouldProcess),
- OutputFormat.Json => new JsonOutputWriter(output, Option, ShouldProcess),
- OutputFormat.NUnit3 => new NUnit3OutputWriter(output, Option, ShouldProcess),
- OutputFormat.Yaml => new YamlOutputWriter(output, Option, ShouldProcess),
- OutputFormat.Markdown => new MarkdownOutputWriter(output, Option, ShouldProcess),
- OutputFormat.Wide => new WideOutputWriter(output, Option, ShouldProcess),
- OutputFormat.Sarif => new SarifOutputWriter(Source, output, Option, ShouldProcess),
- _ => output,
- };
- return _Writer;
- }
-
- protected virtual PipelineWriter GetOutput(bool writeHost = false)
- {
- // Redirect to file instead
- return !string.IsNullOrEmpty(Option.Output.Path)
- ? new FileOutputWriter(
- inner: _Output,
- option: Option,
- encoding: Option.Output.GetEncoding(),
- path: Option.Output.Path,
- shouldProcess: HostContext.ShouldProcess,
- writeHost: writeHost
- )
- : _Output;
- }
-
- protected static string[] GetCulture(string[] culture)
- {
- var result = new List();
- var parent = new List();
- var set = new HashSet();
- for (var i = 0; culture != null && i < culture.Length; i++)
- {
- var c = CultureInfo.CreateSpecificCulture(culture[i]);
- if (!set.Contains(c.Name))
- {
- result.Add(c.Name);
- set.Add(c.Name);
- }
- for (var p = c.Parent; !string.IsNullOrEmpty(p.Name); p = p.Parent)
- {
- if (!set.Contains(p.Name))
- {
- parent.Add(p.Name);
- set.Add(p.Name);
- }
- }
- }
- if (parent.Count > 0)
- result.AddRange(parent);
-
- return result.Count == 0 ? null : result.ToArray();
- }
-
- protected static RepositoryOption GetRepository(RepositoryOption option)
- {
- var result = new RepositoryOption(option);
- if (string.IsNullOrEmpty(result.Url) && GitHelper.TryRepository(out var url))
- result.Url = url;
-
- if (string.IsNullOrEmpty(result.BaseRef) && GitHelper.TryBaseRef(out var baseRef))
- result.BaseRef = baseRef;
-
- return result;
- }
-
- ///
- /// Coalesce execution options with defaults.
- ///
- protected static ExecutionOption GetExecutionOption(ExecutionOption option)
- {
- var result = ExecutionOption.Combine(option, ExecutionOption.Default);
-
- // Handle when preference is set to none. The default should be used.
- result.AliasReference = result.AliasReference == ExecutionActionPreference.None ? ExecutionOption.Default.AliasReference.Value : result.AliasReference;
- result.DuplicateResourceId = result.DuplicateResourceId == ExecutionActionPreference.None ? ExecutionOption.Default.DuplicateResourceId.Value : result.DuplicateResourceId;
- result.InvariantCulture = result.InvariantCulture == ExecutionActionPreference.None ? ExecutionOption.Default.InvariantCulture.Value : result.InvariantCulture;
- result.RuleExcluded = result.RuleExcluded == ExecutionActionPreference.None ? ExecutionOption.Default.RuleExcluded.Value : result.RuleExcluded;
- result.RuleInconclusive = result.RuleInconclusive == ExecutionActionPreference.None ? ExecutionOption.Default.RuleInconclusive.Value : result.RuleInconclusive;
- result.RuleSuppressed = result.RuleSuppressed == ExecutionActionPreference.None ? ExecutionOption.Default.RuleSuppressed.Value : result.RuleSuppressed;
- result.SuppressionGroupExpired = result.SuppressionGroupExpired == ExecutionActionPreference.None ? ExecutionOption.Default.SuppressionGroupExpired.Value : result.SuppressionGroupExpired;
- result.UnprocessedObject = result.UnprocessedObject == ExecutionActionPreference.None ? ExecutionOption.Default.UnprocessedObject.Value : result.UnprocessedObject;
- return result;
- }
-
- protected PathFilter GetInputObjectSourceFilter()
- {
- return Option.Input.IgnoreObjectSource.GetValueOrDefault(InputOption.Default.IgnoreObjectSource.Value) ? GetInputFilter() : null;
- }
-
- protected PathFilter GetInputFilter()
- {
- if (_InputFilter == null)
- {
- var basePath = Environment.GetWorkingPath();
- var ignoreGitPath = Option.Input.IgnoreGitPath ?? InputOption.Default.IgnoreGitPath.Value;
- var ignoreRepositoryCommon = Option.Input.IgnoreRepositoryCommon ?? InputOption.Default.IgnoreRepositoryCommon.Value;
- var builder = PathFilterBuilder.Create(basePath, Option.Input.PathIgnore, ignoreGitPath, ignoreRepositoryCommon);
- builder.UseGitIgnore();
-
- _InputFilter = builder.Build();
- }
- return _InputFilter;
- }
-
- private OptionContextBuilder GetOptionBuilder()
- {
- return new OptionContextBuilder(Option, _Include, _Tag, _Convention);
- }
-
- protected void ConfigureBinding(PSRuleOption option)
- {
- if (option.Pipeline.BindTargetName != null && option.Pipeline.BindTargetName.Count > 0)
- {
- // Do not allow custom binding functions to be used with constrained language mode
- if (Option.Execution.LanguageMode == LanguageMode.ConstrainedLanguage)
- throw new PipelineConfigurationException(optionName: "BindTargetName", message: PSRuleResources.ConstrainedTargetBinding);
-
- foreach (var action in option.Pipeline.BindTargetName)
- BindTargetNameHook = AddBindTargetAction(action, BindTargetNameHook);
- }
-
- if (option.Pipeline.BindTargetType != null && option.Pipeline.BindTargetType.Count > 0)
- {
- // Do not allow custom binding functions to be used with constrained language mode
- if (Option.Execution.LanguageMode == LanguageMode.ConstrainedLanguage)
- throw new PipelineConfigurationException(optionName: "BindTargetType", message: PSRuleResources.ConstrainedTargetBinding);
-
- foreach (var action in option.Pipeline.BindTargetType)
- BindTargetTypeHook = AddBindTargetAction(action, BindTargetTypeHook);
- }
- }
-
- private static BindTargetMethod AddBindTargetAction(BindTargetFunc action, BindTargetMethod previous)
- {
- // Nest the previous write action in the new supplied action
- // Execution chain will be: action -> previous -> previous..n
- return (string[] propertyNames, bool caseSensitive, bool preferTargetInfo, object targetObject, out string path) =>
- {
- return action(propertyNames, caseSensitive, preferTargetInfo, targetObject, previous, out path);
- };
- }
-
- private static BindTargetMethod AddBindTargetAction(BindTargetName action, BindTargetMethod previous)
- {
- return AddBindTargetAction((string[] propertyNames, bool caseSensitive, bool preferTargetInfo, object targetObject, BindTargetMethod next, out string path) =>
- {
- path = null;
- var targetType = action(targetObject);
- return string.IsNullOrEmpty(targetType) ? next(propertyNames, caseSensitive, preferTargetInfo, targetObject, out path) : targetType;
- }, previous);
- }
-
- protected void AddVisitTargetObjectAction(VisitTargetObjectAction action)
- {
- // Nest the previous write action in the new supplied action
- // Execution chain will be: action -> previous -> previous..n
- var previous = VisitTargetObject;
- VisitTargetObject = (targetObject) => action(targetObject, previous);
- }
-
- ///
- /// Normalizes JSON indent range between minimum 0 and maximum 4.
- ///
- ///
- /// The number of characters to indent.
- protected static int NormalizeJsonIndentRange(int? jsonIndent)
- {
- if (jsonIndent.HasValue)
- {
- if (jsonIndent < MIN_JSON_INDENT)
- return MIN_JSON_INDENT;
-
- else if (jsonIndent > MAX_JSON_INDENT)
- return MAX_JSON_INDENT;
-
- return jsonIndent.Value;
- }
- return MIN_JSON_INDENT;
- }
-
- protected bool TryChangedFiles(out string[] files)
- {
- files = null;
- if (!Option.Input.IgnoreUnchangedPath.GetValueOrDefault(InputOption.Default.IgnoreUnchangedPath.Value) ||
- !GitHelper.TryGetChangedFiles(Option.Repository.BaseRef, "d", null, out files))
- return false;
-
- for (var i = 0; i < files.Length; i++)
- HostContext.Verbose(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.UsingChangedFile, files[i]));
-
- return true;
- }
-
- protected bool ShouldProcess(string target, string action)
- {
- return HostContext == null || HostContext.ShouldProcess(target, action);
- }
-
- protected static OutputStyle GetStyle(OutputStyle style)
- {
- if (style != OutputStyle.Detect)
- return style;
-
- if (Environment.IsAzurePipelines())
- return OutputStyle.AzurePipelines;
-
- if (Environment.IsGitHubActions())
- return OutputStyle.GitHubActions;
-
- return Environment.IsVisualStudioCode() ?
- OutputStyle.VisualStudioCode :
- OutputStyle.Client;
- }
-}
diff --git a/src/PSRule/Pipeline/PipelineBuilderBase.cs b/src/PSRule/Pipeline/PipelineBuilderBase.cs
new file mode 100644
index 0000000000..03a7fab8a9
--- /dev/null
+++ b/src/PSRule/Pipeline/PipelineBuilderBase.cs
@@ -0,0 +1,440 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Collections;
+using System.Globalization;
+using PSRule.Configuration;
+using PSRule.Data;
+using PSRule.Definitions;
+using PSRule.Definitions.Baselines;
+using PSRule.Options;
+using PSRule.Pipeline.Output;
+using PSRule.Resources;
+
+namespace PSRule.Pipeline;
+
+internal abstract class PipelineBuilderBase : IPipelineBuilder
+{
+ private const string ENGINE_MODULE_NAME = "PSRule";
+
+ protected readonly PSRuleOption Option;
+ protected readonly Source[] Source;
+ protected readonly IHostContext HostContext;
+ protected BindTargetMethod BindTargetNameHook;
+ protected BindTargetMethod BindTargetTypeHook;
+ protected BindTargetMethod BindFieldHook;
+ protected VisitTargetObject VisitTargetObject;
+
+ private string[] _Include;
+ private Hashtable _Tag;
+ private Configuration.BaselineOption _Baseline;
+ private string[] _Convention;
+ private PathFilter _InputFilter;
+ private PipelineWriter _Writer;
+
+ private readonly HostPipelineWriter _Output;
+
+ private const int MIN_JSON_INDENT = 0;
+ private const int MAX_JSON_INDENT = 4;
+
+ protected PipelineBuilderBase(Source[] source, IHostContext hostContext)
+ {
+ Option = new PSRuleOption();
+ Source = source;
+ _Output = new HostPipelineWriter(hostContext, Option, ShouldProcess);
+ HostContext = hostContext;
+ BindTargetNameHook = PipelineHookActions.BindTargetName;
+ BindTargetTypeHook = PipelineHookActions.BindTargetType;
+ BindFieldHook = PipelineHookActions.BindField;
+ }
+
+ ///
+ /// Determines if the pipeline is executing in a remote PowerShell session.
+ ///
+ public bool InSession => HostContext != null && HostContext.InSession;
+
+ ///
+ public void Name(string[] name)
+ {
+ if (name == null || name.Length == 0)
+ return;
+
+ _Include = name;
+ }
+
+ ///
+ public void Tag(Hashtable tag)
+ {
+ if (tag == null || tag.Count == 0)
+ return;
+
+ _Tag = tag;
+ }
+
+ ///
+ public void Convention(string[] convention)
+ {
+ if (convention == null || convention.Length == 0)
+ return;
+
+ _Convention = convention;
+ }
+
+ ///
+ public virtual IPipelineBuilder Configure(PSRuleOption option)
+ {
+ if (option == null)
+ return this;
+
+ Option.Baseline = new Options.BaselineOption(option.Baseline);
+ Option.Binding = new BindingOption(option.Binding);
+ Option.Convention = new ConventionOption(option.Convention);
+ Option.Execution = GetExecutionOption(option.Execution);
+ Option.Input = new InputOption(option.Input);
+ Option.Input.Format ??= InputOption.Default.Format;
+ Option.Output = new OutputOption(option.Output);
+ Option.Output.Outcome ??= OutputOption.Default.Outcome;
+ Option.Output.Banner ??= OutputOption.Default.Banner;
+ Option.Output.Style = GetStyle(option.Output.Style ?? OutputOption.Default.Style.Value);
+ Option.Repository = GetRepository(option.Repository);
+ return this;
+ }
+
+ ///
+ public abstract IPipeline Build(IPipelineWriter writer = null);
+
+ ///
+ public void Baseline(Configuration.BaselineOption baseline)
+ {
+ if (baseline == null)
+ return;
+
+ _Baseline = baseline;
+ }
+
+ ///
+ /// Require correct module versions for pipeline execution.
+ ///
+ protected bool RequireModules()
+ {
+ var result = true;
+ if (Option.Requires.TryGetValue(ENGINE_MODULE_NAME, out var requiredVersion))
+ {
+ var engineVersion = Engine.GetVersion();
+ if (GuardModuleVersion(ENGINE_MODULE_NAME, engineVersion, requiredVersion))
+ result = false;
+ }
+ for (var i = 0; Source != null && i < Source.Length; i++)
+ {
+ if (Source[i].Module != null && Option.Requires.TryGetValue(Source[i].Module.Name, out requiredVersion))
+ {
+ if (GuardModuleVersion(Source[i].Module.Name, Source[i].Module.Version, requiredVersion))
+ result = false;
+ }
+ }
+ return result;
+ }
+
+ ///
+ /// Require sources for pipeline execution.
+ ///
+ protected bool RequireSources()
+ {
+ if (Source == null || Source.Length == 0)
+ {
+ PrepareWriter().WarnRulePathNotFound();
+ return false;
+ }
+ return true;
+ }
+
+ private bool GuardModuleVersion(string moduleName, string moduleVersion, string requiredVersion)
+ {
+ if (!TryModuleVersion(moduleVersion, requiredVersion))
+ {
+ var writer = PrepareWriter();
+ writer.ErrorRequiredVersionMismatch(moduleName, moduleVersion, requiredVersion);
+ writer.End(new DefaultPipelineResult(null, BreakLevel.None) { HadErrors = true });
+ return true;
+ }
+ return false;
+ }
+
+ private static bool TryModuleVersion(string moduleVersion, string requiredVersion)
+ {
+ return SemanticVersion.TryParseVersion(moduleVersion, out var version) &&
+ SemanticVersion.TryParseConstraint(requiredVersion, out var constraint) &&
+ constraint.Equals(version);
+ }
+
+ protected PipelineContext PrepareContext(BindTargetMethod bindTargetName, BindTargetMethod bindTargetType, BindTargetMethod bindField)
+ {
+ var unresolved = new List();
+ if (_Baseline is Configuration.BaselineOption.BaselineRef baselineRef)
+ unresolved.Add(new BaselineRef(ResolveBaselineGroup(baselineRef.Name), ScopeType.Explicit));
+
+ return PipelineContext.New(
+ option: Option,
+ hostContext: HostContext,
+ reader: PrepareReader(),
+ bindTargetName: bindTargetName,
+ bindTargetType: bindTargetType,
+ bindField: bindField,
+ optionBuilder: GetOptionBuilder(),
+ unresolved: unresolved
+ );
+ }
+
+ protected string[] ResolveBaselineGroup(string[] name)
+ {
+ for (var i = 0; name != null && i < name.Length; i++)
+ name[i] = ResolveBaselineGroup(name[i]);
+
+ return name;
+ }
+
+ protected string ResolveBaselineGroup(string name)
+ {
+ if (name == null || name.Length < 2 || !name.StartsWith("@") ||
+ Option == null || Option.Baseline == null || Option.Baseline.Group == null ||
+ Option.Baseline.Group.Count == 0)
+ return name;
+
+ var key = name.Substring(1);
+ if (!Option.Baseline.Group.TryGetValue(key, out var baselines) || baselines.Length == 0)
+ throw new PipelineConfigurationException("Baseline.Group", string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.PSR0003, key));
+
+ var writer = PrepareWriter();
+ writer.WriteVerbose($"Using baseline group '{key}': {baselines[0]}");
+ return baselines[0];
+ }
+
+ protected virtual PipelineInputStream PrepareReader()
+ {
+ return new PipelineInputStream(null, GetInputObjectSourceFilter(), Option);
+ }
+
+ protected virtual PipelineWriter PrepareWriter()
+ {
+ if (_Writer != null)
+ return _Writer;
+
+ var output = GetOutput();
+ _Writer = Option.Output.Format switch
+ {
+ OutputFormat.Csv => new CsvOutputWriter(output, Option, ShouldProcess),
+ OutputFormat.Json => new JsonOutputWriter(output, Option, ShouldProcess),
+ OutputFormat.NUnit3 => new NUnit3OutputWriter(output, Option, ShouldProcess),
+ OutputFormat.Yaml => new YamlOutputWriter(output, Option, ShouldProcess),
+ OutputFormat.Markdown => new MarkdownOutputWriter(output, Option, ShouldProcess),
+ OutputFormat.Wide => new WideOutputWriter(output, Option, ShouldProcess),
+ OutputFormat.Sarif => new SarifOutputWriter(Source, output, Option, ShouldProcess),
+ _ => output,
+ };
+ return _Writer;
+ }
+
+ protected virtual PipelineWriter GetOutput(bool writeHost = false)
+ {
+ // Redirect to file instead
+ return !string.IsNullOrEmpty(Option.Output.Path)
+ ? new FileOutputWriter(
+ inner: _Output,
+ option: Option,
+ encoding: Option.Output.GetEncoding(),
+ path: Option.Output.Path,
+ shouldProcess: HostContext.ShouldProcess,
+ writeHost: writeHost
+ )
+ : _Output;
+ }
+
+ protected static string[] GetCulture(string[] culture)
+ {
+ var result = new List();
+ var parent = new List();
+ var set = new HashSet();
+ for (var i = 0; culture != null && i < culture.Length; i++)
+ {
+ var c = CultureInfo.CreateSpecificCulture(culture[i]);
+ if (!set.Contains(c.Name))
+ {
+ result.Add(c.Name);
+ set.Add(c.Name);
+ }
+ for (var p = c.Parent; !string.IsNullOrEmpty(p.Name); p = p.Parent)
+ {
+ if (!set.Contains(p.Name))
+ {
+ parent.Add(p.Name);
+ set.Add(p.Name);
+ }
+ }
+ }
+ if (parent.Count > 0)
+ result.AddRange(parent);
+
+ return result.Count == 0 ? null : result.ToArray();
+ }
+
+ protected static RepositoryOption GetRepository(RepositoryOption option)
+ {
+ var result = new RepositoryOption(option);
+ if (string.IsNullOrEmpty(result.Url) && GitHelper.TryRepository(out var url))
+ result.Url = url;
+
+ if (string.IsNullOrEmpty(result.BaseRef) && GitHelper.TryBaseRef(out var baseRef))
+ result.BaseRef = baseRef;
+
+ return result;
+ }
+
+ ///
+ /// Coalesce execution options with defaults.
+ ///
+ protected static ExecutionOption GetExecutionOption(ExecutionOption option)
+ {
+ var result = ExecutionOption.Combine(option, ExecutionOption.Default);
+
+ // Handle when preference is set to none. The default should be used.
+ result.AliasReference = result.AliasReference == ExecutionActionPreference.None ? ExecutionOption.Default.AliasReference.Value : result.AliasReference;
+ result.DuplicateResourceId = result.DuplicateResourceId == ExecutionActionPreference.None ? ExecutionOption.Default.DuplicateResourceId.Value : result.DuplicateResourceId;
+ result.InvariantCulture = result.InvariantCulture == ExecutionActionPreference.None ? ExecutionOption.Default.InvariantCulture.Value : result.InvariantCulture;
+ result.RuleExcluded = result.RuleExcluded == ExecutionActionPreference.None ? ExecutionOption.Default.RuleExcluded.Value : result.RuleExcluded;
+ result.RuleInconclusive = result.RuleInconclusive == ExecutionActionPreference.None ? ExecutionOption.Default.RuleInconclusive.Value : result.RuleInconclusive;
+ result.RuleSuppressed = result.RuleSuppressed == ExecutionActionPreference.None ? ExecutionOption.Default.RuleSuppressed.Value : result.RuleSuppressed;
+ result.SuppressionGroupExpired = result.SuppressionGroupExpired == ExecutionActionPreference.None ? ExecutionOption.Default.SuppressionGroupExpired.Value : result.SuppressionGroupExpired;
+ result.UnprocessedObject = result.UnprocessedObject == ExecutionActionPreference.None ? ExecutionOption.Default.UnprocessedObject.Value : result.UnprocessedObject;
+ return result;
+ }
+
+ protected PathFilter GetInputObjectSourceFilter()
+ {
+ return Option.Input.IgnoreObjectSource.GetValueOrDefault(InputOption.Default.IgnoreObjectSource.Value) ? GetInputFilter() : null;
+ }
+
+ protected PathFilter GetInputFilter()
+ {
+ if (_InputFilter == null)
+ {
+ var basePath = Environment.GetWorkingPath();
+ var ignoreGitPath = Option.Input.IgnoreGitPath ?? InputOption.Default.IgnoreGitPath.Value;
+ var ignoreRepositoryCommon = Option.Input.IgnoreRepositoryCommon ?? InputOption.Default.IgnoreRepositoryCommon.Value;
+ var builder = PathFilterBuilder.Create(basePath, Option.Input.PathIgnore, ignoreGitPath, ignoreRepositoryCommon);
+ builder.UseGitIgnore();
+
+ _InputFilter = builder.Build();
+ }
+ return _InputFilter;
+ }
+
+ private OptionContextBuilder GetOptionBuilder()
+ {
+ return new OptionContextBuilder(Option, _Include, _Tag, _Convention);
+ }
+
+ protected void ConfigureBinding(PSRuleOption option)
+ {
+ if (option.Pipeline.BindTargetName != null && option.Pipeline.BindTargetName.Count > 0)
+ {
+ // Do not allow custom binding functions to be used with constrained language mode
+ if (Option.Execution.LanguageMode == LanguageMode.ConstrainedLanguage)
+ throw new PipelineConfigurationException(optionName: "BindTargetName", message: PSRuleResources.ConstrainedTargetBinding);
+
+ foreach (var action in option.Pipeline.BindTargetName)
+ BindTargetNameHook = AddBindTargetAction(action, BindTargetNameHook);
+ }
+
+ if (option.Pipeline.BindTargetType != null && option.Pipeline.BindTargetType.Count > 0)
+ {
+ // Do not allow custom binding functions to be used with constrained language mode
+ if (Option.Execution.LanguageMode == LanguageMode.ConstrainedLanguage)
+ throw new PipelineConfigurationException(optionName: "BindTargetType", message: PSRuleResources.ConstrainedTargetBinding);
+
+ foreach (var action in option.Pipeline.BindTargetType)
+ BindTargetTypeHook = AddBindTargetAction(action, BindTargetTypeHook);
+ }
+ }
+
+ private static BindTargetMethod AddBindTargetAction(BindTargetFunc action, BindTargetMethod previous)
+ {
+ // Nest the previous write action in the new supplied action
+ // Execution chain will be: action -> previous -> previous..n
+ return (string[] propertyNames, bool caseSensitive, bool preferTargetInfo, object targetObject, out string path) =>
+ {
+ return action(propertyNames, caseSensitive, preferTargetInfo, targetObject, previous, out path);
+ };
+ }
+
+ private static BindTargetMethod AddBindTargetAction(BindTargetName action, BindTargetMethod previous)
+ {
+ return AddBindTargetAction((string[] propertyNames, bool caseSensitive, bool preferTargetInfo, object targetObject, BindTargetMethod next, out string path) =>
+ {
+ path = null;
+ var targetType = action(targetObject);
+ return string.IsNullOrEmpty(targetType) ? next(propertyNames, caseSensitive, preferTargetInfo, targetObject, out path) : targetType;
+ }, previous);
+ }
+
+ protected void AddVisitTargetObjectAction(VisitTargetObjectAction action)
+ {
+ // Nest the previous write action in the new supplied action
+ // Execution chain will be: action -> previous -> previous..n
+ var previous = VisitTargetObject;
+ VisitTargetObject = (targetObject) => action(targetObject, previous);
+ }
+
+ ///
+ /// Normalizes JSON indent range between minimum 0 and maximum 4.
+ ///
+ ///
+ /// The number of characters to indent.
+ protected static int NormalizeJsonIndentRange(int? jsonIndent)
+ {
+ if (jsonIndent.HasValue)
+ {
+ if (jsonIndent < MIN_JSON_INDENT)
+ return MIN_JSON_INDENT;
+
+ else if (jsonIndent > MAX_JSON_INDENT)
+ return MAX_JSON_INDENT;
+
+ return jsonIndent.Value;
+ }
+ return MIN_JSON_INDENT;
+ }
+
+ protected bool TryChangedFiles(out string[] files)
+ {
+ files = null;
+ if (!Option.Input.IgnoreUnchangedPath.GetValueOrDefault(InputOption.Default.IgnoreUnchangedPath.Value) ||
+ !GitHelper.TryGetChangedFiles(Option.Repository.BaseRef, "d", null, out files))
+ return false;
+
+ for (var i = 0; i < files.Length; i++)
+ HostContext.Verbose(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.UsingChangedFile, files[i]));
+
+ return true;
+ }
+
+ protected bool ShouldProcess(string target, string action)
+ {
+ return HostContext == null || HostContext.ShouldProcess(target, action);
+ }
+
+ protected static OutputStyle GetStyle(OutputStyle style)
+ {
+ if (style != OutputStyle.Detect)
+ return style;
+
+ if (Environment.IsAzurePipelines())
+ return OutputStyle.AzurePipelines;
+
+ if (Environment.IsGitHubActions())
+ return OutputStyle.GitHubActions;
+
+ return Environment.IsVisualStudioCode() ?
+ OutputStyle.VisualStudioCode :
+ OutputStyle.Client;
+ }
+}
diff --git a/src/PSRule/Pipeline/PipelineInputStream.cs b/src/PSRule/Pipeline/PipelineInputStream.cs
index 9d72cd8244..abac0452d3 100644
--- a/src/PSRule/Pipeline/PipelineInputStream.cs
+++ b/src/PSRule/Pipeline/PipelineInputStream.cs
@@ -14,17 +14,15 @@ namespace PSRule.Pipeline;
///
/// A stream of input objects that will be evaluated.
///
-internal sealed class PipelineInputStream
+internal sealed class PipelineInputStream : IPipelineReader
{
- private readonly VisitTargetObject _Input;
private readonly InputPathBuilder _InputPath;
private readonly PathFilter _InputFilter;
private readonly ConcurrentQueue _Queue;
private readonly EmitterCollection _EmitterCollection;
- public PipelineInputStream(VisitTargetObject input, InputPathBuilder inputPath, PathFilter inputFilter, PSRuleOption option)
+ public PipelineInputStream(InputPathBuilder inputPath, PathFilter inputFilter, PSRuleOption option)
{
- _Input = input;
_InputPath = inputPath;
_InputFilter = inputFilter;
_Queue = new ConcurrentQueue();
@@ -35,12 +33,7 @@ public PipelineInputStream(VisitTargetObject input, InputPathBuilder inputPath,
public bool IsEmpty => _Queue.IsEmpty;
- ///
- /// Add a new object into the stream.
- ///
- /// An object to process.
- /// A pre-bound type.
- /// Determines if expansion is skipped.
+ ///
public void Enqueue(object sourceObject, string? targetType = null, bool skipExpansion = false)
{
if (sourceObject == null)
@@ -55,11 +48,13 @@ public void Enqueue(object sourceObject, string? targetType = null, bool skipExp
_EmitterCollection.Visit(sourceObject);
}
+ ///
public bool TryDequeue(out ITargetObject sourceObject)
{
return _Queue.TryDequeue(out sourceObject);
}
+ ///
public void Open()
{
if (_InputPath == null || _InputPath.Count == 0)
@@ -97,11 +92,8 @@ private bool ShouldQueue(TargetObject targetObject)
return true;
}
- ///
- /// Add a path to the list of inputs.
- ///
- /// The path of files to add.
- internal void Add(string path)
+ ///
+ public void Add(string path)
{
_InputPath.Add(path);
}
diff --git a/src/PSRule/Pipeline/PipelineWriterExtensions.cs b/src/PSRule/Pipeline/PipelineWriterExtensions.cs
index 0790d862ac..052f05d15d 100644
--- a/src/PSRule/Pipeline/PipelineWriterExtensions.cs
+++ b/src/PSRule/Pipeline/PipelineWriterExtensions.cs
@@ -109,6 +109,14 @@ internal static void WriteDebug(this IPipelineWriter writer, string message, par
));
}
+ 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}");
+ }
+
private static string Format(string message, params object[] args)
{
return args == null || args.Length == 0 ? message : string.Format(Thread.CurrentThread.CurrentCulture, message, args);
diff --git a/src/PSRule/Runtime/RunspaceContext.cs b/src/PSRule/Runtime/RunspaceContext.cs
index 5db8f4641b..0f061d8d14 100644
--- a/src/PSRule/Runtime/RunspaceContext.cs
+++ b/src/PSRule/Runtime/RunspaceContext.cs
@@ -673,16 +673,16 @@ internal void Import(IConvention resource)
internal void AddService(string id, object service)
{
ResourceHelper.ParseIdString(LanguageScope.Name, id, out var scopeName, out var name);
- if (!StringComparer.OrdinalIgnoreCase.Equals(LanguageScope.Name, scopeName))
+ if (!StringComparer.OrdinalIgnoreCase.Equals(LanguageScope.Name, scopeName) || string.IsNullOrEmpty(name))
return;
- LanguageScope.AddService(name, service);
+ LanguageScope.AddService(name!, service);
}
internal object? GetService(string id)
{
ResourceHelper.ParseIdString(LanguageScope.Name, id, out var scopeName, out var name);
- return !_LanguageScopes.TryScope(scopeName, out var scope) ? null : scope.GetService(name);
+ return !_LanguageScopes.TryScope(scopeName, out var scope) || string.IsNullOrEmpty(name) ? null : scope.GetService(name!);
}
private void RunConventionInitialize()
diff --git a/tests/PSRule.Tests/PipelineTests.cs b/tests/PSRule.Tests/PipelineTests.cs
index ffa0e34b2f..1fcf5c6c5f 100644
--- a/tests/PSRule.Tests/PipelineTests.cs
+++ b/tests/PSRule.Tests/PipelineTests.cs
@@ -180,7 +180,7 @@ public void PipelineWithInvariantCulture()
Environment.UseCurrentCulture(CultureInfo.InvariantCulture);
var context = PipelineContext.New(GetOption(), null, null, null, null, null, new OptionContextBuilder(), null);
var writer = new TestWriter(GetOption());
- var pipeline = new GetRulePipeline(context, GetSource(), new PipelineInputStream(null, null, null, null), writer, false);
+ var pipeline = new GetRulePipeline(context, GetSource(), new PipelineInputStream(null, null, null), writer, false);
try
{
pipeline.Begin();
@@ -202,7 +202,7 @@ public void PipelineWithInvariantCultureDisabled()
option.Execution.InvariantCulture = ExecutionActionPreference.Ignore;
var context = PipelineContext.New(option, null, null, null, null, null, new OptionContextBuilder(), null);
var writer = new TestWriter(option);
- var pipeline = new GetRulePipeline(context, GetSource(), new PipelineInputStream(null, null, null, null), writer, false);
+ var pipeline = new GetRulePipeline(context, GetSource(), new PipelineInputStream(null, null, null), writer, false);
try
{
pipeline.Begin();