Repository: aws-cloudformation/cfn-lint

Issue:
New Lines in Custom Rules file causes failures
### CloudFormation Lint Version

cfn-lint 0.77.6

### What operating system are you using?

Mac

### Describe the bug

Similar to the issue reported in https://github.com/aws-cloudformation/cfn-lint/issues/2069, adding a new blank line in the custom rules file causes `cfn-lint` to fail

My rules.txt file is the following. Note that `2` is an empty line (mostly just for readability)

```
  1 AWS::S3::Bucket AccessControl EQUALS Public ERROR
  2
  3 AWS::S3::Bucket WebsiteConfiguration.IndexDocument EQUALS Retains WARN
```

When i run the following command: `cfn-lint -t cft.yml -z rules.txt --format pretty --debug` - I get

```
2023-06-02 17:20:25,429 - cfnlint - DEBUG - Begin linting of file: cft.yml
2023-06-02 17:20:25,799 - cfnlint.rules - DEBUG - Processing Custom Rule Line 1
2023-06-02 17:20:25,800 - cfnlint.rules - DEBUG - Processing Custom Rule Line 2
Traceback (most recent call last):
  File "/usr/local/bin/cfn-lint", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/usr/local/lib/python3.11/site-packages/cfnlint/__main__.py", line 39, in main
    matches = list(cfnlint.core.get_matches(filenames, args))
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/cfnlint/core.py", line 169, in get_matches
    (template, rules, errors) = get_template_rules(filename, args)
                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/cfnlint/core.py", line 310, in get_template_rules
    _build_rule_cache(args)
  File "/usr/local/lib/python3.11/site-packages/cfnlint/core.py", line 270, in _build_rule_cache
    __CACHED_RULES = cfnlint.core.get_rules(
                     ^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/cfnlint/core.py", line 158, in get_rules
    rules.create_from_custom_rules_file(custom_rules)
  File "/usr/local/lib/python3.11/site-packages/cfnlint/rules/__init__.py", line 603, in create_from_custom_rules_file
    custom_rule = cfnlint.rules.custom.make_rule(line, line_number)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/cfnlint/rules/custom/__init__.py", line 113, in make_rule
    "E" + str(rule_id), operator
                        ^^^^^^^^
UnboundLocalError: cannot access local variable 'operator' where it is not associated with a value
```

### Expected behavior

Expected for this to just skip the new line and not parse it and fail.

I assume these `if` statement needs to be altered to check for `line == ""`

https://github.com/aws-cloudformation/cfn-lint/blob/431cf4fb9fefdfec25cb4cb69005f4dd064421ba/src/cfnlint/rules/custom/__init__.py#L44-L48

I believe we can also fix #2069 a similar way by checking if the line starts with `#`

### Reproduction template

```
{
  2   "AWSTemplateFormatVersion" : "2010-09-09",
  3
  4   "Description" : "AWS CloudFormation Sample Template S3_Website_Bucket_With_Retain_On_Delete: Sample template showing how to create a publicly accessible S3 bucket config    ured for website access with a deletion policy of retain on delete. **WARNING** This template creates an S3 bucket that will NOT be deleted when the stack is deleted. You     will be billed for the AWS resources used if you create a stack from this template.",
  5
  6   "Resources" : {
  7     "S3Bucket" : {
  8       "Type" : "AWS::S3::Bucket",
  9       "Properties" : {
 10         "AccessControl" : "PublicRead",
 11         "WebsiteConfiguration" : {
 12           "IndexDocument" : "index.html",
 13           "ErrorDocument" : "error.html"
 14          }
 15       },
 16       "DeletionPolicy" : "Retain"
 17     }
 18   },
 19
 20   "Outputs" : {
 21     "WebsiteURL" : {
 22       "Value" : { "Fn::GetAtt" : [ "S3Bucket", "WebsiteURL" ] },
 23       "Description" : "URL for website hosted on S3"
 24     },
 25     "S3BucketSecureURL" : {
 26       "Value" : { "Fn::Join" : [ "", [ "https://", { "Fn::GetAtt" : [ "S3Bucket", "DomainName" ] } ] ] },
 27       "Description" : "Name of S3 bucket to hold website content"
 28     }
 29   }
```


Discussion:

