diff --git a/aws/internal/service/guardduty/waiter/waiter.go b/aws/internal/service/guardduty/waiter/waiter.go index 85972523476..bf56332c4d9 100644 --- a/aws/internal/service/guardduty/waiter/waiter.go +++ b/aws/internal/service/guardduty/waiter/waiter.go @@ -3,6 +3,7 @@ package waiter import ( "time" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/guardduty" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -14,12 +15,19 @@ const ( // Maximum amount of time to wait for an AdminAccount to return NotFound AdminAccountNotFoundTimeout = 5 * time.Minute + // Maximum amount of time to wait for a PublishingDestination to return Publishing + PublishingDestinationCreatedTimeout = 5 * time.Minute + PublishingDestinationCreatedMinTimeout = 3 * time.Second + // Maximum amount of time to wait for membership to propagate // When removing Organization Admin Accounts, there is eventual // consistency even after the account is no longer listed. // Reference error message: // BadRequestException: The request is rejected because the current account cannot delete detector while it has invited or associated members. MembershipPropagationTimeout = 2 * time.Minute + + // Constants not currently provided by the AWS Go SDK + guardDutyPublishingStatusFailed = "FAILED" ) // AdminAccountEnabled waits for an AdminAccount to return Enabled @@ -57,3 +65,35 @@ func AdminAccountNotFound(conn *guardduty.GuardDuty, adminAccountID string) (*gu return nil, err } + +// PublishingDestinationCreated waits for GuardDuty to return Publishing +func PublishingDestinationCreated(conn *guardduty.GuardDuty, destinationID, detectorID string) (*guardduty.CreatePublishingDestinationOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{guardduty.PublishingStatusPendingVerification}, + Target: []string{guardduty.PublishingStatusPublishing}, + Refresh: guardDutyPublishingDestinationRefreshStatusFunc(conn, destinationID, detectorID), + Timeout: PublishingDestinationCreatedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*guardduty.CreatePublishingDestinationOutput); ok { + return v, err + } + + return nil, err +} + +func guardDutyPublishingDestinationRefreshStatusFunc(conn *guardduty.GuardDuty, destinationID, detectorID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &guardduty.DescribePublishingDestinationInput{ + DetectorId: aws.String(detectorID), + DestinationId: aws.String(destinationID), + } + resp, err := conn.DescribePublishingDestination(input) + if err != nil { + return nil, guardDutyPublishingStatusFailed, err + } + return resp, aws.StringValue(resp.Status), nil + } +} diff --git a/aws/provider.go b/aws/provider.go index 550b235a5dd..a1b76404f1c 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -626,6 +626,7 @@ func Provider() *schema.Provider { "aws_glue_trigger": resourceAwsGlueTrigger(), "aws_glue_workflow": resourceAwsGlueWorkflow(), "aws_guardduty_detector": resourceAwsGuardDutyDetector(), + "aws_guardduty_publishing_destination": resourceAwsGuardDutyPublishingDestination(), "aws_guardduty_invite_accepter": resourceAwsGuardDutyInviteAccepter(), "aws_guardduty_ipset": resourceAwsGuardDutyIpset(), "aws_guardduty_member": resourceAwsGuardDutyMember(), diff --git a/aws/resource_aws_guardduty_detector_test.go b/aws/resource_aws_guardduty_detector_test.go index 558318f0427..4cf851a7a35 100644 --- a/aws/resource_aws_guardduty_detector_test.go +++ b/aws/resource_aws_guardduty_detector_test.go @@ -15,8 +15,9 @@ import ( func init() { resource.AddTestSweepers("aws_guardduty_detector", &resource.Sweeper{ - Name: "aws_guardduty_detector", - F: testSweepGuarddutyDetectors, + Name: "aws_guardduty_detector", + F: testSweepGuarddutyDetectors, + Dependencies: []string{"aws_guardduty_publishing_destination"}, }) } diff --git a/aws/resource_aws_guardduty_publishing_destination.go b/aws/resource_aws_guardduty_publishing_destination.go new file mode 100644 index 00000000000..df5a3ae814f --- /dev/null +++ b/aws/resource_aws_guardduty_publishing_destination.go @@ -0,0 +1,181 @@ +package aws + +import ( + "fmt" + "log" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/guardduty" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/guardduty/waiter" +) + +func resourceAwsGuardDutyPublishingDestination() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsGuardDutyPublishingDestinationCreate, + Read: resourceAwsGuardDutyPublishingDestinationRead, + Update: resourceAwsGuardDutyPublishingDestinationUpdate, + Delete: resourceAwsGuardDutyPublishingDestinationDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "detector_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "destination_type": { + Type: schema.TypeString, + Optional: true, + Default: guardduty.DestinationTypeS3, + ValidateFunc: validation.StringInSlice([]string{ + guardduty.DestinationTypeS3, + }, false), + }, + "destination_arn": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateArn, + }, + "kms_key_arn": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateArn, + }, + }, + } +} + +func resourceAwsGuardDutyPublishingDestinationCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).guarddutyconn + + detectorID := d.Get("detector_id").(string) + input := guardduty.CreatePublishingDestinationInput{ + DetectorId: aws.String(detectorID), + DestinationProperties: &guardduty.DestinationProperties{ + DestinationArn: aws.String(d.Get("destination_arn").(string)), + KmsKeyArn: aws.String(d.Get("kms_key_arn").(string)), + }, + DestinationType: aws.String(d.Get("destination_type").(string)), + } + + log.Printf("[DEBUG] Creating GuardDuty publishing destination: %s", input) + output, err := conn.CreatePublishingDestination(&input) + if err != nil { + return fmt.Errorf("Creating GuardDuty publishing destination failed: %w", err) + } + + _, err = waiter.PublishingDestinationCreated(conn, *output.DestinationId, detectorID) + + if err != nil { + return fmt.Errorf("Error waiting for GuardDuty PublishingDestination status to be \"%s\": %w", + guardduty.PublishingStatusPublishing, err) + } + + d.SetId(fmt.Sprintf("%s:%s", d.Get("detector_id"), aws.StringValue(output.DestinationId))) + + return resourceAwsGuardDutyPublishingDestinationRead(d, meta) +} + +func resourceAwsGuardDutyPublishingDestinationRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).guarddutyconn + + destinationId, detectorId, errStateRead := decodeGuardDutyPublishDestinationID(d.Id()) + + if errStateRead != nil { + return errStateRead + } + + input := &guardduty.DescribePublishingDestinationInput{ + DetectorId: aws.String(detectorId), + DestinationId: aws.String(destinationId), + } + + log.Printf("[DEBUG] Reading GuardDuty publishing destination: %s", input) + gdo, err := conn.DescribePublishingDestination(input) + if err != nil { + if isAWSErr(err, guardduty.ErrCodeBadRequestException, "The request is rejected because the one or more input parameters have invalid values.") { + log.Printf("[WARN] GuardDuty publishing destination: %q not found, removing from state", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("Reading GuardDuty publishing destination: '%s' failed: %w", d.Id(), err) + } + + d.Set("detector_id", detectorId) + d.Set("destination_type", gdo.DestinationType) + d.Set("kms_key_arn", gdo.DestinationProperties.KmsKeyArn) + d.Set("destination_arn", gdo.DestinationProperties.DestinationArn) + return nil +} + +func resourceAwsGuardDutyPublishingDestinationUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).guarddutyconn + + destinationId, detectorId, errStateRead := decodeGuardDutyPublishDestinationID(d.Id()) + + if errStateRead != nil { + return errStateRead + } + + input := guardduty.UpdatePublishingDestinationInput{ + DestinationId: aws.String(destinationId), + DetectorId: aws.String(detectorId), + DestinationProperties: &guardduty.DestinationProperties{ + DestinationArn: aws.String(d.Get("destination_arn").(string)), + KmsKeyArn: aws.String(d.Get("kms_key_arn").(string)), + }, + } + + log.Printf("[DEBUG] Update GuardDuty publishing destination: %s", input) + _, err := conn.UpdatePublishingDestination(&input) + if err != nil { + return fmt.Errorf("Updating GuardDuty publishing destination '%s' failed: %w", d.Id(), err) + } + + return resourceAwsGuardDutyPublishingDestinationRead(d, meta) +} + +func resourceAwsGuardDutyPublishingDestinationDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).guarddutyconn + + destinationId, detectorId, errStateRead := decodeGuardDutyPublishDestinationID(d.Id()) + + if errStateRead != nil { + return errStateRead + } + + input := guardduty.DeletePublishingDestinationInput{ + DestinationId: aws.String(destinationId), + DetectorId: aws.String(detectorId), + } + + log.Printf("[DEBUG] Delete GuardDuty publishing destination: %s", input) + _, err := conn.DeletePublishingDestination(&input) + + if isAWSErr(err, guardduty.ErrCodeBadRequestException, "") { + return nil + } + + if err != nil { + return fmt.Errorf("Deleting GuardDuty publishing destination '%s' failed: %w", d.Id(), err) + } + + return nil +} + +func decodeGuardDutyPublishDestinationID(id string) (destinationID, detectorID string, err error) { + parts := strings.Split(id, ":") + if len(parts) != 2 { + err = fmt.Errorf("GuardDuty Publishing Destination ID must be of the form :, was provided: %s", id) + return + } + destinationID = parts[1] + detectorID = parts[0] + return +} diff --git a/aws/resource_aws_guardduty_publishing_destination_test.go b/aws/resource_aws_guardduty_publishing_destination_test.go new file mode 100644 index 00000000000..665eb2bc3b2 --- /dev/null +++ b/aws/resource_aws_guardduty_publishing_destination_test.go @@ -0,0 +1,292 @@ +package aws + +import ( + "fmt" + "log" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/guardduty" + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func init() { + resource.AddTestSweepers("aws_guardduty_publishing_destination", &resource.Sweeper{ + Name: "aws_guardduty_publishing_destination", + F: testSweepGuarddutyPublishingDestinations, + }) +} + +func testSweepGuarddutyPublishingDestinations(region string) error { + client, err := sharedClientForRegion(region) + + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + + conn := client.(*AWSClient).guarddutyconn + var sweeperErrs *multierror.Error + + detect_input := &guardduty.ListDetectorsInput{} + + err = conn.ListDetectorsPages(detect_input, func(page *guardduty.ListDetectorsOutput, lastPage bool) bool { + for _, detectorID := range page.DetectorIds { + list_input := &guardduty.ListPublishingDestinationsInput{ + DetectorId: detectorID, + } + + err = conn.ListPublishingDestinationsPages(list_input, func(page *guardduty.ListPublishingDestinationsOutput, lastPage bool) bool { + for _, destination_element := range page.Destinations { + input := &guardduty.DeletePublishingDestinationInput{ + DestinationId: destination_element.DestinationId, + DetectorId: detectorID, + } + + log.Printf("[INFO] Deleting GuardDuty Publish Destination: %s", *destination_element.DestinationId) + _, err := conn.DeletePublishingDestination(input) + + if err != nil { + sweeperErr := fmt.Errorf("error deleting GuardDuty Pusblish Destination (%s): %w", *destination_element.DestinationId, err) + log.Printf("[ERROR] %s", sweeperErr) + sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) + } + } + return !lastPage + }) + } + return !lastPage + }) + + if err != nil { + sweeperErr := fmt.Errorf("Error receiving Guardduty detectors for publish sweep : %w", err) + log.Printf("[ERROR] %s", sweeperErr) + sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) + } + + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping GuardDuty Publish Destination sweep for %s: %s", region, err) + return nil + } + + if err != nil { + return fmt.Errorf("error retrieving GuardDuty Publish Destinations: %s", err) + } + + return sweeperErrs.ErrorOrNil() +} + +func TestAccAwsGuardDutyPublishingDestination_basic(t *testing.T) { + resourceName := "aws_guardduty_publishing_destination.test" + bucketName := acctest.RandomWithPrefix("tf-acc-test") + detectorResourceName := "aws_guardduty_detector.test_gd" + bucketResourceName := "aws_s3_bucket.gd_bucket" + kmsKeyResourceName := "aws_kms_key.gd_key" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsGuardDutyPublishingDestinationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsGuardDutyPublishingDestinationConfig_basic(bucketName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsGuardDutyPublishingDestinationExists(resourceName), + resource.TestCheckResourceAttrPair(resourceName, "detector_id", detectorResourceName, "id"), + resource.TestCheckResourceAttrPair(resourceName, "destination_arn", bucketResourceName, "arn"), + resource.TestCheckResourceAttrPair(resourceName, "kms_key_arn", kmsKeyResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "destination_type", "S3")), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsGuardDutyPublishingDestination_disappears(t *testing.T) { + resourceName := "aws_guardduty_publishing_destination.test" + bucketName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsGuardDutyPublishingDestinationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsGuardDutyPublishingDestinationConfig_basic(bucketName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsGuardDutyPublishingDestinationExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsGuardDutyPublishingDestination(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccAwsGuardDutyPublishingDestinationConfig_basic(bucketName string) string { + return fmt.Sprintf(` + +data "aws_caller_identity" "current" {} + +data "aws_region" "current" {} + +data "aws_iam_policy_document" "bucket_pol" { + statement { + sid = "Allow PutObject" + actions = [ + "s3:PutObject" + ] + + resources = [ + "${aws_s3_bucket.gd_bucket.arn}/*" + ] + + principals { + type = "Service" + identifiers = ["guardduty.amazonaws.com"] + } + } + + statement { + sid = "Allow GetBucketLocation" + actions = [ + "s3:GetBucketLocation" + ] + + resources = [ + aws_s3_bucket.gd_bucket.arn + ] + + principals { + type = "Service" + identifiers = ["guardduty.amazonaws.com"] + } + } +} + +data "aws_iam_policy_document" "kms_pol" { + + statement { + sid = "Allow GuardDuty to encrypt findings" + actions = [ + "kms:GenerateDataKey" + ] + + resources = [ + "arn:aws:kms:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:key/*" + ] + + principals { + type = "Service" + identifiers = ["guardduty.amazonaws.com"] + } + } + + statement { + sid = "Allow all users to modify/delete key (test only)" + actions = [ + "kms:*" + ] + + resources = [ + "arn:aws:kms:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:key/*" + ] + + principals { + type = "AWS" + identifiers = [ "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" ] + } + } + +} + +resource "aws_guardduty_detector" "test_gd" { + enable = true +} + +resource "aws_s3_bucket" "gd_bucket" { + bucket = %[1]q + acl = "private" + force_destroy = true +} + +resource "aws_s3_bucket_policy" "gd_bucket_policy" { + bucket = aws_s3_bucket.gd_bucket.id + policy = data.aws_iam_policy_document.bucket_pol.json +} + +resource "aws_kms_key" "gd_key" { + description = "Temporary key for AccTest of TF" + deletion_window_in_days = 7 + policy = data.aws_iam_policy_document.kms_pol.json +} + +resource "aws_guardduty_publishing_destination" "test" { + detector_id = aws_guardduty_detector.test_gd.id + destination_arn = aws_s3_bucket.gd_bucket.arn + kms_key_arn = aws_kms_key.gd_key.arn + + depends_on = [ + aws_s3_bucket_policy.gd_bucket_policy, + ] +}`, bucketName) +} + +func testAccCheckAwsGuardDutyPublishingDestinationExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + destination_id, detector_id, err_state_read := decodeGuardDutyPublishDestinationID(rs.Primary.ID) + + if err_state_read != nil { + return err_state_read + } + + input := &guardduty.DescribePublishingDestinationInput{ + DetectorId: aws.String(detector_id), + DestinationId: aws.String(destination_id), + } + + conn := testAccProvider.Meta().(*AWSClient).guarddutyconn + _, err := conn.DescribePublishingDestination(input) + return err + } +} + +func testAccCheckAwsGuardDutyPublishingDestinationDestroy(s *terraform.State) error { + + conn := testAccProvider.Meta().(*AWSClient).guarddutyconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_guardduty_publishing_destination" { + continue + } + + destination_id, detector_id, err_state_read := decodeGuardDutyPublishDestinationID(rs.Primary.ID) + + if err_state_read != nil { + return err_state_read + } + + input := &guardduty.DescribePublishingDestinationInput{ + DetectorId: aws.String(detector_id), + DestinationId: aws.String(destination_id), + } + + _, err := conn.DescribePublishingDestination(input) + // Catch expected error. + if err == nil { + return fmt.Errorf("Resource still exists.") + } + } + return nil +} diff --git a/aws/resource_aws_guardduty_test.go b/aws/resource_aws_guardduty_test.go index 2c507424505..ff9ad1b0e80 100644 --- a/aws/resource_aws_guardduty_test.go +++ b/aws/resource_aws_guardduty_test.go @@ -36,6 +36,9 @@ func TestAccAWSGuardDuty_serial(t *testing.T) { "inviteDisassociate": testAccAwsGuardDutyMember_invite_disassociate, "invitationMessage": testAccAwsGuardDutyMember_invitationMessage, }, + "PublishingDestination": { + "basic": TestAccAwsGuardDutyPublishingDestination_basic, + }, } for group, m := range testCases { diff --git a/website/docs/r/guardduty_publishing_destination.html.markdown b/website/docs/r/guardduty_publishing_destination.html.markdown new file mode 100644 index 00000000000..24ba172f752 --- /dev/null +++ b/website/docs/r/guardduty_publishing_destination.html.markdown @@ -0,0 +1,147 @@ +--- +subcategory: "GuardDuty" +layout: aws +page_title: 'AWS: aws_guardduty_publishing_destination' +description: Provides a resource to manage a GuardDuty PublishingDestination +--- + +# Resource: aws_guardduty_publishing_destination + +Provides a resource to manage a GuardDuty PublishingDestination. Requires an existing GuardDuty Detector. + +## Example Usage + +```hcl +data "aws_caller_identity" "current" {} + +data "aws_region" "current" {} + +data "aws_iam_policy_document" "bucket_pol" { + statement { + sid = "Allow PutObject" + actions = [ + "s3:PutObject" + ] + + resources = [ + "${aws_s3_bucket.gd_bucket.arn}/*" + ] + + principals { + type = "Service" + identifiers = ["guardduty.amazonaws.com"] + } + } + + statement { + sid = "Allow GetBucketLocation" + actions = [ + "s3:GetBucketLocation" + ] + + resources = [ + aws_s3_bucket.gd_bucket.arn + ] + + principals { + type = "Service" + identifiers = ["guardduty.amazonaws.com"] + } + } +} + +data "aws_iam_policy_document" "kms_pol" { + + statement { + sid = "Allow GuardDuty to encrypt findings" + actions = [ + "kms:GenerateDataKey" + ] + + resources = [ + "arn:aws:kms:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:key/*" + ] + + principals { + type = "Service" + identifiers = ["guardduty.amazonaws.com"] + } + } + + statement { + sid = "Allow all users to modify/delete key (test only)" + actions = [ + "kms:*" + ] + + resources = [ + "arn:aws:kms:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:key/*" + ] + + principals { + type = "AWS" + identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"] + } + } + +} + +resource "aws_guardduty_detector" "test_gd" { + enable = true +} + +resource "aws_s3_bucket" "gd_bucket" { + bucket = "example" + acl = "private" + force_destroy = true +} + +resource "aws_s3_bucket_policy" "gd_bucket_policy" { + bucket = aws_s3_bucket.gd_bucket.id + policy = data.aws_iam_policy_document.bucket_pol.json +} + +resource "aws_kms_key" "gd_key" { + description = "Temporary key for AccTest of TF" + deletion_window_in_days = 7 + policy = data.aws_iam_policy_document.kms_pol.json +} + +resource "aws_guardduty_publishing_destination" "test" { + detector_id = aws_guardduty_detector.test_gd.id + destination_arn = aws_s3_bucket.gd_bucket.arn + kms_key_arn = aws_kms_key.gd_key.arn + + depends_on = [ + aws_s3_bucket_policy.gd_bucket_policy, + ] +} +``` + +~> **Note:** Please do not use this simple example for Bucket-Policy and KMS Key Policy in a production environment. It is much too open for such a use-case. Refer to the AWS documentation here: https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_exportfindings.html + +## Argument Reference + +The following arguments are supported: + +* `detector_id` - (Required) The detector ID of the GuardDuty. +* `destination_arn` - (Required) The bucket arn and prefix under which the findings get exported. Bucket-ARN is required, the prefix is optional and will be `AWSLogs/[Account-ID]/GuardDuty/[Region]/` if not provided +* `kms_key_arn` - (Required) The ARN of the KMS key used to encrypt GuardDuty findings. GuardDuty enforces this to be encrypted. +* `destination_type`- (Optional) Currently there is only "S3" available as destination type which is also the default value + + +~> **Note:** In case of missing permissions (S3 Bucket Policy _or_ KMS Key permissions) the resource will fail to create. If the permissions are changed after resource creation, this can be asked from the AWS API via the "DescribePublishingDestination" call (https://docs.aws.amazon.com/cli/latest/reference/guardduty/describe-publishing-destination.html). + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The ID of the GuardDuty PublishingDestination and the detector ID. Format: `:` + +## Import + +GuardDuty PublishingDestination can be imported using the the master GuardDuty detector ID and PublishingDestinationID, e.g. + +``` +$ terraform import aws_guardduty_publishing_destination.test a4b86f26fa42e7e7cf0d1c333ea77777:a4b86f27a0e464e4a7e0516d242f1234 +```