Repository: getmoto/moto
Pull request #8903 (2025-05-15 15:06:04)

[Cloudwatch] Add insight_rules support for cloudwatch
This PR supports the following added capabilities:

- put_insight_rule
- describe_insight_rules
- delete_insight_rules
- disable_insight_rules
- enable_insight_rules

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/8903?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
Attention: Patch coverage is `93.97590%` with `5 lines` in your changes missing coverage. Please review.
> Project coverage is 92.85%. Comparing base [(`648a829`)](https://app.codecov.io/gh/getmoto/moto/commit/648a829d56f15502aa8ed46ab8a20ed2ac847195?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`a877567`)](https://app.codecov.io/gh/getmoto/moto/commit/a87756709a17110c153e20aeed0a0051096080b3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).
> Report is 2 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/getmoto/moto/pull/8903?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Patch % | Lines |
|---|---|---|
| [moto/cloudwatch/models.py](https://app.codecov.io/gh/getmoto/moto/pull/8903?src=pr&el=tree&filepath=moto%2Fcloudwatch%2Fmodels.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#diff-bW90by9jbG91ZHdhdGNoL21vZGVscy5weQ==) | 89.58% | [5 Missing :warning: ](https://app.codecov.io/gh/getmoto/moto/pull/8903?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #8903      +/-   ##
==========================================
- Coverage   92.85%   92.85%   -0.01%     
==========================================
  Files        1282     1282              
  Lines      111878   111959      +81     
==========================================
+ Hits       103885   103960      +75     
- Misses       7993     7999       +6     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/8903/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/8903/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `27.95% <10.84%> (-0.02%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/8903/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `92.83% <93.97%> (-0.01%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/getmoto/moto/pull/8903?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
ApogeeAC: 3/4 Codecov missing lines manually tested and verified. Until Managed InsightRules added no real sense in adding unit tests for those.

Patch:
diff --git a/moto/acmpca/models.py b/moto/acmpca/models.py
index 428fb2037..a5351271b 100644
--- a/moto/acmpca/models.py
+++ b/moto/acmpca/models.py
@@ -1,6 +1,7 @@
 """ACMPCABackend class with methods for supported APIs."""
 
 import base64
+import contextlib
 import datetime
 from typing import Any, Dict, List, Optional, Tuple, cast
 
@@ -248,13 +249,16 @@ class CertificateAuthority(BaseModel):
                 ]
             )
 
-        cn = csr.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
-        if cn:
+        # Subject Alternative Name passthrough from CSR to the new certificate
+        with contextlib.suppress(x509.ExtensionNotFound):
+            san = csr.extensions.get_extension_for_oid(
+                x509.ExtensionOID.SUBJECT_ALTERNATIVE_NAME
+            )
             extensions.append(
                 (
-                    x509.SubjectAlternativeName([x509.DNSName(cn[0].value)]),  # type: ignore[arg-type]
-                    False,
-                ),
+                    san.value,
+                    san.critical,
+                )
             )
 
         return extensions
diff --git a/moto/cloudwatch/models.py b/moto/cloudwatch/models.py
index c98dd9d6b..150871f97 100644
--- a/moto/cloudwatch/models.py
+++ b/moto/cloudwatch/models.py
@@ -24,7 +24,11 @@ from .exceptions import (
     ValidationError,
 )
 from .metric_data_expression_parser import parse_expression
-from .utils import make_arn_for_alarm, make_arn_for_dashboard
+from .utils import (
+    make_arn_for_alarm,
+    make_arn_for_dashboard,
+    make_arn_for_rule,
+)
 
 _EMPTY_LIST: Any = tuple()
 
@@ -433,6 +437,25 @@ class Statistics:
         )
 
 
