Repository: aws-cloudformation/cfn-lint

Issue:
E3003 on missing `SourceAccount` when `SourceArn` is specified using `!Sub`
### CloudFormation Lint Version

1.9.1

### What operating system are you using?

WSL Ubuntu 22.04.4 LTS

### Describe the bug

In an `AWS::Lambda::Permission` resource, when specifying `SourceArn` using `!Sub` (see attached repro template), `cfn-lint` complains about missing `SourceAccount`:

```
$ cfn-lint -t linttest.yml
E3003 'SourceAccount' is a required property
linttest.yml:27:5
```

Whereas when using `!GetAtt`, (`!GetAtt HelloLambdaSchedule.Arn`), we get no error.

### Expected behavior

My understanding is that when `SourceArn` is specified, `SourceAccount` isn't strictly required. I found this issue https://github.com/aws-cloudformation/cfn-lint/issues/2155 for which a warning was added in case the `SourceAccount` is missing, but that's not firing in this case. In this case when `SourceArn` is specified using `!Sub`, I would expect either a warning about the usage of `!Sub` in case that's not guaranteed to provide the account information, or alternatively a warning (instead of an error) about a possibly missing `SourceAccount`

### Reproduction template

```yaml
Resources:

  HelloLambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: HelloLambdaFunction
      Role: arn:aws:iam::123456789012:role/lambda-role
      Runtime: python3.12
      Handler: index.my_handler
      Code:
        ZipFile: |
          def my_handler(event, context):
            message = 'Hello Lambda World!'
            return message

  HelloLambdaSchedule:
    Type: "AWS::Events::Rule"
    Properties:
      ScheduleExpression: 'cron(0 4 * * ? *)'
      State: ENABLED
      Targets:
        - Arn: !GetAtt HelloLambdaFunction.Arn
          Id: LambdaSchedule

  HelloLambdaPermission:
    Type: "AWS::Lambda::Permission"
    Properties:
      Action: 'lambda:InvokeFunction'
      FunctionName: HelloLambdaFunction
      Principal: 'events.amazonaws.com'
      SourceArn: !Sub ${HelloLambdaSchedule.Arn}
```


