Repository: aws-cloudformation/cfn-lint

Issue:
v1: E3027 'minute' is not one of ['minutes', 'hours', 'days'] when 'Fn::Sub' is resolved
### CloudFormation Lint Version

1.3.3

### What operating system are you using?

Ubuntu

### Describe the bug

When using a parameter for setting the rate expression unit, specifying any of the following values as part of the `AllowedValues` property fails validation:

- `minute`
- `hour`
- `day`

### Expected behavior

Using one of the values specified above should pass validation.

### Reproduction template

```yaml
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  Frequency:
    Description: Value of frequency
    Type: Number
    Default: 5
  FrequencyUnit:
    Description: Unit of frequency
    Type: String
    Default: minutes
    AllowedValues:
      - minute
      - minutes
      - hour
      - hours
      - day
      - days
Resources:
  EventsRule:
    Type: AWS::Events::Rule
    Properties:
      ScheduleExpression: !Sub "rate(${Frequency} ${FrequencyUnit})"
      State: ENABLED
```


Discussion:
I think what makes sense here is when we used the resolved values that if one is correct we don't alert.  The other option would be to use the Default values together and not mix and match `Default` and `AllowedValues`.

Since the default is 5 we are validating the scenario of `rate(5 minute)` so any of the singular values are wrong. 
I have a fix for this to basically say if at least one of the values are valid we won't return an error.  Need to add some tests but PR coming soon.



Patch:
diff --git a/src/cfnlint/jsonschema/_resolvers_cfn.py b/src/cfnlint/jsonschema/_resolvers_cfn.py
index b9cf63cd3b..d5c1eb3028 100644
--- a/src/cfnlint/jsonschema/_resolvers_cfn.py
+++ b/src/cfnlint/jsonschema/_resolvers_cfn.py
@@ -11,7 +11,7 @@
 
 import regex as re
 
-from cfnlint.helpers import AVAILABILITY_ZONES
+from cfnlint.helpers import AVAILABILITY_ZONES, REGEX_SUB_PARAMETERS
 from cfnlint.jsonschema import ValidationError, Validator
 from cfnlint.jsonschema._typing import ResolutionResult
 from cfnlint.jsonschema._utils import equal
@@ -313,7 +313,21 @@ def sub(validator: Validator, instance: Any) -> ResolutionResult:
         return
 
     # its a string
-    yield from _sub_string(validator, instance)
+    sub_parameters = REGEX_SUB_PARAMETERS.findall(instance)
+    parameters = {}
+    for parameter in sub_parameters:
+        if "." in parameter:
+            parameters[parameter] = {"Fn::GetAtt": parameter}
+        else:
+            parameters[parameter] = {"Ref": parameter}
+    for resolved_parameters in _sub_parameter_expansion(validator, parameters):
+        resolved_validator = validator.evolve(
+            context=validator.context.evolve(
+                ref_values=resolved_parameters,
+            )
+        )
+        yield from _sub_string(resolved_validator, instance)
+        # yield from _sub_string(validator, instance)
 
 
 def if_(validator: Validator, instance: Any) -> ResolutionResult:
diff --git a/src/cfnlint/rules/functions/_BaseFn.py b/src/cfnlint/rules/functions/_BaseFn.py
index fffb050e55..cc48f886eb 100644
--- a/src/cfnlint/rules/functions/_BaseFn.py
+++ b/src/cfnlint/rules/functions/_BaseFn.py
@@ -62,20 +62,34 @@ def resolve(
     ) -> ValidationResult:
         key, _ = self.key_value(instance)
 
+        return_err: ValidationError | None = None
         for value, v, resolve_err in validator.resolve_value(instance):
             if resolve_err:
                 yield resolve_err
                 continue
