Skip to content

Commit

Permalink
Warn when an obsolete baseline is used microsoft#499
Browse files Browse the repository at this point in the history
  • Loading branch information
BernieWhite committed Aug 27, 2020
1 parent 5cb86ec commit 6974d6e
Show file tree
Hide file tree
Showing 18 changed files with 177 additions and 14 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

## Unreleased

- Engine features:
- Baselines can now be flagged as obsolete. [#499](https://github.com/microsoft/PSRule/issues/499)
- Set the `metadata.annotations.obsolete` property to `true` to flag a baseline as obsolete.
- When an obsolete baseline is used, a warning will be generated.
- Engineering:
- Bump YamlDotNet dependency to v8.1.2. [#439](https://github.com/microsoft/PSRule/issues/439)

Expand Down
24 changes: 24 additions & 0 deletions docs/concepts/PSRule/en-US/about_PSRule_Baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ To define a baseline spec use the following structure:
kind: Baseline
metadata:
name: <name>
annotations: { }
spec:
# One or more baseline options
binding: { }
Expand Down Expand Up @@ -113,6 +114,29 @@ When baseline options are set, PSRule uses the following order to determine prec

After precedence is determined, baselines are merged and null values are ignored, such that:

### Annotations

Additional baseline annotations can be provided as key/ value pairs.
Annotations can be used to provide additional information that is available in `Get-PSRuleBaseline` output.

The following reserved annotation exists:

- `obsolete` - Marks the baseline as obsolete when set to `true`.
PSRule will generate a warning when an obsolete baseline is used.

For example:

```yaml
---
# Synopsis: This is an example baseline that is obsolete
kind: Baseline
metadata:
name: ObsoleteBaseline
annotations:
obsolete: true
spec: { }
```

## EXAMPLES

### Example ps-rule.yaml
Expand Down
4 changes: 4 additions & 0 deletions schemas/PSRule-language.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
"title": "Name",
"description": "The name of the resource. This must be unique.",
"minLength": 3
},
"annotations": {
"type": "object",
"title": "Annotations"
}
},
"required": [
Expand Down
14 changes: 14 additions & 0 deletions src/PSRule/Common/DictionaryExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ public static bool TryPopValue(this IDictionary<string, object> dictionary, stri
return dictionary.TryGetValue(key, out value) && dictionary.Remove(key);
}

public static bool TryGetBool(this IDictionary<string, object> dictionary, string key, out bool? value)
{
value = null;
if (!dictionary.TryGetValue(key, out object o))
return false;

if (o is bool bvalue || (o is string svalue && bool.TryParse(svalue, out bvalue)))
{
value = bvalue;
return true;
}
return false;
}

[DebuggerStepThrough]
public static void AddUnique(this IDictionary<string, object> dictionary, IEnumerable<KeyValuePair<string, object>> values)
{
Expand Down
5 changes: 5 additions & 0 deletions src/PSRule/Definitions/Baseline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ internal interface IBaselineSpec
public sealed class Baseline : Resource<BaselineSpec>, IResource
{
public Baseline(SourceFile source, ResourceMetadata metadata, ResourceHelpInfo info, BaselineSpec spec)
: base(metadata)
{
Info = info;
Source = source;
Spec = spec;
Name = BaselineId = metadata.Name;
Obsolete = ResourceHelper.IsObsolete(metadata);
}

[YamlIgnore()]
Expand All @@ -39,6 +41,9 @@ public Baseline(SourceFile source, ResourceMetadata metadata, ResourceHelpInfo i
[YamlIgnore()]
public readonly string Name;

[YamlIgnore()]
internal readonly bool Obsolete;

/// <summary>
/// The script file path where the baseline is defined.
/// </summary>
Expand Down
33 changes: 33 additions & 0 deletions src/PSRule/Definitions/ISpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using PSRule.Host;
using System.Collections.Generic;

namespace PSRule.Definitions
{
Expand Down Expand Up @@ -39,9 +40,21 @@ internal ResourceObject(IResource block)
internal IResource Block { get; }
}

public sealed class ResourceAnnotations : Dictionary<string, object>
{

}

public sealed class ResourceMetadata
{
public ResourceMetadata()
{
Annotations = new ResourceAnnotations();
}

public string Name { get; set; }

public ResourceAnnotations Annotations { get; set; }
}

public sealed class ResourceExtent
Expand All @@ -63,9 +76,29 @@ internal ResourceHelpInfo(string synopsis)

public abstract class Resource<TSpec> where TSpec : Spec, new()
{
protected Resource(ResourceMetadata metadata)
{
Metadata = metadata;
}

public ResourceMetadata Metadata { get; }

public abstract TSpec Spec { get; }
}

internal static class ResourceHelper
{
private const string ANNOTATION_OBSOLETE = "obsolete";

internal static bool IsObsolete(ResourceMetadata metadata)
{
if (metadata == null || metadata.Annotations == null || !metadata.Annotations.TryGetBool(ANNOTATION_OBSOLETE, out bool? obsolete))
return false;

return obsolete.GetValueOrDefault(false);
}
}

public abstract class Spec
{

Expand Down
1 change: 1 addition & 0 deletions src/PSRule/Definitions/ModuleConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ namespace PSRule.Definitions
internal sealed class ModuleConfig : Resource<ModuleConfigSpec>, IResource
{
public ModuleConfig(SourceFile source, ResourceMetadata metadata, ResourceHelpInfo info, ModuleConfigSpec spec)
: base(metadata)
{
Info = info;
Source = source;
Expand Down
7 changes: 1 addition & 6 deletions src/PSRule/Definitions/SpecFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,6 @@ public Type SpecType
get { return typeof(TSpec); }
}

public Type StepType
{
get { return typeof(T); }
}

public bool SupportsFlat { get; private set; }

public IResource CreateInstance(SourceFile source, ResourceMetadata metadata, CommentMetadata comment, object spec)
Expand Down Expand Up @@ -100,7 +95,7 @@ private void Bind()
}
}

private void SetDefaultProperty(Action<object, object> set, Type propertyType, Spec option, string value)
private static void SetDefaultProperty(Action<object, object> set, Type propertyType, Spec option, string value)
{
object v = value;
if (!propertyType.IsAssignableFrom(typeof(string)))
Expand Down
18 changes: 17 additions & 1 deletion src/PSRule/Pipeline/OptionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ protected OptionScope(ScopeType type, string moduleName)

internal sealed class BaselineScope : OptionScope
{
public string Id;
public bool Obsolete;

// Rule
public string[] Include;
public string[] Exclude;
Expand All @@ -90,9 +93,11 @@ internal sealed class BaselineScope : OptionScope
public string[] TargetType;
public bool? UseQualifiedName;

public BaselineScope(ScopeType type, string moduleName, IBaselineSpec option)
public BaselineScope(ScopeType type, string baselineId, string moduleName, IBaselineSpec option, bool obsolete)
: base(type, moduleName)
{
Id = baselineId;
Obsolete = obsolete;
Field = option.Binding?.Field;
IgnoreCase = option.Binding?.IgnoreCase;
NameSeparator = option?.Binding?.NameSeparator;
Expand Down Expand Up @@ -267,6 +272,17 @@ public string[] GetCulture()
return _Culture = _WorkspaceConfig?.Culture ?? _ModuleConfig?.Culture ?? _DefaultCulture;
}

internal void Init(RunspaceContext context)
{
foreach (var baseline in _ModuleBaselineScope.Values)
{
if (baseline.Obsolete)
context.WarnBaselineObsolete(baseline.Id);
}
if (_Explicit != null && _Explicit.Obsolete)
context.WarnBaselineObsolete(_Explicit.Id);
}

internal void Add(BaselineScope scope)
{
if (scope.Type == ScopeType.Module && !string.IsNullOrEmpty(scope.ModuleName))
Expand Down
2 changes: 1 addition & 1 deletion src/PSRule/Pipeline/PipelineBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ private OptionContext GetOptionContext()
var result = new OptionContext();

// Baseline
var baselineScope = new OptionContext.BaselineScope(type: OptionContext.ScopeType.Workspace, moduleName: null, option: Option);
var baselineScope = new OptionContext.BaselineScope(type: OptionContext.ScopeType.Workspace, baselineId: null, moduleName: null, option: Option, obsolete: false);
result.Add(baselineScope);
baselineScope = new OptionContext.BaselineScope(type: OptionContext.ScopeType.Parameter, include: _Include, tag: _Tag);
result.Add(baselineScope);
Expand Down
2 changes: 1 addition & 1 deletion src/PSRule/Pipeline/PipelineContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ internal void Import(IResource resource)
if (resource.Kind == ResourceKind.Baseline && resource is Baseline baseline && _Unresolved.TryGetValue(resource.Id, out ResourceRef rr) && rr is BaselineRef baselineRef)
{
_Unresolved.Remove(resource.Id);
Baseline.Add(new OptionContext.BaselineScope(baselineRef.Type, resource.Module, baseline.Spec));
Baseline.Add(new OptionContext.BaselineScope(baselineRef.Type, baseline.BaselineId, resource.Module, baseline.Spec, baseline.Obsolete));
}
else if (TryModuleConfig(resource, out ModuleConfig moduleConfig))
{
Expand Down
12 changes: 12 additions & 0 deletions src/PSRule/Pipeline/PipelineWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using PSRule.Rules;
using System.Collections.Generic;
using System.Management.Automation;
using System.Threading;

namespace PSRule.Pipeline
{
Expand Down Expand Up @@ -71,6 +72,17 @@ public virtual void WriteWarning(string message)
_Writer.WriteWarning(message);
}

public void WriteWarning(string message, params object[] args)
{
if (!ShouldWriteWarning() || string.IsNullOrEmpty(message))
return;

if (args == null || args.Length == 0)
WriteWarning(message);
else
WriteWarning(string.Format(Thread.CurrentThread.CurrentCulture, message, args));
}

public virtual bool ShouldWriteWarning()
{
return _Writer != null && _Writer.ShouldWriteWarning();
Expand Down
15 changes: 12 additions & 3 deletions src/PSRule/Pipeline/RunspaceContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using System.Management.Automation;
using System.Management.Automation.Language;
using System.Text;
using System.Threading;
using static PSRule.Pipeline.PipelineContext;

namespace PSRule.Pipeline
Expand Down Expand Up @@ -124,13 +125,21 @@ public void WarnRuleNotFound()
Writer.WriteWarning(PSRuleResources.RuleNotFound);
}

public void WarnBaselineObsolete(string baselineId)
{
if (Writer == null || !Writer.ShouldWriteWarning())
return;

Writer.WriteWarning(PSRuleResources.BaselineObsolete, baselineId);
}

public void ErrorInvaildRuleResult()
{
if (Writer == null || !Writer.ShouldWriteError())
return;

Writer.WriteError(new ErrorRecord(
exception: new RuleRuntimeException(message: string.Format(PSRuleResources.InvalidRuleResult, RuleBlock.RuleId)),
exception: new RuleRuntimeException(message: string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.InvalidRuleResult, RuleBlock.RuleId)),
errorId: ERRORID_INVALIDRULERESULT,
errorCategory: ErrorCategory.InvalidResult,
targetObject: null
Expand Down Expand Up @@ -166,7 +175,7 @@ public void VerboseConditionMessage(string condition, string message, params obj
if (Writer == null || !Writer.ShouldWriteVerbose())
return;

Writer.WriteVerbose(string.Concat(GetLogPrefix(), "[", condition, "] -- ", string.Format(message, args)));
Writer.WriteVerbose(string.Concat(GetLogPrefix(), "[", condition, "] -- ", string.Format(Thread.CurrentThread.CurrentCulture, message, args)));
}

public void VerboseConditionResult(string condition, int pass, int count, bool outcome)
Expand Down Expand Up @@ -438,7 +447,7 @@ public void WriteReason(string text)

public void Begin()
{
// Do nothing
Pipeline.Baseline.Init(this);
}

public string GetLocalizedPath(string file)
Expand Down
9 changes: 9 additions & 0 deletions src/PSRule/Resources/PSRuleResources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/PSRule/Resources/PSRuleResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="BaselineObsolete" xml:space="preserve">
<value>The baseline '{0}' is obsolete. Consider switching to an alternative baseline.</value>
<comment>Occurs when a baseline is used that has been flagged as obsolete.</comment>
</data>
<data name="ConstrainedTargetBinding" xml:space="preserve">
<value>Binding functions are not supported in this language mode.</value>
</data>
Expand Down
11 changes: 11 additions & 0 deletions tests/PSRule.Tests/Baseline.Rule.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
kind: Baseline
metadata:
name: TestBaseline1
annotations:
key: value
spec:
binding:
field:
Expand Down Expand Up @@ -71,6 +73,15 @@ spec:
- 'high'
- 'low'

---
# Synopsis: This is an example obsolete baseline
kind: Baseline
metadata:
name: TestBaseline5
annotations:
obsolete: true
spec: { }

---
kind: ObjectSelector
metadata:
Expand Down
9 changes: 9 additions & 0 deletions tests/PSRule.Tests/BaselineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,16 @@ public void ReadBaseline()
var context = new RunspaceContext(PipelineContext.New(GetOption(), null, null, new OptionContext(), null), null);
var baseline = HostHelper.GetBaseline(GetSource(), context).ToArray();
Assert.NotNull(baseline);
Assert.Equal(5, baseline.Length);

// TestBaseline1
Assert.Equal("TestBaseline1", baseline[0].Name);
Assert.Equal("value", baseline[0].Metadata.Annotations["key"]);
Assert.False(baseline[0].Obsolete);

// TestBaseline5
Assert.Equal("TestBaseline5", baseline[4].Name);
Assert.True(baseline[4].Obsolete);
}

private PSRuleOption GetOption()
Expand Down
Loading

0 comments on commit 6974d6e

Please sign in to comment.