Repository: aws-cloudformation/cfn-lint

Issue:
[Bug] Rules support `Condition`, which is not a rule-specific intrinsic function
### CloudFormation Lint Version

1.15.0

### What operating system are you using?

Mac `pre-commit`

### Background

Note that I described similar behavior in #3630, which #3634 and #3703 attempted to address. Unfortunately it appears that these changes resulted in `cfn-lint` allowing the `Condition` function inside of rules, which is not an allowed [rule-specific intrinsic function](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/rules-section-structure.html#rules-specific-intrinsic-section-structure).

In particular, templates of the form described at https://github.com/aws-cloudformation/cfn-lint/issues/3630#issuecomment-2364176770 will throw:

```
Changeset creation failed. The reason was Template format error: Following functions are not supported in the Rules block of the template: [Condition].
```

### Describe the bug

The reproduction template throws:

```
W1001 Ref to resource 'InternetGateway' that may not be available when condition 'DeployGateway' is False and when condition 'DeployVpc' is True at Resources/InternetGatewayAttachment/Properties/InternetGatewayId
src/cloud-formation/indexer.cfn.yaml:32:7
```

Specifically, the purported hypothetical situation

> when condition 'DeployGateway' is False and when condition 'DeployVpc' is True

is impossible per the DeployVpc rule.

### Expected behavior

The lint should pass without template modification.

### Reproduction template

```yaml
---
Conditions:
  DeployGateway: !Equals
  - !Ref 'DeployGateway'
  - 'true'
  DeployVpc: !Equals
  - !Ref 'DeployVpc'
  - 'true'
Parameters:
  DeployAnything:
    AllowedValues:
    - 'false'
    - 'true'
    Type: 'String'
  DeployGateway:
    AllowedValues:
    - 'false'
    - 'true'
    Type: 'String'
  DeployVpc:
    AllowedValues:
    - 'false'
    - 'true'
    Type: 'String'
Resources:
  InternetGateway:
    Condition: 'DeployGateway'
    Type: 'AWS::EC2::InternetGateway'
  InternetGatewayAttachment:
    Condition: 'DeployVpc'
    Properties:
      InternetGatewayId: !Ref 'InternetGateway'
      VpcId: !Ref 'Vpc'
    Type: 'AWS::EC2::VPCGatewayAttachment'
  Vpc:
    Condition: 'DeployVpc'
    Properties:
      CidrBlock: '0.0.0.0/16'
    Type: 'AWS::EC2::VPC'
Rules:
  DeployGateway:
    Assertions:
    - Assert: !Or
      - !Equals
        - !Ref 'DeployAnything'
        - 'true'
      - !Equals
        - !Ref 'DeployGateway'
        - 'false'
  DeployVpc:
    Assertions:
    - Assert: !Or
      - !Equals
        - !Ref 'DeployGateway'
        - 'true'
      - !Equals
        - !Ref 'DeployVpc'
        - 'false'
...
```



Discussion:
#3712 will fix the `Condition` issue.  Looking into the scenario you described above
@kddejong thanks for the quick fix!