-            for err in self.fix_errors(
-                v.descend(
-                    instance=value,
-                    schema=s,
-                    path=key,
+            errs = list(
+                self.fix_errors(
+                    v.descend(
+                        instance=value,
+                        schema=s,
+                        path=key,
+                    )
                 )
-            ):
-                err.message = err.message.replace(f"{value!r}", f"{instance!r}")
-                err.message = f"{err.message} when {self.fn.name!r} is resolved"
-                yield err
+            )
+
+            if not errs:
+                return
+
+            return_err = errs[0]
+
+        if return_err:
+            return_err.message = return_err.message.replace(
+                f"{value!r}", f"{instance!r}"
+            )
+            return_err.message = (
+                f"{return_err.message} when {self.fn.name!r} is resolved"
+            )
+            yield return_err
 
     def _resolve_ref(self, validator, schema) -> Any:
 


Test patch:
diff --git a/test/unit/module/cfn_yaml/test_yaml.py b/test/unit/module/cfn_yaml/test_yaml.py
index f83b1ade47..1b60db93ab 100644
--- a/test/unit/module/cfn_yaml/test_yaml.py
+++ b/test/unit/module/cfn_yaml/test_yaml.py
@@ -27,7 +27,7 @@ def setUp(self):
             },
             "generic_bad": {
                 "filename": "test/fixtures/templates/bad/generic.yaml",
-                "failures": 33,
+                "failures": 28,
             },
         }
 
diff --git a/test/unit/module/test_rules_collections.py b/test/unit/module/test_rules_collections.py
index 7a391981a5..46e8aac53a 100644
--- a/test/unit/module/test_rules_collections.py
+++ b/test/unit/module/test_rules_collections.py
@@ -69,7 +69,7 @@ def test_fail_run(self):
         filename = "test/fixtures/templates/bad/generic.yaml"
         template = cfnlint.decode.cfn_yaml.load(filename)
         cfn = Template(filename, template, ["us-east-1"])
-        expected_err_count = 36
+        expected_err_count = 31
         matches = []
         matches.extend(self.rules.run(filename, cfn))
         assert (
diff --git a/test/unit/rules/functions/test_sub.py b/test/unit/rules/functions/test_sub.py
index 28b1059f7d..11ee1de866 100644
--- a/test/unit/rules/functions/test_sub.py
+++ b/test/unit/rules/functions/test_sub.py
@@ -26,6 +26,15 @@ def cfn():
     return Template(
         "",
         {
+            "Parameters": {
+                "MyParameter": {
+                    "Type": "String",
+                    "AllowedValues": [
+                        "one",
+                        "two",
+                    ],
+                }
+            },
             "Resources": {
                 "MyResource": {"Type": "AWS::S3::Bucket"},
                 "MySimpleAd": {"Type": "AWS::DirectoryService::SimpleAD"},
@@ -96,8 +105,8 @@ def context(cfn):
                 ValidationError(
                     (
                         "'bar' is not one of ["
-                        "'MyResource', 'MySimpleAd', 'MyPolicy', 'AWS::AccountId', "
-                        "'AWS::NoValue', 'AWS::NotificationARNs', "
+                        "'MyParameter', 'MyResource', 'MySimpleAd', 'MyPolicy', "
+                        "'AWS::AccountId', 'AWS::NoValue', 'AWS::NotificationARNs', "
                         "'AWS::Partition', 'AWS::Region', "
                         "'AWS::StackId', 'AWS::StackName', 'AWS::URLSuffix']"
                     ),
@@ -115,8 +124,8 @@ def context(cfn):
                 ValidationError(
                     (
                         "'foo' is not one of ["
-                        "'MyResource', 'MySimpleAd', 'MyPolicy', 'AWS::AccountId', "
-                        "'AWS::NoValue', 'AWS::NotificationARNs', "
+                        "'MyParameter', 'MyResource', 'MySimpleAd', 'MyPolicy', "
+                        "'AWS::AccountId', 'AWS::NoValue', 'AWS::NotificationARNs', "
                         "'AWS::Partition', 'AWS::Region', "
                         "'AWS::StackId', 'AWS::StackName', 'AWS::URLSuffix']"
                     ),
@@ -264,6 +273,25 @@ def context(cfn):
                 ),
             ],
         ),
+        (
+            "One valid resolution",
+            {"Fn::Sub": "${MyParameter}"},
+            {"type": "string", "const": "two"},
+            [],
+        ),
+        (
+            "No valid resolution",
+            {"Fn::Sub": "${MyParameter}"},
+            {"type": "string", "const": "three"},
+            [
+                ValidationError(
+                    ("'three' was expected when 'Fn::Sub' is resolved"),
+                    path=deque(["Fn::Sub"]),
+                    schema_path=deque(["const"]),
+                    validator="fn_sub",
+                ),
+            ],
+        ),
     ],
 )
 def test_validate(name, instance, schema, expected, rule, context, cfn):