Resolving patch:
diff --git a/src/cfnlint/rules/custom/__init__.py b/src/cfnlint/rules/custom/__init__.py
--- a/src/cfnlint/rules/custom/__init__.py
+++ b/src/cfnlint/rules/custom/__init__.py
@@ -42,73 +42,79 @@ def process_sets(raw_value):
         return raw_value
 
     line = line.rstrip()
-    rule_id = lineNumber + 9000
-    line = line.split(" ", 3)
-    error_level = "E"
-    if len(line) == 4:
-        resourceType = line[0]
-        prop = line[1]
-        operator = line[2]
-        value = None
-        error_message = None
-        if "WARN" in line[3]:
-            error_level = "W"
-            value, error_message = set_arguments(line[3], "WARN")
-        elif "ERROR" in line[3]:
-            error_level = "E"
-            value, error_message = set_arguments(line[3], "ERROR")
-        else:
-            value = process_sets(line[3])
-            value = get_value(line[3])
+    # check line is not a comment or empty line
+    if not line.startswith("#") and line != "":
+        rule_id = lineNumber + 9000
+        line = line.split(" ", 3)
+        error_level = "E"
+        if len(line) == 4:
+            resourceType = line[0]
+            prop = line[1]
+            operator = line[2]
+            value = None
+            error_message = None
+            if "WARN" in line[3]:
+                error_level = "W"
+                value, error_message = set_arguments(line[3], "WARN")
+            elif "ERROR" in line[3]:
+                error_level = "E"
+                value, error_message = set_arguments(line[3], "ERROR")
+            else:
+                value = process_sets(line[3])
+                value = get_value(line[3])
 
-        if isinstance(value, str):
-            value = value.strip().strip('"')
+            if isinstance(value, str):
+                value = value.strip().strip('"')
 
