Skip to content

Commit

Permalink
Added file path assertion helpers microsoft#679 microsoft#680 microso…
Browse files Browse the repository at this point in the history
  • Loading branch information
BernieWhite committed Apr 4, 2021
1 parent 0c23542 commit 9399ad5
Show file tree
Hide file tree
Showing 20 changed files with 528 additions and 110 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ The following conceptual topics exist in the `PSRule` module:
- [In](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#in)
- [IsArray](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#isarray)
- [IsBoolean](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#isboolean)
- [IsDateTime](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#isdatetime)
- [IsInteger](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#isinteger)
- [IsLower](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#islower)
- [IsNumeric](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#isnumeric)
Expand All @@ -286,11 +287,13 @@ The following conceptual topics exist in the `PSRule` module:
- [NotIn](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#notin)
- [NotMatch](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#notmatch)
- [NotNull](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#notnull)
- [NotWithinPath](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#notwithinpath)
- [Null](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#null)
- [NullOrEmpty](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#nullorempty)
- [TypeOf](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#typeof)
- [StartsWith](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#startswith)
- [Version](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#version)
- [WithinPath](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#withinpath)
- [Baselines](docs/concepts/PSRule/en-US/about_PSRule_Baseline.md)
- [Baseline specs](docs/concepts/PSRule/en-US/about_PSRule_Baseline.md#baseline-specs)
- [Baseline scopes](docs/concepts/PSRule/en-US/about_PSRule_Baseline.md#baseline-scopes)
Expand Down
12 changes: 12 additions & 0 deletions docs/CHANGELOG-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ See [upgrade notes][upgrade-notes] for helpful information when upgrading from p

## Unreleased

- General improvements:
- Added file path assertion helpers. [#679](https://github.com/microsoft/PSRule/issues/679)
- Added `WithinPath` to check the file path field is within a specified path.
- Added `NotWithinPath` to check the file path field is not within a specified path
- See [about_PSRule_Assert] for details.
- Added DateTime type assertion helper. [#680](https://github.com/microsoft/PSRule/issues/680)
- Added `IsDateTime` to check of object field is `[DateTime]`.
- See [about_PSRule_Assert] for details.
- Improved numeric comparison assertion helpers to compare `[DateTime]` fields. [#685](https://github.com/microsoft/PSRule/issues/685)
- `Less`, `LessOrEqual`, `Greater`, and `GreaterOrEqual` compare the number of days from the current time.
- See [about_PSRule_Assert] for details.

## v1.2.0

What's changed since v1.1.0:
Expand Down
111 changes: 108 additions & 3 deletions docs/concepts/PSRule/en-US/about_PSRule_Assert.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ The following built-in assertion methods are provided:
- [In](#in) - The field value must be included in the set.
- [IsArray](#isarray) - The field value must be an array.
- [IsBoolean](#isboolean) - The field value must be a boolean.
- [IsDateTime](#isdatetime) - The field value must be a DateTime.
- [IsInteger](#isinteger) - The field value must be an integer.
- [IsLower](#islower) - The field value must include only lowercase characters.
- [IsNumeric](#isnumeric) - The field value must be a numeric type.
Expand All @@ -42,11 +43,13 @@ The following built-in assertion methods are provided:
- [NotIn](#notin) - The field value must not be included in the set.
- [NotMatch](#notmatch) - The field value does not match a regular expression pattern.
- [NotNull](#notnull) - The field value must not be null.
- [NotWithinPath](#notwithinpath) - The field must not be within the specified path.
- [Null](#null) - The field value must not exist or be null.
- [NullOrEmpty](#nullorempty) - The object must not have the specified field or it must be empty.
- [TypeOf](#typeof) - The field value must be of the specified type.
- [StartsWith](#startswith) - The field value must match at least one prefix.
- [Version](#version) - The field value must be a semantic version string.
- [WithinPath](#withinpath) - The field value must be within the specified path.

The `$Assert` variable can only be used within a rule definition block or script pre-conditions.

Expand Down Expand Up @@ -219,7 +222,7 @@ Rule 'FileHeader' {
### FilePath

The `FilePath` assertion method checks the file exists.
Checks use OS case-sensitivity rules.
Checks use file system case-sensitivity rules.

The following parameters are accepted:

Expand Down Expand Up @@ -261,6 +264,7 @@ When the field value is:
- An integer or float, a numerical comparison is used.
- An array, the number of elements is compared.
- A string, the length of the string is compared.
- A DateTime, the number of days from the current time is compared.

The following parameters are accepted:

Expand Down Expand Up @@ -295,6 +299,7 @@ When the field value is:
- An integer or float, a numerical comparison is used.
- An array, the number of elements is compared.
- A string, the length of the string is compared.
- A DateTime, the number of days from the current time is compared.

The following parameters are accepted:

Expand Down Expand Up @@ -593,6 +598,38 @@ Rule 'IsBoolean' {
}
```

### IsDateTime

The `IsBoolean` assertion method checks the field value is a DateTime type.

The following parameters are accepted:

- `inputObject` - The object being checked for the specified field.
- `field` - The name of the field to check.
This is a case insensitive compare.
- `convert` (optional) - Try to convert strings.
By default strings are not converted.

Reasons include:

- _The parameter 'inputObject' is null._
- _The parameter 'field' is null or empty._
- _The field '{0}' does not exist._
- _The field value '{0}' is null._
- _The field value '{1}' of type {0} is not \[DateTime\]._

Examples:

```powershell
Rule 'IsBoolean' {
# Require Value1 to be a DateTime
$Assert.IsDateTime($TargetObject, 'Value1')
# Require Value1 to be a DateTime or a DateTime string
$Assert.IsDateTime($TargetObject, 'Value1', $True)
}
```

### IsInteger

The `IsInteger` assertion method checks the field value is a integer type.
Expand Down Expand Up @@ -761,6 +798,7 @@ When the field value is:
- An integer or float, a numerical comparison is used.
- An array, the number of elements is compared.
- A string, the length of the string is compared.
- A DateTime, the number of days from the current time is compared.

The following parameters are accepted:

Expand Down Expand Up @@ -795,14 +833,15 @@ When the field value is:
- An integer or float, a numerical comparison is used.
- An array, the number of elements is compared.
- A string, the length of the string is compared.
- `convert` (optional) - Convert numerical strings and use a numerical comparison instead of using string length.
By default the string length is compared.
- A DateTime, the number of days from the current time is compared.

The following parameters are accepted:

- `inputObject` - The object being checked for the specified field.
- `field` - The name of the field to check. This is a case insensitive compare.
- `value` - A integer to compare the field value against.
- `convert` (optional) - Convert numerical strings and use a numerical comparison instead of using string length.
By default the string length is compared.

Reasons include:

Expand Down Expand Up @@ -966,6 +1005,39 @@ Rule 'NotNull' {
}
```

### NotWithinPath

The `NotWithinPath` assertion method checks the file is not within a specified path.
Checks use file system case-sensitivity rules by default.

The following parameters are accepted:

- `inputObject` - The object being checked for the specified field.
- `field` - The name of the field containing a file path.
When the field is `InputFileInfo` or `FileInfo`, PSRule will automatically resolve the file path.
- `path` - An array of one or more directory paths to check.
Only one path must match.
- `caseSensitive` (optional) - Determines if case-sensitive path matching is used.
This can be set to `$True` or `$False`.
When not set or `$Null`, the case-sensitivity rules of the working path file system will be used.

Reasons include:

- _The parameter 'inputObject' is null._
- _The parameter 'field' is null or empty._
- _The parameter 'path' is null or empty._
- _The field '{0}' does not exist._
- _The file '{0}' is within the path '{1}'._

Examples:

```powershell
Rule 'NotWithinPath' {
# The file must not be within either policy/ or security/ sub-directories.
$Assert.NotWithinPath($TargetObject, 'FullName', @('policy/', 'security/'));
}
```

### Null

The `Null` assertion method checks the field value of the object is null.
Expand Down Expand Up @@ -1145,6 +1217,39 @@ Rule 'MinimumVersion' {
}
```

### WithinPath

The `WithinPath` assertion method checks if the file path is within a required path.
Checks use file system case-sensitivity rules by default.

The following parameters are accepted:

- `inputObject` - The object being checked for the specified field.
- `field` - The name of the field containing a file path.
When the field is `InputFileInfo` or `FileInfo`, PSRule will automatically resolve the file path.
- `path` - An array of one or more directory paths to check.
Only one path must match.
- `caseSensitive` (optional) - Determines if case-sensitive path matching is used.
This can be set to `$True` or `$False`.
When not set or `$Null`, the case-sensitivity rules of the working path file system will be used.

Reasons include:

- _The parameter 'inputObject' is null._
- _The parameter 'field' is null or empty._
- _The parameter 'path' is null or empty._
- _The field '{0}' does not exist._
- _The file '{0}' is not within the path '{1}'._

Examples:

```powershell
Rule 'WithinPath' {
# Require the file to be within either policy/ or security/ sub-directories.
$Assert.WithinPath($TargetObject, 'FullName', @('policy/', 'security/'));
}
```

### Advanced usage

The `AssertResult` object returned from assertion methods:
Expand Down
2 changes: 1 addition & 1 deletion src/PSRule.Benchmark/PSRule.Benchmark.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.1'">
<PackageReference Include="Microsoft.PowerShell.SDK" Version="7.0.3" />
<PackageReference Include="Microsoft.PowerShell.SDK" Version="7.0.6" />
</ItemGroup>

<ItemGroup>
Expand Down
81 changes: 81 additions & 0 deletions src/PSRule/Common/ExpressionHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
// Licensed under the MIT License.

using Newtonsoft.Json.Linq;
using PSRule.Configuration;
using PSRule.Data;
using PSRule.Pipeline;
using PSRule.Runtime;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Management.Automation;
using System.Text.RegularExpressions;
using System.Threading;
Expand All @@ -18,6 +21,9 @@ internal static class ExpressionHelpers
private const string CACHE_MATCH = "MatchRegex";
private const string CACHE_MATCH_C = "MatchRegexCaseSensitive";

private const char Backslash = '\\';
private const char Slash = '/';

internal static bool NullOrEmpty(object o)
{
if (o == null)
Expand Down Expand Up @@ -72,6 +78,12 @@ internal static bool CompareNumeric(object actual, object expected, bool convert
value = actualFloat;
return true;
}
else if (TryDateTime(actual, convert, out DateTime actualDateTime) && TryDateTime(expected, convert: true, value: out DateTime expectedDateTime))
{
compare = Comparer<DateTime>.Default.Compare(actualDateTime, expectedDateTime);
value = actualDateTime;
return true;
}
else if ((TryStringLength(actual, out actualInt) || TryArrayLength(actual, out actualInt)) && TryInt(expected, convert: true, value: out expectedInt))
{
compare = Comparer<int>.Default.Compare(actualInt, expectedInt);
Expand Down Expand Up @@ -288,6 +300,33 @@ internal static bool TryArrayLength(object o, out int value)
return false;
}

internal static bool TryDateTime(object o, bool convert, out DateTime value)
{
o = GetBaseObject(o);
if (o is DateTime dvalue)
{
value = dvalue;
return true;
}
else if (o is JToken token && token.Type == JTokenType.Date)
{
value = token.Value<DateTime>();
return true;
}
else if (convert && TryString(o, out string s) && DateTime.TryParse(s, out dvalue))
{
value = dvalue;
return true;
}
else if (convert && TryInt(o, convert: false, out int daysOffset))
{
value = DateTime.Now.AddDays(daysOffset);
return true;
}
value = default(DateTime);
return false;
}

internal static bool Match(string pattern, string value, bool caseSensitive)
{
var expression = GetRegularExpression(pattern, caseSensitive);
Expand Down Expand Up @@ -320,6 +359,43 @@ internal static bool AnyValue(object actualValue, object expectedValue, bool cas
return false;
}

internal static bool WithinPath(string actualPath, string expectedPath, bool caseSensitive)
{
var expected = PSRuleOption.GetRootedBasePath(expectedPath);
var actual = PSRuleOption.GetRootedPath(actualPath);
return actual.StartsWith(expected, ignoreCase: !caseSensitive, Thread.CurrentThread.CurrentCulture);
}

internal static string NormalizePath(string basePath, string path)
{
path = Path.IsPathRooted(path) ? Path.GetFullPath(path) : Path.GetFullPath(Path.Combine(basePath, path));
basePath = PSRuleOption.GetRootedBasePath(basePath);
return path.Substring(basePath.Length).Replace(Backslash, Slash);
}

internal static string GetObjectOriginPath(object o)
{
var baseObject = GetBaseObject(o);
var targetInfo = GetTargetInfo(o);
if (baseObject is InputFileInfo inputFileInfo)
{
return PSRuleOption.GetRootedPath(inputFileInfo.FullName);
}
else if (baseObject is FileInfo fileInfo)
{
return PSRuleOption.GetRootedPath(fileInfo.FullName);
}
else if (targetInfo != null)
{
return PSRuleOption.GetRootedPath(targetInfo.PSPath);
}
else if (baseObject is string s)
{
return PSRuleOption.GetRootedPath(s);
}
return null;
}

private static Regex GetRegularExpression(string pattern, bool caseSensitive)
{
if (!TryPipelineCache(caseSensitive ? CACHE_MATCH_C : CACHE_MATCH, pattern, out Regex expression))
Expand Down Expand Up @@ -355,6 +431,11 @@ private static object GetBaseObject(object o)
return o is PSObject pso && pso.BaseObject != null ? pso.BaseObject : o;
}

private static PSRuleTargetInfo GetTargetInfo(object o)
{
return o is PSObject pso && pso.TryTargetInfo(out PSRuleTargetInfo targetInfo) ? targetInfo : null;
}

private static bool StringEqual(string expectedValue, string actualValue, bool caseSensitive)
{
return caseSensitive ? StringComparer.Ordinal.Equals(expectedValue, actualValue) : StringComparer.OrdinalIgnoreCase.Equals(expectedValue, actualValue);
Expand Down

0 comments on commit 9399ad5

Please sign in to comment.