diff --git a/aws/config.go b/aws/config.go index 35b9cc1099b..0bd0c4cebaa 100644 --- a/aws/config.go +++ b/aws/config.go @@ -88,6 +88,7 @@ import ( "github.com/aws/aws-sdk-go/service/route53" "github.com/aws/aws-sdk-go/service/s3" "github.com/aws/aws-sdk-go/service/secretsmanager" + "github.com/aws/aws-sdk-go/service/securityhub" "github.com/aws/aws-sdk-go/service/servicecatalog" "github.com/aws/aws-sdk-go/service/servicediscovery" "github.com/aws/aws-sdk-go/service/ses" @@ -197,6 +198,7 @@ type AWSClient struct { autoscalingconn *autoscaling.AutoScaling s3conn *s3.S3 secretsmanagerconn *secretsmanager.SecretsManager + securityhubconn *securityhub.SecurityHub scconn *servicecatalog.ServiceCatalog sesConn *ses.SES simpledbconn *simpledb.SimpleDB @@ -567,6 +569,7 @@ func (c *Config) Client() (interface{}, error) { client.sdconn = servicediscovery.New(sess) client.sesConn = ses.New(sess) client.secretsmanagerconn = secretsmanager.New(sess) + client.securityhubconn = securityhub.New(sess) client.sfnconn = sfn.New(sess) client.snsconn = sns.New(awsSnsSess) client.sqsconn = sqs.New(awsSqsSess) diff --git a/aws/provider.go b/aws/provider.go index 398ce94e55b..20cd974d089 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -623,6 +623,12 @@ func Provider() terraform.ResourceProvider { "aws_network_interface_sg_attachment": resourceAwsNetworkInterfaceSGAttachment(), "aws_default_security_group": resourceAwsDefaultSecurityGroup(), "aws_security_group_rule": resourceAwsSecurityGroupRule(), + "aws_securityhub_account": resourceAwsSecurityHubAccount(), + "aws_securityhub_insight": resourceAwsSecurityHubInsight(), + "aws_securityhub_invite_accepter": resourceAwsSecurityHubInviteAccepter(), + "aws_securityhub_member": resourceAwsSecurityHubMember(), + "aws_securityhub_product_subscription": resourceAwsSecurityHubProductSubscription(), + "aws_securityhub_standards_subscription": resourceAwsSecurityHubStandardsSubscription(), "aws_servicecatalog_portfolio": resourceAwsServiceCatalogPortfolio(), "aws_service_discovery_private_dns_namespace": resourceAwsServiceDiscoveryPrivateDnsNamespace(), "aws_service_discovery_public_dns_namespace": resourceAwsServiceDiscoveryPublicDnsNamespace(), diff --git a/aws/resource_aws_securityhub_account.go b/aws/resource_aws_securityhub_account.go new file mode 100644 index 00000000000..ed41bcb3e88 --- /dev/null +++ b/aws/resource_aws_securityhub_account.go @@ -0,0 +1,68 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/service/securityhub" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsSecurityHubAccount() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsSecurityHubAccountCreate, + Read: resourceAwsSecurityHubAccountRead, + Delete: resourceAwsSecurityHubAccountDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{}, + } +} + +func resourceAwsSecurityHubAccountCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).securityhubconn + log.Print("[DEBUG] Enabling Security Hub for account") + + _, err := conn.EnableSecurityHub(&securityhub.EnableSecurityHubInput{}) + + if err != nil { + return fmt.Errorf("Error enabling Security Hub for account: %s", err) + } + + d.SetId("securityhub-account") + + return resourceAwsSecurityHubAccountRead(d, meta) +} + +func resourceAwsSecurityHubAccountRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).securityhubconn + + log.Printf("[DEBUG] Checking if Security Hub is enabled") + _, err := conn.GetEnabledStandards(&securityhub.GetEnabledStandardsInput{}) + + if err != nil { + // Can only read enabled standards if Security Hub is enabled + if isAWSErr(err, "InvalidAccessException", "not subscribed to AWS Security Hub") { + d.SetId("") + return nil + } + return fmt.Errorf("Error checking if Security Hub is enabled: %s", err) + } + + return nil +} + +func resourceAwsSecurityHubAccountDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).securityhubconn + log.Print("[DEBUG] Disabling Security Hub for account") + + _, err := conn.DisableSecurityHub(&securityhub.DisableSecurityHubInput{}) + + if err != nil { + return fmt.Errorf("Error disabling Security Hub for account: %s", err) + } + + return nil +} diff --git a/aws/resource_aws_securityhub_account_test.go b/aws/resource_aws_securityhub_account_test.go new file mode 100644 index 00000000000..a39bcf34b70 --- /dev/null +++ b/aws/resource_aws_securityhub_account_test.go @@ -0,0 +1,84 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/securityhub" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSSecurityHubAccount_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSecurityHubAccountDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSecurityHubAccountConfig(), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSecurityHubAccountExists("aws_securityhub_account.example"), + ), + }, + { + ResourceName: "aws_securityhub_account.example", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckAWSSecurityHubAccountExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + _, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := testAccProvider.Meta().(*AWSClient).securityhubconn + + _, err := conn.GetEnabledStandards(&securityhub.GetEnabledStandardsInput{}) + + if err != nil { + // Can only read enabled standards if Security Hub is enabled + if isAWSErr(err, "InvalidAccessException", "not subscribed to AWS Security Hub") { + return fmt.Errorf("Security Hub account not found") + } + return err + } + + return nil + } +} + +func testAccCheckAWSSecurityHubAccountDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).securityhubconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_securityhub_account" { + continue + } + + _, err := conn.GetEnabledStandards(&securityhub.GetEnabledStandardsInput{}) + + if err != nil { + // Can only read enabled standards if Security Hub is enabled + if isAWSErr(err, "InvalidAccessException", "not subscribed to AWS Security Hub") { + return nil + } + return err + } + + return fmt.Errorf("Security Hub account still exists") + } + + return nil +} + +func testAccAWSSecurityHubAccountConfig() string { + return ` +resource "aws_securityhub_account" "example" {} +` +} diff --git a/aws/resource_aws_securityhub_insight.go b/aws/resource_aws_securityhub_insight.go new file mode 100644 index 00000000000..a8987748c4c --- /dev/null +++ b/aws/resource_aws_securityhub_insight.go @@ -0,0 +1,650 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/securityhub" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" +) + +const ( + SecurityHubFilterComparisonEquals = "EQUALS" + SecurityHubFilterComparisonContains = "CONTAINS" + SecurityHubFilterComparisonPrefix = "PREFIX" +) + +const ( + SecurityHubFilterDateRangeUnitDays = "DAYS" +) + +func resourceAwsSecurityHubInsight() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsSecurityHubInsightCreate, + Read: resourceAwsSecurityHubInsightRead, + Update: resourceAwsSecurityHubInsightUpdate, + Delete: resourceAwsSecurityHubInsightDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "group_by_attribute": { + Type: schema.TypeString, + Required: true, + }, + "filter": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "product_arn": securityHubStringFilterSchema(), + "aws_account_id": securityHubStringFilterSchema(), + "id": securityHubStringFilterSchema(), + "generator_id": securityHubStringFilterSchema(), + "type": securityHubStringFilterSchema(), + "first_observed_at": securityHubDateFilterSchema(), + "last_observed_at": securityHubDateFilterSchema(), + "created_at": securityHubDateFilterSchema(), + "updated_at": securityHubDateFilterSchema(), + "severity_product": securityHubNumberFilterSchema(), + "severity_normalized": securityHubNumberFilterSchema(), + "severity_label": securityHubStringFilterSchema(), + "confidence": securityHubNumberFilterSchema(), + "criticality": securityHubNumberFilterSchema(), + "title": securityHubStringFilterSchema(), + "description": securityHubStringFilterSchema(), + "recommendation_text": securityHubStringFilterSchema(), + "source_url": securityHubStringFilterSchema(), + "product_fields": securityHubMapFilterSchema(), + "product_name": securityHubStringFilterSchema(), + "company_name": securityHubStringFilterSchema(), + "user_defined_fields": securityHubMapFilterSchema(), + "malware_name": securityHubStringFilterSchema(), + "malware_type": securityHubStringFilterSchema(), + "malware_path": securityHubStringFilterSchema(), + "malware_state": securityHubStringFilterSchema(), + "network_direction": securityHubStringFilterSchema(), + "network_protocol": securityHubStringFilterSchema(), + "network_source_ip_v4": securityHubIpFilterSchema(), + "network_source_ip_v6": securityHubIpFilterSchema(), + "network_source_port": securityHubNumberFilterSchema(), + "network_source_domain": securityHubStringFilterSchema(), + "network_source_mac": securityHubStringFilterSchema(), + "network_destination_ip_v4": securityHubIpFilterSchema(), + "network_destination_ip_v6": securityHubIpFilterSchema(), + "network_destination_port": securityHubNumberFilterSchema(), + "network_destination_domain": securityHubStringFilterSchema(), + "process_name": securityHubStringFilterSchema(), + "process_path": securityHubStringFilterSchema(), + "process_pid": securityHubNumberFilterSchema(), + "process_parent_pid": securityHubNumberFilterSchema(), + "process_launched_at": securityHubDateFilterSchema(), + "process_terminated_at": securityHubDateFilterSchema(), + "threat_intel_indicator_type": securityHubStringFilterSchema(), + "threat_intel_indicator_value": securityHubStringFilterSchema(), + "threat_intel_indicator_category": securityHubStringFilterSchema(), + "threat_intel_indicator_last_observed_at": securityHubDateFilterSchema(), + "threat_intel_indicator_source": securityHubStringFilterSchema(), + "threat_intel_indicator_source_url": securityHubStringFilterSchema(), + "resource_type": securityHubStringFilterSchema(), + "resource_id": securityHubStringFilterSchema(), + "resource_partition": securityHubStringFilterSchema(), + "resource_region": securityHubStringFilterSchema(), + "resource_tags": securityHubMapFilterSchema(), + "resource_aws_ec2_instance_type": securityHubStringFilterSchema(), + "resource_aws_ec2_instance_image_id": securityHubStringFilterSchema(), + "resource_aws_ec2_instance_ip_v4_addresses": securityHubIpFilterSchema(), + "resource_aws_ec2_instance_ip_v6_addresses": securityHubIpFilterSchema(), + "resource_aws_ec2_instance_key_name": securityHubStringFilterSchema(), + "resource_aws_ec2_instance_iam_instance_profile_arn": securityHubStringFilterSchema(), + "resource_aws_ec2_instance_vpc_id": securityHubStringFilterSchema(), + "resource_aws_ec2_instance_subnet_id": securityHubStringFilterSchema(), + "resource_aws_ec2_instance_launched_at": securityHubDateFilterSchema(), + "resource_aws_s3_bucket_owner_id": securityHubStringFilterSchema(), + "resource_aws_s3_bucket_owner_name": securityHubStringFilterSchema(), + "resource_aws_iam_access_key_user_name": securityHubStringFilterSchema(), + "resource_aws_iam_access_key_status": securityHubStringFilterSchema(), + "resource_aws_iam_access_key_created_at": securityHubDateFilterSchema(), + "resource_container_name": securityHubStringFilterSchema(), + "resource_container_image_id": securityHubStringFilterSchema(), + "resource_container_image_name": securityHubStringFilterSchema(), + "resource_container_launched_at": securityHubDateFilterSchema(), + "resource_details_other": securityHubMapFilterSchema(), + "compliance_status": securityHubStringFilterSchema(), + "verification_state": securityHubStringFilterSchema(), + "workflow_state": securityHubStringFilterSchema(), + "record_state": securityHubStringFilterSchema(), + "related_findings_product_arn": securityHubStringFilterSchema(), + "related_findings_id": securityHubStringFilterSchema(), + "note_text": securityHubStringFilterSchema(), + "note_updated_at": securityHubDateFilterSchema(), + "note_updated_by": securityHubStringFilterSchema(), + "keyword": securityHubKeywordFilterSchema(), + }, + }, + }, + }, + } +} + +func expandSecurityHubAwsSecurityFindingFilters(d *schema.ResourceData) *securityhub.AwsSecurityFindingFilters { + configs := d.Get("filter").([]interface{}) + data := configs[0].(map[string]interface{}) + + return expandSecurityHubAwsSecurityFindingFiltersData(data) +} + +func expandSecurityHubAwsSecurityFindingFiltersData(in map[string]interface{}) *securityhub.AwsSecurityFindingFilters { + filters := &securityhub.AwsSecurityFindingFilters{} + + filters.ProductArn = expandSecurityHubStringFilters(in["product_arn"].(*schema.Set).List()) + filters.AwsAccountId = expandSecurityHubStringFilters(in["aws_account_id"].(*schema.Set).List()) + filters.Id = expandSecurityHubStringFilters(in["id"].(*schema.Set).List()) + filters.GeneratorId = expandSecurityHubStringFilters(in["generator_id"].(*schema.Set).List()) + filters.Type = expandSecurityHubStringFilters(in["type"].(*schema.Set).List()) + filters.FirstObservedAt = expandSecurityHubDateFilters(in["first_observed_at"].(*schema.Set).List()) + filters.LastObservedAt = expandSecurityHubDateFilters(in["last_observed_at"].(*schema.Set).List()) + filters.CreatedAt = expandSecurityHubDateFilters(in["created_at"].(*schema.Set).List()) + filters.UpdatedAt = expandSecurityHubDateFilters(in["updated_at"].(*schema.Set).List()) + filters.SeverityProduct = expandSecurityHubNumberFilters(in["severity_product"].(*schema.Set).List()) + filters.SeverityNormalized = expandSecurityHubNumberFilters(in["severity_normalized"].(*schema.Set).List()) + filters.SeverityLabel = expandSecurityHubStringFilters(in["severity_label"].(*schema.Set).List()) + filters.Confidence = expandSecurityHubNumberFilters(in["confidence"].(*schema.Set).List()) + filters.Criticality = expandSecurityHubNumberFilters(in["criticality"].(*schema.Set).List()) + filters.Title = expandSecurityHubStringFilters(in["title"].(*schema.Set).List()) + filters.Description = expandSecurityHubStringFilters(in["description"].(*schema.Set).List()) + filters.RecommendationText = expandSecurityHubStringFilters(in["recommendation_text"].(*schema.Set).List()) + filters.SourceUrl = expandSecurityHubStringFilters(in["source_url"].(*schema.Set).List()) + filters.ProductFields = expandSecurityHubMapFilters(in["product_fields"].(*schema.Set).List()) + filters.ProductName = expandSecurityHubStringFilters(in["product_name"].(*schema.Set).List()) + filters.CompanyName = expandSecurityHubStringFilters(in["company_name"].(*schema.Set).List()) + filters.UserDefinedFields = expandSecurityHubMapFilters(in["user_defined_fields"].(*schema.Set).List()) + filters.MalwareName = expandSecurityHubStringFilters(in["malware_name"].(*schema.Set).List()) + filters.MalwareType = expandSecurityHubStringFilters(in["malware_type"].(*schema.Set).List()) + filters.MalwarePath = expandSecurityHubStringFilters(in["malware_path"].(*schema.Set).List()) + filters.MalwareState = expandSecurityHubStringFilters(in["malware_state"].(*schema.Set).List()) + filters.NetworkDirection = expandSecurityHubStringFilters(in["network_direction"].(*schema.Set).List()) + filters.NetworkProtocol = expandSecurityHubStringFilters(in["network_protocol"].(*schema.Set).List()) + filters.NetworkSourceIpV4 = expandSecurityHubIpFilters(in["network_source_ip_v4"].(*schema.Set).List()) + filters.NetworkSourceIpV6 = expandSecurityHubIpFilters(in["network_source_ip_v6"].(*schema.Set).List()) + filters.NetworkSourcePort = expandSecurityHubNumberFilters(in["network_source_port"].(*schema.Set).List()) + filters.NetworkSourceDomain = expandSecurityHubStringFilters(in["network_source_domain"].(*schema.Set).List()) + filters.NetworkSourceMac = expandSecurityHubStringFilters(in["network_source_mac"].(*schema.Set).List()) + filters.NetworkDestinationIpV4 = expandSecurityHubIpFilters(in["network_destination_ip_v4"].(*schema.Set).List()) + filters.NetworkDestinationIpV6 = expandSecurityHubIpFilters(in["network_destination_ip_v6"].(*schema.Set).List()) + filters.NetworkDestinationPort = expandSecurityHubNumberFilters(in["network_destination_port"].(*schema.Set).List()) + filters.NetworkDestinationDomain = expandSecurityHubStringFilters(in["network_destination_domain"].(*schema.Set).List()) + filters.ProcessName = expandSecurityHubStringFilters(in["process_name"].(*schema.Set).List()) + filters.ProcessPath = expandSecurityHubStringFilters(in["process_path"].(*schema.Set).List()) + filters.ProcessPid = expandSecurityHubNumberFilters(in["process_pid"].(*schema.Set).List()) + filters.ProcessParentPid = expandSecurityHubNumberFilters(in["process_parent_pid"].(*schema.Set).List()) + filters.ProcessLaunchedAt = expandSecurityHubDateFilters(in["process_launched_at"].(*schema.Set).List()) + filters.ProcessTerminatedAt = expandSecurityHubDateFilters(in["process_terminated_at"].(*schema.Set).List()) + filters.ThreatIntelIndicatorType = expandSecurityHubStringFilters(in["threat_intel_indicator_type"].(*schema.Set).List()) + filters.ThreatIntelIndicatorValue = expandSecurityHubStringFilters(in["threat_intel_indicator_value"].(*schema.Set).List()) + filters.ThreatIntelIndicatorCategory = expandSecurityHubStringFilters(in["threat_intel_indicator_category"].(*schema.Set).List()) + filters.ThreatIntelIndicatorLastObservedAt = expandSecurityHubDateFilters(in["threat_intel_indicator_last_observed_at"].(*schema.Set).List()) + filters.ThreatIntelIndicatorSource = expandSecurityHubStringFilters(in["threat_intel_indicator_source"].(*schema.Set).List()) + filters.ThreatIntelIndicatorSourceUrl = expandSecurityHubStringFilters(in["threat_intel_indicator_source_url"].(*schema.Set).List()) + filters.ResourceType = expandSecurityHubStringFilters(in["resource_type"].(*schema.Set).List()) + filters.ResourceId = expandSecurityHubStringFilters(in["resource_id"].(*schema.Set).List()) + filters.ResourcePartition = expandSecurityHubStringFilters(in["resource_partition"].(*schema.Set).List()) + filters.ResourceRegion = expandSecurityHubStringFilters(in["resource_region"].(*schema.Set).List()) + filters.ResourceTags = expandSecurityHubMapFilters(in["resource_tags"].(*schema.Set).List()) + filters.ResourceAwsEc2InstanceType = expandSecurityHubStringFilters(in["resource_aws_ec2_instance_type"].(*schema.Set).List()) + filters.ResourceAwsEc2InstanceImageId = expandSecurityHubStringFilters(in["resource_aws_ec2_instance_image_id"].(*schema.Set).List()) + filters.ResourceAwsEc2InstanceIpV4Addresses = expandSecurityHubIpFilters(in["resource_aws_ec2_instance_ip_v4_addresses"].(*schema.Set).List()) + filters.ResourceAwsEc2InstanceIpV6Addresses = expandSecurityHubIpFilters(in["resource_aws_ec2_instance_ip_v6_addresses"].(*schema.Set).List()) + filters.ResourceAwsEc2InstanceKeyName = expandSecurityHubStringFilters(in["resource_aws_ec2_instance_key_name"].(*schema.Set).List()) + filters.ResourceAwsEc2InstanceIamInstanceProfileArn = expandSecurityHubStringFilters(in["resource_aws_ec2_instance_iam_instance_profile_arn"].(*schema.Set).List()) + filters.ResourceAwsEc2InstanceVpcId = expandSecurityHubStringFilters(in["resource_aws_ec2_instance_vpc_id"].(*schema.Set).List()) + filters.ResourceAwsEc2InstanceSubnetId = expandSecurityHubStringFilters(in["resource_aws_ec2_instance_subnet_id"].(*schema.Set).List()) + filters.ResourceAwsEc2InstanceLaunchedAt = expandSecurityHubDateFilters(in["resource_aws_ec2_instance_launched_at"].(*schema.Set).List()) + filters.ResourceAwsS3BucketOwnerId = expandSecurityHubStringFilters(in["resource_aws_s3_bucket_owner_id"].(*schema.Set).List()) + filters.ResourceAwsS3BucketOwnerName = expandSecurityHubStringFilters(in["resource_aws_s3_bucket_owner_name"].(*schema.Set).List()) + filters.ResourceAwsIamAccessKeyUserName = expandSecurityHubStringFilters(in["resource_aws_iam_access_key_user_name"].(*schema.Set).List()) + filters.ResourceAwsIamAccessKeyStatus = expandSecurityHubStringFilters(in["resource_aws_iam_access_key_status"].(*schema.Set).List()) + filters.ResourceAwsIamAccessKeyCreatedAt = expandSecurityHubDateFilters(in["resource_aws_iam_access_key_created_at"].(*schema.Set).List()) + filters.ResourceContainerName = expandSecurityHubStringFilters(in["resource_container_name"].(*schema.Set).List()) + filters.ResourceContainerImageId = expandSecurityHubStringFilters(in["resource_container_image_id"].(*schema.Set).List()) + filters.ResourceContainerImageName = expandSecurityHubStringFilters(in["resource_container_image_name"].(*schema.Set).List()) + filters.ResourceContainerLaunchedAt = expandSecurityHubDateFilters(in["resource_container_launched_at"].(*schema.Set).List()) + filters.ResourceDetailsOther = expandSecurityHubMapFilters(in["resource_details_other"].(*schema.Set).List()) + filters.ComplianceStatus = expandSecurityHubStringFilters(in["compliance_status"].(*schema.Set).List()) + filters.VerificationState = expandSecurityHubStringFilters(in["verification_state"].(*schema.Set).List()) + filters.WorkflowState = expandSecurityHubStringFilters(in["workflow_state"].(*schema.Set).List()) + filters.RecordState = expandSecurityHubStringFilters(in["record_state"].(*schema.Set).List()) + filters.RelatedFindingsProductArn = expandSecurityHubStringFilters(in["related_findings_product_arn"].(*schema.Set).List()) + filters.RelatedFindingsId = expandSecurityHubStringFilters(in["related_findings_id"].(*schema.Set).List()) + filters.NoteText = expandSecurityHubStringFilters(in["note_text"].(*schema.Set).List()) + filters.NoteUpdatedAt = expandSecurityHubDateFilters(in["note_updated_at"].(*schema.Set).List()) + filters.NoteUpdatedBy = expandSecurityHubStringFilters(in["note_updated_by"].(*schema.Set).List()) + filters.Keyword = expandSecurityHubKeywordFilters(in["keyword"].(*schema.Set).List()) + + return filters +} + +func flattenSecurityHubAwsSecurityFindingFilters(filters *securityhub.AwsSecurityFindingFilters) []map[string]interface{} { + m := make(map[string]interface{}, 1) + + if filters.ResourceAwsEc2InstanceIpV4Addresses != nil { + m["resource_aws_ec2_instance_ip_v4_addresses"] = flattenSecurityHubIpFilters(filters.ResourceAwsEc2InstanceIpV4Addresses) + } + + var out = make([]map[string]interface{}, 1, 1) + out[0] = m + + log.Printf("flattenSecurityHubAwsSecurityFindingFilters: %v", out) + return out +} + +func securityHubStringFilterSchema() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "comparison": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + SecurityHubFilterComparisonEquals, + SecurityHubFilterComparisonContains, + SecurityHubFilterComparisonPrefix, + }, false), + }, + "value": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + } +} + +func expandSecurityHubStringFilters(in []interface{}) []*securityhub.StringFilter { + if in == nil || len(in) == 0 { + return nil + } + + var out = make([]*securityhub.StringFilter, 0) + for _, mRaw := range in { + if mRaw == nil { + continue + } + m := mRaw.(map[string]interface{}) + filter := &securityhub.StringFilter{ + Comparison: aws.String(m["comparison"].(string)), + Value: aws.String(m["value"].(string)), + } + out = append(out, filter) + } + return out +} + +func securityHubNumberFilterSchema() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "eq": { + Type: schema.TypeFloat, + Optional: true, + }, + "gte": { + Type: schema.TypeFloat, + Optional: true, + }, + "lte": { + Type: schema.TypeFloat, + Optional: true, + }, + }, + }, + } +} + +func expandSecurityHubNumberFilters(in []interface{}) []*securityhub.NumberFilter { + if in == nil || len(in) == 0 { + return nil + } + + var out = make([]*securityhub.NumberFilter, 0) + for _, mRaw := range in { + if mRaw == nil { + continue + } + m := mRaw.(map[string]interface{}) + filter := &securityhub.NumberFilter{} + if m["eq"] != nil { + filter.Eq = aws.Float64(m["eq"].(float64)) + } + if m["gte"] != nil { + filter.Gte = aws.Float64(m["gte"].(float64)) + } + if m["lte"] != nil { + filter.Lte = aws.Float64(m["lte"].(float64)) + } + out = append(out, filter) + } + return out +} + +func securityHubDateFilterSchema() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "date_range": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "unit": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + SecurityHubFilterDateRangeUnitDays, + }, false), + }, + "value": { + Type: schema.TypeInt, + Optional: true, + }, + }, + }, + }, + "end": { + Type: schema.TypeString, + Optional: true, + }, + "start": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + } +} + +func expandSecurityHubDateFilters(in []interface{}) []*securityhub.DateFilter { + if in == nil || len(in) == 0 { + return nil + } + + var out = make([]*securityhub.DateFilter, 0) + for _, mRaw := range in { + if mRaw == nil { + continue + } + m := mRaw.(map[string]interface{}) + filter := &securityhub.DateFilter{} + if m["date_range"] != nil { + filter.DateRange = expandSecurityHubDateRange(m["date_range"].([]interface{})) + } + if m["end"] != nil { + filter.End = aws.String(m["end"].(string)) + } + if m["start"] != nil { + filter.Start = aws.String(m["start"].(string)) + } + out = append(out, filter) + } + return out +} + +func expandSecurityHubDateRange(in []interface{}) *securityhub.DateRange { + if in == nil || len(in) == 0 { + return nil + } + + m := in[0].(map[string]interface{}) + dateRange := &securityhub.DateRange{} + if m["unit"] != nil { + dateRange.Unit = aws.String(m["unit"].(string)) + } + if m["value"] != nil { + dateRange.Value = aws.Int64(int64(m["value"].(int))) + } + + return dateRange +} + +func securityHubKeywordFilterSchema() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "value": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + } +} + +func expandSecurityHubKeywordFilters(in []interface{}) []*securityhub.KeywordFilter { + if in == nil || len(in) == 0 { + return nil + } + + var out = make([]*securityhub.KeywordFilter, 0) + for _, mRaw := range in { + if mRaw == nil { + continue + } + m := mRaw.(map[string]interface{}) + filter := &securityhub.KeywordFilter{} + if m["value"] != nil { + filter.Value = aws.String(m["value"].(string)) + } + out = append(out, filter) + } + return out +} + +func securityHubIpFilterSchema() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "cidr": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + } +} + +func expandSecurityHubIpFilters(in []interface{}) []*securityhub.IpFilter { + if in == nil || len(in) == 0 { + return nil + } + + var out = make([]*securityhub.IpFilter, 0) + for _, mRaw := range in { + if mRaw == nil { + continue + } + m := mRaw.(map[string]interface{}) + filter := &securityhub.IpFilter{} + if m["cidr"] != nil { + filter.Cidr = aws.String(m["cidr"].(string)) + } + out = append(out, filter) + } + return out +} + +func flattenSecurityHubIpFilters(in []*securityhub.IpFilter) []map[string]interface{} { + var out = make([]map[string]interface{}, len(in), len(in)) + for i, v := range in { + m := make(map[string]interface{}) + if *v.Cidr != "" { + m["cidr"] = *v.Cidr + } + out[i] = m + } + log.Printf("flattenSecurityHubIpFilters: %v", out) + return out +} + +func securityHubMapFilterSchema() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "comparison": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + SecurityHubFilterComparisonContains, + }, false), + }, + "key": { + Type: schema.TypeString, + Optional: true, + }, + "value": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + } +} + +func expandSecurityHubMapFilters(in []interface{}) []*securityhub.MapFilter { + if in == nil || len(in) == 0 { + return nil + } + + var out = make([]*securityhub.MapFilter, 0) + for _, mRaw := range in { + if mRaw == nil { + continue + } + m := mRaw.(map[string]interface{}) + filter := &securityhub.MapFilter{} + if m["comparison"] != nil { + filter.Comparison = aws.String(m["comparison"].(string)) + } + if m["key"] != nil { + filter.Key = aws.String(m["key"].(string)) + } + if m["value"] != nil { + filter.Value = aws.String(m["value"].(string)) + } + out = append(out, filter) + } + return out +} + +func resourceAwsSecurityHubInsightCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).securityhubconn + + log.Printf("expandSecurityHubAwsSecurityFindingFilters: %v", d.Get("filter")) + + log.Print("[DEBUG] Enabling Security Hub insight") + + resp, err := conn.CreateInsight(&securityhub.CreateInsightInput{ + Name: aws.String(d.Get("name").(string)), + GroupByAttribute: aws.String(d.Get("group_by_attribute").(string)), + Filters: expandSecurityHubAwsSecurityFindingFilters(d), + }) + + if err != nil { + return fmt.Errorf("Error creating Security Hub insight: %s", err) + } + + d.SetId(*resp.InsightArn) + + return resourceAwsSecurityHubInsightRead(d, meta) +} + +func resourceAwsSecurityHubInsightRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).securityhubconn + + log.Printf("[DEBUG] Reading Security Hub insight %s", d.Id()) + resp, err := conn.GetInsights(&securityhub.GetInsightsInput{ + InsightArns: []*string{aws.String(d.Id())}, + }) + + if err != nil { + if isAWSErr(err, securityhub.ErrCodeResourceNotFoundException, "") { + log.Printf("[WARN] Security Hub insight (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("Error reading Security Hub insight %s: %s", d.Id(), err) + } + + insight := resp.Insights[0] + + if insight == nil { + log.Printf("[WARN] Security Hub insight (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + d.Set("name", insight.Name) + d.Set("group_by_attribute", insight.GroupByAttribute) + d.Set("filters", flattenSecurityHubAwsSecurityFindingFilters(insight.Filters)) + + return nil +} + +func resourceAwsSecurityHubInsightUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).securityhubconn + + log.Printf("expandSecurityHubAwsSecurityFindingFilters: %v", d.Get("filter")) + + log.Printf("[DEBUG] Updating Security Hub insight %s", d.Id()) + _, err := conn.UpdateInsight(&securityhub.UpdateInsightInput{ + InsightArn: aws.String(d.Id()), + Name: aws.String(d.Get("name").(string)), + GroupByAttribute: aws.String(d.Get("group_by_attribute").(string)), + Filters: expandSecurityHubAwsSecurityFindingFilters(d), + }) + + if err != nil { + return fmt.Errorf("Error updating Security Hub insight %s: %s", d.Id(), err) + } + + return nil +} + +func resourceAwsSecurityHubInsightDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).securityhubconn + log.Printf("[DEBUG] Deleting Security Hub insight %s", d.Id()) + + _, err := conn.DeleteInsight(&securityhub.DeleteInsightInput{ + InsightArn: aws.String(d.Id()), + }) + + if err != nil { + if !isAWSErr(err, securityhub.ErrCodeResourceNotFoundException, "") { + log.Printf("[WARN] Security Hub insight (%s) not found", d.Id()) + return nil + } + return fmt.Errorf("Error deleting Security Hub insight %s: %s", d.Id(), err) + } + + return nil +} diff --git a/aws/resource_aws_securityhub_invite_accepter.go b/aws/resource_aws_securityhub_invite_accepter.go new file mode 100644 index 00000000000..75097bfb67e --- /dev/null +++ b/aws/resource_aws_securityhub_invite_accepter.go @@ -0,0 +1,123 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/securityhub" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsSecurityHubInviteAccepter() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsSecurityHubInviteAccepterCreate, + Read: resourceAwsSecurityHubInviteAccepterRead, + Delete: resourceAwsSecurityHubInviteAccepterDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "master_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "invitation_id": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceAwsSecurityHubInviteAccepterCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).securityhubconn + log.Print("[DEBUG] Accepting Security Hub invitation") + + invitationId, err := resourceAwsSecurityHubInviteAccepterGetInvitationId(conn, d.Get("master_id").(string)) + + if err != nil { + return err + } + + _, err = conn.AcceptInvitation(&securityhub.AcceptInvitationInput{ + InvitationId: aws.String(invitationId), + MasterId: aws.String(d.Get("master_id").(string)), + }) + + if err != nil { + return fmt.Errorf("Error accepting Security Hub invitation: %s", err) + } + + d.SetId("securityhub-invitation-accepter") + + return resourceAwsSecurityHubInviteAccepterRead(d, meta) +} + +func resourceAwsSecurityHubInviteAccepterGetInvitationId(conn *securityhub.SecurityHub, masterId string) (string, error) { + log.Printf("[DEBUG] Getting InvitationId for MasterId %s", masterId) + + resp, err := conn.ListInvitations(&securityhub.ListInvitationsInput{}) + + if err != nil { + return "", fmt.Errorf("Error listing Security Hub invitations: %s", err) + } + + for _, invitation := range resp.Invitations { + log.Printf("[DEBUG] Invitation: %s", invitation) + if *invitation.AccountId == masterId { + return *invitation.InvitationId, nil + } + } + + return "", fmt.Errorf("Cannot find InvitationId for MasterId %s", masterId) +} + +func resourceAwsSecurityHubInviteAccepterRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).securityhubconn + log.Print("[DEBUG] Reading Security Hub master account") + + resp, err := conn.GetMasterAccount(&securityhub.GetMasterAccountInput{}) + + if err != nil { + if isAWSErr(err, securityhub.ErrCodeResourceNotFoundException, "") { + log.Print("[WARN] Security Hub master account not found, removing from state") + d.SetId("") + return nil + } + return err + } + + master := resp.Master + + if master == nil { + log.Print("[WARN] Security Hub master account not found, removing from state") + d.SetId("") + return nil + } + + d.Set("invitation_id", master.InvitationId) + d.Set("master_id", master.AccountId) + + return nil +} + +func resourceAwsSecurityHubInviteAccepterDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).securityhubconn + log.Print("[DEBUG] Disassociating from Security Hub master account") + + _, err := conn.DisassociateFromMasterAccount(&securityhub.DisassociateFromMasterAccountInput{}) + + if err != nil { + if isAWSErr(err, "BadRequestException", "The request is rejected because the current account is not associated to a master account") { + log.Print("[WARN] Security Hub account is not a member account") + return nil + } + return err + } + + return nil +} diff --git a/aws/resource_aws_securityhub_member.go b/aws/resource_aws_securityhub_member.go new file mode 100644 index 00000000000..913cebc4f99 --- /dev/null +++ b/aws/resource_aws_securityhub_member.go @@ -0,0 +1,192 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/securityhub" + "github.com/hashicorp/terraform/helper/schema" +) + +const ( + SecurityHubMemberStatusCreated = "Created" + SecurityHubMemberStatusInvited = "Invited" + SecurityHubMemberStatusAssociated = "Associated" + SecurityHubMemberStatusResigned = "Resigned" + SecurityHubMemberStatusRemoved = "Removed" +) + +func resourceAwsSecurityHubMember() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsSecurityHubMemberCreate, + Read: resourceAwsSecurityHubMemberRead, + Update: resourceAwsSecurityHubMemberUpdate, + Delete: resourceAwsSecurityHubMemberDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "account_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateAwsAccountId, + }, + "email": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "invite": { + Type: schema.TypeBool, + Optional: true, + }, + "master_id": { + Type: schema.TypeString, + Computed: true, + }, + "member_status": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceAwsSecurityHubMemberCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).securityhubconn + log.Printf("[DEBUG] Creating Security Hub member %s", d.Get("account_id").(string)) + + resp, err := conn.CreateMembers(&securityhub.CreateMembersInput{ + AccountDetails: []*securityhub.AccountDetails{ + { + AccountId: aws.String(d.Get("account_id").(string)), + Email: aws.String(d.Get("email").(string)), + }, + }, + }) + + if err != nil { + return fmt.Errorf("Error creating Security Hub member %s: %s", d.Get("account_id").(string), err) + } + + if len(resp.UnprocessedAccounts) > 0 { + return fmt.Errorf("Error creating Security Hub member %s: UnprocessedAccounts is not empty", d.Get("account_id").(string)) + } + + d.SetId(d.Get("account_id").(string)) + + if !d.Get("invite").(bool) { + return resourceAwsSecurityHubMemberRead(d, meta) + } + + log.Printf("[INFO] Inviting Security Hub member %s", d.Id()) + iresp, err := conn.InviteMembers(&securityhub.InviteMembersInput{ + AccountIds: []*string{aws.String(d.Get("account_id").(string))}, + }) + + if err != nil { + return fmt.Errorf("Error inviting Security Hub member %s: %s", d.Id(), err) + } + + if len(iresp.UnprocessedAccounts) > 0 { + return fmt.Errorf("Error inviting Security Hub member %s: UnprocessedAccounts is not empty", d.Id()) + } + + return resourceAwsSecurityHubMemberRead(d, meta) +} + +func resourceAwsSecurityHubMemberRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).securityhubconn + + log.Printf("[DEBUG] Reading Security Hub member %s", d.Id()) + resp, err := conn.GetMembers(&securityhub.GetMembersInput{ + AccountIds: []*string{aws.String(d.Id())}, + }) + + if err != nil { + if isAWSErr(err, securityhub.ErrCodeResourceNotFoundException, "") { + log.Printf("[WARN] Security Hub member (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + return err + } + + if len(resp.Members) == 0 { + log.Printf("[WARN] Security Hub member (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + member := resp.Members[0] + + d.Set("account_id", member.AccountId) + d.Set("email", member.Email) + d.Set("master_id", member.MasterId) + + status := aws.StringValue(member.MemberStatus) + d.Set("member_status", status) + + d.Set("invite", false) + if status == SecurityHubMemberStatusInvited || status == SecurityHubMemberStatusAssociated || status == SecurityHubMemberStatusRemoved { + d.Set("invite", true) + } + + return nil +} + +func resourceAwsSecurityHubMemberUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).securityhubconn + + if d.HasChange("invite") { + if d.Get("invite").(bool) { + log.Printf("[INFO] Inviting Security Hub member %s", d.Id()) + + resp, err := conn.InviteMembers(&securityhub.InviteMembersInput{ + AccountIds: []*string{aws.String(d.Get("account_id").(string))}, + }) + + if err != nil { + return fmt.Errorf("Error inviting Security Hub member %s: %s", d.Id(), err) + } + + if len(resp.UnprocessedAccounts) > 0 { + return fmt.Errorf("Error inviting Security Hub member %s: UnprocessedAccounts is not empty", d.Id()) + } + } else { + log.Printf("[INFO] Disassociating Security Hub member %s", d.Id()) + + _, err := conn.DisassociateMembers(&securityhub.DisassociateMembersInput{ + AccountIds: []*string{aws.String(d.Id())}, + }) + + if err != nil { + return fmt.Errorf("Error disassociating Security Hub member %s: %s", d.Id(), err) + } + } + } + + return resourceAwsSecurityHubMemberRead(d, meta) +} + +func resourceAwsSecurityHubMemberDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).securityhubconn + log.Printf("[DEBUG] Deleting Security Hub member: %s", d.Id()) + + _, err := conn.DeleteMembers(&securityhub.DeleteMembersInput{ + AccountIds: []*string{aws.String(d.Id())}, + }) + + if err != nil { + if isAWSErr(err, securityhub.ErrCodeResourceNotFoundException, "") { + log.Printf("[WARN] Security Hub member (%s) not found", d.Id()) + return nil + } + return fmt.Errorf("Error deleting Security Hub member %s: %s", d.Id(), err) + } + + return nil +} diff --git a/aws/resource_aws_securityhub_member_test.go b/aws/resource_aws_securityhub_member_test.go new file mode 100644 index 00000000000..b6ada989515 --- /dev/null +++ b/aws/resource_aws_securityhub_member_test.go @@ -0,0 +1,205 @@ +package aws + +import ( + "fmt" + "os" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/securityhub" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSSecurityHubMember_basic(t *testing.T) { + var member securityhub.Member + resourceName := "aws_securityhub_member.example" + accountId, email := testAccAWSSecurityHubMemberFromEnv(t) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSecurityHubMemberDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSecurityHubMemberConfig_basic(accountId, email), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSecurityHubMemberExists(resourceName, &member), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSSecurityHubMember_invite_create(t *testing.T) { + var member securityhub.Member + resourceName := "aws_securityhub_member.example" + accountId, email := testAccAWSSecurityHubMemberFromEnv(t) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSecurityHubMemberDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSecurityHubMemberConfig_invite(accountId, email, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSecurityHubMemberExists(resourceName, &member), + resource.TestCheckResourceAttr(resourceName, "member_status", "Invited"), + resource.TestCheckResourceAttr(resourceName, "invite", "true"), + ), + }, + // Disassociate member + { + Config: testAccAWSSecurityHubMemberConfig_invite(accountId, email, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSecurityHubMemberExists(resourceName, &member), + // These may fail: https://github.com/aws/aws-sdk-go/issues/2332#issuecomment-445535357 + resource.TestCheckResourceAttr(resourceName, "member_status", "Removed"), + resource.TestCheckResourceAttr(resourceName, "invite", "false"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSSecurityHubMember_invite_update(t *testing.T) { + var member securityhub.Member + resourceName := "aws_securityhub_member.example" + accountId, email := testAccAWSSecurityHubMemberFromEnv(t) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSecurityHubMemberDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSecurityHubMemberConfig_invite(accountId, email, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSecurityHubMemberExists(resourceName, &member), + resource.TestCheckResourceAttr(resourceName, "member_status", "Created"), + resource.TestCheckResourceAttr(resourceName, "invite", "false"), + ), + }, + // Invite member + { + Config: testAccAWSSecurityHubMemberConfig_invite(accountId, email, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSecurityHubMemberExists(resourceName, &member), + resource.TestCheckResourceAttr(resourceName, "member_status", "Invited"), + resource.TestCheckResourceAttr(resourceName, "invite", "true"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccAWSSecurityHubMemberFromEnv(t *testing.T) (string, string) { + accountId := os.Getenv("AWS_ALTERNATE_ACCOUNT_ID") + if accountId == "" { + accountId = "111111111111" + } + + email := os.Getenv("AWS_ALTERNATE_EMAIL") + if email == "" { + email = "example@example.com" + } + + return accountId, email +} + +func testAccCheckAWSSecurityHubMemberExists(n string, member *securityhub.Member) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := testAccProvider.Meta().(*AWSClient).securityhubconn + + resp, err := conn.GetMembers(&securityhub.GetMembersInput{ + AccountIds: []*string{aws.String(rs.Primary.ID)}, + }) + + if err != nil { + return err + } + + if len(resp.Members) == 0 { + return fmt.Errorf("Security Hub member %s not found", rs.Primary.ID) + } + + member = resp.Members[0] + + return nil + } +} + +func testAccCheckAWSSecurityHubMemberDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).securityhubconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_securityhub_member" { + continue + } + + resp, err := conn.GetMembers(&securityhub.GetMembersInput{ + AccountIds: []*string{aws.String(rs.Primary.ID)}, + }) + + if err != nil { + if isAWSErr(err, securityhub.ErrCodeResourceNotFoundException, "") { + return nil + } + return err + } + + if len(resp.Members) != 0 { + return fmt.Errorf("Security Hub member still exists") + } + + return nil + } + + return nil +} + +func testAccAWSSecurityHubMemberConfig_basic(accountId, email string) string { + return fmt.Sprintf(` +resource "aws_securityhub_account" "example" {} + +resource "aws_securityhub_member" "example" { + depends_on = ["aws_securityhub_account.example"] + account_id = "%s" + email = "%s" +} +`, accountId, email) +} + +func testAccAWSSecurityHubMemberConfig_invite(accountId, email string, invite bool) string { + return fmt.Sprintf(` +resource "aws_securityhub_account" "example" {} + +resource "aws_securityhub_member" "example" { + depends_on = ["aws_securityhub_account.example"] + account_id = "%s" + email = "%s" + invite = %t +} +`, accountId, email, invite) +} diff --git a/aws/resource_aws_securityhub_product_subscription.go b/aws/resource_aws_securityhub_product_subscription.go new file mode 100644 index 00000000000..a6d12693132 --- /dev/null +++ b/aws/resource_aws_securityhub_product_subscription.go @@ -0,0 +1,85 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/securityhub" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsSecurityHubProductSubscription() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsSecurityHubProductSubscriptionCreate, + Read: resourceAwsSecurityHubProductSubscriptionRead, + Delete: resourceAwsSecurityHubProductSubscriptionDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "product_arn": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateArn, + }, + }, + } +} + +func resourceAwsSecurityHubProductSubscriptionCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).securityhubconn + log.Printf("[DEBUG] Enabling Security Hub product subscription for product %s", d.Get("product_arn")) + + resp, err := conn.EnableImportFindingsForProduct(&securityhub.EnableImportFindingsForProductInput{ + ProductArn: aws.String(d.Get("product_arn").(string)), + }) + + if err != nil { + return fmt.Errorf("Error enabling Security Hub product subscription for product %s: %s", d.Get("product_arn"), err) + } + + d.SetId(*resp.ProductSubscriptionArn) + + return resourceAwsSecurityHubProductSubscriptionRead(d, meta) +} + +func resourceAwsSecurityHubProductSubscriptionRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).securityhubconn + + log.Printf("[DEBUG] Reading Security Hub product subscriptions to find %s", d.Id()) + resp, err := conn.ListEnabledProductsForImport(&securityhub.ListEnabledProductsForImportInput{}) + + if err != nil { + return fmt.Errorf("Error reading Security Hub product subscriptions to find %s: %s", d.Id(), err) + } + + productSubscriptions := make([]interface{}, len(resp.ProductSubscriptions)) + for i := range resp.ProductSubscriptions { + productSubscriptions[i] = *resp.ProductSubscriptions[i] + } + + if _, contains := sliceContainsString(productSubscriptions, d.Id()); !contains { + log.Printf("[WARN] Security Hub product subscriptions (%s) not found, removing from state", d.Id()) + d.SetId("") + } + + return nil +} + +func resourceAwsSecurityHubProductSubscriptionDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).securityhubconn + log.Printf("[DEBUG] Disabling Security Hub product subscription %s", d.Id()) + + _, err := conn.DisableImportFindingsForProduct(&securityhub.DisableImportFindingsForProductInput{ + ProductSubscriptionArn: aws.String(d.Id()), + }) + + if err != nil { + return fmt.Errorf("Error disabling Security Hub product subscription %s: %s", d.Id(), err) + } + + return nil +} diff --git a/aws/resource_aws_securityhub_product_subscription_test.go b/aws/resource_aws_securityhub_product_subscription_test.go new file mode 100644 index 00000000000..4b307f0e2f5 --- /dev/null +++ b/aws/resource_aws_securityhub_product_subscription_test.go @@ -0,0 +1,106 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/securityhub" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSSecurityHubProductSubscription_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSecurityHubProductSubscriptionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSecurityHubProductSubscriptionConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSecurityHubProductSubscriptionExists("aws_securityhub_product_subscription.example"), + ), + }, + // Import is not supported, since the API to lookup product_arn from a product + // subscription is currrently private + { + // Check Destroy - but only target the specific resource (otherwise Security Hub + // will be disabled and the destroy check will fail) + Config: testAccAWSSecurityHubProductSubscriptionConfig_empty, + Check: testAccCheckAWSSecurityHubProductSubscriptionDestroy, + }, + }, + }) +} + +func testAccCheckAWSSecurityHubProductSubscriptionExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := testAccProvider.Meta().(*AWSClient).securityhubconn + + resp, err := conn.ListEnabledProductsForImport(&securityhub.ListEnabledProductsForImportInput{}) + + if err != nil { + return err + } + + productSubscriptions := make([]interface{}, len(resp.ProductSubscriptions)) + for i := range resp.ProductSubscriptions { + productSubscriptions[i] = *resp.ProductSubscriptions[i] + } + + if _, contains := sliceContainsString(productSubscriptions, rs.Primary.ID); !contains { + return fmt.Errorf("Security Hub product subscription %s not found", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckAWSSecurityHubProductSubscriptionDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).securityhubconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_securityhub_product_subscription" { + continue + } + + resp, err := conn.ListEnabledProductsForImport(&securityhub.ListEnabledProductsForImportInput{}) + + if err != nil { + return err + } + + productSubscriptions := make([]interface{}, len(resp.ProductSubscriptions)) + for i := range resp.ProductSubscriptions { + productSubscriptions[i] = *resp.ProductSubscriptions[i] + } + + if _, contains := sliceContainsString(productSubscriptions, rs.Primary.ID); contains { + return fmt.Errorf("Security Hub product subscription %s still exists", rs.Primary.ID) + } + + return nil + } + + return nil +} + +const testAccAWSSecurityHubProductSubscriptionConfig_empty = ` +resource "aws_securityhub_account" "example" {} +` + +const testAccAWSSecurityHubProductSubscriptionConfig_basic = ` +resource "aws_securityhub_account" "example" {} + +data "aws_region" "current" {} + +resource "aws_securityhub_product_subscription" "example" { + depends_on = ["aws_securityhub_account.example"] + product_arn = "arn:aws:securityhub:${data.aws_region.current.name}:733251395267:product/alertlogic/althreatmanagement" +} +` diff --git a/aws/resource_aws_securityhub_standards_subscription.go b/aws/resource_aws_securityhub_standards_subscription.go new file mode 100644 index 00000000000..b40bf660a3d --- /dev/null +++ b/aws/resource_aws_securityhub_standards_subscription.go @@ -0,0 +1,93 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/securityhub" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsSecurityHubStandardsSubscription() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsSecurityHubStandardsSubscriptionCreate, + Read: resourceAwsSecurityHubStandardsSubscriptionRead, + Delete: resourceAwsSecurityHubStandardsSubscriptionDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "standards_arn": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateArn, + }, + }, + } +} + +func resourceAwsSecurityHubStandardsSubscriptionCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).securityhubconn + log.Printf("[DEBUG] Enabling Security Hub standard %s", d.Get("standards_arn")) + + resp, err := conn.BatchEnableStandards(&securityhub.BatchEnableStandardsInput{ + StandardsSubscriptionRequests: []*securityhub.StandardsSubscriptionRequest{ + { + StandardsArn: aws.String(d.Get("standards_arn").(string)), + }, + }, + }) + + if err != nil { + return fmt.Errorf("Error enabling Security Hub standard: %s", err) + } + + standardsSubscription := resp.StandardsSubscriptions[0] + + d.SetId(*standardsSubscription.StandardsSubscriptionArn) + + return resourceAwsSecurityHubStandardsSubscriptionRead(d, meta) +} + +func resourceAwsSecurityHubStandardsSubscriptionRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).securityhubconn + + log.Printf("[DEBUG] Reading Security Hub standard %s", d.Id()) + resp, err := conn.GetEnabledStandards(&securityhub.GetEnabledStandardsInput{ + StandardsSubscriptionArns: []*string{aws.String(d.Id())}, + }) + + if err != nil { + return fmt.Errorf("Error reading Security Hub standard %s: %s", d.Id(), err) + } + + if len(resp.StandardsSubscriptions) == 0 { + log.Printf("[WARN] Security Hub standard (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + standardsSubscription := resp.StandardsSubscriptions[0] + + d.Set("standards_arn", standardsSubscription.StandardsArn) + + return nil +} + +func resourceAwsSecurityHubStandardsSubscriptionDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).securityhubconn + log.Printf("[DEBUG] Disabling Security Hub standard %s", d.Id()) + + _, err := conn.BatchDisableStandards(&securityhub.BatchDisableStandardsInput{ + StandardsSubscriptionArns: []*string{aws.String(d.Id())}, + }) + + if err != nil { + return fmt.Errorf("Error disabling Security Hub standard %s: %s", d.Id(), err) + } + + return nil +} diff --git a/aws/resource_aws_securityhub_standards_subscription_test.go b/aws/resource_aws_securityhub_standards_subscription_test.go new file mode 100644 index 00000000000..9f165a79649 --- /dev/null +++ b/aws/resource_aws_securityhub_standards_subscription_test.go @@ -0,0 +1,108 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/securityhub" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSSecurityHubStandardsSubscription_basic(t *testing.T) { + var standardsSubscription *securityhub.StandardsSubscription + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAWSSecurityHubStandardsSubscriptionConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSecurityHubStandardsSubscriptionExists("aws_securityhub_standards_subscription.example", standardsSubscription), + ), + }, + { + ResourceName: "aws_securityhub_standards_subscription.example", + ImportState: true, + ImportStateVerify: true, + }, + { + // Check Destroy - but only target the specific resource (otherwise Security Hub + // will be disabled and the destroy check will fail) + Config: testAccAWSSecurityHubStandardsSubscriptionConfig_empty, + Check: testAccCheckAWSSecurityHubStandardsSubscriptionDestroy, + }, + }, + }) +} + +func testAccCheckAWSSecurityHubStandardsSubscriptionExists(n string, standardsSubscription *securityhub.StandardsSubscription) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := testAccProvider.Meta().(*AWSClient).securityhubconn + + resp, err := conn.GetEnabledStandards(&securityhub.GetEnabledStandardsInput{ + StandardsSubscriptionArns: []*string{aws.String(rs.Primary.ID)}, + }) + + if err != nil { + return err + } + + if len(resp.StandardsSubscriptions) == 0 { + return fmt.Errorf("Security Hub standard %s not found", rs.Primary.ID) + } + + standardsSubscription = resp.StandardsSubscriptions[0] + + return nil + } +} + +func testAccCheckAWSSecurityHubStandardsSubscriptionDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).securityhubconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_securityhub_standards_subscription" { + continue + } + + resp, err := conn.GetEnabledStandards(&securityhub.GetEnabledStandardsInput{ + StandardsSubscriptionArns: []*string{aws.String(rs.Primary.ID)}, + }) + + if err != nil { + if isAWSErr(err, securityhub.ErrCodeResourceNotFoundException, "") { + return nil + } + return err + } + + if len(resp.StandardsSubscriptions) != 0 { + return fmt.Errorf("Security Hub standard %s still exists", rs.Primary.ID) + } + + return nil + } + + return nil +} + +const testAccAWSSecurityHubStandardsSubscriptionConfig_empty = ` +resource "aws_securityhub_account" "example" {} +` + +const testAccAWSSecurityHubStandardsSubscriptionConfig_basic = ` +resource "aws_securityhub_account" "example" {} + +resource "aws_securityhub_standards_subscription" "example" { + depends_on = ["aws_securityhub_account.example"] + standards_arn = "arn:aws:securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0" +} +` diff --git a/website/aws.erb b/website/aws.erb index 8b385476db6..ad2d5e51835 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -2176,6 +2176,33 @@ +