-        if operator in ["EQUALS", "=="]:
-            return cfnlint.rules.custom.Operators.CreateEqualsRule(
-                error_level + str(rule_id), resourceType, prop, value, error_message
-            )
-        if operator in ["NOT_EQUALS", "!="]:
-            return cfnlint.rules.custom.Operators.CreateNotEqualsRule(
-                error_level + str(rule_id), resourceType, prop, value, error_message
-            )
-        if operator == "REGEX_MATCH":
-            return cfnlint.rules.custom.Operators.CreateRegexMatchRule(
-                error_level + str(rule_id), resourceType, prop, value, error_message
-            )
-        if operator == "IN":
-            return cfnlint.rules.custom.Operators.CreateInSetRule(
-                error_level + str(rule_id), resourceType, prop, value, error_message
-            )
-        if operator == "NOT_IN":
-            return cfnlint.rules.custom.Operators.CreateNotInSetRule(
-                error_level + str(rule_id), resourceType, prop, value, error_message
-            )
-        if operator == ">":
-            return cfnlint.rules.custom.Operators.CreateGreaterRule(
-                error_level + str(rule_id), resourceType, prop, value, error_message
-            )
-        if operator == ">=":
-            return cfnlint.rules.custom.Operators.CreateGreaterEqualRule(
-                error_level + str(rule_id), resourceType, prop, value, error_message
-            )
-        if operator == "<":
-            return cfnlint.rules.custom.Operators.CreateLesserRule(
-                error_level + str(rule_id), resourceType, prop, value, error_message
-            )
-        if operator == "<=":
-            return cfnlint.rules.custom.Operators.CreateLesserEqualRule(
-                error_level + str(rule_id), resourceType, prop, value, error_message
-            )
-        if operator == "IS":
-            if value in ["DEFINED", "NOT_DEFINED"]:
-                return cfnlint.rules.custom.Operators.CreateCustomIsDefinedRule(
+            if operator in ["EQUALS", "=="]:
+                return cfnlint.rules.custom.Operators.CreateEqualsRule(
+                    error_level + str(rule_id), resourceType, prop, value, error_message
+                )
+            if operator in ["NOT_EQUALS", "!="]:
+                return cfnlint.rules.custom.Operators.CreateNotEqualsRule(
+                    error_level + str(rule_id), resourceType, prop, value, error_message
+                )
+            if operator == "REGEX_MATCH":
+                return cfnlint.rules.custom.Operators.CreateRegexMatchRule(
+                    error_level + str(rule_id), resourceType, prop, value, error_message
+                )
+            if operator == "IN":
+                return cfnlint.rules.custom.Operators.CreateInSetRule(
+                    error_level + str(rule_id), resourceType, prop, value, error_message
+                )
+            if operator == "NOT_IN":
+                return cfnlint.rules.custom.Operators.CreateNotInSetRule(
                     error_level + str(rule_id), resourceType, prop, value, error_message
                 )
-            return cfnlint.rules.custom.Operators.CreateInvalidRule(
-                "E" + str(rule_id), f"{operator} {value}"
-            )
+            if operator == ">":
+                return cfnlint.rules.custom.Operators.CreateGreaterRule(
+                    error_level + str(rule_id), resourceType, prop, value, error_message
+                )
+            if operator == ">=":
+                return cfnlint.rules.custom.Operators.CreateGreaterEqualRule(
+                    error_level + str(rule_id), resourceType, prop, value, error_message
+                )
+            if operator == "<":
+                return cfnlint.rules.custom.Operators.CreateLesserRule(
+                    error_level + str(rule_id), resourceType, prop, value, error_message
+                )
+            if operator == "<=":
+                return cfnlint.rules.custom.Operators.CreateLesserEqualRule(
+                    error_level + str(rule_id), resourceType, prop, value, error_message
+                )
+            if operator == "IS":
+                if value in ["DEFINED", "NOT_DEFINED"]:
+                    return cfnlint.rules.custom.Operators.CreateCustomIsDefinedRule(
+                        error_level + str(rule_id),
+                        resourceType,
+                        prop,
+                        value,
+                        error_message,
+                    )
+                return cfnlint.rules.custom.Operators.CreateInvalidRule(
+                    "E" + str(rule_id), f"{operator} {value}"
+                )
 
-    return cfnlint.rules.custom.Operators.CreateInvalidRule(
-        "E" + str(rule_id), operator
-    )
+        return cfnlint.rules.custom.Operators.CreateInvalidRule(
+            "E" + str(rule_id), operator
+        )


Test patch:
diff --git a/test/fixtures/custom_rules/good/custom_rule_perfect.txt b/test/fixtures/custom_rules/good/custom_rule_perfect.txt
--- a/test/fixtures/custom_rules/good/custom_rule_perfect.txt
+++ b/test/fixtures/custom_rules/good/custom_rule_perfect.txt
@@ -1,3 +1,4 @@
+#This is a comment to ensure comments in custom rules work
 AWS::IAM::Role AssumeRolePolicyDocument.Version EQUALS "2012-10-17"
 AWS::IAM::Role AssumeRolePolicyDocument.Version IN [2012-10-16,2012-10-17,2012-10-18]
 AWS::IAM::Role AssumeRolePolicyDocument.Version NOT_EQUALS "2012-10-15"
@@ -6,10 +7,12 @@ AWS::IAM::Policy PolicyName EQUALS "root" WARN ABC
 AWS::IAM::Policy PolicyName IN [2012-10-16,root,2012-10-18] ERROR ABC
 AWS::IAM::Policy PolicyName NOT_EQUALS "user" WARN ABC
 AWS::IAM::Policy PolicyName NOT_IN [2012-10-16,2012-11-20,2012-10-18] ERROR ABC
+# Adding a comment in the middle of custom rules and new line for readability
+
 AWS::EC2::Instance BlockDeviceMappings.Ebs.VolumeSize >= 20 WARN
 AWS::EC2::Instance BlockDeviceMappings.Ebs.VolumeSize > 10 ERROR ABC
 AWS::EC2::Instance BlockDeviceMappings.Ebs.VolumeSize <= 50 ERROR DEF
 AWS::EC2::Instance BlockDeviceMappings.Ebs.VolumeSize < 40 WARN ABC
 AWS::CloudFormation::Stack TemplateURL REGEX_MATCH "^https.*$" WARN ABC
 AWS::Lambda::Function Environment.Variables.NODE_ENV IS DEFINED
-AWS::Lambda::Function Environment.Variables.PRIVATE_KEY IS NOT_DEFINED
\ No newline at end of file
+AWS::Lambda::Function Environment.Variables.PRIVATE_KEY IS NOT_DEFINED