Repository: aws-cloudformation/cfn-lint

Issue:
False positive E3004 circular dependency for SAM templates
### CloudFormation Lint Version

cfn-lint 0.72.0

### What operating system are you using?

Linux

### Describe the bug

When `AWS::Serverless::Function` references an alarm in its `DeploymentPreference.Alarms` and the `AWS::CloudWatch::Alarm` references the function (eg. in Dimensions), a `E3004 Circular dependency` error is reported by cfn-lint.  Afaik this is a valid use case.

This was introduced in cfn-lint 0.72.0 (0.71.1 did not report E3004), most likely as part of PR aws-cloudformation/cfn-lint#2452.

```
E3004 Circular Dependencies for resource TestFunction. Circular dependency with [LatestVersionErrorMetricGreaterThanZeroAlarm]
template.yaml:5:3

E3004 Circular Dependencies for resource LatestVersionErrorMetricGreaterThanZeroAlarm. Circular dependency with [TestFunction]
template.yaml:28:3
```

(The template is based on https://gist.github.com/alexcasalboni/242071568a6adde805027345cfbb6d99.  But we first noticed this in one of our production functions when upgrading cfn-lint to 0.72.0.)

### Expected behavior

No E3004 is reported.

### Reproduction template

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Resources:

  TestFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.test
      Runtime: nodejs16.x
      MemorySize: 128
      Timeout: 30
      Tracing: Active
      CodeUri: ./
      Events:
        GetResource:
          Type: Api
          Properties:
            Path: /test
            Method: get
            RestApiId: !Ref 'Api'
      
      AutoPublishAlias: live
      DeploymentPreference:
        Type: Linear10PercentEvery1Minute
        Alarms:
          - !Ref LatestVersionErrorMetricGreaterThanZeroAlarm

  LatestVersionErrorMetricGreaterThanZeroAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: alarm-sam-demo-latestversion
      AlarmDescription: "alarm to check for errors in the function"
      ComparisonOperator: GreaterThanOrEqualToThreshold
      TreatMissingData: missing
      Dimensions:
        - Name: FunctionName
          Value: !Ref TestFunction
        - Name: Resource
          Value: !Join [":", [!Ref TestFunction, !Select ["7", !Split [":", !Ref TestFunction.Version]]]]
      EvaluationPeriods: 1
      MetricName: Errors
      Namespace: AWS/Lambda
      Period: 60
      Statistic: Sum
      Threshold: 1

  Api:
    Type: AWS::Serverless::Api
    Properties:
      StageName: foo
      TracingEnabled: true

  LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub
        - /aws/lambda/${FunctionName}
        - FunctionName: !Ref 'TestFunction'
      RetentionInDays: 1
    DeletionPolicy: Delete
    UpdateReplacePolicy: Delete
```


Discussion:

Resolving patch:
diff --git a/src/cfnlint/graph.py b/src/cfnlint/graph.py
--- a/src/cfnlint/graph.py
+++ b/src/cfnlint/graph.py
@@ -240,6 +240,7 @@ def _add_subs(self, cfn: Any) -> None:
     def _add_node(self, node_id, label, settings):
         if settings.node_type in ['Parameter', 'Output']:
             node_id = f'{settings.node_type}-{node_id}'
+
         self.graph.add_node(
             node_id,
             label=label,
@@ -255,7 +256,6 @@ def _add_edge(self, source_id, target_id, source_path, settings):
             source_paths=source_path,
             label=settings.label,
             color=settings.color,
-            penwidth=1,
         )
 
     def _is_resource(self, cfn, identifier):
diff --git a/src/cfnlint/runner.py b/src/cfnlint/runner.py
--- a/src/cfnlint/runner.py
+++ b/src/cfnlint/runner.py
@@ -6,7 +6,8 @@
 from typing import List, Optional, Sequence, Union
 from cfnlint.template import Template
 from cfnlint.transform import Transform
-from .rules import Match, RulesCollection
+from cfnlint.graph import Graph
+from cfnlint.rules import Match, RulesCollection
 
 
 LOGGER = logging.getLogger(__name__)
@@ -52,6 +53,7 @@ def transform(self):
         transform = Transform(self.filename, self.cfn.template, self.cfn.regions[0])
         matches = transform.transform_template()
         self.cfn.template = transform.template()
+        self.cfn.graph = Graph(self.cfn)
         return matches
 
     def run(self) -> List[Match]:


Test patch:
diff --git a/test/unit/module/test_template.py b/test/unit/module/test_template.py
--- a/test/unit/module/test_template.py
+++ b/test/unit/module/test_template.py
@@ -53,10 +53,10 @@ def test_build_graph(self):
 IamPipeline [color=black, label="IamPipeline\\n<AWS::CloudFormation::Stack>", shape=ellipse, type=Resource];
 CustomResource [color=black, label="CustomResource\\n<Custom::Function>", shape=ellipse, type=Resource];
 WaitCondition [color=black, label="WaitCondition\\n<AWS::CloudFormation::WaitCondition>", shape=ellipse, type=Resource];
-RolePolicies -> RootRole  [color=black, key=0, label=Ref, penwidth=1, source_paths="['Properties', 'Roles', 0]"];
-RootInstanceProfile -> RootRole  [color=black, key=0, label=Ref, penwidth=1, source_paths="['Properties', 'Roles', 0]"];
-MyEC2Instance -> RootInstanceProfile  [color=black, key=0, label=Ref, penwidth=1, source_paths="['Properties', 'IamInstanceProfile']"];
-ElasticLoadBalancer -> MyEC2Instance  [color=black, key=0, label=Ref, penwidth=1, source_paths="['Properties', 'Instances', 0]"];
+RolePolicies -> RootRole  [color=black, key=0, label=Ref, source_paths="['Properties', 'Roles', 0]"];
+RootInstanceProfile -> RootRole  [color=black, key=0, label=Ref, source_paths="['Properties', 'Roles', 0]"];
+MyEC2Instance -> RootInstanceProfile  [color=black, key=0, label=Ref, source_paths="['Properties', 'IamInstanceProfile']"];
+ElasticLoadBalancer -> MyEC2Instance  [color=black, key=0, label=Ref, source_paths="['Properties', 'Instances', 0]"];
 }
 """.split('\n')