This resolved the issues with the reproduction template, and for [the template I've been working on](https://github.com/econia-labs/emojicoin-dot-fun/blob/ECO-2188/src/cloud-formation/indexer.cfn.yaml)



Patch:
diff --git a/src/cfnlint/conditions/_equals.py b/src/cfnlint/conditions/_equals.py
index 06872c4223..401e48f17e 100644
--- a/src/cfnlint/conditions/_equals.py
+++ b/src/cfnlint/conditions/_equals.py
@@ -10,7 +10,7 @@
 from typing import Any, Mapping, Tuple
 
 from sympy import Symbol
-from sympy.logic.boolalg import BooleanFalse, BooleanFunction, BooleanTrue
+from sympy.logic.boolalg import BooleanFalse, BooleanTrue
 
 from cfnlint.conditions._utils import get_hash
 from cfnlint.helpers import is_function
@@ -145,7 +145,9 @@ def left(self):
     def right(self):
         return self._right
 
-    def build_cnf(self, params: dict[str, Symbol]) -> BooleanFunction:
+    def build_cnf(
+        self, params: dict[str, Symbol]
+    ) -> BooleanTrue | BooleanFalse | Symbol:
         """Build a SymPy CNF solver based on the provided params
         Args:
             params dict[str, Symbol]: params is a dict that represents
@@ -158,10 +160,7 @@ def build_cnf(self, params: dict[str, Symbol]) -> BooleanFunction:
                 return BooleanTrue()
             return BooleanFalse()
 
-        if self.hash in params:
-            return params.get(self.hash)
-
-        return Symbol(self.hash)
+        return params.get(self.hash, Symbol(self.hash))
 
     def test(self, scenarios: Mapping[str, str]) -> bool:
         """Do an equals based on the provided scenario"""
diff --git a/src/cfnlint/conditions/conditions.py b/src/cfnlint/conditions/conditions.py
index 235eac7236..6d612ad5cc 100644
--- a/src/cfnlint/conditions/conditions.py
+++ b/src/cfnlint/conditions/conditions.py
@@ -110,13 +110,12 @@ def _build_cnf(
         cnf = EncodedCNF()
 
         # build parameters and equals into solver
-        equal_vars: dict[str, Symbol] = {}
+        equal_vars: dict[str, Symbol | BooleanFalse | BooleanTrue] = {}
 
         equals: dict[str, Equal] = {}
-        for condition_name in condition_names:
-            c_equals = self._conditions[condition_name].equals
+
+        def _build_equal_vars(c_equals: list[Equal]):
             for c_equal in c_equals:
-                # check to see if equals already matches another one
                 if c_equal.hash in equal_vars:
                     continue
 
@@ -139,6 +138,12 @@ def _build_cnf(
                                 )
                 equals[c_equal.hash] = c_equal
 
+        for rule in self._rules:
+            _build_equal_vars(rule.equals)
+
+        for condition_name in condition_names:
+            _build_equal_vars(self._conditions[condition_name].equals)
+
         # Determine if a set of conditions can never be all false
         allowed_values = self._parameters.copy()
         if allowed_values:


Test patch:
diff --git a/test/unit/module/conditions/test_rules.py b/test/unit/module/conditions/test_rules.py
index f9871bc44c..49f09be655 100644
--- a/test/unit/module/conditions/test_rules.py
+++ b/test/unit/module/conditions/test_rules.py
@@ -23,14 +23,14 @@ def test_conditions_with_rules(self):
             Assertions:
             - Assert:
                 Fn::And:
-                - !Condition IsProd
-                - !Condition IsUsEast1
+                - !Equals [!Ref Environment, "prod"]
+                - !Equals [!Ref "AWS::Region", "us-east-1"]
           Rule2:
             Assertions:
             - Assert:
                 Fn::Or:
-                - !Condition IsProd
-                - !Condition IsUsEast1
+                - !Equals [!Ref Environment, "prod"]
+                - !Equals [!Ref "AWS::Region", "us-east-1"]
         """
         )[0]
 
@@ -79,9 +79,9 @@ def test_conditions_with_rules_implies(self):
           IsUsEast1: !Equals [!Ref "AWS::Region", "us-east-1"]
         Rules:
           Rule:
-            RuleCondition: !Condition IsProd
+            RuleCondition: !Equals [!Ref Environment, "prod"]
             Assertions:
-            - Assert: !Condition IsUsEast1
+            - Assert: !Equals [!Ref "AWS::Region", "us-east-1"]
 
         """
         )[0]
@@ -143,11 +143,11 @@ def test_conditions_with_multiple_rules(self):
           Rule1:
             RuleCondition: !Equals [!Ref Environment, "prod"]
             Assertions:
-            - Assert: !Condition IsUsEast1
+            - Assert: !Equals [!Ref "AWS::Region", "us-east-1"]
           Rule2:
             RuleCondition: !Equals [!Ref Environment, "dev"]
             Assertions:
-            - Assert: !Not [!Condition IsUsEast1]
+            - Assert: !Not [!Equals [!Ref "AWS::Region", "us-east-1"]]
         """
         )[0]
 
