Repository: getmoto/moto
Pull request #8145 (2024-09-23 20:50:11)

GuardDuty Detector - Add "Feature" support to Create, Delete and Update apis
GuardDuty Detector - Add "Feature" support to Create, Delete and Update apis



Ref:
https://docs.aws.amazon.com/guardduty/latest/APIReference/API_DetectorFeatureConfiguration.html
https://docs.aws.amazon.com/guardduty/latest/APIReference/API_UpdateDetector.html#guardduty-UpdateDetector-request-features


Patch:
diff --git a/moto/guardduty/models.py b/moto/guardduty/models.py
index e3174b98a..fb901ac7e 100644
--- a/moto/guardduty/models.py
+++ b/moto/guardduty/models.py
@@ -21,6 +21,7 @@ class GuardDutyBackend(BaseBackend):
         finding_publishing_frequency: str,
         data_sources: Dict[str, Any],
         tags: Dict[str, str],
+        features: List[Dict[str, Any]],
     ) -> str:
         if finding_publishing_frequency not in [
             "FIFTEEN_MINUTES",
@@ -37,6 +38,7 @@ class GuardDutyBackend(BaseBackend):
             enabled=enable,
             datasources=data_sources,
             tags=tags,
+            features=features,
         )
         self.detectors[detector.id] = detector
         return detector.id
@@ -94,9 +96,10 @@ class GuardDutyBackend(BaseBackend):
         enable: bool,
         finding_publishing_frequency: str,
         data_sources: Dict[str, Any],
+        features: List[Dict[str, Any]],
     ) -> None:
         detector = self.get_detector(detector_id)
-        detector.update(enable, finding_publishing_frequency, data_sources)
+        detector.update(enable, finding_publishing_frequency, data_sources, features)
 
     def update_filter(
         self,
@@ -168,6 +171,7 @@ class Detector(BaseModel):
         enabled: bool,
         datasources: Dict[str, Any],
         tags: Dict[str, str],
+        features: List[Dict[str, Any]],
     ):
         self.id = mock_random.get_random_hex(length=32)
         self.created_at = created_at
@@ -177,6 +181,9 @@ class Detector(BaseModel):
         self.updated_at = created_at
         self.datasources = datasources or {}
         self.tags = tags or {}
+        # TODO: Implement feature configuration object and validation
+        # https://docs.aws.amazon.com/guardduty/latest/APIReference/API_DetectorFeatureConfiguration.html
+        self.features = features or []
 
         self.filters: Dict[str, Filter] = dict()
 
@@ -212,6 +219,7 @@ class Detector(BaseModel):
         enable: bool,
         finding_publishing_frequency: str,
         data_sources: Dict[str, Any],
+        features: List[Dict[str, Any]],
     ) -> None:
         if enable is not None:
             self.enabled = enable
@@ -219,6 +227,8 @@ class Detector(BaseModel):
             self.finding_publish_freq = finding_publishing_frequency
         if data_sources is not None:
             self.datasources = data_sources
+        if features is not None:
+            self.features = features
 
     def to_json(self) -> Dict[str, Any]:
         data_sources = {
@@ -248,6 +258,7 @@ class Detector(BaseModel):
             "updatedAt": self.updated_at.strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
             "dataSources": data_sources,
             "tags": self.tags,
+            "features": self.features,
         }
 
 
diff --git a/moto/guardduty/responses.py b/moto/guardduty/responses.py
index 9b7691542..614e9add2 100644
--- a/moto/guardduty/responses.py
+++ b/moto/guardduty/responses.py
@@ -32,9 +32,10 @@ class GuardDutyResponse(BaseResponse):
         finding_publishing_frequency = self._get_param("findingPublishingFrequency")
         data_sources = self._get_param("dataSources")
         tags = self._get_param("tags")
+        features = self._get_param("features")
 
         detector_id = self.guardduty_backend.create_detector(
-            enable, finding_publishing_frequency, data_sources, tags
+            enable, finding_publishing_frequency, data_sources, tags, features
         )
 
         return json.dumps(dict(detectorId=detector_id))
@@ -89,9 +90,10 @@ class GuardDutyResponse(BaseResponse):
         enable = self._get_param("enable")
         finding_publishing_frequency = self._get_param("findingPublishingFrequency")
         data_sources = self._get_param("dataSources")
+        features = self._get_param("features")
 
         self.guardduty_backend.update_detector(
-            detector_id, enable, finding_publishing_frequency, data_sources
+            detector_id, enable, finding_publishing_frequency, data_sources, features
         )
         return "{}"
 
diff --git a/moto/route53/responses.py b/moto/route53/responses.py
index 6150ae02d..56cf9a987 100644
--- a/moto/route53/responses.py
+++ b/moto/route53/responses.py
@@ -373,13 +373,16 @@ class Route53(BaseResponse):
             return 200, headers, template.render()
 
     def list_tags_for_resources(self) -> str:
-        if id_matcher := re.search(r"/tags/[-a-z]+/(.+)", self.parsed_url.path):
+        if id_matcher := re.search(r"/tags/(.+)", self.parsed_url.path):
             resource_type = unquote(id_matcher.group(1))
         else:
             resource_type = ""
         resource_ids = xmltodict.parse(self.body)["ListTagsForResourcesRequest"][
             "ResourceIds"
         ]["ResourceId"]
