diff --git a/.vscode/settings.json b/.vscode/settings.json index a20257902f..fc6bbbc17a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,18 +25,14 @@ "/ps-rule.yaml" ], "./schemas/PSRule-language.schema.json": [ - "/tests/PSRule.Tests/**.Rule.yaml", - "/tests/PSRule.Tests/**/**.Rule.yaml", - "/docs/scenarios/*/*.Rule.yaml", - "/docs/expressions/**/*.Rule.yaml" + "/**/**.Rule.yaml", + "/**/docs/scenarios/baselines/Baseline.rule.yaml" ] }, "json.schemas": [ { "fileMatch": [ - "/tests/PSRule.Tests/**.Rule.jsonc", - "/tests/PSRule.Tests/**/**.Rule.jsonc", - "/docs/expressions/**/*.Rule.jsonc" + "/**/**.Rule.jsonc" ], "url": "./schemas/PSRule-resources.schema.json" } diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index a1d0245438..360e6a11c7 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -32,6 +32,14 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since pre-release v2.9.0-B0013: + +- New features: + - Added sub-selector quantifiers for `allOf` or `anyOf` operators by @BernieWhite. + [#1423](https://github.com/microsoft/PSRule/issues/1423) + - Quantifiers allow you to specify the number of matches with `count`, `less`, `lessOrEqual`, `greater`, or `greaterOrEqual`. + - See [Sub-selectors][4] for more information. + ## v2.9.0-B0013 (pre-release) What's changed since release v2.8.1: diff --git a/docs/concepts/PSRule/en-US/about_PSRule_Expressions.md b/docs/concepts/PSRule/en-US/about_PSRule_Expressions.md index 41ccf147f2..da86b349d2 100644 --- a/docs/concepts/PSRule/en-US/about_PSRule_Expressions.md +++ b/docs/concepts/PSRule/en-US/about_PSRule_Expressions.md @@ -77,6 +77,10 @@ The `allOf` operator is used to require all nested expressions to match. When any nested expression does not match, `allOf` does not match. This is similar to a logical _and_ operation. +Additionally sub-selectors can be used to modify the `allOf` operator. +Sub-selectors allow filtering and looping through arrays of objects before the `allOf` operator is applied. +See sub-selectors for more information. + Syntax: ```yaml @@ -121,6 +125,10 @@ The `anyOf` operator is used to require one or more nested expressions to match. When any nested expression matches, `allOf` matches. This is similar to a logical _or_ operation. +Additionally sub-selectors can be used to modify the `anyOf` operator. +Sub-selectors allow filtering and looping through arrays of objects before the `anyOf` operator is applied. +See sub-selectors for more information. + Syntax: ```yaml diff --git a/docs/expressions/functions.md b/docs/expressions/functions.md index 7269c74910..b3461b8ce7 100644 --- a/docs/expressions/functions.md +++ b/docs/expressions/functions.md @@ -136,3 +136,9 @@ spec: - string: '-' - path: name ``` + +## Recommended content + +- [Create a standalone rule](../quickstart/standalone-rule.md) +- [Expressions](../concepts/PSRule/en-US/about_PSRule_Expressions.md) +- [Sub-selectors](sub-selectors.md) diff --git a/docs/expressions/sub-selectors.md b/docs/expressions/sub-selectors.md index 995a21a538..57cea7450b 100644 --- a/docs/expressions/sub-selectors.md +++ b/docs/expressions/sub-selectors.md @@ -184,7 +184,7 @@ In the example: ### When there are no results -Given the example, is important to understand what happens if: +Given the example, is important to understand what happens by default if: - The `resources` property doesn't exist. **OR** - The `resources` property doesn't contain any items that match the sub-selector condition. @@ -196,6 +196,7 @@ If this was not the desired behavior, you could: - Use a pre-condition to avoid running the rule. - Group the sub-selector into a `anyOf`, and provide a secondary condition. +- Use a quantifier to determine how many items must match sub-selector and match the `allOf` / `anyOf` operator. For example: @@ -270,3 +271,85 @@ In the example: - If the `resources` property exists but has 0 items of type `Microsoft.Web/sites/config`, the rule fails. - If the `resources` property exists and has any items of type `Microsoft.Web/sites/config` but any fail, the rule fails. - If the `resources` property exists and has any items of type `Microsoft.Web/sites/config` and all pass, the rule passes. + +### Using a quantifier with sub-selectors + +When iterating over a list of items, you may want to determine how many items must match. +A quantifier determines how many items in the list match. +Matching items must be: + +- Selected by the sub-selector. +- Match the condition of the operator. + +Supported quantifiers are: + +- `count` — The number of items must equal then the specified value. +- `less` — The number of items must less then the specified value. +- `lessOrEqual` — The number of items must less or equal to the specified value. +- `greater` — The number of items must greater then the specified value. +- `greaterOrEqual` — The number of items must greater or equal to the specified value. + +For example: + +=== "YAML" + + ```yaml hl_lines="13" + --- + # Synopsis: A rule with a sub-selector quantifier. + apiVersion: github.com/microsoft/PSRule/v1 + kind: Rule + metadata: + name: Yaml.Subselector.Quantifier + spec: + condition: + field: resources + where: + type: '.' + equals: 'Microsoft.Web/sites/config' + greaterOrEqual: 1 + allOf: + - field: properties.detailedErrorLoggingEnabled + equals: true + ``` + +=== "JSON" + + ```json hl_lines="15" + { + // Synopsis: A rule with a sub-selector quantifier. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "Json.Subselector.Quantifier" + }, + "spec": { + "condition": { + "field": "resources", + "where": { + "type": ".", + "equals": "Microsoft.Web/sites/config" + }, + "greaterOrEqual": 1, + "allOf": [ + { + "field": "properties.detailedErrorLoggingEnabled", + "equals": true + } + ] + } + } + } + ``` + +In the example: + +- If the array property `resources` exists, any items with a type of `Microsoft.Web/sites/config` are evaluated. + - Each item must have the `properties.detailedErrorLoggingEnabled` property set to `true` to pass. + - The number of items that pass must be greater or equal to `1`. +- If the `resources` property does not exist or is empty, the number of items is `0` which fails greater or equal to `1`. + +## Recommended content + +- [Create a standalone rule](../quickstart/standalone-rule.md) +- [Functions](functions.md) +- [Expressions](../concepts/PSRule/en-US/about_PSRule_Expressions.md) diff --git a/schemas/PSRule-language.schema.json b/schemas/PSRule-language.schema.json index bd1a6feb4d..e64eafeac6 100644 --- a/schemas/PSRule-language.schema.json +++ b/schemas/PSRule-language.schema.json @@ -845,14 +845,12 @@ }, { "type": "object", - "ztitle": "Value for object", - "zdescription": "A value to compare.", + "title": "Value for object", + "description": "A value to compare.", "not": { - "propertyNames": { - "enum": [ - "$" - ] - } + "required": [ + "$" + ] } }, { @@ -1365,15 +1363,7 @@ "markdownDescription": "Must have the specified value.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#equals)", "properties": { "equals": { - "title": "Equals", - "description": "Must have the specified value.", - "markdownDescription": "Must have the specified value.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#equals)", - "default": "", - "oneOf": [ - { - "$ref": "#/definitions/selectorExpressionValue" - } - ] + "$ref": "#/definitions/selectorExpressionValue" }, "convert": { "type": "boolean", @@ -1415,7 +1405,8 @@ { "$ref": "#/definitions/expressions/definitions/operands" } - ] + ], + "additionalProperties": false }, "count": { "type": "object", @@ -1454,34 +1445,27 @@ "required": [ "exists", "field" - ] + ], + "additionalProperties": false }, "notEquals": { "type": "object", "properties": { "notEquals": { - "title": "Not Equals", - "description": "Must not have the specified value.", - "markdownDescription": "Must not have the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notequals)", - "default": "", - "oneOf": [ - { - "$ref": "#/definitions/selectorExpressionValue" - } - ] + "$ref": "#/definitions/selectorExpressionValue" }, "convert": { "type": "boolean", "title": "Type conversion", "description": "Convert type of compared operand.", - "markdownDescription": "Convert type of compared operand. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notequals)", + "markdownDescription": "Convert type of compared operand.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notequals)", "default": false }, "caseSensitive": { "type": "boolean", "title": "Case sensitive", "description": "Determines if comparing values is case-sensitive. Only applies to string values.", - "markdownDescription": "Determines if comparing values is case-sensitive. Only applies to string values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notequals)", + "markdownDescription": "Determines if comparing values is case-sensitive. Only applies to string values.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notequals)", "default": false }, "field": { @@ -1510,7 +1494,8 @@ { "$ref": "#/definitions/expressions/definitions/operands" } - ] + ], + "additionalProperties": false }, "hasValue": { "type": "object", @@ -1536,7 +1521,8 @@ { "$ref": "#/definitions/expressions/definitions/operands" } - ] + ], + "additionalProperties": false }, "match": { "type": "object", @@ -1545,14 +1531,14 @@ "type": "string", "title": "Match", "description": "Must match the regular expression.", - "markdownDescription": "Must match the regular expression. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#match)", + "markdownDescription": "Must match the regular expression.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#match)", "default": "" }, "caseSensitive": { "type": "boolean", "title": "Case sensitive", "description": "Determines if the regular expression uses case-sensitive matching.", - "markdownDescription": "Determines if the regular expression uses case-sensitive matching. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#match)", + "markdownDescription": "Determines if the regular expression uses case-sensitive matching.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#match)", "default": false }, "field": { @@ -1590,14 +1576,14 @@ "type": "string", "title": "Not Match", "description": "Must not match the regular expression.", - "markdownDescription": "Must not match the regular expression. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notmatch)", + "markdownDescription": "Must not match the regular expression.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notmatch)", "default": "" }, "caseSensitive": { "type": "boolean", "title": "Case sensitive", "description": "Determines if the regular expression uses case-sensitive matching.", - "markdownDescription": "Determines if the regular expression uses case-sensitive matching. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notmatch)", + "markdownDescription": "Determines if the regular expression uses case-sensitive matching.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notmatch)", "default": false }, "field": { @@ -1635,7 +1621,7 @@ "type": "array", "title": "In", "description": "Must equal one of the specified values.", - "markdownDescription": "Must equal one of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#in)", + "markdownDescription": "Must equal one of the specified values.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#in)", "default": [ "" ], @@ -1676,7 +1662,7 @@ "type": "array", "title": "Not In", "description": "Must not equal any of the specified values.", - "markdownDescription": "Must not equal any of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notin)", + "markdownDescription": "Must not equal any of the specified values.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notin)", "default": [ "" ], @@ -1717,7 +1703,7 @@ "type": "array", "title": "SetOf", "description": "Must include all of but only specified values.", - "markdownDescription": "Must include all of but only values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#setof)", + "markdownDescription": "Must include all of but only values.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#setof)", "default": [ "" ], @@ -1727,7 +1713,7 @@ "type": "boolean", "title": "Case sensitive", "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#setof)", + "markdownDescription": "Determines if comparing values is case-sensitive.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#setof)", "default": false }, "field": { @@ -1762,7 +1748,7 @@ "type": "array", "title": "Subset", "description": "Must include all of the specified values.", - "markdownDescription": "Must include all of the specified values. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#subset)", + "markdownDescription": "Must include all of the specified values.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#subset)", "default": [ "" ], @@ -1772,7 +1758,7 @@ "type": "boolean", "title": "Case sensitive", "description": "Determines if comparing values is case-sensitive.", - "markdownDescription": "Determines if comparing values is case-sensitive. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#subset)", + "markdownDescription": "Determines if comparing values is case-sensitive.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#subset)", "default": false }, "unique": { @@ -1814,7 +1800,7 @@ "type": "integer", "title": "NotCount", "description": "Determines if operand does not have number of items.", - "markdownDescription": "Determines if operand does not have number of items. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notcount)", + "markdownDescription": "Determines if operand does not have number of items.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#notcount)", "minimum": 0, "default": 0 }, @@ -1841,7 +1827,7 @@ "less": { "title": "Less", "description": "Must be less then the specified value.", - "markdownDescription": "Must be less then the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", + "markdownDescription": "Must be less then the specified value.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", "default": 0, "oneOf": [ { @@ -1857,7 +1843,7 @@ "type": "boolean", "title": "Type conversion", "description": "Convert type of compared operand.", - "markdownDescription": "Convert type of compared operand. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", + "markdownDescription": "Convert type of compared operand.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", "default": false }, "field": { @@ -1895,7 +1881,7 @@ "lessOrEquals": { "title": "Less or Equal to", "description": "Must be less or equal to the specified value.", - "markdownDescription": "Must be less or equal to the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#lessorequals)", + "markdownDescription": "Must be less or equal to the specified value.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#lessorequals)", "default": 0, "oneOf": [ { @@ -1911,7 +1897,7 @@ "type": "boolean", "title": "Type conversion", "description": "Convert type of compared operand.", - "markdownDescription": "Convert type of compared operand. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", + "markdownDescription": "Convert type of compared operand.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", "default": false }, "field": { @@ -1949,7 +1935,7 @@ "greater": { "title": "Greater", "description": "Must be greater then the specified value.", - "markdownDescription": "Must be greater then the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#greater)", + "markdownDescription": "Must be greater then the specified value.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#greater)", "default": 0, "oneOf": [ { @@ -1965,7 +1951,7 @@ "type": "boolean", "title": "Type conversion", "description": "Convert type of compared operand.", - "markdownDescription": "Convert type of compared operand. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", + "markdownDescription": "Convert type of compared operand.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", "default": false }, "field": { @@ -1999,57 +1985,232 @@ }, "greaterOrEquals": { "type": "object", - "properties": { - "greaterOrEquals": { - "title": "Greater or Equal to", - "description": "Must be greater or equal to the specified value.", - "markdownDescription": "Must be greater or equal to the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#greaterorequals)", - "default": 0, - "oneOf": [ - { - "type": "integer" + "oneOf": [ + { + "type": "object", + "properties": { + "greaterOrEquals": { + "title": "Greater or Equal to", + "description": "Must be greater or equal to the specified value.", + "markdownDescription": "Must be greater or equal to the specified value.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#greaterorequals)", + "default": 0, + "oneOf": [ + { + "type": "integer" + }, + { + "type": "object", + "$ref": "#/definitions/fn" + } + ] }, - { - "type": "object", - "$ref": "#/definitions/fn" + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type of compared operand.", + "markdownDescription": "Convert type of compared operand.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", + "default": false + }, + "field": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/field" } - ] - }, - "convert": { - "type": "boolean", - "title": "Type conversion", - "description": "Convert type of compared operand.", - "markdownDescription": "Convert type of compared operand. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", - "default": false - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" + }, + "required": [ + "greaterOrEquals", + "field" + ], + "additionalProperties": false }, - "value": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + { + "type": "object", + "properties": { + "greaterOrEquals": { + "title": "Greater or Equal to", + "description": "Must be greater or equal to the specified value.", + "markdownDescription": "Must be greater or equal to the specified value.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#greaterorequals)", + "default": 0, + "oneOf": [ + { + "type": "integer" + }, + { + "type": "object", + "$ref": "#/definitions/fn" + } + ] + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type of compared operand.", + "markdownDescription": "Convert type of compared operand.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", + "default": false + }, + "value": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/value" + } + }, + "required": [ + "greaterOrEquals", + "value" + ], + "additionalProperties": false }, - "type": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + { + "type": "object", + "properties": { + "greaterOrEquals": { + "title": "Greater or Equal to", + "description": "Must be greater or equal to the specified value.", + "markdownDescription": "Must be greater or equal to the specified value.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#greaterorequals)", + "default": 0, + "oneOf": [ + { + "type": "integer" + }, + { + "type": "object", + "$ref": "#/definitions/fn" + } + ] + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type of compared operand.", + "markdownDescription": "Convert type of compared operand.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", + "default": false + }, + "type": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/type" + } + }, + "required": [ + "greaterOrEquals", + "type" + ], + "additionalProperties": false }, - "name": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + { + "type": "object", + "properties": { + "greaterOrEquals": { + "title": "Greater or Equal to", + "description": "Must be greater or equal to the specified value.", + "markdownDescription": "Must be greater or equal to the specified value.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#greaterorequals)", + "default": 0, + "oneOf": [ + { + "type": "integer" + }, + { + "type": "object", + "$ref": "#/definitions/fn" + } + ] + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type of compared operand.", + "markdownDescription": "Convert type of compared operand.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", + "default": false + }, + "name": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/name" + } + }, + "required": [ + "greaterOrEquals", + "name" + ], + "additionalProperties": false }, - "source": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + { + "type": "object", + "properties": { + "greaterOrEquals": { + "title": "Greater or Equal to", + "description": "Must be greater or equal to the specified value.", + "markdownDescription": "Must be greater or equal to the specified value.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#greaterorequals)", + "default": 0, + "oneOf": [ + { + "type": "integer" + }, + { + "type": "object", + "$ref": "#/definitions/fn" + } + ] + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type of compared operand.", + "markdownDescription": "Convert type of compared operand.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", + "default": false + }, + "source": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/source" + } + }, + "required": [ + "greaterOrEquals", + "source" + ], + "additionalProperties": false }, - "scope": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/scope" - } - }, - "required": [ - "greaterOrEquals" - ], - "oneOf": [ { - "$ref": "#/definitions/expressions/definitions/operands" + "type": "object", + "properties": { + "greaterOrEquals": { + "title": "Greater or Equal to", + "description": "Must be greater or equal to the specified value.", + "markdownDescription": "Must be greater or equal to the specified value.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#greaterorequals)", + "default": 0, + "oneOf": [ + { + "type": "integer" + }, + { + "type": "object", + "$ref": "#/definitions/fn" + } + ] + }, + "convert": { + "type": "boolean", + "title": "Type conversion", + "description": "Convert type of compared operand.", + "markdownDescription": "Convert type of compared operand.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#less)", + "default": false + }, + "scope": { + "$ref": "#/definitions/expressions/definitions/properties/definitions/scope" + } + }, + "required": [ + "greaterOrEquals", + "scope" + ], + "additionalProperties": false } ], - "additionalProperties": false + "not": { + "anyOf": [ + { + "required": [ + "allOf" + ] + }, + { + "required": [ + "anyOf" + ] + } + ] + } }, "startsWith": { "type": "object", @@ -2977,75 +3138,149 @@ "title": "allOf", "description": "All of the expressions must be true.", "markdownDescription": "All of the expressions must be true.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#allof)", - "properties": { - "allOf": { - "type": "array", - "title": "AllOf", - "description": "All of the expressions must be true.", - "markdownDescription": "All of the expressions must be true.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#allof)", - "items": { - "$ref": "#/definitions/expressions" - } - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "where": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/where" - } - }, - "required": [ - "allOf" - ], "oneOf": [ { + "type": "object", + "properties": { + "allOf": { + "type": "array", + "title": "AllOf", + "description": "All of the expressions must be true.", + "markdownDescription": "All of the expressions must be true.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#allof)", + "items": { + "$ref": "#/definitions/expressions" + } + } + }, + "required": [ + "allOf" + ], + "additionalProperties": false + }, + { + "type": "object", "properties": { + "allOf": { + "type": "array", + "title": "AllOf", + "description": "All of the expressions must be true.", + "markdownDescription": "All of the expressions must be true.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#allof)", + "items": { + "$ref": "#/definitions/expressions" + } + }, "field": { "$ref": "#/definitions/expressions/definitions/properties/definitions/field" }, "where": { "$ref": "#/definitions/expressions/definitions/properties/definitions/where" + }, + "less": { + "type": "integer", + "title": "Less than", + "minimum": 0 + }, + "lessOrEqual": { + "type": "integer", + "title": "Less or equal to", + "minimum": 0 + }, + "greater": { + "type": "integer", + "title": "Greater than", + "minimum": 0 + }, + "greaterOrEqual": { + "type": "integer", + "title": "Greater or equal to", + "minimum": 0 + }, + "count": { + "type": "integer", + "title": "Count", + "minimum": 0 } - } + }, + "required": [ + "allOf", + "field" + ], + "additionalProperties": false } - ], - "additionalProperties": false + ] }, "anyOf": { "type": "object", - "properties": { - "anyOf": { - "type": "array", - "title": "AnyOf", - "description": "One of the expressions must be true.", - "markdownDescription": "All of the expressions must be true.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#anyof)", - "items": { - "$ref": "#/definitions/expressions" - } - }, - "field": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/field" - }, - "where": { - "$ref": "#/definitions/expressions/definitions/properties/definitions/where" - } - }, - "required": [ - "anyOf" - ], "oneOf": [ { + "type": "object", + "properties": { + "anyOf": { + "type": "array", + "title": "AnyOf", + "description": "One of the expressions must be true.", + "markdownDescription": "All of the expressions must be true.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#anyof)", + "items": { + "$ref": "#/definitions/expressions" + } + } + }, + "required": [ + "anyOf" + ], + "additionalProperties": false + }, + { + "type": "object", "properties": { + "anyOf": { + "type": "array", + "title": "AnyOf", + "description": "One of the expressions must be true.", + "markdownDescription": "All of the expressions must be true.\n\n[See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#anyof)", + "items": { + "$ref": "#/definitions/expressions" + } + }, "field": { "$ref": "#/definitions/expressions/definitions/properties/definitions/field" }, "where": { "$ref": "#/definitions/expressions/definitions/properties/definitions/where" + }, + "less": { + "type": "integer", + "title": "Less than", + "minimum": 0 + }, + "lessOrEqual": { + "type": "integer", + "title": "Less or equal to", + "minimum": 0 + }, + "greater": { + "type": "integer", + "title": "Greater than", + "minimum": 0 + }, + "greaterOrEqual": { + "type": "integer", + "title": "Greater or equal to", + "minimum": 0 + }, + "count": { + "type": "integer", + "title": "Count", + "minimum": 0 } - } + }, + "required": [ + "anyOf", + "field" + ], + "additionalProperties": false } - ], - "additionalProperties": false + ] }, "not": { "type": "object", @@ -3068,9 +3303,10 @@ } }, "fn": { - "ztitle": "Value from function", - "zdescription": "A function expression that once evaluated specifies the value.", - "zmarkdownDescription": "A function expression that once evaluated specifies the value.", + "type": "object", + "title": "Value from function", + "description": "A function expression that once evaluated specifies the value.", + "markdownDescription": "A function expression that once evaluated specifies the value.", "properties": { "$": { "type": "object", @@ -3507,13 +3743,13 @@ "oneOf": [ { "type": "string", - "description": "The split function returns converts a string into an array by spliting based on a delimiter.", - "markdownDescription": "The `split` function returns converts a string into an array by spliting based on a delimiter." + "description": "The split function converts a string into an array by spliting based on a delimiter.", + "markdownDescription": "The `split` function converts a string into an array by spliting based on a delimiter." }, { "type": "object", - "description": "The split function returns converts a string into an array by spliting based on a delimiter.", - "markdownDescription": "The `split` function returns converts a string into an array by spliting based on a delimiter.", + "description": "The split function converts a string into an array by spliting based on a delimiter.", + "markdownDescription": "The `split` function converts a string into an array by spliting based on a delimiter.", "$ref": "#/definitions/fn/definitions/function" } ], diff --git a/src/PSRule/Definitions/Expressions/LanguageExpressions.cs b/src/PSRule/Definitions/Expressions/LanguageExpressions.cs index c15f9d1e8a..7dce3d47d8 100644 --- a/src/PSRule/Definitions/Expressions/LanguageExpressions.cs +++ b/src/PSRule/Definitions/Expressions/LanguageExpressions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using PSRule.Configuration; @@ -86,7 +87,13 @@ internal sealed class LanguageExpressionBuilder private const char Dot = '.'; private const char OpenBracket = '['; private const char CloseBracket = ']'; - private const string Where = ".where"; + + private const string DOTWHERE = ".where"; + private const string LESS = "less"; + private const string LESSOREQUAL = "lessOrEqual"; + private const string GREATER = "greater"; + private const string GREATEROREQUAL = "greaterOrEqual"; + private const string COUNT = "count"; private readonly bool _Debugger; @@ -279,27 +286,59 @@ private LanguageExpressionOuterFn Operator(string path, LanguageOperator express } else { - var subselector = expression.Subselector != null ? Expression(string.Concat(path, Where), expression.Subselector) : null; + var subselector = expression.Subselector != null ? Expression(string.Concat(path, DOTWHERE), expression.Subselector) : null; return (context, o) => { - if (!ObjectHelper.GetPath(context, o, Value(context, expression.Property["field"]), caseSensitive: false, out object[] items) || - items == null || items.Length == 0) - return false; + ObjectHelper.GetPath(context, o, Value(context, expression.Property["field"]), caseSensitive: false, out object[] items); + + var quantifier = GetQuantifier(expression); + var pass = 0; // If any fail, all fail - for (var i = 0; i < items.Length; i++) + for (var i = 0; items != null && i < items.Length; i++) { if (subselector == null || subselector(context, items[i]).GetValueOrDefault(true)) { if (!expression.Descriptor.Fn(context, info, innerA, items[i])) - return false; + { + if (quantifier == null) + return false; + } + else + { + pass++; + } } } - return true; + return quantifier == null || quantifier(pass); }; } } + /// + /// Returns a quantifier function if set for the expression. + /// + private Func GetQuantifier(LanguageOperator expression) + { + if (expression.Property.TryGetLong(GREATEROREQUAL, out var q)) + return (number) => number >= q.Value; + + if (expression.Property.TryGetLong(GREATER, out q)) + return (number) => number > q.Value; + + if (expression.Property.TryGetLong(LESSOREQUAL, out q)) + return (number) => number <= q.Value; + + if (expression.Property.TryGetLong(LESS, out q)) + return (number) => number < q.Value; + + if (expression.Property.TryGetLong(COUNT, out q)) + return (number) => number == q.Value; + + return null; + } + + [DebuggerStepThrough] private string Value(ExpressionContext context, object v) { return v as string; diff --git a/tests/PSRule.Tests/FromFileSubSelector.Rule.jsonc b/tests/PSRule.Tests/FromFileSubSelector.Rule.jsonc index f831aa0394..93a3eaba2f 100644 --- a/tests/PSRule.Tests/FromFileSubSelector.Rule.jsonc +++ b/tests/PSRule.Tests/FromFileSubSelector.Rule.jsonc @@ -1,66 +1,90 @@ [ - { - // Synopsis: A rule with sub-selector pre-condition. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Rule", - "metadata": { - "name": "JsonRuleWithPrecondition" - }, - "spec": { - "where": { - "field": "kind", - "equals": "test" - }, - "condition": { - "field": "resources", - "count": 2 - } - } + { + // Synopsis: A rule with sub-selector pre-condition. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "JsonRuleWithPrecondition" }, - { - // Synopsis: A rule with sub-selector filter. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Rule", - "metadata": { - "name": "JsonRuleWithSubselector" - }, - "spec": { - "condition": { - "field": "resources", - "where": { - "field": ".", - "isString": true - }, - "allOf": [ - { - "field": ".", - "equals": "abc" - } - ] - } - } + "spec": { + "where": { + "field": "kind", + "equals": "test" + }, + "condition": { + "field": "resources", + "count": 2 + } + } + }, + { + // Synopsis: A rule with sub-selector filter. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "JsonRuleWithSubselector" }, - { - // Synopsis: A rule with sub-selector filter. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Rule", - "metadata": { - "name": "JsonRuleWithSubselectorReordered" + "spec": { + "condition": { + "field": "resources", + "where": { + "field": ".", + "isString": true }, - "spec": { - "condition": { - "allOf": [ - { - "field": ".", - "equals": "abc" - } - ], - "field": "resources", - "where": { - "field": ".", - "equals": "abc" - } - } + "allOf": [ + { + "field": ".", + "equals": "abc" + } + ] + } + } + }, + { + // Synopsis: A rule with sub-selector filter. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "JsonRuleWithSubselectorReordered" + }, + "spec": { + "condition": { + "allOf": [ + { + "field": ".", + "equals": "abc" + } + ], + "field": "resources", + "where": { + "field": ".", + "equals": "abc" } + } + } + }, + { + // Synopsis: A rule with a sub-selector quantifier. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Rule", + "metadata": { + "name": "JsonRuleWithQuantifier" + }, + "spec": { + "condition": { + "field": "resources[*].properties.logs[*]", + "greaterOrEqual": 1, + "allOf": [ + { + "field": "category", + "equals": "firewall" + }, + { + "field": "enabled", + "equals": true + } + ] + } } + } ] diff --git a/tests/PSRule.Tests/FromFileSubSelector.Rule.yaml b/tests/PSRule.Tests/FromFileSubSelector.Rule.yaml index 9ff8d2b065..c65dcf83a7 100644 --- a/tests/PSRule.Tests/FromFileSubSelector.Rule.yaml +++ b/tests/PSRule.Tests/FromFileSubSelector.Rule.yaml @@ -32,8 +32,8 @@ spec: field: '.' isString: true allOf: - - field: '.' - equals: abc + - field: '.' + equals: abc --- # Synopsis: A rule with sub-selector filter. @@ -44,9 +44,25 @@ metadata: spec: condition: allOf: - - field: '.' - equals: abc + - field: '.' + equals: abc field: resources where: field: '.' equals: 'abc' + +--- +# Synopsis: A rule with a sub-selector quantifier. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Rule +metadata: + name: YamlRuleWithQuantifier +spec: + condition: + field: resources[*].properties.logs[*] + greaterOrEqual: 1 + allOf: + - field: category + equals: firewall + - field: enabled + equals: true diff --git a/tests/PSRule.Tests/RulesTests.cs b/tests/PSRule.Tests/RulesTests.cs index c47e72ce3d..5102999f01 100644 --- a/tests/PSRule.Tests/RulesTests.cs +++ b/tests/PSRule.Tests/RulesTests.cs @@ -72,12 +72,14 @@ public void ReadYamlSubSelectorRule() Assert.Equal("YamlRuleWithPrecondition", rule[0].Name); Assert.Equal("YamlRuleWithSubselector", rule[1].Name); Assert.Equal("YamlRuleWithSubselectorReordered", rule[2].Name); + Assert.Equal("YamlRuleWithQuantifier", rule[3].Name); context.Init(GetSource("FromFileSubSelector.Rule.yaml")); context.Begin(); var subselector1 = GetRuleVisitor(context, "YamlRuleWithPrecondition", GetSource("FromFileSubSelector.Rule.yaml")); var subselector2 = GetRuleVisitor(context, "YamlRuleWithSubselector", GetSource("FromFileSubSelector.Rule.yaml")); var subselector3 = GetRuleVisitor(context, "YamlRuleWithSubselectorReordered", GetSource("FromFileSubSelector.Rule.yaml")); + var subselector4 = GetRuleVisitor(context, "YamlRuleWithQuantifier", GetSource("FromFileSubSelector.Rule.yaml")); context.EnterLanguageScope(subselector1.Source); var actual1 = GetObject((name: "kind", value: "test"), (name: "resources", value: new string[] { "abc", "abc" })); @@ -109,6 +111,24 @@ public void ReadYamlSubSelectorRule() context.EnterTargetObject(actual2); context.EnterRuleBlock(subselector3); Assert.True(subselector3.Condition.If().AllOf()); + + // YamlRuleWithQuantifier + var fromFile = GetObjectAsTarget("ObjectFromFile3.json"); + actual1 = fromFile[0]; + actual2 = fromFile[1]; + var actual3 = fromFile[2]; + + context.EnterTargetObject(actual1); + context.EnterRuleBlock(subselector4); + Assert.True(subselector4.Condition.If().AllOf()); + + context.EnterTargetObject(actual2); + context.EnterRuleBlock(subselector4); + Assert.False(subselector4.Condition.If().AllOf()); + + context.EnterTargetObject(actual3); + context.EnterRuleBlock(subselector4); + Assert.True(subselector4.Condition.If().AllOf()); } [Fact] @@ -255,12 +275,14 @@ public void ReadJsonSubSelectorRule() Assert.Equal("JsonRuleWithPrecondition", rule[0].Name); Assert.Equal("JsonRuleWithSubselector", rule[1].Name); Assert.Equal("JsonRuleWithSubselectorReordered", rule[2].Name); + Assert.Equal("JsonRuleWithQuantifier", rule[3].Name); context.Init(GetSource("FromFileSubSelector.Rule.yaml")); context.Begin(); var subselector1 = GetRuleVisitor(context, "JsonRuleWithPrecondition", GetSource("FromFileSubSelector.Rule.jsonc")); var subselector2 = GetRuleVisitor(context, "JsonRuleWithSubselector", GetSource("FromFileSubSelector.Rule.jsonc")); var subselector3 = GetRuleVisitor(context, "JsonRuleWithSubselectorReordered", GetSource("FromFileSubSelector.Rule.jsonc")); + var subselector4 = GetRuleVisitor(context, "JsonRuleWithQuantifier", GetSource("FromFileSubSelector.Rule.jsonc")); context.EnterLanguageScope(subselector1.Source); var actual1 = GetObject((name: "kind", value: "test"), (name: "resources", value: new string[] { "abc", "abc" })); @@ -292,6 +314,24 @@ public void ReadJsonSubSelectorRule() context.EnterTargetObject(actual2); context.EnterRuleBlock(subselector3); Assert.True(subselector3.Condition.If().AllOf()); + + // JsonRuleWithQuantifier + var fromFile = GetObjectAsTarget("ObjectFromFile3.json"); + actual1 = fromFile[0]; + actual2 = fromFile[1]; + var actual3 = fromFile[2]; + + context.EnterTargetObject(actual1); + context.EnterRuleBlock(subselector4); + Assert.True(subselector4.Condition.If().AllOf()); + + context.EnterTargetObject(actual2); + context.EnterRuleBlock(subselector4); + Assert.False(subselector4.Condition.If().AllOf()); + + context.EnterTargetObject(actual3); + context.EnterRuleBlock(subselector4); + Assert.True(subselector4.Condition.If().AllOf()); } #endregion Json rules @@ -324,6 +364,11 @@ private static object[] GetObject(string path) return JsonConvert.DeserializeObject(File.ReadAllText(path)); } + private static TargetObject[] GetObjectAsTarget(string path) + { + return JsonConvert.DeserializeObject(File.ReadAllText(path)).Select(o => new TargetObject(new PSObject(o))).ToArray(); + } + private static RuleBlock GetRuleVisitor(RunspaceContext context, string name, Source[] source = null) { var block = HostHelper.GetRuleBlockGraph(source ?? GetSource(), context).GetAll(); diff --git a/tests/PSRule.Tests/SelectorTests.cs b/tests/PSRule.Tests/SelectorTests.cs index e5ca17ec58..c9be56f7a8 100644 --- a/tests/PSRule.Tests/SelectorTests.cs +++ b/tests/PSRule.Tests/SelectorTests.cs @@ -41,7 +41,7 @@ public void ReadSelector(string type, string path) context.Begin(); var selector = HostHelper.GetSelectorForTests(GetSource(path), context).ToArray(); Assert.NotNull(selector); - Assert.Equal(101, selector.Length); + Assert.Equal(102, selector.Length); var actual = selector[0]; var visitor = new SelectorVisitor(context, actual.Id, actual.Source, actual.Spec.If); @@ -1661,6 +1661,23 @@ public void AllOf(string type, string path) Assert.False(allOf.Match(actual2)); Assert.True(allOf.Match(actual3)); Assert.False(allOf.Match(actual4)); + + // With quantifier + allOf = GetSelectorVisitor($"{type}AllOfWithQuantifier", GetSource(path), out _); + actual1 = GetObject((name: "Name", value: "TargetObject1"), (name: "properties", value: GetObject((name: "logs", value: new object[] + { + GetObject((name: "name", value: "log1")) + })))); + actual2 = GetObject((name: "Name", value: "TargetObject1"), (name: "properties", value: GetObject((name: "logs", value: new object[] + { + GetObject((name: "name", value: "log1")), + GetObject((name: "name", value: "log2")) + })))); + actual3 = GetObject((name: "Name", value: "TargetObject1"), (name: "properties", value: GetObject((name: "logs", value: new object[] {})))); + + Assert.True(allOf.Match(actual1)); + Assert.True(allOf.Match(actual2)); + Assert.False(allOf.Match(actual3)); } [Theory] diff --git a/tests/PSRule.Tests/Selectors.Rule.jsonc b/tests/PSRule.Tests/Selectors.Rule.jsonc index 70e121a8e6..342d69d78d 100644 --- a/tests/PSRule.Tests/Selectors.Rule.jsonc +++ b/tests/PSRule.Tests/Selectors.Rule.jsonc @@ -1904,5 +1904,25 @@ "apiVersion": "" } } + }, + { + // Synopsis: Test all of with quantifier. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonAllOfWithQuantifier" + }, + "spec": { + "if": { + "field": "properties.logs[*]", + "greaterOrEqual": 1, + "allOf": [ + { + "field": "name", + "equals": "log1" + } + ] + } + } } ] diff --git a/tests/PSRule.Tests/Selectors.Rule.yaml b/tests/PSRule.Tests/Selectors.Rule.yaml index da9613ec34..4bbf6a30b1 100644 --- a/tests/PSRule.Tests/Selectors.Rule.yaml +++ b/tests/PSRule.Tests/Selectors.Rule.yaml @@ -1363,3 +1363,17 @@ spec: if: field: dateVersion apiVersion: '' + +--- +# Synopsis: Test all of with quantifier. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: YamlAllOfWithQuantifier +spec: + if: + field: properties.logs[*] + greaterOrEqual: 1 + allOf: + - field: name + equals: log1