Repository: getmoto/moto
Pull request #6849 (2023-09-26 01:58:10)

Config: Add checks for valid html when pulling managed rules
Also updated the managed rule list.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/6849?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
> Merging [#6849](https://app.codecov.io/gh/getmoto/moto/pull/6849?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) (790caad) into [master](https://app.codecov.io/gh/getmoto/moto/commit/0cbde05795769fcf53199cc96c10375d7e32bd83?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) (0cbde05) will **not change** coverage.
> The diff coverage is `n/a`.

```diff
@@           Coverage Diff           @@
##           master    #6849   +/-   ##
=======================================
  Coverage   96.20%   96.20%           
=======================================
  Files         816      816           
  Lines       80090    80090           
=======================================
  Hits        77053    77053           
  Misses       3037     3037           
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/6849/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/6849/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `37.05% <ø> (+<0.01%)` | :arrow_up: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/6849/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `96.15% <ø> (ø)` | |

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.




Patch:
diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index d6801d036..b9475906f 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -6413,7 +6413,7 @@
 - [ ] list_model_cards
 - [ ] list_model_explainability_job_definitions
 - [ ] list_model_metadata
-- [ ] list_model_package_groups
+- [X] list_model_package_groups
 - [X] list_model_packages
 - [ ] list_model_quality_job_definitions
 - [X] list_models
diff --git a/moto/config/resources/aws_managed_rules.json b/moto/config/resources/aws_managed_rules.json
index daa88e30b..14d0f5e6c 100644
--- a/moto/config/resources/aws_managed_rules.json
+++ b/moto/config/resources/aws_managed_rules.json
@@ -194,6 +194,18 @@
       "Resource Types": "AWS::AppSync::GraphQLApi",
       "Trigger type": "Periodic"
     },
+    "APPSYNC_AUTHORIZATION_CHECK": {
+      "AWS Region": "All supported AWS regions except Asia Pacific (Jakarta), Africa (Cape Town), Middle East (UAE), Asia Pacific (Hyderabad), Asia Pacific (Melbourne), AWS GovCloud (US-East), AWS GovCloud (US-West), Israel (Tel Aviv), Europe (Spain), Europe (Zurich) Region",
+      "Parameters": [
+        {
+          "Name": "AllowedAuthorizationTypes",
+          "Optional": false,
+          "Type": "CSV"
+        }
+      ],
+      "Resource Types": "AWS::AppSync::GraphQLApi",
+      "Trigger type": "Configuration changes"
+    },
     "APPSYNC_CACHE_ENCRYPTION_AT_REST": {
       "AWS Region": "All supported AWS regions except China (Beijing), Asia Pacific (Jakarta), Africa (Cape Town), Middle East (UAE), Asia Pacific (Hyderabad), Asia Pacific (Melbourne), AWS GovCloud (US-East), AWS GovCloud (US-West), Israel (Tel Aviv), Europe (Spain), China (Ningxia), Europe (Zurich) Region",
       "Parameters": [],
@@ -874,6 +886,12 @@
       "Resource Types": "AWS::CodePipeline::Pipeline",
       "Trigger type": "Configuration changes"
     },
+    "CUSTOM_EVENTBUS_POLICY_ATTACHED": {
+      "AWS Region": "All supported AWS regions except Middle East (UAE), Asia Pacific (Hyderabad), Asia Pacific (Melbourne), AWS GovCloud (US-East), AWS GovCloud (US-West), Israel (Tel Aviv), Europe (Spain), Europe (Zurich) Region",
+      "Parameters": [],
+      "Resource Types": "AWS::Events::EventBus",
+      "Trigger type": "Configuration changes"
+    },
     "CUSTOM_SCHEMA_REGISTRY_POLICY_ATTACHED": {
       "AWS Region": "All supported AWS regions except Middle East (Bahrain), China (Beijing), Asia Pacific (Jakarta), Africa (Cape Town), Middle East (UAE), Asia Pacific (Hyderabad), Asia Pacific (Osaka), Asia Pacific (Melbourne), Europe (Milan), AWS GovCloud (US-East), AWS GovCloud (US-West), Israel (Tel Aviv), Europe (Spain), China (Ningxia), Europe (Zurich) Region",
       "Parameters": [],
@@ -962,11 +980,41 @@
       "Resource Types": "AWS::EC2::Instance",
       "Trigger type": "Configuration changes"
     },
+    "DMS_AUTO_MINOR_VERSION_UPGRADE_CHECK": {
+      "AWS Region": "All supported AWS regions except Asia Pacific (Jakarta), Middle East (UAE), Asia Pacific (Hyderabad), Asia Pacific (Melbourne), AWS GovCloud (US-East), AWS GovCloud (US-West), Israel (Tel Aviv), Europe (Spain), Europe (Zurich) Region",
+      "Parameters": [],
+      "Resource Types": "AWS::DMS::ReplicationInstance",
+      "Trigger type": "Configuration changes"
+    },
+    "DMS_ENDPOINT_SSL_CONFIGURED": {
+      "AWS Region": "All supported AWS regions except Asia Pacific (Jakarta), Middle East (UAE), Asia Pacific (Hyderabad), Asia Pacific (Melbourne), AWS GovCloud (US-East), AWS GovCloud (US-West), Israel (Tel Aviv), Europe (Spain), Europe (Zurich) Region",
+      "Parameters": [],
+      "Resource Types": "AWS::DMS::Endpoint",
+      "Trigger type": "Configuration changes"
+    },
     "DMS_REPLICATION_NOT_PUBLIC": {
       "AWS Region": "All supported AWS regions except Asia Pacific (Jakarta), Africa (Cape Town), Middle East (UAE), Asia Pacific (Hyderabad), Asia Pacific (Melbourne), Europe (Milan), Israel (Tel Aviv), Europe (Spain), Europe (Zurich) Region",
       "Parameters": [],
       "Trigger type": "Periodic"
     },
+    "DMS_REPLICATION_TASK_SOURCEDB_LOGGING": {
+      "AWS Region": "All supported AWS regions except Asia Pacific (Jakarta), Middle East (UAE), Asia Pacific (Hyderabad), Asia Pacific (Osaka), Asia Pacific (Melbourne), AWS GovCloud (US-East), AWS GovCloud (US-West), Israel (Tel Aviv), Europe (Spain), Europe (Zurich) Region",
+      "Parameters": [],
+      "Resource Types": "AWS::DMS::ReplicationTask",
+      "Trigger type": "Configuration changes"
+    },
+    "DMS_REPLICATION_TASK_TARGETDB_LOGGING": {
+      "AWS Region": "All supported AWS regions except Asia Pacific (Jakarta), Middle East (UAE), Asia Pacific (Hyderabad), Asia Pacific (Osaka), Asia Pacific (Melbourne), AWS GovCloud (US-East), AWS GovCloud (US-West), Israel (Tel Aviv), Europe (Spain), Europe (Zurich) Region",
+      "Parameters": [],
+      "Resource Types": "AWS::DMS::ReplicationTask",
+      "Trigger type": "Configuration changes"
+    },
+    "DOCDB_CLUSTER_AUDIT_LOGGING_ENABLED": {
+      "AWS Region": "Only available in Asia Pacific (Mumbai), Europe (Paris), US East (Ohio), Europe (Ireland), Europe (Frankfurt), South America (Sao Paulo), US East (N. Virginia), Asia Pacific (Seoul), Europe (London), Europe (Milan), Asia Pacific (Tokyo), US West (Oregon), Asia Pacific (Singapore), Asia Pacific (Sydney), Canada (Central), China (Ningxia) Region",
+      "Parameters": [],
+      "Resource Types": "AWS::RDS::DBCluster",
+      "Trigger type": "Configuration changes"
+    },
     "DOCDB_CLUSTER_BACKUP_RETENTION_CHECK": {
       "AWS Region": "Only available in Asia Pacific (Mumbai), Europe (Paris), US East (Ohio), Europe (Ireland), Europe (Frankfurt), South America (Sao Paulo), US East (N. Virginia), Asia Pacific (Seoul), Europe (London), Europe (Milan), Asia Pacific (Tokyo), US West (Oregon), Asia Pacific (Singapore), Asia Pacific (Sydney), Canada (Central), China (Ningxia) Region",
       "Parameters": [
@@ -979,6 +1027,12 @@
       "Resource Types": "AWS::RDS::DBCluster",
       "Trigger type": "Configuration changes"
     },
+    "DOCDB_CLUSTER_DELETION_PROTECTION_ENABLED": {
+      "AWS Region": "Only available in Asia Pacific (Mumbai), Europe (Paris), US East (Ohio), Europe (Ireland), Europe (Frankfurt), South America (Sao Paulo), US East (N. Virginia), Asia Pacific (Seoul), Europe (London), Europe (Milan), Asia Pacific (Tokyo), US West (Oregon), Asia Pacific (Singapore), Asia Pacific (Sydney), Canada (Central), China (Ningxia) Region",
+      "Parameters": [],
+      "Resource Types": "AWS::RDS::DBCluster",
+      "Trigger type": "Configuration changes"
+    },
     "DOCDB_CLUSTER_ENCRYPTED": {
       "AWS Region": "Only available in Asia Pacific (Mumbai), Europe (Paris), US East (Ohio), Europe (Ireland), Europe (Frankfurt), South America (Sao Paulo), US East (N. Virginia), Asia Pacific (Seoul), Europe (London), Europe (Milan), Asia Pacific (Tokyo), US West (Oregon), Asia Pacific (Singapore), Asia Pacific (Sydney), Canada (Central), China (Ningxia) Region",
       "Parameters": [
@@ -991,6 +1045,12 @@
       "Resource Types": "AWS::RDS::DBCluster",
       "Trigger type": "Configuration changes"
     },
+    "DOCDB_CLUSTER_SNAPSHOT_PUBLIC_PROHIBITED": {
+      "AWS Region": "Only available in Asia Pacific (Mumbai), Europe (Paris), US East (Ohio), Europe (Ireland), Europe (Frankfurt), South America (Sao Paulo), US East (N. Virginia), Asia Pacific (Seoul), Europe (London), Europe (Milan), Asia Pacific (Tokyo), US West (Oregon), Asia Pacific (Singapore), Asia Pacific (Sydney), Canada (Central) Region",
+      "Parameters": [],
+      "Resource Types": "AWS::RDS::DBClusterSnapshot",
+      "Trigger type": "Configuration changes"
+    },
     "DYNAMODB_AUTOSCALING_ENABLED": {
       "AWS Region": "All supported AWS regions",
       "Parameters": [
@@ -2242,6 +2302,12 @@
       "Resource Types": "AWS::FSx::FileSystem",
       "Trigger type": "Periodic"
     },
+    "GLOBAL_ENDPOINT_EVENT_REPLICATION_ENABLED": {
+      "AWS Region": "All supported AWS regions except Middle East (Bahrain), China (Beijing), Asia Pacific (Jakarta), Africa (Cape Town), Middle East (UAE), Asia Pacific (Hong Kong), Asia Pacific (Hyderabad), Asia Pacific (Melbourne), Europe (Milan), AWS GovCloud (US-East), AWS GovCloud (US-West), Israel (Tel Aviv), Europe (Spain), China (Ningxia), Europe (Zurich) Region",
+      "Parameters": [],
+      "Resource Types": "AWS::Events::Endpoint",
+      "Trigger type": "Configuration changes"
+    },
     "GUARDDUTY_ENABLED_CENTRALIZED": {
       "AWS Region": "All supported AWS regions except Middle East (Bahrain), China (Beijing), Africa (Cape Town), Middle East (UAE), Asia Pacific (Osaka), Europe (Milan), Israel (Tel Aviv), Europe (Spain), China (Ningxia), Europe (Zurich) Region",
       "Parameters": [
@@ -2628,6 +2694,12 @@
       "Parameters": [],
       "Trigger type": "Periodic"
     },
+    "MQ_ACTIVE_DEPLOYMENT_MODE": {
+      "AWS Region": "All supported AWS regions except Asia Pacific (Hyderabad), Asia Pacific (Melbourne), AWS GovCloud (US-East), AWS GovCloud (US-West), Israel (Tel Aviv), Europe (Spain), Europe (Zurich) Region",
+      "Parameters": [],
+      "Resource Types": "AWS::AmazonMQ::Broker",
+      "Trigger type": "Configuration changes"
+    },
     "MQ_AUTOMATIC_MINOR_VERSION_UPGRADE_ENABLED": {
       "AWS Region": "All supported AWS regions except China (Beijing), Asia Pacific (Jakarta), Africa (Cape Town), Middle East (UAE), Asia Pacific (Hyderabad), Asia Pacific (Melbourne), AWS GovCloud (US-East), AWS GovCloud (US-West), Israel (Tel Aviv), Europe (Spain), China (Ningxia), Europe (Zurich) Region",
       "Parameters": [],
@@ -2646,6 +2718,18 @@
       "Resource Types": "AWS::AmazonMQ::Broker",
       "Trigger type": "Periodic"
     },
+    "MQ_RABBIT_DEPLOYMENT_MODE": {
+      "AWS Region": "All supported AWS regions except Asia Pacific (Hyderabad), Asia Pacific (Melbourne), AWS GovCloud (US-East), AWS GovCloud (US-West), Israel (Tel Aviv), Europe (Spain), Europe (Zurich) Region",
+      "Parameters": [],
+      "Resource Types": "AWS::AmazonMQ::Broker",
+      "Trigger type": "Configuration changes"
+    },
+    "MSK_IN_CLUSTER_NODE_REQUIRE_TLS": {
+      "AWS Region": "All supported AWS regions except Asia Pacific (Jakarta), Middle East (UAE), Asia Pacific (Hyderabad), Asia Pacific (Melbourne), AWS GovCloud (US-East), AWS GovCloud (US-West), Israel (Tel Aviv), Europe (Spain), Europe (Zurich) Region",
+      "Parameters": [],
+      "Resource Types": "AWS::MSK::Cluster",
+      "Trigger type": "Configuration changes"
+    },
     "MULTI_REGION_CLOUD_TRAIL_ENABLED": {
       "AWS Region": "All supported AWS regions except Middle East (UAE) Region",
       "Parameters": [
@@ -2743,6 +2827,12 @@
       "Resource Types": "AWS::RDS::DBClusterSnapshot",
       "Trigger type": "Configuration changes"
     },
+    "NETFW_DELETION_PROTECTION_ENABLED": {
+      "AWS Region": "All supported AWS regions except China (Beijing), Middle East (UAE), Asia Pacific (Hyderabad), Asia Pacific (Melbourne), AWS GovCloud (US-East), AWS GovCloud (US-West), Israel (Tel Aviv), Europe (Spain), China (Ningxia), Europe (Zurich) Region",
+      "Parameters": [],
+      "Resource Types": "AWS::NetworkFirewall::Firewall",
+      "Trigger type": "Configuration changes"
+    },
     "NETFW_LOGGING_ENABLED": {
       "AWS Region": "All supported AWS regions except China (Beijing), Middle East (UAE), Asia Pacific (Hyderabad), Asia Pacific (Melbourne), AWS GovCloud (US-East), AWS GovCloud (US-West), Israel (Tel Aviv), Europe (Spain), China (Ningxia), Europe (Zurich) Region",
       "Parameters": [
@@ -2887,12 +2977,24 @@
       "Resource Types": "AWS::OpenSearch::Domain",
       "Trigger type": "Configuration changes"
     },
+    "RDS_AURORA_MYSQL_AUDIT_LOGGING_ENABLED": {
+      "AWS Region": "All supported AWS regions except China (Beijing), Asia Pacific (Hyderabad), Asia Pacific (Melbourne), AWS GovCloud (US-East), AWS GovCloud (US-West), Israel (Tel Aviv), Europe (Spain) Region",
+      "Parameters": [],
+      "Resource Types": "AWS::RDS::DBCluster",
+      "Trigger type": "Configuration changes"
+    },
     "RDS_AUTOMATIC_MINOR_VERSION_UPGRADE_ENABLED": {
       "AWS Region": "All supported AWS regions except Asia Pacific (Osaka), Europe (Spain) Region",
       "Parameters": [],
       "Resource Types": "AWS::RDS::DBInstance",
       "Trigger type": "Configuration changes"
     },
+    "RDS_CLUSTER_AUTO_MINOR_VERSION_UPGRADE_ENABLE": {
+      "AWS Region": "All supported AWS regions except China (Beijing), Middle East (UAE), Asia Pacific (Hyderabad), Asia Pacific (Osaka), Asia Pacific (Melbourne), AWS GovCloud (US-East), AWS GovCloud (US-West), US West (N. California), Israel (Tel Aviv), Europe (Spain), Europe (Zurich) Region",
+      "Parameters": [],
+      "Resource Types": "AWS::RDS::DBCluster",
+      "Trigger type": "Configuration changes"
+    },
     "RDS_CLUSTER_DEFAULT_ADMIN_CHECK": {
       "AWS Region": "All supported AWS regions except Middle East (Bahrain), China (Beijing), Asia Pacific (Jakarta), Middle East (UAE), South America (Sao Paulo), Asia Pacific (Hyderabad), Asia Pacific (Melbourne), AWS GovCloud (US-East), AWS GovCloud (US-West), Israel (Tel Aviv), Europe (Spain), China (Ningxia), Europe (Zurich) Region",
       "Parameters": [
@@ -3353,6 +3455,12 @@
       "Parameters": [],
       "Trigger type": "Periodic"
     },
+    "ROUTE53_QUERY_LOGGING_ENABLED": {
+      "AWS Region": "Only available in US East (N. Virginia) Region",
+      "Parameters": [],
+      "Resource Types": "AWS::Route53::HostedZone",
+      "Trigger type": "Configuration changes"
+    },
     "S3_ACCOUNT_LEVEL_PUBLIC_ACCESS_BLOCKS": {
       "AWS Region": "All supported AWS regions except Middle East (Bahrain), Middle East (UAE), Asia Pacific (Hyderabad), Asia Pacific (Osaka), Asia Pacific (Melbourne), Europe (Milan), Israel (Tel Aviv), Europe (Spain), Europe (Zurich) Region",
       "Parameters": [
@@ -4103,6 +4211,12 @@
       ],
       "Trigger type": "Periodic"
     },
+    "WAFV2_RULEGROUP_LOGGING_ENABLED": {
+      "AWS Region": "All supported AWS regions except AWS GovCloud (US-East), AWS GovCloud (US-West), Israel (Tel Aviv) Region",
+      "Parameters": [],
+      "Resource Types": "AWS::WAFv2::RuleGroup",
+      "Trigger type": "Configuration changes"
+    },
     "WAFV2_RULEGROUP_NOT_EMPTY": {
       "AWS Region": "All supported AWS regions except China (Beijing), Asia Pacific (Jakarta), Middle East (UAE), Asia Pacific (Hyderabad), Asia Pacific (Melbourne), AWS GovCloud (US-East), AWS GovCloud (US-West), Israel (Tel Aviv), Europe (Spain), China (Ningxia), Europe (Zurich) Region",
       "Parameters": [],
diff --git a/moto/sagemaker/models.py b/moto/sagemaker/models.py
index 7d1419587..c1e94056e 100644
--- a/moto/sagemaker/models.py
+++ b/moto/sagemaker/models.py
@@ -48,6 +48,12 @@ PAGINATION_MODEL = {
         "limit_default": 50,
         "unique_attribute": "Key",
     },
+    "list_model_package_groups": {
+        "input_token": "next_token",
+        "limit_key": "max_results",
+        "limit_default": 100,
+        "unique_attribute": "ModelPackageGroupArn",
+    },
     "list_model_packages": {
         "input_token": "next_token",
         "limit_key": "max_results",
@@ -943,10 +949,11 @@ class ModelPackageGroup(BaseObject):
             account_id=account_id,
             region_name=region_name,
         )
+        datetime_now = datetime.now(tzutc())
         self.model_package_group_name = model_package_group_name
         self.model_package_group_arn = model_package_group_arn
         self.model_package_group_description = model_package_group_description
-        self.creation_time = datetime.now()
+        self.creation_time = datetime_now
         self.created_by = {
             "UserProfileArn": fake_user_profile_arn,
             "UserProfileName": fake_user_profile_name,
@@ -955,6 +962,20 @@ class ModelPackageGroup(BaseObject):
         self.model_package_group_status = "Completed"
         self.tags = tags
 
+    def gen_response_object(self) -> Dict[str, Any]:
+        response_object = super().gen_response_object()
+        for k, v in response_object.items():
+            if isinstance(v, datetime):
+                response_object[k] = v.isoformat()
+        response_values = [
+            "ModelPackageGroupName",
+            "ModelPackageGroupArn",
+            "ModelPackageGroupDescription",
+            "CreationTime",
+            "ModelPackageGroupStatus",
+        ]
+        return {k: v for k, v in response_object.items() if k in response_values}
+
 
 class ModelPackage(BaseObject):
     def __init__(
@@ -994,7 +1015,9 @@ class ModelPackage(BaseObject):
             region_name=region_name,
             account_id=account_id,
             _type="model-package",
-            _id=model_package_name,
+            _id=f"{model_package_name}/{model_package_version}"
+            if model_package_version
+            else model_package_name,
         )
         datetime_now = datetime.now(tzutc())
         self.model_package_name = model_package_name
@@ -2912,6 +2935,53 @@ class SageMakerModelBackend(BaseBackend):
             return True
         raise ValueError(f"Invalid model package type: {model_package_type}")
 
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    def list_model_package_groups(  # type: ignore[misc]
+        self,
+        creation_time_after: Optional[int],
+        creation_time_before: Optional[int],
+        name_contains: Optional[str],
+        sort_by: Optional[str],
+        sort_order: Optional[str],
+    ) -> List[ModelPackageGroup]:
+        if isinstance(creation_time_before, int):
+            creation_time_before_datetime = datetime.fromtimestamp(
+                creation_time_before, tz=tzutc()
+            )
+        if isinstance(creation_time_after, int):
+            creation_time_after_datetime = datetime.fromtimestamp(
+                creation_time_after, tz=tzutc()
+            )
+        model_package_group_summary_list = list(
+            filter(
+                lambda x: (
+                    creation_time_after is None
+                    or x.creation_time > creation_time_after_datetime
+                )
+                and (
+                    creation_time_before is None
+                    or x.creation_time < creation_time_before_datetime
+                )
+                and (
+                    name_contains is None
+                    or x.model_package_group_name.find(name_contains) != -1
+                ),
+                self.model_package_groups.values(),
+            )
+        )
+        model_package_group_summary_list = list(
+            sorted(
+                model_package_group_summary_list,
+                key={
+                    "Name": lambda x: x.model_package_group_name,
+                    "CreationTime": lambda x: x.creation_time,
+                    None: lambda x: x.creation_time,
+                }[sort_by],
+                reverse=sort_order == "Descending",
+            )
+        )
+        return model_package_group_summary_list
+
     @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_model_packages(  # type: ignore[misc]
         self,
diff --git a/moto/sagemaker/responses.py b/moto/sagemaker/responses.py
index b40f8b537..ec83c10e2 100644
--- a/moto/sagemaker/responses.py
+++ b/moto/sagemaker/responses.py
@@ -799,6 +799,36 @@ class SageMakerResponse(BaseResponse):
         )
         return 200, {}, json.dumps({"EndpointArn": endpoint_arn})
 
+    def list_model_package_groups(self) -> str:
+        creation_time_after = self._get_param("CreationTimeAfter")
+        creation_time_before = self._get_param("CreationTimeBefore")
+        max_results = self._get_param("MaxResults")
+        name_contains = self._get_param("NameContains")
+        next_token = self._get_param("NextToken")
+        sort_by = self._get_param("SortBy")
+        sort_order = self._get_param("SortOrder")
+        (
+            model_package_group_summary_list,
+            next_token,
+        ) = self.sagemaker_backend.list_model_package_groups(
+            creation_time_after=creation_time_after,
+            creation_time_before=creation_time_before,
+            max_results=max_results,
+            name_contains=name_contains,
+            next_token=next_token,
+            sort_by=sort_by,
+            sort_order=sort_order,
+        )
+        model_package_group_summary_list_response_object = [
+            x.gen_response_object() for x in model_package_group_summary_list
+        ]
+        return json.dumps(
+            dict(
+                ModelPackageGroupSummaryList=model_package_group_summary_list_response_object,
+                NextToken=next_token,
+            )
+        )
+
     def list_model_packages(self) -> str:
         creation_time_after = self._get_param("CreationTimeAfter")
         creation_time_before = self._get_param("CreationTimeBefore")
diff --git a/scripts/pull_down_aws_managed_rules.py b/scripts/pull_down_aws_managed_rules.py
index 66cf5ab5d..3f92014d6 100755
--- a/scripts/pull_down_aws_managed_rules.py
+++ b/scripts/pull_down_aws_managed_rules.py
@@ -58,7 +58,7 @@ AWS_CONFIG_MANAGED_RULES_URL_START = (
 LIST_OF_RULES_URL = "managed-rules-by-aws-config.html"
 
 
-def extract_param_info(page_content):
+def extract_param_info(page_content, rule_name):
     """Return dict containing parameter info extracted from page.
 
     The info for all (not each) parameters is contained within a "dl" tag,
@@ -70,12 +70,16 @@ def extract_param_info(page_content):
     dl_tags = page_content.xpath('//div[@class="variablelist"]//dl')
     if len(dl_tags) > 1:
         print(
-            f"ERROR: Found {len(dl_tags)} 'dl' tags for parameters; "
-            "only expecting one.  Ignoring extra 'dl' tag.",
-            file=sys.stderr
+            f"ERROR: '{rule_name}' has {len(dl_tags)} 'dl' tags for "
+            "rule parameters; only expecting one.",
+            file=sys.stderr,
         )
+        sys.exit(1)
 
     dt_tags = dl_tags[0].xpath(".//dt")
+    if len(dt_tags) < 1:
+        print(f"ERROR: '{rule_name}' has no rule parameters.", file=sys.stderr)
+        sys.exit(1)
 
     all_params = []
     param_details = {}
@@ -110,7 +114,7 @@ def extract_param_info(page_content):
     return all_params
 
 
-def extract_managed_rule_info(page_content):
+def extract_managed_rule_info(page_content, rule_name):
     """Return dict of qualifiers/rules extracted from web page.
 
     An example of the html that's being processed:
@@ -141,6 +145,13 @@ def extract_managed_rule_info(page_content):
     """
     rule_info = {}
     paragraphs = page_content.xpath('//div[@id="main-content"]/descendant::p')
+    if len(paragraphs) < 1:
+        print(
+            f"ERROR: '{rule_name}' has no managed rule details (i.e., "
+            "Identifier, AWS Region).",
+            file=sys.stderr,
+        )
+        sys.exit(1)
 
     for paragraph in paragraphs:
         text = paragraph.text_content()
@@ -155,7 +166,7 @@ def extract_managed_rule_info(page_content):
             rule_info[parts[0]] = parts[1]
 
     # The parameters are in their own "div", so handle them separately.
-    rule_info["Parameters"] = extract_param_info(page_content)
+    rule_info["Parameters"] = extract_param_info(page_content, rule_name)
     return rule_info
 
 
@@ -180,7 +191,16 @@ def main():
     # Get the list of links for all the services.
     page = requests.get(AWS_CONFIG_MANAGED_RULES_URL_START + LIST_OF_RULES_URL)
     tree = html.fromstring(page.content)
-    links = [x.lstrip("./") for x in tree.xpath('//div[@class="highlights"]//ul//a/@href')]
+    links = [
+        x.lstrip("./") for x in tree.xpath('//div[@class="highlights"]//ul//a/@href')
+    ]
+    if len(links) < 300:
+        print(
+            f"ERROR: Found {len(links)} links to managed rules, expected "
+            "over 300 links",
+            file=sys.stderr,
+        )
+        sys.exit(1)
 
     # From each linked page, extract the id, region, trigger type and parameter
     # information.
@@ -189,7 +209,8 @@ def main():
         if args.verbose:
             print(f"Extracting from {link} ...")
         page = requests.get(AWS_CONFIG_MANAGED_RULES_URL_START + link)
-        rules = extract_managed_rule_info(html.fromstring(page.content))
+        rule_name = link.rstrip(".html")
+        rules = extract_managed_rule_info(html.fromstring(page.content), rule_name)
 
         rule_id = rules.pop("Identifier")
         managed_rules["ManagedRules"][rule_id] = rules


Test patch:
diff --git a/tests/test_sagemaker/test_sagemaker_model_package_groups.py b/tests/test_sagemaker/test_sagemaker_model_package_groups.py
new file mode 100644
index 000000000..49a1d99fb
--- /dev/null
+++ b/tests/test_sagemaker/test_sagemaker_model_package_groups.py
@@ -0,0 +1,165 @@
+"""Unit tests for sagemaker-supported APIs."""
+from unittest import SkipTest
+
+import boto3
+from freezegun import freeze_time
+
+from moto import mock_sagemaker, settings
+
+# See our Development Tips on writing tests for hints on how to write good tests:
+# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html
+
+
+@mock_sagemaker
+def test_create_model_package_group():
+    client = boto3.client("sagemaker", region_name="us-east-2")
+    resp = client.create_model_package_group(
+        ModelPackageGroupName="test-model-package-group",
+        ModelPackageGroupDescription="test-model-package-group-description",
+        Tags=[
+            {"Key": "test-key", "Value": "test-value"},
+        ],
+    )
+    assert (
+        resp["ModelPackageGroupArn"]
+        == "arn:aws:sagemaker:us-east-2:123456789012:model-package-group/test-model-package-group"
+    )
+
+
+@mock_sagemaker
+def test_list_model_package_groups():
+    client = boto3.client("sagemaker", region_name="eu-west-1")
+    client.create_model_package_group(
+        ModelPackageGroupName="test-model-package-group-1",
+        ModelPackageGroupDescription="test-model-package-group-description-1",
+    )
+    client.create_model_package_group(
+        ModelPackageGroupName="test-model-package-group-2",
+        ModelPackageGroupDescription="test-model-package-group-description-2",
+    )
+    resp = client.list_model_package_groups()
+
+    assert (
+        resp["ModelPackageGroupSummaryList"][0]["ModelPackageGroupName"]
+        == "test-model-package-group-1"
+    )
+    assert "ModelPackageGroupDescription" in resp["ModelPackageGroupSummaryList"][0]
+    assert (
+        resp["ModelPackageGroupSummaryList"][0]["ModelPackageGroupDescription"]
+        == "test-model-package-group-description-1"
+    )
+    assert (
+        resp["ModelPackageGroupSummaryList"][1]["ModelPackageGroupName"]
+        == "test-model-package-group-2"
+    )
+    assert "ModelPackageGroupDescription" in resp["ModelPackageGroupSummaryList"][1]
+    assert (
+        resp["ModelPackageGroupSummaryList"][1]["ModelPackageGroupDescription"]
+        == "test-model-package-group-description-2"
+    )
+
+
+@mock_sagemaker
+def test_list_model_package_groups_creation_time_before():
+    if settings.TEST_SERVER_MODE:
+        raise SkipTest("Can't freeze time in ServerMode")
+    client = boto3.client("sagemaker", region_name="eu-west-1")
+    with freeze_time("2020-01-01 00:00:00"):
+        client.create_model_package_group(
+            ModelPackageGroupName="test-model-package-group-1",
+            ModelPackageGroupDescription="test-model-package-group-description-1",
+        )
+    with freeze_time("2021-01-01 00:00:00"):
+        client.create_model_package_group(
+            ModelPackageGroupName="test-model-package-group-2",
+            ModelPackageGroupDescription="test-model-package-group-description-2",
+        )
+    resp = client.list_model_package_groups(CreationTimeBefore="2020-01-01T02:00:00Z")
+
+    assert len(resp["ModelPackageGroupSummaryList"]) == 1
+
+
+@mock_sagemaker
+def test_list_model_package_groups_creation_time_after():
+    if settings.TEST_SERVER_MODE:
+        raise SkipTest("Can't freeze time in ServerMode")
+    client = boto3.client("sagemaker", region_name="eu-west-1")
+    with freeze_time("2020-01-01 00:00:00"):
+        client.create_model_package_group(
+            ModelPackageGroupName="test-model-package-group-1",
+            ModelPackageGroupDescription="test-model-package-group-description-1",
+        )
+    with freeze_time("2021-01-01 00:00:00"):
+        client.create_model_package_group(
+            ModelPackageGroupName="test-model-package-group-2",
+            ModelPackageGroupDescription="test-model-package-group-description-2",
+        )
+    resp = client.list_model_package_groups(CreationTimeAfter="2020-01-02T00:00:00Z")
+
+    assert len(resp["ModelPackageGroupSummaryList"]) == 1
+
+
+@mock_sagemaker
+def test_list_model_package_groups_name_contains():
+    client = boto3.client("sagemaker", region_name="eu-west-1")
+    client.create_model_package_group(
+        ModelPackageGroupName="test-model-package-group-1",
+        ModelPackageGroupDescription="test-model-package-group-description-1",
+    )
+    client.create_model_package_group(
+        ModelPackageGroupName="test-model-package-group-2",
+        ModelPackageGroupDescription="test-model-package-group-description-2",
+    )
+    client.create_model_package_group(
+        ModelPackageGroupName="another-model-package-group",
+        ModelPackageGroupDescription="another-model-package-group-description",
+    )
+    resp = client.list_model_package_groups(NameContains="test-model-package")
+
+    assert len(resp["ModelPackageGroupSummaryList"]) == 2
+
+
+@mock_sagemaker
+def test_list_model_package_groups_sort_by():
+    client = boto3.client("sagemaker", region_name="eu-west-1")
+    client.create_model_package_group(
+        ModelPackageGroupName="test-model-package-group-1",
+        ModelPackageGroupDescription="test-model-package-group-description-1",
+    )
+    client.create_model_package_group(
+        ModelPackageGroupName="test-model-package-group-2",
+        ModelPackageGroupDescription="test-model-package-group-description-2",
+    )
+    resp = client.list_model_package_groups(SortBy="CreationTime")
+
+    assert (
+        resp["ModelPackageGroupSummaryList"][0]["ModelPackageGroupName"]
+        == "test-model-package-group-1"
+    )
+    assert (
+        resp["ModelPackageGroupSummaryList"][1]["ModelPackageGroupName"]
+        == "test-model-package-group-2"
+    )
+
+
+@mock_sagemaker
+def test_list_model_package_groups_sort_order():
+    client = boto3.client("sagemaker", region_name="eu-west-1")
+    client.create_model_package_group(
+        ModelPackageGroupName="test-model-package-group-1",
+        ModelPackageGroupDescription="test-model-package-group-description-1",
+    )
+    client.create_model_package_group(
+        ModelPackageGroupName="test-model-package-group-2",
+        ModelPackageGroupDescription="test-model-package-group-description-2",
+    )
+    resp = client.list_model_package_groups(SortOrder="Descending")
+
+    assert (
+        resp["ModelPackageGroupSummaryList"][0]["ModelPackageGroupName"]
+        == "test-model-package-group-2"
+    )
+    assert (
+        resp["ModelPackageGroupSummaryList"][1]["ModelPackageGroupName"]
+        == "test-model-package-group-1"
+    )
diff --git a/tests/test_sagemaker/test_sagemaker_model_packages.py b/tests/test_sagemaker/test_sagemaker_model_packages.py
index 1b9a8d752..c510c2c9c 100644
--- a/tests/test_sagemaker/test_sagemaker_model_packages.py
+++ b/tests/test_sagemaker/test_sagemaker_model_packages.py
@@ -15,11 +15,15 @@ def test_list_model_packages():
     client = boto3.client("sagemaker", region_name="eu-west-1")
     client.create_model_package(
         ModelPackageName="test-model-package",
-        ModelPackageDescription="test-model-package-description",
+        ModelPackageDescription="test-model-package-description-v1",
+    )
+    client.create_model_package(
+        ModelPackageName="test-model-package",
+        ModelPackageDescription="test-model-package-description-v2",
     )
     client.create_model_package(
         ModelPackageName="test-model-package-2",
-        ModelPackageDescription="test-model-package-description-2",
+        ModelPackageDescription="test-model-package-description-v1-2",
     )
     resp = client.list_model_packages()
 
@@ -29,7 +33,7 @@ def test_list_model_packages():
     assert "ModelPackageDescription" in resp["ModelPackageSummaryList"][0]
     assert (
         resp["ModelPackageSummaryList"][0]["ModelPackageDescription"]
-        == "test-model-package-description"
+        == "test-model-package-description-v2"
     )
     assert (
         resp["ModelPackageSummaryList"][1]["ModelPackageName"] == "test-model-package-2"
@@ -37,7 +41,7 @@ def test_list_model_packages():
     assert "ModelPackageDescription" in resp["ModelPackageSummaryList"][1]
     assert (
         resp["ModelPackageSummaryList"][1]["ModelPackageDescription"]
-        == "test-model-package-description-2"
+        == "test-model-package-description-v1-2"
     )
 
 
@@ -128,13 +132,22 @@ def test_list_model_packages_model_package_group_name():
         ModelPackageGroupName="test-model-package-group",
     )
     client.create_model_package(
-        ModelPackageName="test-model-package-2",
+        ModelPackageName="test-model-package",
         ModelPackageDescription="test-model-package-description-2",
         ModelPackageGroupName="test-model-package-group",
     )
+    client.create_model_package(
+        ModelPackageName="test-model-package-2",
+        ModelPackageDescription="test-model-package-description-3",
+        ModelPackageGroupName="test-model-package-group",
+    )
+    client.create_model_package(
+        ModelPackageName="test-model-package-without-group",
+        ModelPackageDescription="test-model-package-description-without-group",
+    )
     resp = client.list_model_packages(ModelPackageGroupName="test-model-package-group")
 
-    assert len(resp["ModelPackageSummaryList"]) == 2
+    assert len(resp["ModelPackageSummaryList"]) == 3
 
 
 @mock_sagemaker
@@ -222,16 +235,24 @@ def test_create_model_package():
 
 
 @mock_sagemaker
-def test_create_model_package_group():
-    client = boto3.client("sagemaker", region_name="us-east-2")
-    resp = client.create_model_package_group(
+def test_create_model_package_in_model_package_group():
+    client = boto3.client("sagemaker", region_name="eu-west-1")
+    client.create_model_package_group(ModelPackageGroupName="test-model-package-group")
+    resp_version_1 = client.create_model_package(
+        ModelPackageName="test-model-package",
+        ModelPackageGroupName="test-model-package-group",
+        ModelPackageDescription="test-model-package-description",
+    )
+    resp_version_2 = client.create_model_package(
+        ModelPackageName="test-model-package",
         ModelPackageGroupName="test-model-package-group",
-        ModelPackageGroupDescription="test-model-package-group-description",
-        Tags=[
-            {"Key": "test-key", "Value": "test-value"},
-        ],
+        ModelPackageDescription="test-model-package-description",
+    )
+    assert (
+        resp_version_1["ModelPackageArn"]
+        == "arn:aws:sagemaker:eu-west-1:123456789012:model-package/test-model-package/1"
     )
     assert (
-        resp["ModelPackageGroupArn"]
-        == "arn:aws:sagemaker:us-east-2:123456789012:model-package-group/test-model-package-group"
+        resp_version_2["ModelPackageArn"]
+        == "arn:aws:sagemaker:eu-west-1:123456789012:model-package/test-model-package/2"
     )