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 all commits
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
134 changes: 134 additions & 0 deletions aws/resource_aws_securityhub_product_subscription.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package aws

import (
"fmt"
"log"
"strings"

"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,
},
"arn": {
Type: schema.TypeString,
Computed: true,
},
},
}
}

func resourceAwsSecurityHubProductSubscriptionCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).securityhubconn
productArn := d.Get("product_arn").(string)

log.Printf("[DEBUG] Enabling Security Hub product subscription for product %s", productArn)

resp, err := conn.EnableImportFindingsForProduct(&securityhub.EnableImportFindingsForProductInput{
ProductArn: aws.String(productArn),
})

if err != nil {
return fmt.Errorf("Error enabling Security Hub product subscription for product %s: %s", productArn, err)
}

d.SetId(fmt.Sprintf("%s,%s", productArn, *resp.ProductSubscriptionArn))

return resourceAwsSecurityHubProductSubscriptionRead(d, meta)
}

func resourceAwsSecurityHubProductSubscriptionRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).securityhubconn

productArn, productSubscriptionArn, err := resourceAwsSecurityHubProductSubscriptionParseId(d.Id())

if err != nil {
return err
}

log.Printf("[DEBUG] Reading Security Hub product subscriptions to find %s", d.Id())

exists, err := resourceAwsSecurityHubProductSubscriptionCheckExists(conn, productSubscriptionArn)

if err != nil {
return fmt.Errorf("Error reading Security Hub product subscriptions to find %s: %s", d.Id(), err)
}

if !exists {
log.Printf("[WARN] Security Hub product subscriptions (%s) not found, removing from state", d.Id())
d.SetId("")
}

d.Set("product_arn", productArn)
d.Set("arn", productSubscriptionArn)

return nil
}

func resourceAwsSecurityHubProductSubscriptionCheckExists(conn *securityhub.SecurityHub, productSubscriptionArn string) (bool, error) {
input := &securityhub.ListEnabledProductsForImportInput{}
exists := false

err := conn.ListEnabledProductsForImportPages(input, func(page *securityhub.ListEnabledProductsForImportOutput, lastPage bool) bool {
for _, readProductSubscriptionArn := range page.ProductSubscriptions {
if aws.StringValue(readProductSubscriptionArn) == productSubscriptionArn {
exists = true
return false
}
}
return !lastPage
})

if err != nil {
return false, err
}

return exists, nil
}

func resourceAwsSecurityHubProductSubscriptionParseId(id string) (string, string, error) {
parts := strings.SplitN(id, ",", 2)

if len(parts) != 2 {
return "", "", fmt.Errorf("Expected Security Hub product subscription ID in format <product_arn>,<arn> - received: %s", id)
}

return parts[0], parts[1], nil
}

func resourceAwsSecurityHubProductSubscriptionDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).securityhubconn
log.Printf("[DEBUG] Disabling Security Hub product subscription %s", d.Id())

_, productSubscriptionArn, err := resourceAwsSecurityHubProductSubscriptionParseId(d.Id())

if err != nil {
return err
}

_, err = conn.DisableImportFindingsForProduct(&securityhub.DisableImportFindingsForProductInput{
ProductSubscriptionArn: aws.String(productSubscriptionArn),
})

if err != nil {
return fmt.Errorf("Error disabling Security Hub product subscription %s: %s", d.Id(), err)
}

return nil
}
108 changes: 108 additions & 0 deletions aws/resource_aws_securityhub_product_subscription_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package aws

import (
"fmt"
"testing"

"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"),
),
},
{
ResourceName: "aws_securityhub_product_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: 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

_, productSubscriptionArn, err := resourceAwsSecurityHubProductSubscriptionParseId(rs.Primary.ID)

if err != nil {
return err
}

exists, err := resourceAwsSecurityHubProductSubscriptionCheckExists(conn, productSubscriptionArn)

if err != nil {
return err
}

if !exists {
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
}

_, productSubscriptionArn, err := resourceAwsSecurityHubProductSubscriptionParseId(rs.Primary.ID)

if err != nil {
return err
}

exists, err := resourceAwsSecurityHubProductSubscriptionCheckExists(conn, productSubscriptionArn)

if err != nil {
return err
}

if exists {
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
78 changes: 78 additions & 0 deletions website/docs/r/securityhub_product_subscription.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
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:

* `arn` - 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 in the form `product_arn,arn`, e.g.

```sh
$ terraform import aws_securityhub_product_subscription.example arn:aws:securityhub:eu-west-1:733251395267:product/alertlogic/althreatmanagement,arn:aws:securityhub:eu-west-1:123456789012:product-subscription/alertlogic/althreatmanagement
```