diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index 0f2e7c4ff..464893986 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -2481,6 +2481,70 @@
- [ ] update_workflow
+## guardduty
+
+3% implemented
+
+- [ ] accept_invitation
+- [ ] archive_findings
+- [X] create_detector
+- [ ] create_filter
+- [ ] create_ip_set
+- [ ] create_members
+- [ ] create_publishing_destination
+- [ ] create_sample_findings
+- [ ] create_threat_intel_set
+- [ ] decline_invitations
+- [ ] delete_detector
+- [ ] delete_filter
+- [ ] delete_invitations
+- [ ] delete_ip_set
+- [ ] delete_members
+- [ ] delete_publishing_destination
+- [ ] delete_threat_intel_set
+- [ ] describe_organization_configuration
+- [ ] describe_publishing_destination
+- [ ] disable_organization_admin_account
+- [ ] disassociate_from_master_account
+- [ ] disassociate_members
+- [ ] enable_organization_admin_account
+- [ ] get_detector
+- [ ] get_filter
+- [ ] get_findings
+- [ ] get_findings_statistics
+- [ ] get_invitations_count
+- [ ] get_ip_set
+- [ ] get_master_account
+- [ ] get_member_detectors
+- [ ] get_members
+- [ ] get_threat_intel_set
+- [ ] get_usage_statistics
+- [ ] invite_members
+- [X] list_detectors
+- [ ] list_filters
+- [ ] list_findings
+- [ ] list_invitations
+- [ ] list_ip_sets
+- [ ] list_members
+- [ ] list_organization_admin_accounts
+- [ ] list_publishing_destinations
+- [ ] list_tags_for_resource
+- [ ] list_threat_intel_sets
+- [ ] start_monitoring_members
+- [ ] stop_monitoring_members
+- [ ] tag_resource
+- [ ] unarchive_findings
+- [ ] untag_resource
+- [ ] update_detector
+- [ ] update_filter
+- [ ] update_findings_feedback
+- [ ] update_ip_set
+- [ ] update_member_detectors
+- [ ] update_organization_configuration
+- [ ] update_publishing_destination
+- [ ] update_threat_intel_set
+
+
## iam
67% implemented
@@ -4945,7 +5009,6 @@
- greengrass
- greengrassv2
- groundstation
-- guardduty
- health
- healthlake
- honeycode
diff --git a/docs/docs/services/guardduty.rst b/docs/docs/services/guardduty.rst
new file mode 100644
index 000000000..9189ce47a
--- /dev/null
+++ b/docs/docs/services/guardduty.rst
@@ -0,0 +1,90 @@
+.. _implementedservice_guardduty:
+
+.. |start-h3| raw:: html
+
+
+
+.. |end-h3| raw:: html
+
+
+
+=========
+guardduty
+=========
+
+|start-h3| Example usage |end-h3|
+
+.. sourcecode:: python
+
+ @mock_guardduty
+ def test_guardduty_behaviour:
+ boto3.client("guardduty")
+ ...
+
+
+
+|start-h3| Implemented features for this service |end-h3|
+
+- [ ] accept_invitation
+- [ ] archive_findings
+- [X] create_detector
+- [ ] create_filter
+- [ ] create_ip_set
+- [ ] create_members
+- [ ] create_publishing_destination
+- [ ] create_sample_findings
+- [ ] create_threat_intel_set
+- [ ] decline_invitations
+- [ ] delete_detector
+- [ ] delete_filter
+- [ ] delete_invitations
+- [ ] delete_ip_set
+- [ ] delete_members
+- [ ] delete_publishing_destination
+- [ ] delete_threat_intel_set
+- [ ] describe_organization_configuration
+- [ ] describe_publishing_destination
+- [ ] disable_organization_admin_account
+- [ ] disassociate_from_master_account
+- [ ] disassociate_members
+- [ ] enable_organization_admin_account
+- [ ] get_detector
+- [ ] get_filter
+- [ ] get_findings
+- [ ] get_findings_statistics
+- [ ] get_invitations_count
+- [ ] get_ip_set
+- [ ] get_master_account
+- [ ] get_member_detectors
+- [ ] get_members
+- [ ] get_threat_intel_set
+- [ ] get_usage_statistics
+- [ ] invite_members
+- [X] list_detectors
+
+ The MaxResults and NextToken-parameter have not yet been implemented.
+
+
+- [ ] list_filters
+- [ ] list_findings
+- [ ] list_invitations
+- [ ] list_ip_sets
+- [ ] list_members
+- [ ] list_organization_admin_accounts
+- [ ] list_publishing_destinations
+- [ ] list_tags_for_resource
+- [ ] list_threat_intel_sets
+- [ ] start_monitoring_members
+- [ ] stop_monitoring_members
+- [ ] tag_resource
+- [ ] unarchive_findings
+- [ ] untag_resource
+- [ ] update_detector
+- [ ] update_filter
+- [ ] update_findings_feedback
+- [ ] update_ip_set
+- [ ] update_member_detectors
+- [ ] update_organization_configuration
+- [ ] update_publishing_destination
+- [ ] update_threat_intel_set
+
diff --git a/moto/__init__.py b/moto/__init__.py
index b5879e4c0..f7191f1ea 100644
--- a/moto/__init__.py
+++ b/moto/__init__.py
@@ -95,6 +95,7 @@ mock_forecast = lazy_load(".forecast", "mock_forecast")
mock_glacier = lazy_load(".glacier", "mock_glacier")
mock_glacier_deprecated = lazy_load(".glacier", "mock_glacier_deprecated")
mock_glue = lazy_load(".glue", "mock_glue")
+mock_guardduty = lazy_load(".guardduty", "mock_guardduty")
mock_iam = lazy_load(".iam", "mock_iam")
mock_iam_deprecated = lazy_load(".iam", "mock_iam_deprecated")
mock_iot = lazy_load(".iot", "mock_iot")
@@ -198,7 +199,6 @@ class MockAll(ContextDecorator):
mock_all = MockAll
-
# import logging
# logging.getLogger('boto').setLevel(logging.CRITICAL)
diff --git a/moto/backend_index.py b/moto/backend_index.py
index b85b7e3a2..3fed47bfb 100644
--- a/moto/backend_index.py
+++ b/moto/backend_index.py
@@ -66,6 +66,7 @@ backend_url_patterns = [
("forecast", re.compile("https?://forecast\\.(.+)\\.amazonaws\\.com")),
("glacier", re.compile("https?://glacier\\.(.+)\\.amazonaws.com")),
("glue", re.compile("https?://glue\\.(.+)\\.amazonaws\\.com")),
+ ("guardduty", re.compile("https?://guardduty\\.(.+)\\.amazonaws\\.com")),
("iam", re.compile("https?://iam\\.(.*\\.)?amazonaws\\.com")),
("iot", re.compile("https?://iot\\.(.+)\\.amazonaws\\.com")),
("iot-data", re.compile("https?://data\\.iot\\.(.+)\\.amazonaws.com")),
diff --git a/moto/guardduty/__init__.py b/moto/guardduty/__init__.py
new file mode 100644
index 000000000..68e09dedd
--- /dev/null
+++ b/moto/guardduty/__init__.py
@@ -0,0 +1,7 @@
+from __future__ import unicode_literals
+
+from .models import guardduty_backends
+from ..core.models import base_decorator
+
+guardduty_backend = guardduty_backends["us-east-1"]
+mock_guardduty = base_decorator(guardduty_backends)
diff --git a/moto/guardduty/models.py b/moto/guardduty/models.py
new file mode 100644
index 000000000..38518e313
--- /dev/null
+++ b/moto/guardduty/models.py
@@ -0,0 +1,79 @@
+from __future__ import unicode_literals
+from boto3 import Session
+from moto.core import BaseBackend, BaseModel
+from datetime import datetime
+from uuid import uuid4
+
+
+class GuardDutyBackend(BaseBackend):
+ def __init__(self, region_name=None):
+ super(GuardDutyBackend, self).__init__()
+ self.region_name = region_name
+ self.detectors = {}
+
+ def reset(self):
+ region_name = self.region_name
+ self.__dict__ = {}
+ self.__init__(region_name)
+
+ def create_detector(
+ self, enable, client_token, finding_publishing_frequency, data_sources, tags
+ ):
+ if finding_publishing_frequency not in [
+ "FIFTEEN_MINUTES",
+ "ONE_HOUR",
+ "SIX_HOURS",
+ ]:
+ finding_publishing_frequency = "SIX_HOURS"
+
+ service_role = "AWSServiceRoleForAmazonGuardDuty"
+ detector = Detector(
+ self,
+ datetime.now,
+ finding_publishing_frequency,
+ service_role,
+ enable,
+ data_sources,
+ tags,
+ )
+ self.detectors[detector.id] = detector
+ return detector.id
+
+ def list_detectors(self):
+ """
+ The MaxResults and NextToken-parameter have not yet been implemented.
+ """
+ detectorids = []
+ for detector in self.detectors:
+ detectorids.append(self.detectors[detector].id)
+ return detectorids
+
+
+class Detector(BaseModel):
+ def __init__(
+ self,
+ created_at,
+ finding_publish_freq,
+ service_role,
+ status,
+ updated_at,
+ datasources,
+ tags,
+ ):
+ self.id = str(uuid4())
+ self.created_at = created_at
+ self.finding_publish_freq = finding_publish_freq
+ self.service_role = service_role
+ self.status = status
+ self.updated_at = updated_at
+ self.datasources = datasources
+ self.tags = tags
+
+
+guardduty_backends = {}
+for region in Session().get_available_regions("guardduty"):
+ guardduty_backends[region] = GuardDutyBackend()
+for region in Session().get_available_regions("guardduty", partition_name="aws-us-gov"):
+ guardduty_backends[region] = GuardDutyBackend()
+for region in Session().get_available_regions("guardduty", partition_name="aws-cn"):
+ guardduty_backends[region] = GuardDutyBackend()
diff --git a/moto/guardduty/responses.py b/moto/guardduty/responses.py
new file mode 100644
index 000000000..77d55c384
--- /dev/null
+++ b/moto/guardduty/responses.py
@@ -0,0 +1,39 @@
+from __future__ import unicode_literals
+from moto.core.responses import BaseResponse
+from .models import guardduty_backends
+import json
+
+
+class GuardDutyResponse(BaseResponse):
+ SERVICE_NAME = "guardduty"
+
+ @property
+ def guardduty_backend(self):
+ return guardduty_backends[self.region]
+
+ def detector(self, request, full_url, headers):
+ self.setup_class(request, full_url, headers)
+ if request.method == "POST":
+ return self.create_detector()
+ elif request.method == "GET":
+ return self.list_detectors()
+ else:
+ return 404, {}, ""
+
+ def create_detector(self):
+ enable = self._get_param("enable")
+ client_token = self._get_param("clientToken")
+ finding_publishing_frequency = self._get_param("findingPublishingFrequency")
+ data_sources = self._get_param("dataSources")
+ tags = self._get_param("tags")
+
+ detector_id = self.guardduty_backend.create_detector(
+ enable, client_token, finding_publishing_frequency, data_sources, tags
+ )
+
+ return 200, {}, json.dumps(dict(detectorId=detector_id))
+
+ def list_detectors(self):
+ detector_ids = self.guardduty_backend.list_detectors()
+
+ return 200, {}, json.dumps({"detectorIds": detector_ids})
diff --git a/moto/guardduty/urls.py b/moto/guardduty/urls.py
new file mode 100644
index 000000000..6012d35c9
--- /dev/null
+++ b/moto/guardduty/urls.py
@@ -0,0 +1,13 @@
+from __future__ import unicode_literals
+from .responses import GuardDutyResponse
+
+response = GuardDutyResponse()
+
+url_bases = [
+ "https?://guardduty\\.(.+)\\.amazonaws\\.com",
+]
+
+
+url_paths = {
+ "{0}/detector$": response.detector,
+}
diff --git a/tests/test_guardduty/__init__.py b/tests/test_guardduty/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_guardduty/test_guardduty.py b/tests/test_guardduty/test_guardduty.py
new file mode 100644
index 000000000..72ff4c9fb
--- /dev/null
+++ b/tests/test_guardduty/test_guardduty.py
@@ -0,0 +1,51 @@
+import boto3
+import sure # noqa # pylint: disable=unused-import
+
+from moto import mock_guardduty
+
+
+@mock_guardduty
+def test_create_detector():
+ client = boto3.client("guardduty", region_name="us-east-1")
+ response = client.create_detector(
+ Enable=True,
+ ClientToken="745645734574758463758",
+ FindingPublishingFrequency="ONE_HOUR",
+ DataSources={"S3Logs": {"Enable": True}},
+ Tags={},
+ )
+ response.should.have.key("DetectorId")
+ response["DetectorId"].shouldnt.equal(None)
+
+
+@mock_guardduty
+def test_create_detector_with_minimal_params():
+ client = boto3.client("guardduty", region_name="us-east-1")
+ response = client.create_detector(Enable=True)
+ response.should.have.key("DetectorId")
+ response["DetectorId"].shouldnt.equal(None)
+
+
+@mock_guardduty
+def test_list_detectors_initial():
+ client = boto3.client("guardduty", region_name="us-east-1")
+
+ response = client.list_detectors()
+ response.should.have.key("DetectorIds").equals([])
+
+
+@mock_guardduty
+def test_list_detectors():
+ client = boto3.client("guardduty", region_name="us-east-1")
+ d1 = client.create_detector(
+ Enable=True,
+ ClientToken="745645734574758463758",
+ FindingPublishingFrequency="ONE_HOUR",
+ DataSources={"S3Logs": {"Enable": True}},
+ Tags={},
+ )["DetectorId"]
+ d2 = client.create_detector(Enable=False,)["DetectorId"]
+
+ response = client.list_detectors()
+ response.should.have.key("DetectorIds")
+ set(response["DetectorIds"]).should.equal({d1, d2})
diff --git a/tests/test_guardduty/test_server.py b/tests/test_guardduty/test_server.py
new file mode 100644
index 000000000..4a09532fa
--- /dev/null
+++ b/tests/test_guardduty/test_server.py
@@ -0,0 +1,14 @@
+import json
+import sure # noqa # pylint: disable=unused-import
+
+import moto.server as server
+
+
+def test_create_without_enable_option():
+ backend = server.create_backend_app("guardduty")
+ test_client = backend.test_client()
+
+ body = {"enable": "True"}
+ response = test_client.post("/detector", data=json.dumps(body))
+ response.status_code.should.equal(200)
+ json.loads(response.data).should.have.key("detectorId")