Repository: aws-cloudformation/cfn-lint

Issue:
E8005 - False positive in Rules section, thinks !Contains is not returning a boolean
### CloudFormation Lint Version

1.15.0

### What operating system are you using?

Fedora

### Describe the bug

We have a rule which has a RuleCondition which has

!Not [ !Contains [ [ <list of items ], !Ref SomeParameter ] ]

This is now returning an error

E8005 {'Fn::Contains': [ [<list of items> ], {'Ref': 'SomeParameter'}]} is not of type boolean

This was working against cfn-lint 1.14.2

### Expected behavior

Rule should not cause E8005 error

### Reproduction template

```
AWSTemplateFormatVersion: 2010-09-09

Description: FalsePositive-E8005
Parameters:

    ElasticsearchSize:
        Description: This value is NOT required when using an instance type which uses instance store storage (r6gd or i3)
        # This must be a string as it can be empty in some scenarios (which makes it an invalid number...
        Type: String

    ElasticsearchInstanceType:
        Description: The ES data instance type
        Type: String
        Default: r5.large.elasticsearch
        AllowedValues: [
            m5.large.elasticsearch, m5.xlarge.elasticsearch, m5.2xlarge.elasticsearch, m5.4xlarge.elasticsearch, m5.12xlarge.elasticsearch,
            r5.large.elasticsearch, r5.xlarge.elasticsearch, r5.2xlarge.elasticsearch, r5.4xlarge.elasticsearch, r5.8xlarge.elasticsearch, r5.16xlarge.elasticsearch,
            c5.large.elasticsearch, c5.xlarge.elasticsearch, c5.2xlarge.elasticsearch, c5.4xlarge.elasticsearch, c5.9xlarge.elasticsearch, c5.18xlarge.elasticsearch,
            c6g.large.elasticsearch, c6g.xlarge.elasticsearch, c6g.2xlarge.elasticsearch, c6g.4xlarge.elasticsearch, c6g.8xlarge.elasticsearch, c6g.12xlarge.elasticsearch,
            r6gd.large.elasticsearch, r6gd.xlarge.elasticsearch, r6gd.2xlarge.elasticsearch, r6gd.4xlarge.elasticsearch, r6gd.8xlarge.elasticsearch, r6gd.16xlarge.elasticsearch,
            i3.large.elasticsearch, i3.xlarge.elasticsearch, i3.2xlarge.elasticsearch, i3.4xlarge.elasticsearch, i3.8xlarge.elasticsearch, i3.16xlarge.elasticsearch
        ]
        ConstraintDescription: The elasticache data instance type must be of a type which supports encryption at rest

Rules:
        
        #this rule passes
        ESInstanceStorageInstanceStore:
            RuleCondition: !Contains [ [
                r6gd.large.elasticsearch, r6gd.xlarge.elasticsearch, r6gd.2xlarge.elasticsearch, r6gd.4xlarge.elasticsearch, r6gd.8xlarge.elasticsearch, r6gd.16xlarge.elasticsearch,
                i3.large.elasticsearch, i3.xlarge.elasticsearch, i3.2xlarge.elasticsearch, i3.4xlarge.elasticsearch, i3.8xlarge.elasticsearch, i3.16xlarge.elasticsearch
            ], !Ref ElasticsearchInstanceType ]
            Assertions:
              - Assert: !Equals [ !Ref ElasticsearchSize, '' ]
                AssertDescription: "The Elasticsearch EBS size parameter must NOT be set when the instance type is r6gd or i3"

        #This rule fails as it thinks !Contains is not returning a boolean
        ESInstanceStorageEBS:
            RuleCondition: !Not [ !Contains [ [
                r6gd.large.elasticsearch, r6gd.xlarge.elasticsearch, r6gd.2xlarge.elasticsearch, r6gd.4xlarge.elasticsearch, r6gd.8xlarge.elasticsearch, r6gd.16xlarge.elasticsearch,
                i3.large.elasticsearch, i3.xlarge.elasticsearch, i3.2xlarge.elasticsearch, i3.4xlarge.elasticsearch, i3.8xlarge.elasticsearch, i3.16xlarge.elasticsearch
            ], !Ref ElasticsearchInstanceType ] ]
            Assertions:
              - Assert: !Not [ !Equals [ !Ref ElasticsearchSize, '' ] ]
                AssertDescription: "The Elasticsearch EBS size parameter must be set for this instance type"

Resources:

    Resource:
        Type: AWS::IAM::Role
        Properties:
            RoleName: 'Resource'
            AssumeRolePolicyDocument:
                Version: '2012-10-17'
                Statement:
                  - Effect: 'Allow'
                    Principal:
                        Service:
                          - events.amazonaws.com
                          - scheduler.amazonaws.com
                    Action:
                      - 'sts:AssumeRole'

```