Discussion:
This is supported by the CloudFormation documentation (i.e. it's not a "Required" property): https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-permission.html#cfn-lambda-permission-sourceaccount

This should likely be reported as W3663 instead (if even then), which was introduced in 1.9.0. https://github.com/aws-cloudformation/cfn-lint/releases/tag/v1.9.0

But even that is specified in cfn-lint documentation as "When configuration a Lambda permission with a SourceArn that doesn't have an AccountId you should also specify the SourceAccount". So if SourceArn includes the account ID, even W3663 shouldn't be necessary.
To many adventures with Fn::Sub so we will remove validation if we run into a Sub.  Also fixed the issue where the wrong error number was returned so this will become a warning.



Patch:
diff --git a/src/cfnlint/rules/resources/ectwo/InstanceImageId.py b/src/cfnlint/rules/resources/ectwo/InstanceImageId.py
index 7219d3c10b..db809da416 100644
--- a/src/cfnlint/rules/resources/ectwo/InstanceImageId.py
+++ b/src/cfnlint/rules/resources/ectwo/InstanceImageId.py
@@ -70,11 +70,20 @@ def validate(
             launch_templates = list(
                 self._get_related_launch_template(instance_image_id_validator, instance)
             )
+            path: deque[str | int] = deque([])
+            if "ImageId" != instance_image_id_validator.context.path.path[-1]:
+                path = deque(
+                    list(instance_image_id_validator.context.path.path)[
+                        len(validator.context.path.path) :
+                    ]
+                )
 
             if not launch_templates:
                 yield ValidationError(
                     "'ImageId' is a required property",
-                    path_override=instance_image_id_validator.context.path.path,
+                    validator="required",
+                    path=path,
+                    rule=self,
                 )
                 continue
 
@@ -123,5 +132,7 @@ def validate(
                     if launch_template_image_id is None and instance_image_id is None:
                         yield ValidationError(
                             "'ImageId' is a required property",
-                            path_override=instance_image_id_validator.context.path.path,
+                            validator="required",
+                            path=path,
+                            rule=self,
                         )
diff --git a/src/cfnlint/rules/resources/lmbd/PermissionSourceAccount.py b/src/cfnlint/rules/resources/lmbd/PermissionSourceAccount.py
index f1137c7ba7..043ea94d9e 100644
--- a/src/cfnlint/rules/resources/lmbd/PermissionSourceAccount.py
+++ b/src/cfnlint/rules/resources/lmbd/PermissionSourceAccount.py
@@ -32,16 +32,6 @@ def __init__(self):
             keywords=["Resources/AWS::Lambda::Permission/Properties"],
         )
 
-    def _validate_sub_has_account_id(self, validator: Validator, value: Any) -> bool:
-        value = ensure_list(value)
-
-        if isinstance(value[0], str):
-            if re.search(r":(\d{12}|\${AWS::AccountId}):", value[0]):
-                return True
-
-            return False
-        return True
-
     def _validate_is_gettatt_to_bucket(self, validator: Validator, value: Any) -> bool:
         value = ensure_list(value)[0].split(".")[0]
 
@@ -81,10 +71,7 @@ def validate(
 
             fn_k, fn_v = is_function(source_arn)
             if fn_k is not None:
-                if fn_k == "Fn::Sub":
-                    if self._validate_sub_has_account_id(scenario_validator, fn_v):
-                        continue
-                elif fn_k == "Fn::GetAtt":
+                if fn_k == "Fn::GetAtt":
                     if not self._validate_is_gettatt_to_bucket(
                         scenario_validator, fn_v
                     ):
@@ -96,4 +83,5 @@ def validate(
                 yield ValidationError(
                     "'SourceAccount' is a required property",
                     validator="required",
+                    rule=self,
                 )


Test patch:
diff --git a/test/unit/rules/resources/ec2/test_instance_image_id.py b/test/unit/rules/resources/ec2/test_instance_image_id.py
index 845d5876ae..2db06f443e 100644
--- a/test/unit/rules/resources/ec2/test_instance_image_id.py
+++ b/test/unit/rules/resources/ec2/test_instance_image_id.py
@@ -184,9 +184,9 @@ def rule():
             [
                 ValidationError(
                     "'ImageId' is a required property",
-                    path_override=deque(
-                        ["Resources", "Instance", "Properties", "ImageId"]
-                    ),
+                    validator="required",
+                    rule=InstanceImageId(),
+                    path=deque([]),
                 )
             ],
         ),
@@ -221,9 +221,9 @@ def rule():
             [
                 ValidationError(
                     "'ImageId' is a required property",
-                    path_override=deque(
-                        ["Resources", "Instance", "Properties", "ImageId"]
-                    ),
+                    validator="required",
+                    rule=InstanceImageId(),
+                    path=deque([]),
                 )
             ],
         ),
@@ -268,17 +268,9 @@ def rule():
             [
                 ValidationError(
                     "'ImageId' is a required property",
-                    path_override=deque(
-                        [
-                            "Resources",
-                            "Instance",
-                            "Properties",
-                            "ImageId",
-                            "Fn::If",
-                            2,
-                            "Ref",
-                        ]
-                    ),
+                    validator="required",
+                    rule=InstanceImageId(),
+                    path=deque(["ImageId", "Fn::If", 2, "Ref"]),
                 )
             ],
         ),
@@ -288,6 +280,10 @@ def rule():
 def test_validate(name, instance, expected, rule, validator):
     errs = list(rule.validate(validator, "", instance, {}))
 
+    for err in errs:
+        print(err.path)
+        print(err.validator)
+        print(err.rule)
     assert (
         errs == expected
     ), f"Expected test {name!r} to have {expected!r} but got {errs!r}"
diff --git a/test/unit/rules/resources/lmbd/test_permission_source_account.py b/test/unit/rules/resources/lmbd/test_permission_source_account.py
index 3135a86ff1..9d370bcda6 100644
--- a/test/unit/rules/resources/lmbd/test_permission_source_account.py
+++ b/test/unit/rules/resources/lmbd/test_permission_source_account.py
@@ -61,6 +61,7 @@ def template():
                 ValidationError(
                     "'SourceAccount' is a required property",
                     validator="required",
+                    rule=PermissionSourceAccount(),
                 )
             ],
         ),
@@ -85,24 +86,6 @@ def template():
             {
                 "SourceArn": {"Fn::Sub": "arn:${AWS::Partition}:s3:::bucket"},
             },
-            [
-                ValidationError(
-                    "'SourceAccount' is a required property",
-                    validator="required",
-                )
-            ],
-        ),
-        (
-            {
-                "SourceArn": {"Fn::Sub": [[], {}]},
-            },
-            [],
-        ),
-        (
-            {
-                "SourceArn": {"Fn::Sub": "arn:${AWS::Partition}:s3:::bucket"},
-                "SourceAccount": {"Ref": "AWS::AccountId"},
-            },
             [],
         ),
         (
@@ -120,6 +103,7 @@ def template():
                 ValidationError(
                     "'SourceAccount' is a required property",
                     validator="required",
+                    rule=PermissionSourceAccount(),
                 )
             ],
         ),
@@ -182,6 +166,7 @@ def template():
                 ValidationError(
                     "'SourceAccount' is a required property",
                     validator="required",
+                    rule=PermissionSourceAccount(),
                 )
             ],
         ),