+        # If only one resource id is passed, it is a string, not a list
+        if not isinstance(resource_ids, list):
+            resource_ids = [resource_ids]
         tag_sets = self.backend.list_tags_for_resources(resource_ids=resource_ids)
         template = Template(LIST_TAGS_FOR_RESOURCES_RESPONSE)
         return template.render(tag_sets=tag_sets, resource_type=resource_type)


Test patch:
diff --git a/tests/test_guardduty/test_guardduty.py b/tests/test_guardduty/test_guardduty.py
index aab40d6c2..4cd44688a 100644
--- a/tests/test_guardduty/test_guardduty.py
+++ b/tests/test_guardduty/test_guardduty.py
@@ -13,6 +13,7 @@ def test_create_detector():
         ClientToken="745645734574758463758",
         FindingPublishingFrequency="ONE_HOUR",
         DataSources={"S3Logs": {"Enable": True}},
+        Features=[{"Name": "Test", "Status": "ENABLED"}],
         Tags={},
     )
     assert "DetectorId" in response
@@ -65,6 +66,35 @@ def test_get_detector_with_all_data_sources():
     assert "CreatedAt" in resp
 
 
+@mock_aws
+def test_get_detector_with_features():
+    client = boto3.client("guardduty", region_name="us-east-1")
+    detector_id = client.create_detector(
+        Enable=True,
+        Features=[
+            {
+                "Name": "EKS_AUDIT_LOGS",
+                "Status": "ENABLED",
+                "AdditionalConfiguration": [
+                    {"Name": "EKS_ADDON_MANAGEMENT", "Status": "ENABLED"}
+                ],
+            },
+            {"Name": "TS3_DATA_EVENTS", "Status": "DISABLED"},
+        ],
+    )["DetectorId"]
+
+    resp = client.get_detector(DetectorId=detector_id)
+    assert len(resp["Features"]) == 2
+    assert resp["Features"][0]["Name"] == "EKS_AUDIT_LOGS"
+    assert resp["Features"][0]["Status"] == "ENABLED"
+    assert (
+        resp["Features"][0]["AdditionalConfiguration"][0]["Name"]
+        == "EKS_ADDON_MANAGEMENT"
+    )
+    assert resp["Features"][1]["Name"] == "TS3_DATA_EVENTS"
+    assert resp["Features"][1]["Status"] == "DISABLED"
+
+
 @mock_aws
 def test_update_detector():
     client = boto3.client("guardduty", region_name="us-east-1")
@@ -83,12 +113,14 @@ def test_update_detector():
             "S3Logs": {"Enable": True},
             "Kubernetes": {"AuditLogs": {"Enable": False}},
         },
+        Features=[{"Name": "Test", "Status": "ENABLED"}],
     )
 
     resp = client.get_detector(DetectorId=detector_id)
     assert resp["FindingPublishingFrequency"] == "SIX_HOURS"
     assert resp["DataSources"]["S3Logs"] == {"Status": "ENABLED"}
     assert resp["DataSources"]["Kubernetes"]["AuditLogs"] == {"Status": "DISABLED"}
+    assert resp["Features"] == [{"Name": "Test", "Status": "ENABLED"}]
 
 
 @mock_aws
diff --git a/tests/test_route53/test_route53.py b/tests/test_route53/test_route53.py
index 309333f3f..9e3d60d0b 100644
--- a/tests/test_route53/test_route53.py
+++ b/tests/test_route53/test_route53.py
@@ -634,6 +634,7 @@ def test_list_tags_for_resources():
     assert len(response["ResourceTagSets"]) == 2
     for set in response["ResourceTagSets"]:
         assert set["ResourceId"] in (zone1_id, zone2_id)
+        assert set["ResourceType"] == "hostedzone"
         if set["ResourceId"] == zone1_id:
             assert tag1 in set["Tags"]
             assert tag2 in set["Tags"]
@@ -645,6 +646,19 @@ def test_list_tags_for_resources():
             assert tag3 in set["Tags"]
             assert tag4 in set["Tags"]
 
+    # Test hostedzone with single resource
+    response = conn.list_tags_for_resources(
+        ResourceIds=[zone1_id], ResourceType="hostedzone"
+    )
+    assert len(response["ResourceTagSets"]) == 1
+    for set in response["ResourceTagSets"]:
+        assert set["ResourceId"] == zone1_id
+        assert set["ResourceType"] == "hostedzone"
+        assert tag1 in set["Tags"]
+        assert tag2 in set["Tags"]
+        assert tag3 not in set["Tags"]
+        assert tag4 not in set["Tags"]
+
     # Test healthcheck
     response = conn.list_tags_for_resources(
         ResourceIds=[healthcheck1_id, healthcheck2_id], ResourceType="healthcheck"
@@ -652,6 +666,7 @@ def test_list_tags_for_resources():
     assert len(response["ResourceTagSets"]) == 2
     for set in response["ResourceTagSets"]:
         assert set["ResourceId"] in (healthcheck1_id, healthcheck2_id)
+        assert set["ResourceType"] == "healthcheck"
         if set["ResourceId"] == healthcheck1_id:
             assert tag1 in set["Tags"]
             assert tag2 in set["Tags"]