Discussion:

Patch:
diff --git a/src/cfnlint/rules/conditions/And.py b/src/cfnlint/rules/conditions/And.py
index aec7bb76c8..08e5829e2b 100644
--- a/src/cfnlint/rules/conditions/And.py
+++ b/src/cfnlint/rules/conditions/And.py
@@ -7,6 +7,7 @@
 
 from typing import Any
 
+from cfnlint.helpers import FUNCTION_RULES
 from cfnlint.jsonschema import Validator
 from cfnlint.rules.functions._BaseFn import BaseFn
 
@@ -25,18 +26,23 @@ def __init__(self) -> None:
         self.fn_and = self.validate
 
     def schema(self, validator: Validator, instance: Any) -> dict[str, Any]:
+        if validator.context.path.path and validator.context.path.path[0] == "Rules":
+            functions = list(FUNCTION_RULES)
+        else:
+            functions = [
+                "Condition",
+                "Fn::Equals",
+                "Fn::Not",
+                "Fn::And",
+                "Fn::Or",
+            ]
+
         return {
             "type": "array",
             "minItems": 2,
             "maxItems": 10,
             "fn_items": {
-                "functions": [
-                    "Condition",
-                    "Fn::Equals",
-                    "Fn::Not",
-                    "Fn::And",
-                    "Fn::Or",
-                ],
+                "functions": functions,
                 "schema": {
                     "type": ["boolean"],
                 },
diff --git a/src/cfnlint/rules/conditions/Not.py b/src/cfnlint/rules/conditions/Not.py
index 391d51cf16..f6036fe4c5 100644
--- a/src/cfnlint/rules/conditions/Not.py
+++ b/src/cfnlint/rules/conditions/Not.py
@@ -7,6 +7,7 @@
 
 from typing import Any
 
+from cfnlint.helpers import FUNCTION_RULES
 from cfnlint.jsonschema import Validator
 from cfnlint.rules.functions._BaseFn import BaseFn
 
@@ -25,18 +26,24 @@ def __init__(self) -> None:
         self.fn_not = self.validate
 
     def schema(self, validator: Validator, instance: Any) -> dict[str, Any]:
+
+        if validator.context.path.path and validator.context.path.path[0] == "Rules":
+            functions = list(FUNCTION_RULES)
+        else:
+            functions = [
+                "Condition",
+                "Fn::Equals",
+                "Fn::Not",
+                "Fn::And",
+                "Fn::Or",
+            ]
+
         return {
             "type": "array",
             "maxItems": 1,
             "minItems": 1,
             "fn_items": {
-                "functions": [
-                    "Condition",
-                    "Fn::Equals",
-                    "Fn::Not",
-                    "Fn::And",
-                    "Fn::Or",
-                ],
+                "functions": functions,
                 "schema": {
                     "type": ["boolean"],
                 },
diff --git a/src/cfnlint/rules/conditions/Or.py b/src/cfnlint/rules/conditions/Or.py
index b102236520..d8dcfe43cb 100644
--- a/src/cfnlint/rules/conditions/Or.py
+++ b/src/cfnlint/rules/conditions/Or.py
@@ -7,6 +7,7 @@
 
 from typing import Any
 
+from cfnlint.helpers import FUNCTION_RULES
 from cfnlint.jsonschema import Validator
 from cfnlint.rules.functions._BaseFn import BaseFn
 
@@ -25,18 +26,23 @@ def __init__(self) -> None:
         self.fn_or = self.validate
 
     def schema(self, validator: Validator, instance: Any) -> dict[str, Any]:
+        if validator.context.path.path and validator.context.path.path[0] == "Rules":
+            functions = list(FUNCTION_RULES)
+        else:
+            functions = [
+                "Condition",
+                "Fn::Equals",
+                "Fn::Not",
+                "Fn::And",
+                "Fn::Or",
+            ]
+
         return {
             "type": "array",
             "minItems": 2,
             "maxItems": 10,
             "fn_items": {
-                "functions": [
-                    "Condition",
-                    "Fn::Equals",
-                    "Fn::Not",
-                    "Fn::And",
-                    "Fn::Or",
-                ],
+                "functions": functions,
                 "schema": {
                     "type": ["boolean"],
                 },


Test patch:
diff --git a/test/unit/rules/conditions/test_and.py b/test/unit/rules/conditions/test_and.py
new file mode 100644
index 0000000000..beee37f9bf
--- /dev/null
+++ b/test/unit/rules/conditions/test_and.py
@@ -0,0 +1,101 @@
+"""
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: MIT-0
+"""
+
+from collections import deque
+
+import pytest
+
+from cfnlint.jsonschema import ValidationError
+from cfnlint.rules.conditions.And import And
+
+
+@pytest.fixture
+def rule():
+    rule = And()
+    yield rule
+
+
+@pytest.fixture
+def template():
+    return {
+        "Parameters": {
+            "Environment": {
+                "Type": "String",
+            }
+        },
+        "Conditions": {
+            "IsUsEast1": {"Fn::Equals": [{"Ref": "AWS::Region"}, "us-east-1"]},
+            "IsProduction": {"Fn::Equals": [{"Ref": "Environment"}, "Production"]},
+        },
+    }
+
+
+@pytest.mark.parametrize(
+    "name,instance,path,errors",
+    [
+        (
+            "Valid and with other conditions",
+            {"Fn::And": [{"Condition": "IsUsEast1"}, {"Condition": "IsProduction"}]},
+            {},
+            [],
+        ),
+        (
+            "Valid and with boolean types",
+            {"Fn::And": [True, False]},
+            {},
+            [],
+        ),
+        (
+            "Invalid Type",
+            {"Fn::And": {}},
+            {},
+            [
+                ValidationError(
+                    "{} is not of type 'array'",
+                    validator="fn_and",
+                    schema_path=deque(["type"]),
+                    path=deque(["Fn::And"]),
+                ),
+            ],
+        ),
+        (
+            "Integer type",
+            {"Fn::And": ["a", True]},
+            {},
+            [
+                ValidationError(
+                    "'a' is not of type 'boolean'",
+                    validator="fn_and",
+                    schema_path=deque(["fn_items", "type"]),
+                    path=deque(["Fn::And", 0]),
+                )
+            ],
+        ),
+        (
+            "Invalid functions in Conditions",
+            {"Fn::And": [True, {"Fn::Contains": []}]},
+            {"path": deque(["Conditions", "Condition1"])},
+            [
+                ValidationError(
+                    "{'Fn::Contains': []} is not of type 'boolean'",
+                    validator="fn_and",
+                    schema_path=deque(["fn_items", "type"]),
+                    path=deque(["Fn::And", 1]),
+                )
+            ],
+        ),
+        (
+            "Valid functions in Rules",
+            {"Fn::And": [True, {"Fn::Contains": []}]},
+            {"path": deque(["Rules", "Rule1"])},
+            [],
+        ),
+    ],
+    indirect=["path"],
+)
+def test_condition(name, instance, errors, rule, validator):
+    errs = list(rule.validate(validator, {}, instance, {}))
+
+    assert errs == errors, name
diff --git a/test/unit/rules/conditions/test_not.py b/test/unit/rules/conditions/test_not.py
new file mode 100644
index 0000000000..848f779daf
--- /dev/null
+++ b/test/unit/rules/conditions/test_not.py
@@ -0,0 +1,101 @@
+"""
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: MIT-0
+"""
+
+from collections import deque
+
+import pytest
+
+from cfnlint.jsonschema import ValidationError
+from cfnlint.rules.conditions.Not import Not
+
+
+@pytest.fixture
+def rule():
+    rule = Not()
+    yield rule
+
+
+@pytest.fixture
+def template():
+    return {
+        "Parameters": {
+            "Environment": {
+                "Type": "String",
+            }
+        },
+        "Conditions": {
+            "IsUsEast1": {"Fn::Equals": [{"Ref": "AWS::Region"}, "us-east-1"]},
+            "IsProduction": {"Fn::Equals": [{"Ref": "Environment"}, "Production"]},
+        },
+    }
+
+
+@pytest.mark.parametrize(
+    "name,instance,path,errors",
+    [
+        (
+            "Valid or with other conditions",
+            {"Fn::Not": [{"Condition": "IsUsEast1"}]},
+            {},
+            [],
+        ),
+        (
+            "Valid or with boolean types",
+            {"Fn::Not": [True]},
+            {},
+            [],
+        ),
+        (
+            "Invalid Type",
+            {"Fn::Not": {}},
+            {},
+            [
+                ValidationError(
+                    "{} is not of type 'array'",
+                    validator="fn_not",
+                    schema_path=deque(["type"]),
+                    path=deque(["Fn::Not"]),
+                ),
+            ],
+        ),
+        (
+            "Integer type",
+            {"Fn::Not": ["a"]},
+            {},
+            [
+                ValidationError(
+                    "'a' is not of type 'boolean'",
+                    validator="fn_not",
+                    schema_path=deque(["fn_items", "type"]),
+                    path=deque(["Fn::Not", 0]),
+                )
+            ],
+        ),
+        (
+            "Invalid functions in Conditions",
+            {"Fn::Not": [{"Fn::Contains": []}]},
+            {"path": deque(["Conditions", "Condition1"])},
+            [
+                ValidationError(
+                    "{'Fn::Contains': []} is not of type 'boolean'",
+                    validator="fn_not",
+                    schema_path=deque(["fn_items", "type"]),
+                    path=deque(["Fn::Not", 0]),
+                )
+            ],
+        ),
+        (
+            "Valid functions in Rules",
+            {"Fn::Not": [{"Fn::Contains": []}]},
+            {"path": deque(["Rules", "Rule1"])},
+            [],
+        ),
+    ],
+    indirect=["path"],
+)
+def test_condition(name, instance, errors, rule, validator):
+    errs = list(rule.validate(validator, {}, instance, {}))
+
+    assert errs == errors, name
diff --git a/test/unit/rules/conditions/test_or.py b/test/unit/rules/conditions/test_or.py
new file mode 100644
index 0000000000..3dfbedce53
--- /dev/null
+++ b/test/unit/rules/conditions/test_or.py
@@ -0,0 +1,101 @@
+"""
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: MIT-0
+"""
+
+from collections import deque
+
+import pytest
+
+from cfnlint.jsonschema import ValidationError
+from cfnlint.rules.conditions.Or import Or
+
+
+@pytest.fixture
+def rule():
+    rule = Or()
+    yield rule
+
+
+@pytest.fixture
+def template():
+    return {
+        "Parameters": {
+            "Environment": {
+                "Type": "String",
+            }
+        },
+        "Conditions": {
+            "IsUsEast1": {"Fn::Equals": [{"Ref": "AWS::Region"}, "us-east-1"]},
+            "IsProduction": {"Fn::Equals": [{"Ref": "Environment"}, "Production"]},
+        },
+    }
+
+
+@pytest.mark.parametrize(
+    "name,instance,path,errors",
+    [
+        (
+            "Valid or with other conditions",
+            {"Fn::Or": [{"Condition": "IsUsEast1"}, {"Condition": "IsProduction"}]},
+            {},
+            [],
+        ),
+        (
+            "Valid or with boolean types",
+            {"Fn::Or": [True, False]},
+            {},
+            [],
+        ),
+        (
+            "Invalid Type",
+            {"Fn::Or": {}},
+            {},
+            [
+                ValidationError(
+                    "{} is not of type 'array'",
+                    validator="fn_or",
+                    schema_path=deque(["type"]),
+                    path=deque(["Fn::Or"]),
+                ),
+            ],
+        ),
+        (
+            "Integer type",
+            {"Fn::Or": ["a", True]},
+            {},
+            [
+                ValidationError(
+                    "'a' is not of type 'boolean'",
+                    validator="fn_or",
+                    schema_path=deque(["fn_items", "type"]),
+                    path=deque(["Fn::Or", 0]),
+                )
+            ],
+        ),
+        (
+            "Invalid functions in Conditions",
+            {"Fn::Or": [True, {"Fn::Contains": []}]},
+            {"path": deque(["Conditions", "Condition1"])},
+            [
+                ValidationError(
+                    "{'Fn::Contains': []} is not of type 'boolean'",
+                    validator="fn_or",
+                    schema_path=deque(["fn_items", "type"]),
+                    path=deque(["Fn::Or", 1]),
+                )
+            ],
+        ),
+        (
+            "Valid functions in Rules",
+            {"Fn::Or": [True, {"Fn::Contains": []}]},
+            {"path": deque(["Rules", "Rule1"])},
+            [],
+        ),
+    ],
+    indirect=["path"],
+)
+def test_condition(name, instance, errors, rule, validator):
+    errs = list(rule.validate(validator, {}, instance, {}))
+
+    assert errs == errors, name