@@ -366,6 +366,95 @@ def test_fn_equals_assertions_ref_never_satisfiable(self):
             )
         )
 
+    def test_conditions_with_rules_and_parameters(self):
+        template = decode_str(
+            """
+        Conditions:
+            DeployGateway: !Equals
+                - !Ref 'DeployGateway'
+                - 'true'
+            DeployVpc: !Equals
+                - !Ref 'DeployVpc'
+                - 'true'
+        Parameters:
+            DeployAnything:
+                AllowedValues:
+                - 'false'
+                - 'true'
+                Type: 'String'
+            DeployGateway:
+                AllowedValues:
+                - 'false'
+                - 'true'
+                Type: 'String'
+            DeployVpc:
+                AllowedValues:
+                - 'false'
+                - 'true'
+                Type: 'String'
+        Rules:
+            DeployGateway:
+                Assertions:
+                - Assert: !Or
+                    - !Equals
+                        - !Ref 'DeployAnything'
+                        - 'true'
+                    - !Equals
+                        - !Ref 'DeployGateway'
+                        - 'false'
+            DeployVpc:
+                Assertions:
+                - Assert: !Or
+                    - !Equals
+                        - !Ref 'DeployGateway'
+                        - 'true'
+                    - !Equals
+                        - !Ref 'DeployVpc'
+                        - 'false'
+        Resources:
+            InternetGateway:
+                Condition: 'DeployGateway'
+                Type: 'AWS::EC2::InternetGateway'
+            InternetGatewayAttachment:
+                Condition: 'DeployVpc'
+                Type: 'AWS::EC2::VPCGatewayAttachment'
+                Properties:
+                    InternetGatewayId: !Ref 'InternetGateway'
+                    VpcId: !Ref 'Vpc'
+        """
+        )[0]
+
+        cfn = Template("", template)
+        self.assertEqual(len(cfn.conditions._conditions), 2)
+        self.assertEqual(len(cfn.conditions._rules), 2)
+
+        self.assertListEqual(
+            [equal.hash for equal in cfn.conditions._rules[0].equals],
+            [
+                "d0d70a1e66dc83d7a0fce24c2eca396af1f34e53",
+                "bbf5c94c1a4b5a79c7a7863fe9463884cb422450",
+            ],
+        )
+
+        self.assertTrue(
+            cfn.conditions.satisfiable(
+                {},
+                {},
+            )
+        )
+
+        self.assertTrue(
+            cfn.conditions.check_implies({"DeployVpc": True}, "DeployGateway")
+        )
+
+        self.assertFalse(
+            cfn.conditions.check_implies({"DeployVpc": False}, "DeployGateway")
+        )
+
+        self.assertFalse(
+            cfn.conditions.check_implies({"DeployGateway": False}, "DeployVpc")
+        )
+
 
 class TestAssertion(TestCase):
     def test_assertion_errors(self):
@@ -405,7 +494,7 @@ def test_init_rules_with_wrong_assertions_type(self):
             Assertions: {"Foo": "Bar"}
           Rule2:
             Assertions:
-            - Assert: !Condition IsUsEast1
+            - Assert: !Equals [!Ref "AWS::Region", "us-east-1"]
         """
         )[0]
 
@@ -425,8 +514,8 @@ def test_init_rules_with_no_keys(self):
             Assertions:
             - Assert:
                 Fn::Or:
-                - !Condition IsNotUsEast1
-                - !Condition IsUsEast1
+                - !Not [!Equals [!Ref "AWS::Region", "us-east-1"]]
+                - !Equals [!Ref "AWS::Region", "us-east-1"]
           Rule3: []
         """
         )[0]