Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

r/aws_securityhub: Add aws_securityhub_product_subscription resource #6921

Merged
merged 3 commits into from
Dec 21, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,7 @@ func Provider() terraform.ResourceProvider {
"aws_default_security_group": resourceAwsDefaultSecurityGroup(),
"aws_security_group_rule": resourceAwsSecurityGroupRule(),
"aws_securityhub_account": resourceAwsSecurityHubAccount(),
"aws_securityhub_product_subscription": resourceAwsSecurityHubProductSubscription(),
"aws_securityhub_standards_subscription": resourceAwsSecurityHubStandardsSubscription(),
"aws_servicecatalog_portfolio": resourceAwsServiceCatalogPortfolio(),
"aws_service_discovery_private_dns_namespace": resourceAwsServiceDiscoveryPrivateDnsNamespace(),
Expand Down
85 changes: 85 additions & 0 deletions aws/resource_aws_securityhub_product_subscription.go
Original file line number Diff line number Diff line change
@@ -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{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is currently missing acceptance testing and documentation. 😅 I think the current comment in the acceptance testing might be outdated since it looks like the Read function is implemented just fine below.

Copy link
Contributor Author

@gazoakley gazoakley Dec 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left this in accidentally - but I might have a misunderstanding about the import process. Do resources need to be able to read/populate all attributes solely from the resource ID? The API doesn't have a way to read back the product_arn given a product subscription ARN (you can only check a subscription exists). It looks like I might be able to make an import test that works if I specify ImportStateVerifyIgnore: []string{"product_arn"}

Copy link
Contributor

@bflad bflad Dec 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yikes, I was conflating product ARNs with product subscription ARNs. Yeah, for imports we need to set all attributes in the Read function or it'll show them as a difference after import (e.g. attribute: "" => "configured-value").

We have a few options since the API doesn't have a way to read it back:

  • Use ImportStateVerifyIgnore as you mention for now, but it'll have the difference problem noted above
  • Make the resource ID two parts, containing both ARNs, then Read has all the information it needs to search for the subscription and properly set product ARN as well
  • Or as a potentially crazy idea, if we can assume the production subscription ARN is always derivable from the product ARN (maybe looks possible from at least AWS subscriptions?), switch the resource identifier to the product ARN and calculate the subscription ARN
arn:aws:securityhub:us-west-2::product/aws/guardduty
# Set AccountID
# Replace Resource product with product-subscription
arn:aws:securityhub:us-west-2:ACCOUNTID:product-subscription/aws/guardduty

I'd lean towards including both ARNs in the resource ID for ease, operator friendliness after import, and safer than trying to derive it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about option 3 previously and then ran away scared 🤣 (I think it's doable, but the resource would need to know the current account ID and region to build an ARN and it feels brittle). Option 2 sounds good to me - I'll look at it later today.

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{})
bflad marked this conversation as resolved.
Show resolved Hide resolved

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))
bflad marked this conversation as resolved.
Show resolved Hide resolved
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
}
104 changes: 104 additions & 0 deletions aws/resource_aws_securityhub_product_subscription_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
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: testAccCheckAWSSecurityHubAccountDestroy,
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{
  ResourceName: "aws_securityhub_product_subscription.example",
  ImportState: true,
  ImportStateVerify: true,
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added an import test step, but I've need to use ImportStateVerifyIgnore:

{
  ResourceName:            "aws_securityhub_product_subscription.example",
  ImportState:             true,
  ImportStateVerify:       true,
  ImportStateVerifyIgnore: []string{"product_arn"},
},

// 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
}

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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noting here for future posterity and not that we don't like our friends over at AlertLogic, but its a little bit of a bummer that the AWS services are automatically subscribed for acceptance testing purposes since they are effectively free, assumed stable for a long while, and doesn't involve an extra EULA. (I'm a little surprised the API doesn't require accepting the EULA first.)