+class InsightRule(BaseModel):
+    def __init__(
+        self,
+        account_id: str,
+        region_name: str,
+        definition: str,
+        name: str,
+        state: str,
+        schema: Optional[str],
+        managed_rule: Optional[bool],
+    ):
+        self.definition = definition
+        self.name = name
+        self.schema = schema or '{"Name" : "CloudWatchLogRule", "Version" : 1}'
+        self.state = state
+        self.managed_rule = managed_rule or False
+        self.rule_arn = make_arn_for_rule(region_name, account_id, name)
+
+
 class CloudWatchBackend(BaseBackend):
     def __init__(self, region_name: str, account_id: str):
         super().__init__(region_name, account_id)
@@ -440,6 +463,7 @@ class CloudWatchBackend(BaseBackend):
         self.dashboards: Dict[str, Dashboard] = {}
         self.metric_data: List[MetricDatumBase] = []
         self.paged_metric_data: Dict[str, List[MetricDatumBase]] = {}
+        self.insight_rules: Dict[str, InsightRule] = {}
         self.tagger = TaggingService()
 
     @property
@@ -1012,5 +1036,97 @@ class CloudWatchBackend(BaseBackend):
                         f'Missing required parameter in MetricData[{query_num}].StatisticValues: "{stat}"'
                     )
 
+    def put_insight_rule(
+        self,
+        name: str,
+        state: str,
+        definition: str,
+        tags: Optional[List[Dict[str, str]]] = None,
+    ) -> InsightRule:
+        rule = InsightRule(
+            account_id=self.account_id,
+            region_name=self.region_name,
+            definition=definition,
+            name=name,
+            state=state,
+            schema='{"Name" : "CloudWatchLogRule", "Version" : 1}',
+            managed_rule=False,
+        )
+        if tags:
+            self.tagger.tag_resource(rule.rule_arn, tags)
+
+        self.insight_rules[name] = rule
+
+        return rule
+
+    def describe_insight_rules(
+        self,
+        next_token: Optional[str] = "",
+        max_results: Optional[int] = 500,
+    ) -> List[InsightRule]:
+        rules = list(self.insight_rules.values())
+
+        if max_results is None or len(rules) <= max_results:
+            return rules
+
+        return rules[:max_results]
+
+    def delete_insight_rules(self, rule_names: List[str]) -> List[Dict[str, Any]]:
+        failures = []
+        for rule_name in list(self.insight_rules.keys()):
+            if rule_name in rule_names:
+                rule = self.insight_rules.get(rule_name)
+                if rule and rule.managed_rule:
+                    failures.append(
+                        {
+                            "FailureResource": rule_name,
+                            "ExceptionType": "InvalidParameterValue",
+                            "FailureCode": 400,
+                            "FailureDescription": "The value of an input parameter is bad or out-of-range.",
+                        }
+                    )
+                else:
+                    del self.insight_rules[rule_name]
+
+        return failures
+
+    def disable_insight_rules(self, rule_names: List[str]) -> List[Dict[str, Any]]:
+        failures = []
+        for rule_name in list(self.insight_rules.keys()):
+            if rule_name in rule_names:
+                rule = self.insight_rules.get(rule_name)
+                if rule and rule.managed_rule:
+                    failures.append(
+                        {
+                            "FailureResource": rule_name,
+                            "ExceptionType": "InvalidParameterValue",
+                            "FailureCode": 400,
+                            "FailureDescription": "The value of an input parameter is bad or out-of-range.",
+                        }
+                    )
+                else:
+                    self.insight_rules[rule_name].state = "DISABLED"
+
+        return failures
+
+    def enable_insight_rules(self, rule_names: List[str]) -> List[Dict[str, Any]]:
+        failures = []
+        for rule_name in list(self.insight_rules.keys()):
+            if rule_name in rule_names:
+                rule = self.insight_rules.get(rule_name)
+                if rule and rule.managed_rule:
+                    failures.append(
+                        {
+                            "FailureResource": rule_name,
+                            "ExceptionType": "InvalidParameterValue",
+                            "FailureCode": 400,
+                            "FailureDescription": "The value of an input parameter is bad or out-of-range.",
+                        }
+                    )
+                else:
+                    self.insight_rules[rule_name].state = "ENABLED"
+
+        return failures
+
 
 cloudwatch_backends = BackendDict(CloudWatchBackend, "cloudwatch")
diff --git a/moto/cloudwatch/responses.py b/moto/cloudwatch/responses.py
index 40d7d3c9c..0f646f117 100644
--- a/moto/cloudwatch/responses.py
+++ b/moto/cloudwatch/responses.py
@@ -340,6 +340,54 @@ class CloudWatchResponse(BaseResponse):
         template = self.response_template(UNTAG_RESOURCE_TEMPLATE)
         return template.render()
 
+    def put_insight_rule(self) -> str:
+        name = self._get_param("RuleName")
+        state = self._get_param("RuleState")
+        definition = self._get_param("RuleDefinition")
+        tags = self._get_multi_param("Tags.member")
+
+        rule = self.cloudwatch_backend.put_insight_rule(
+            name=name,
+            state=state,
+            definition=definition,
+            tags=tags,
+        )
+
+        template = self.response_template(PUT_INSIGHT_RULE_TEMPLATE)
+        return template.render(rule=rule)
+
+    def describe_insight_rules(self) -> str:
+        rules = self.cloudwatch_backend.describe_insight_rules()
+        next_token = self._get_param("NextToken")
+
+        template = self.response_template(DESCRIBE_INSIGHT_RULES_TEMPLATE)
+
+        return template.render(next_token=next_token, rules=rules)
+
+    def delete_insight_rules(self) -> str:
+        names = self._get_multi_param("RuleNames.member")
+        failures = self.cloudwatch_backend.delete_insight_rules(rule_names=names)
+
+        template = self.response_template(DELETE_INSIGHT_RULES_TEMPLATE)
+
+        return template.render(failures=failures)
+
+    def disable_insight_rules(self) -> str:
+        names = self._get_multi_param("RuleNames.member")
+        rule_names = self.cloudwatch_backend.disable_insight_rules(rule_names=names)
+
+        template = self.response_template(DISABLE_INSIGHT_RULES_TEMPLATE)
+
+        return template.render(rule_names=rule_names)
+
+    def enable_insight_rules(self) -> str:
+        names = self._get_multi_param("RuleNames.member")
+        rule_names = self.cloudwatch_backend.enable_insight_rules(rule_names=names)
+
+        template = self.response_template(ENABLE_INSIGHT_RULES_TEMPLATE)
+
+        return template.render(rule_names=rule_names)
+
 
 PUT_METRIC_ALARM_TEMPLATE = """<PutMetricAlarmResponse xmlns="http://monitoring.amazonaws.com/doc/2010-08-01/">
    <ResponseMetadata>
@@ -736,3 +784,72 @@ UNTAG_RESOURCE_TEMPLATE = """<UntagResourceResponse xmlns="http://monitoring.ama
     <RequestId>{{ request_id }}</RequestId>
   </ResponseMetadata>
 </UntagResourceResponse>"""
+
+PUT_INSIGHT_RULE_TEMPLATE = """<PutInsightRuleResponse xmlns="http://monitoring.amazonaws.com/doc/2010-08-01/">
+   <PutInsightRuleResult></PutInsightRuleResult>
+   <ResponseMetadata>
+      <RequestId>{{ request_id }}</RequestId>
+   </ResponseMetadata>
+</PutInsightRuleResponse>"""
+
+DESCRIBE_INSIGHT_RULES_TEMPLATE = """<DescribeInsightRulesResponse xmlns="http://monitoring.amazonaws.com/doc/2010-08-01/">
+    <DescribeInsightRulesResult>
+        <NextToken> {{next_token}} </NextToken>
+        <InsightRules>
+            {% for rule in rules %}
+            <member>
+                <Name>{{ rule.name }}</Name>
+                <State>{{ rule.state }}</State>
+                <Schema>{{ rule.schema }}</Schema>
+                <Definition>{{ rule.definition }}</Definition>
+                <ManagedRule>{{ rule.managed_rule }}</ManagedRule>
+            </member>
+            {% endfor %}
+        </InsightRules>
+    </DescribeInsightRulesResult>
+</DescribeInsightRulesResponse>"""
+
+DELETE_INSIGHT_RULES_TEMPLATE = """<DeleteInsightRulesResponse xmlns="http://monitoring.amazonaws.com/doc/2010-08-01/">
+    <DeleteInsightRulesResult>
+        <Failures>
+            {% for failure in failures %}
+            <member>
+                <FailureResource>{{ failure.FailureResource }}</FailureResource>
+                <ExceptionType>{{ failure.ExceptionType }}</ExceptionType>
+                <FailureCode>{{ failure.FailureCode }}</FailureCode>
+                <FailureDescription>{{ failure.FailureDescription }}</FailureDescription>
+            </member>
+            {% endfor %}
+        </Failures>
+    </DeleteInsightRulesResult>
+</DeleteInsightRulesResponse>"""
+
+DISABLE_INSIGHT_RULES_TEMPLATE = """<DisableInsightRulesResponse xmlns="http://monitoring.amazonaws.com/doc/2010-08-01/">
+    <DisableInsightRulesResult>
+        <Failures>
+            {% for failure in failures %}
+            <member>
+                <FailureResource>{{ failure.FailureResource }}</FailureResource>
+                <ExceptionType>{{ failure.ExceptionType }}</ExceptionType>
+                <FailureCode>{{ failure.FailureCode }}</FailureCode>
+                <FailureDescription>{{ failure.FailureDescription }}</FailureDescription>
+            </member>
+            {% endfor %}
+        </Failures>
+    </DisableInsightRulesResult>
+</DisableInsightRulesResponse>"""
+
+ENABLE_INSIGHT_RULES_TEMPLATE = """<EnableInsightRulesResponse xmlns="http://monitoring.amazonaws.com/doc/2010-08-01/">
+    <EnableInsightRulesResult>
+        <Failures>
+            {% for failure in failures %}
+            <member>
+                <FailureResource>{{ failure.FailureResource }}</FailureResource>
+                <ExceptionType>{{ failure.ExceptionType }}</ExceptionType>
+                <FailureCode>{{ failure.FailureCode }}</FailureCode>
+                <FailureDescription>{{ failure.FailureDescription }}</FailureDescription>
+            </member>
+            {% endfor %}
+        </Failures>
+    </EnableInsightRulesResult>
+</EnableInsightRulesResponse>"""
diff --git a/moto/cloudwatch/utils.py b/moto/cloudwatch/utils.py
index 539410446..4b70e7ba0 100644
--- a/moto/cloudwatch/utils.py
+++ b/moto/cloudwatch/utils.py
@@ -7,3 +7,7 @@ def make_arn_for_dashboard(account_id: str, region_name: str, name: str) -> str:
 
 def make_arn_for_alarm(region: str, account_id: str, alarm_name: str) -> str:
     return f"arn:{get_partition(region)}:cloudwatch:{region}:{account_id}:alarm:{alarm_name}"
+
+
+def make_arn_for_rule(region: str, account_id: str, rule_name: str) -> str:
+    return f"arn:{get_partition(region)}:cloudwatch:{region}:{account_id}:insight-rule/{rule_name}"
diff --git a/moto/resourcegroupstaggingapi/models.py b/moto/resourcegroupstaggingapi/models.py
index 2685528d9..3c38c0ec9 100644
--- a/moto/resourcegroupstaggingapi/models.py
+++ b/moto/resourcegroupstaggingapi/models.py
@@ -6,6 +6,7 @@ from moto.awslambda.models import LambdaBackend, lambda_backends
 from moto.backup.models import BackupBackend, backup_backends
 from moto.clouddirectory import CloudDirectoryBackend, clouddirectory_backends
 from moto.cloudfront.models import CloudFrontBackend, cloudfront_backends
+from moto.cloudwatch.models import CloudWatchBackend, cloudwatch_backends
 from moto.core.base_backend import BackendDict, BaseBackend
 from moto.core.exceptions import RESTError
 from moto.dms.models import DatabaseMigrationServiceBackend, dms_backends
@@ -205,6 +206,10 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend):
     def cloudfront_backend(self) -> CloudFrontBackend:
         return cloudfront_backends[self.account_id][self.partition]
 
+    @property
+    def cloudwatch_backend(self) -> CloudWatchBackend:
+        return cloudwatch_backends[self.account_id][self.region_name]
+
     def _get_resources_generator(
         self,
         tag_filters: Optional[List[Dict[str, Any]]] = None,
@@ -354,6 +359,35 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend):
                     continue
                 yield {"ResourceARN": f"{dist.arn}", "Tags": tags}
 
+        if self.cloudwatch_backend:
+            cloudwatch_resource_map: Dict[str, Dict[str, Any]] = {
+                "cloudwatch:alarm": self.cloudwatch_backend.alarms,
+                "cloudwatch:insight-rule": self.cloudwatch_backend.insight_rules,
+            }
+            for resource_type, resource_source in cloudwatch_resource_map.items():
+                if (
+                    not resource_type_filters
+                    or "cloudwatch" in resource_type_filters
+                    or resource_type in resource_type_filters
+                ):
+                    for resource in resource_source.values():
+                        if resource_type == "cloudwatch:alarm":
+                            end_of_arn = f"alarm:{resource.name}"
+                        elif resource_type == "cloudwatch:insight-rule":
+                            end_of_arn = f"insight-rule/{resource.name}"
+
+                        arn = f"arn:{get_partition(self.region_name)}:cloudwatch:{self.region_name}:{self.account_id}:{end_of_arn}"
+
+                        cw_tags = self.cloudwatch_backend.list_tags_for_resource(arn)
+
+                        tags = format_tags(cw_tags)
+                        if not tags or not tag_filter(tags):
+                            continue
+                        yield {
+                            "ResourceARN": arn,
+                            "Tags": tags,
+                        }
+
         # DMS
         if not resource_type_filters or "dms:endpoint" in resource_type_filters:
             for endpoint in self.dms_backend.endpoints.values():


Test patch:
diff --git a/.github/workflows/tests_sdk_ruby.yml b/.github/workflows/tests_sdk_ruby.yml
index e28b0226c..26d85d51f 100644
--- a/.github/workflows/tests_sdk_ruby.yml
+++ b/.github/workflows/tests_sdk_ruby.yml
@@ -13,7 +13,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
       - name: Set up Ruby ${{ matrix.ruby-version }}
-        uses: ruby/setup-ruby@eaecf785f6a34567a6d97f686bbb7bccc1ac1e5c
+        uses: ruby/setup-ruby@c95ae3725f6ebdd095f2bd19caed7ebc14435ba5
         with:
           ruby-version: ${{ matrix.ruby-version }}
       - name: Set up Python 3.12
diff --git a/tests/test_acmpca/test_acmpca.py b/tests/test_acmpca/test_acmpca.py
index ec98ae770..2fb446d49 100644
--- a/tests/test_acmpca/test_acmpca.py
+++ b/tests/test_acmpca/test_acmpca.py
@@ -480,11 +480,25 @@ def test_end_entity_certificate_issuance():
     )
     builder = cryptography.x509.verification.PolicyBuilder().store(store)
     verifier = builder.build_server_verifier(DNSName("bezoscorp.com"))
-    chain = verifier.verify(
-        cryptography.x509.load_pem_x509_certificate(ee_cert.encode("utf-8")), []
-    )
+    ee_x509 = cryptography.x509.load_pem_x509_certificate(ee_cert.encode("utf-8"))
+    chain = verifier.verify(ee_x509, [])
     assert len(chain) == 2
 
+    # Ensure extensions are passed through
+    san_extension = ee_x509.extensions.get_extension_for_oid(
+        cryptography.x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME
+    )
+    assert san_extension.value.get_values_for_type(cryptography.x509.OtherName)
+    assert san_extension.value.get_values_for_type(cryptography.x509.RFC822Name) == [
+        "hello@moto.com"
+    ]
+    assert san_extension.value.get_values_for_type(cryptography.x509.DNSName) == [
+        "bezoscorp.com"
+    ]
+    assert san_extension.value.get_values_for_type(
+        cryptography.x509.UniformResourceIdentifier
+    ) == ["https://github.com/getmoto/moto"]
+
 
 @mock_aws
 def test_tag_certificate_authority():
@@ -597,6 +611,22 @@ def create_csr(private_key, country, state, org, cn):
             cryptography.x509.BasicConstraints(ca=False, path_length=None),
             critical=True,
         )
+        .add_extension(
+            cryptography.x509.SubjectAlternativeName(
+                [
+                    cryptography.x509.DNSName(cn),
+                    cryptography.x509.RFC822Name("hello@moto.com"),
+                    cryptography.x509.OtherName(
+                        type_id=cryptography.x509.ObjectIdentifier("2.5.4.3"),
+                        value=b"\x13\x02mo",
+                    ),
+                    cryptography.x509.UniformResourceIdentifier(
+                        "https://github.com/getmoto/moto"
+                    ),
+                ]
+            ),
+            critical=False,
+        )
         .sign(private_key, hashes.SHA256())
     )
     return csr.public_bytes(serialization.Encoding.PEM)
diff --git a/tests/test_cloudwatch/test_cloudwatch_insight_rules.py b/tests/test_cloudwatch/test_cloudwatch_insight_rules.py
new file mode 100644
index 000000000..dbdeda965
--- /dev/null
+++ b/tests/test_cloudwatch/test_cloudwatch_insight_rules.py
@@ -0,0 +1,154 @@
+import boto3
+import pytest
+
+from moto import mock_aws
+
+
+@mock_aws
+@pytest.mark.parametrize(
+    "region,partition", [("eu-west-1", "aws"), ("cn-north-1", "aws-cn")]
+)
+def test_put_insight_rule(region, partition):
+    cloudwatch = boto3.client("cloudwatch", region)
+
+    rule_name = "MySampleInsightRule"
+    log_group_name = "my-log-group"
+    rule_body = f"""
+    {{
+      "Schema": "CloudWatchLogRule",
+      "LogGroupNames": ["{log_group_name}"],
+      "LogFormat": "JSON",
+      "Filter": "[timestamp, request, statusCode = *]",
+      "Contribution": {{
+        "Keys": ["statusCode"],
+        "ValueOf": "Count"
+      }}
+    }}
+    """
+    cloudwatch.put_insight_rule(
+        RuleName=rule_name, RuleDefinition=rule_body, RuleState="ENABLED"
+    )
+
+    rules = cloudwatch.describe_insight_rules()["InsightRules"]
+    assert len(rules) == 1
+    rule = rules[0]
+    assert rule["Name"] == "MySampleInsightRule"
+    assert rule["State"] == "ENABLED"
+    assert rule["Schema"] == '{"Name" : "CloudWatchLogRule", "Version" : 1}'
+    assert rule["Definition"] == rule_body
+    assert rule["ManagedRule"] is not True
+
+
+@mock_aws
+def test_delete_insight_rules():
+    cloudwatch = boto3.client("cloudwatch", region_name="eu-central-1")
+
+    rule_name = "MySampleInsightRule"
+    log_group_name = "my-log-group"
+    rule_body = f"""
+    {{
+      "Schema": "CloudWatchLogRule",
+      "LogGroupNames": ["{log_group_name}"],
+      "LogFormat": "JSON",
+      "Filter": "[timestamp, request, statusCode = *]",
+      "Contribution": {{
+        "Keys": ["statusCode"],
+        "ValueOf": "Count"
+      }}
+    }}
+    """
+
+    cloudwatch.put_insight_rule(
+        RuleName=rule_name, RuleDefinition=rule_body, RuleState="ENABLED"
+    )
+
+    cloudwatch.put_insight_rule(
+        RuleName=f"{rule_name}_Backup", RuleDefinition=rule_body, RuleState="ENABLED"
+    )
+    rules = cloudwatch.describe_insight_rules()["InsightRules"]
+    assert len(rules) == 2
+
+    cloudwatch.delete_insight_rules(RuleNames=[rule_name])
+    rules = cloudwatch.describe_insight_rules()["InsightRules"]
+    assert len(rules) == 1
+
+    rule = rules[0]
+    assert rule["Name"] == "MySampleInsightRule_Backup"
+
+
+@mock_aws
+def test_disable_insight_rules():
+    cloudwatch = boto3.client("cloudwatch", region_name="eu-central-1")
+
+    rule_name = "MySampleInsightRule"
+    log_group_name = "my-log-group"
+    rule_body = f"""
+    {{
+      "Schema": "CloudWatchLogRule",
+      "LogGroupNames": ["{log_group_name}"],
+      "LogFormat": "JSON",
+      "Filter": "[timestamp, request, statusCode = *]",
+      "Contribution": {{
+        "Keys": ["statusCode"],
+        "ValueOf": "Count"
+      }}
+    }}
+    """
+
+    cloudwatch.put_insight_rule(
+        RuleName=rule_name, RuleDefinition=rule_body, RuleState="ENABLED"
+    )
+
+    cloudwatch.put_insight_rule(
+        RuleName=f"{rule_name}_Backup", RuleDefinition=rule_body, RuleState="ENABLED"
+    )
+
+    cloudwatch.disable_insight_rules(RuleNames=[rule_name])
+
+    rules = cloudwatch.describe_insight_rules()
+
+    # Ensuring correct rule was disabled, other should remain enabled
+    for r in rules["InsightRules"]:
+        if r["Name"] == rule_name:
+            assert r["State"] == "DISABLED"
+        else:
+            assert r["State"] == "ENABLED"
+
+
+@mock_aws
+def test_enable_insight_rules():
+    cloudwatch = boto3.client("cloudwatch", region_name="eu-central-1")
+
+    rule_name = "MySampleInsightRule"
+    log_group_name = "my-log-group"
+    rule_body = f"""
+    {{
+      "Schema": "CloudWatchLogRule",
+      "LogGroupNames": ["{log_group_name}"],
+      "LogFormat": "JSON",
+      "Filter": "[timestamp, request, statusCode = *]",
+      "Contribution": {{
+        "Keys": ["statusCode"],
+        "ValueOf": "Count"
+      }}
+    }}
+    """
+
+    cloudwatch.put_insight_rule(
+        RuleName=rule_name, RuleDefinition=rule_body, RuleState="DISABLED"
+    )
+
+    cloudwatch.put_insight_rule(
+        RuleName=f"{rule_name}_Backup", RuleDefinition=rule_body, RuleState="DISABLED"
+    )
+
+    cloudwatch.enable_insight_rules(RuleNames=[rule_name])
+
+    rules = cloudwatch.describe_insight_rules()
+
+    # Ensuring correct rule was enabled, other should remain disabled
+    for r in rules["InsightRules"]:
+        if r["Name"] == rule_name:
+            assert r["State"] == "ENABLED"
+        else:
+            assert r["State"] == "DISABLED"
diff --git a/tests/test_cloudwatch/test_cloudwatch_tags.py b/tests/test_cloudwatch/test_cloudwatch_tags.py
index 4eac59820..8bc7ca612 100644
--- a/tests/test_cloudwatch/test_cloudwatch_tags.py
+++ b/tests/test_cloudwatch/test_cloudwatch_tags.py
@@ -199,3 +199,37 @@ def test_untag_resource_error_not_exists():
     assert ex.response["ResponseMetadata"]["HTTPStatusCode"] == 404
     assert ex.response["Error"]["Code"] == "ResourceNotFoundException"
     assert ex.response["Error"]["Message"] == "Unknown"
+
+
+@mock_aws
+def test_tag_insight_rules_resource():
+    client = boto3.client("cloudwatch", region_name="eu-central-1")
+
+    rule_name = "MySampleInsightRule"
+    log_group_name = "my-log-group"
+    rule_body = f"""
+    {{
+      "Schema": "CloudWatchLogRule",
+      "LogGroupNames": ["{log_group_name}"],
+      "LogFormat": "JSON",
+      "Filter": "[timestamp, request, statusCode = *]",
+      "Contribution": {{
+        "Keys": ["statusCode"],
+        "ValueOf": "Count"
+      }}
+    }}
+    """
+    tags = [{"Key": "ThisIsAKey", "Value": "ThisIsAValue"}]
+
+    client.put_insight_rule(
+        RuleName=rule_name, RuleDefinition=rule_body, RuleState="ENABLED", Tags=tags
+    )
+
+    arn = (
+        "arn:aws:cloudwatch:eu-central-1:123456789012:insight-rule/MySampleInsightRule"
+    )
+
+    response = client.list_tags_for_resource(ResourceARN=arn)
+    assert sorted(response["Tags"], key=itemgetter("Key")) == [
+        {"Key": "ThisIsAKey", "Value": "ThisIsAValue"},
+    ]
diff --git a/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py b/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py
index 1437df8ee..32d175790 100644
--- a/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py
+++ b/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py
@@ -605,6 +605,59 @@ def test_get_tag_values_lexv2_models():
     ]
 
 
+@mock_aws
+def test_get_tag_values_cloudwatch():
+    cloudwatch = boto3.client("cloudwatch", "us-east-1")
+
+    name = "tester"
+    cloudwatch.put_metric_alarm(
+        AlarmActions=["arn:alarm"],
+        AlarmDescription="A test",
+        AlarmName=name,
+        ComparisonOperator="GreaterThanOrEqualToThreshold",
+        Dimensions=[{"Name": "InstanceId", "Value": "i-0123457"}],
+        EvaluationPeriods=5,
+        InsufficientDataActions=["arn:insufficient"],
+        Namespace=f"{name}_namespace",
+        MetricName=f"{name}_metric",
+        OKActions=["arn:ok"],
+        Period=60,
+        Statistic="Average",
+        Threshold=2,
+        Unit="Seconds",
+        Tags=[{"Key": "key-1", "Value": "value-1"}],
+    )
+
+    cloudwatch.put_insight_rule(
+        RuleName=name,
+        RuleDefinition="""{"Schema": "CloudWatchLogRule}""",
+        RuleState="ENABLED",
+        Tags=[{"Key": "key-2", "Value": "value-2"}],
+    )
+    rtapi = boto3.client("resourcegroupstaggingapi", "us-east-1")
+
+    # Test alarm tag filtering
+    resp = rtapi.get_resources(
+        ResourceTypeFilters=["cloudwatch:alarm"],
+        TagFilters=[{"Key": "key-1", "Values": ["value-1"]}],
+    )
+    assert len(resp["ResourceTagMappingList"]) == 1
+    assert {"Key": "key-1", "Value": "value-1"} in resp["ResourceTagMappingList"][0][
+        "Tags"
+    ]
+
+    # Test insight-rule tag filtering
+    resp = rtapi.get_resources(
+        ResourceTypeFilters=["cloudwatch:insight-rule"],
+        TagFilters=[{"Key": "key-2", "Values": ["value-2"]}],
+    )
+
+    assert len(resp["ResourceTagMappingList"]) == 1
+    assert {"Key": "key-2", "Value": "value-2"} in resp["ResourceTagMappingList"][0][
+        "Tags"
+    ]
+
+
 @mock_aws
 def test_get_many_resources():
     elbv2 = boto3.client("elbv2", region_name="us-east-1")