I'll try to switch to using a PreConfig to disable something like GuardDuty and re-enable it here in the configuration, but if that doesn't go as well as I'd expect, the pricing for this service is fine for the (presumably single hour) time it would be billed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉 7cdbfcf

}
`
3 changes: 3 additions & 0 deletions aws/resource_aws_securityhub_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ func TestAccAWSSecurityHub(t *testing.T) {
"Account": {
"basic": testAccAWSSecurityHubAccount_basic,
},
"ProductSubscription": {
"basic": testAccAWSSecurityHubProductSubscription_basic,
},
"StandardsSubscription": {
"basic": testAccAWSSecurityHubStandardsSubscription_basic,
},
Expand Down
4 changes: 4 additions & 0 deletions website/aws.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2190,6 +2190,10 @@
<a href="/docs/providers/aws/r/securityhub_account.html">aws_securityhub_account</a>
</li>

<li<%= sidebar_current("docs-aws-resource-securityhub-product-subscription") %>>
<a href="/docs/providers/aws/r/securityhub_product_subscription.html">aws_securityhub_product_subscription</a>
</li>

<li<%= sidebar_current("docs-aws-resource-securityhub-standards-subscription") %>>
<a href="/docs/providers/aws/r/securityhub_standards_subscription.html">aws_securityhub_standards_subscription</a>
</li>
Expand Down
70 changes: 70 additions & 0 deletions website/docs/r/securityhub_product_subscription.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
layout: "aws"
page_title: "AWS: aws_securityhub_product_subscription"
sidebar_current: "docs-aws-resource-securityhub-product-subscription"
description: |-
Subscribes to a Security Hub product.
---

# aws_securityhub_product_subscription

Subscribes to a Security Hub product.

## Example Usage

```hcl
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"
}
```

## Argument Reference

The following arguments are supported:

* `product_arn` - (Required) The ARN of the product that generates findings that you want to import into Security Hub - see below.

Currently available products (remember to replace `${var.region}` as appropriate):

* `arn:aws:securityhub:${var.region}::product/aws/guardduty`
* `arn:aws:securityhub:${var.region}::product/aws/inspector`
* `arn:aws:securityhub:${var.region}::product/aws/macie`
* `arn:aws:securityhub:${var.region}:733251395267:product/alertlogic/althreatmanagement`
* `arn:aws:securityhub:${var.region}:679703615338:product/armordefense/armoranywhere`
* `arn:aws:securityhub:${var.region}:151784055945:product/barracuda/cloudsecurityguardian`
* `arn:aws:securityhub:${var.region}:758245563457:product/checkpoint/cloudguard-iaas`
* `arn:aws:securityhub:${var.region}:634729597623:product/checkpoint/dome9-arc`
* `arn:aws:securityhub:${var.region}:517716713836:product/crowdstrike/crowdstrike-falcon`
* `arn:aws:securityhub:${var.region}:749430749651:product/cyberark/cyberark-pta`
* `arn:aws:securityhub:${var.region}:250871914685:product/f5networks/f5-advanced-waf`
* `arn:aws:securityhub:${var.region}:123073262904:product/fortinet/fortigate`
* `arn:aws:securityhub:${var.region}:324264561773:product/guardicore/aws-infection-monkey`
* `arn:aws:securityhub:${var.region}:324264561773:product/guardicore/guardicore`
* `arn:aws:securityhub:${var.region}:949680696695:product/ibm/qradar-siem`
* `arn:aws:securityhub:${var.region}:955745153808:product/imperva/imperva-attack-analytics`
* `arn:aws:securityhub:${var.region}:297986523463:product/mcafee-skyhigh/mcafee-mvision-cloud-aws`
* `arn:aws:securityhub:${var.region}:188619942792:product/paloaltonetworks/redlock`
* `arn:aws:securityhub:${var.region}:122442690527:product/paloaltonetworks/vm-series`
* `arn:aws:securityhub:${var.region}:805950163170:product/qualys/qualys-pc`
* `arn:aws:securityhub:${var.region}:805950163170:product/qualys/qualys-vm`
* `arn:aws:securityhub:${var.region}:336818582268:product/rapid7/insightvm`
* `arn:aws:securityhub:${var.region}:062897671886:product/sophos/sophos-server-protection`
* `arn:aws:securityhub:${var.region}:112543817624:product/splunk/splunk-enterprise`
* `arn:aws:securityhub:${var.region}:112543817624:product/splunk/splunk-phantom`
* `arn:aws:securityhub:${var.region}:956882708938:product/sumologicinc/sumologic-mda`
* `arn:aws:securityhub:${var.region}:754237914691:product/symantec-corp/symantec-cwp`
* `arn:aws:securityhub:${var.region}:422820575223:product/tenable/tenable-io`
* `arn:aws:securityhub:${var.region}:679593333241:product/trend-micro/deep-security`
* `arn:aws:securityhub:${var.region}:453761072151:product/turbot/turbot`
* `arn:aws:securityhub:${var.region}:496947949261:product/twistlock/twistlock-enterprise`

## Attributes Reference

The following attributes are exported in addition to the arguments listed above:

* `id` - The ARN of a resource that represents your subscription to the product that generates the findings that you want to import into Security Hub.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* `id` - The ARN of a resource that represents your subscription to the product that generates the findings that you want to import into Security Hub.

## Import

Security Hub Product Subscriptions can be imported via the product ARN, e.g.

```sh
$ terraform import aws_securityhub_product_subscription.example arn:aws:securityhub:us-east-1::product/aws/guardduty
```

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ID is the subscription ARN since that's the only thing I can test still exists - I've added an Import section describing it.