From 68b5cd840761a49c349d16ccee460cd7b66ac28a Mon Sep 17 00:00:00 2001 From: Pengyuan Zhao Date: Wed, 7 Jun 2023 18:54:16 -0400 Subject: [PATCH] feat(resource): add support for compliance policies (#492) --- .../main.tf | 73 ++++++ ...esource_lacework_policy_compliance_test.go | 140 ++++++++++ lacework/provider.go | 1 + .../resource_lacework_policy_compliance.go | 240 ++++++++++++++++++ website/docs/r/policy.html.markdown | 4 +- .../docs/r/policy_compliance.html.markdown | 86 +++++++ 6 files changed, 542 insertions(+), 2 deletions(-) create mode 100644 examples/resource_lacework_policy_compliance/main.tf create mode 100644 integration/resource_lacework_policy_compliance_test.go create mode 100644 lacework/resource_lacework_policy_compliance.go create mode 100644 website/docs/r/policy_compliance.html.markdown diff --git a/examples/resource_lacework_policy_compliance/main.tf b/examples/resource_lacework_policy_compliance/main.tf new file mode 100644 index 000000000..273ffe4a7 --- /dev/null +++ b/examples/resource_lacework_policy_compliance/main.tf @@ -0,0 +1,73 @@ +terraform { + required_providers { + lacework = { + source = "lacework/lacework" + } + } +} + +resource "lacework_policy_compliance" "example" { + title = var.title + query_id = "LW_Global_AWS_Config_S3BucketLoggingNotEnabled" + severity = var.severity + description = var.description + remediation = var.remediation + enabled = true + policy_id_suffix = var.policy_id_suffix + tags = var.tags + alerting_enabled = true +} + + +variable "title" { + type = string + default = "lql-terraform-policy" +} + +variable "description" { + type = string + default = "Policy Created via Terraform" +} + +variable "severity" { + type = string + default = "High" +} + +variable "remediation" { + type = string + default = "Please Investigate" +} + +variable "policy_id_suffix" { + default = "" +} + +variable "tags" { + type = list(string) + default = ["cloud_AWS", "custom"] +} + +output "title" { + value = lacework_policy_compliance.example.title +} + +output "severity" { + value = lacework_policy_compliance.example.severity +} + +output "remediation" { + value = lacework_policy_compliance.example.remediation +} + +output "description" { + value = lacework_policy_compliance.example.description +} + +output "policy_id_suffix" { + value = lacework_policy_compliance.example.policy_id_suffix +} + +output "tags" { + value = lacework_policy_compliance.example.tags +} diff --git a/integration/resource_lacework_policy_compliance_test.go b/integration/resource_lacework_policy_compliance_test.go new file mode 100644 index 000000000..2b46d854e --- /dev/null +++ b/integration/resource_lacework_policy_compliance_test.go @@ -0,0 +1,140 @@ +package integration + +import ( + "fmt" + "math/rand" + "testing" + "time" + + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/stretchr/testify/assert" +) + +// TestPolicyComplianceCreate applies integration terraform: +// => '../examples/resource_lacework_policy_compliance' +// +// It uses the go-sdk to verify the created policy, +// applies an update and destroys it +// nolint +func TestPolicyComplianceCreate(t *testing.T) { + terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ + TerraformDir: "../examples/resource_lacework_policy_compliance", + EnvVars: tokenEnvVar, + Vars: map[string]interface{}{ + "title": "lql-terraform-policy", + "severity": "High", + "description": "Policy Created via Terraform", + "remediation": "Please Investigate", + "tags": []string{"cloud_AWS", "resource_S3_Bucket"}, + }, + }) + defer terraform.Destroy(t, terraformOptions) + + // Create new Policy + create := terraform.InitAndApplyAndIdempotent(t, terraformOptions) + createProps := GetPolicyProps(create) + + actualTitle := terraform.Output(t, terraformOptions, "title") + actualSeverity := terraform.Output(t, terraformOptions, "severity") + actualDescription := terraform.Output(t, terraformOptions, "description") + actualRemediation := terraform.Output(t, terraformOptions, "remediation") + actualTags := terraform.Output(t, terraformOptions, "tags") + + assert.Contains(t, "lql-terraform-policy", createProps.Data.Title) + assert.Contains(t, "high", createProps.Data.Severity) + assert.Contains(t, "Compliance", createProps.Data.PolicyType) + assert.Contains(t, "Policy Created via Terraform", createProps.Data.Description) + assert.Contains(t, "Please Investigate", createProps.Data.Remediation) + assert.NotContains(t, createProps.Data.Tags, "custom") + assert.Contains(t, createProps.Data.Tags, "cloud_AWS") + assert.Contains(t, createProps.Data.Tags, "resource_S3_Bucket") + + assert.Equal(t, "lql-terraform-policy", actualTitle) + assert.Equal(t, "high", actualSeverity) + assert.Equal(t, "Policy Created via Terraform", actualDescription) + assert.Equal(t, "Please Investigate", actualRemediation) + assert.Equal(t, "[cloud_AWS resource_S3_Bucket]", actualTags) + + // Update Policy + terraformOptions.Vars = map[string]interface{}{ + "title": "lql-terraform-policy-updated", + "severity": "Low", + "description": "Policy Created via Terraform Updated", + "remediation": "Please Ignore", + "tags": []string{"cloud_AWS", "resource_S3_Bucket", "custom"}, + } + + update := terraform.ApplyAndIdempotent(t, terraformOptions) + updateProps := GetPolicyProps(update) + + actualTitle = terraform.Output(t, terraformOptions, "title") + actualSeverity = terraform.Output(t, terraformOptions, "severity") + actualDescription = terraform.Output(t, terraformOptions, "description") + actualRemediation = terraform.Output(t, terraformOptions, "remediation") + actualTags = terraform.Output(t, terraformOptions, "tags") + + assert.Contains(t, "lql-terraform-policy-updated", updateProps.Data.Title) + assert.Contains(t, "low", updateProps.Data.Severity) + assert.Contains(t, "Policy Created via Terraform Updated", updateProps.Data.Description) + assert.Contains(t, "Please Ignore", updateProps.Data.Remediation) + assert.Contains(t, updateProps.Data.Tags, "custom") + assert.Contains(t, updateProps.Data.Tags, "cloud_AWS") + assert.Contains(t, updateProps.Data.Tags, "resource_S3_Bucket") + + assert.Equal(t, "lql-terraform-policy-updated", actualTitle) + assert.Equal(t, "low", actualSeverity) + assert.Equal(t, "Policy Created via Terraform Updated", actualDescription) + assert.Equal(t, "Please Ignore", actualRemediation) +} + +func TestPolicyComplianceCreateWithPolicyIDSuffix(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + suffix := fmt.Sprintf("terraform-%d", rand.Intn(1000)) + terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ + TerraformDir: "../examples/resource_lacework_policy_compliance", + Vars: map[string]interface{}{ + "title": "lql-terraform-policy", + "policy_id_suffix": suffix, + "severity": "High", + "description": "Policy Created via Terraform", + "remediation": "Please Investigate", + }, + }) + defer terraform.Destroy(t, terraformOptions) + + // Create new Policy + create := terraform.InitAndApplyAndIdempotent(t, terraformOptions) + createProps := GetPolicyProps(create) + + actualTitle := terraform.Output(t, terraformOptions, "title") + actualSeverity := terraform.Output(t, terraformOptions, "severity") + actualDescription := terraform.Output(t, terraformOptions, "description") + actualRemediation := terraform.Output(t, terraformOptions, "remediation") + actualSuffix := terraform.Output(t, terraformOptions, "policy_id_suffix") + + assert.Contains(t, "lql-terraform-policy", createProps.Data.Title) + assert.Contains(t, "high", createProps.Data.Severity) + assert.Contains(t, "Compliance", createProps.Data.PolicyType) + assert.Contains(t, "Policy Created via Terraform", createProps.Data.Description) + assert.Contains(t, "Please Investigate", createProps.Data.Remediation) + + assert.Equal(t, "lql-terraform-policy", actualTitle) + assert.Equal(t, "high", actualSeverity) + assert.Equal(t, "Policy Created via Terraform", actualDescription) + assert.Equal(t, "Please Investigate", actualRemediation) + assert.Contains(t, suffix, actualSuffix) + + // Update Policy + terraformOptions.Vars = map[string]interface{}{ + "title": "lql-terraform-policy-updated", + "policy_id_suffix": "modified-id-suffix", + "severity": "Low", + "description": "Policy Created via Terraform Updated", + "remediation": "Please Ignore", + } + + msg, err := terraform.ApplyE(t, terraformOptions) + + assert.Error(t, err) + assert.Contains(t, msg, "unable to change ID of an existing policy") +} diff --git a/lacework/provider.go b/lacework/provider.go index 63c018add..ec7641caf 100644 --- a/lacework/provider.go +++ b/lacework/provider.go @@ -111,6 +111,7 @@ func Provider() *schema.Provider { "lacework_integration_proxy_scanner": resourceLaceworkIntegrationProxyScanner(), "lacework_query": resourceLaceworkQuery(), "lacework_policy": resourceLaceworkPolicy(), + "lacework_policy_compliance": resourceLaceworkPolicyCompliance(), "lacework_policy_exception": resourceLaceworkPolicyException(), "lacework_report_rule": resourceLaceworkReportRule(), "lacework_resource_group_account": resourceLaceworkResourceGroupLwAccount(), diff --git a/lacework/resource_lacework_policy_compliance.go b/lacework/resource_lacework_policy_compliance.go new file mode 100644 index 000000000..6f7080efb --- /dev/null +++ b/lacework/resource_lacework_policy_compliance.go @@ -0,0 +1,240 @@ +package lacework + +import ( + "errors" + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/lacework/go-sdk/api" +) + +func resourceLaceworkPolicyCompliance() *schema.Resource { + return &schema.Resource{ + Create: resourceLaceworkPolicyComplianceCreate, + Read: resourceLaceworkPolicyComplianceRead, + Update: resourceLaceworkPolicyComplianceUpdate, + Delete: resourceLaceworkPolicyComplianceDelete, + + Importer: &schema.ResourceImporter{ + State: importLaceworkPolicyCompliance, + }, + + Schema: map[string]*schema.Schema{ + "title": { + Type: schema.TypeString, + Required: true, + Description: "The title of the policy", + }, + "query_id": { + Type: schema.TypeString, + Required: true, + Description: "The id of the query", + }, + "description": { + Type: schema.TypeString, + Required: true, + Description: "The description of the query", + }, + "severity": { + Type: schema.TypeString, + Required: true, + Description: "The severity for the policy. Valid severities are: " + + "Critical, High, Medium, Low, Info", + StateFunc: func(val interface{}) string { + return strings.TrimSpace(strings.ToLower(val.(string))) + }, + ValidateDiagFunc: ValidSeverity(), + }, + "policy_id_suffix": { + Type: schema.TypeString, + Optional: true, + Description: "The string appended to the end of the policy id", + }, + "remediation": { + Type: schema.TypeString, + Optional: true, + Description: "The remediation message to display", + }, + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "The state of the policy", + }, + "tags": { + Type: schema.TypeSet, + Description: "A list of user specified policy tags", + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "alerting_enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Whether alerting is enabled or disabled", + }, + "id": { + Type: schema.TypeString, + Computed: true, + }, + "updated_time": { + Type: schema.TypeString, + Computed: true, + }, + "updated_by": { + Type: schema.TypeString, + Computed: true, + }, + "owner": { + Type: schema.TypeString, + Computed: true, + }, + "computed_tags": { + Type: schema.TypeString, + Description: "All policy tags, server generated and user specified tags", + Computed: true, + }, + }, + } +} + +func resourceLaceworkPolicyComplianceCreate(d *schema.ResourceData, meta interface{}) error { + var ( + lacework = meta.(*api.Client) + ) + + policy := api.NewPolicy{ + PolicyType: api.PolicyTypeCompliance.String(), + QueryID: d.Get("query_id").(string), + Title: d.Get("title").(string), + Enabled: d.Get("enabled").(bool), + Description: d.Get("description").(string), + Remediation: d.Get("remediation").(string), + Severity: d.Get("severity").(string), + PolicyID: d.Get("policy_id_suffix").(string), + Tags: castStringSlice(d.Get("tags").(*schema.Set).List()), + AlertEnabled: d.Get("alerting_enabled").(bool), + } + + log.Printf("[INFO] Creating Policy with data:\n%+v\n", policy) + response, err := lacework.V2.Policy.Create(policy) + if err != nil { + return err + } + + d.SetId(response.Data.PolicyID) + d.Set("owner", response.Data.Owner) + d.Set("updated_time", response.Data.LastUpdateTime) + d.Set("updated_by", response.Data.LastUpdateUser) + d.Set("computed_tags", strings.Join(response.Data.Tags, ",")) + + log.Printf("[INFO] Created Policy with guid %s\n", response.Data.PolicyID) + return nil +} + +func resourceLaceworkPolicyComplianceRead(d *schema.ResourceData, meta interface{}) error { + var ( + lacework = meta.(*api.Client) + ) + + log.Printf("[INFO] Reading Policy with guid %s\n", d.Id()) + response, err := lacework.V2.Policy.Get(d.Id()) + if err != nil { + return resourceNotFound(d, err) + } + + d.SetId(response.Data.PolicyID) + d.Set("title", response.Data.Title) + d.Set("query_id", response.Data.QueryID) + d.Set("enabled", response.Data.Enabled) + d.Set("description", response.Data.Description) + d.Set("severity", response.Data.Severity) + d.Set("remediation", response.Data.Remediation) + d.Set("type", response.Data.PolicyType) + d.Set("alerting_enabled", response.Data.AlertEnabled) + d.Set("owner", response.Data.Owner) + d.Set("updated_time", response.Data.LastUpdateTime) + d.Set("updated_by", response.Data.LastUpdateUser) + d.Set("computed_tags", strings.Join(response.Data.Tags, ",")) + + log.Printf("[INFO] Read Policy with guid %s\n", response.Data.PolicyID) + return nil +} + +func resourceLaceworkPolicyComplianceUpdate(d *schema.ResourceData, meta interface{}) error { + var ( + lacework = meta.(*api.Client) + ) + + if d.HasChange("policy_id_suffix") { + return errors.New("unable to change ID of an existing policy") + } + + policyEnabled := d.Get("enabled").(bool) + + policy := api.UpdatePolicy{ + PolicyType: api.PolicyTypeCompliance.String(), + QueryID: d.Get("query_id").(string), + Title: d.Get("title").(string), + Enabled: &policyEnabled, + Description: d.Get("description").(string), + Remediation: d.Get("remediation").(string), + Severity: d.Get("severity").(string), + PolicyID: d.Id(), + Tags: castStringSlice(d.Get("tags").(*schema.Set).List()), + } + + log.Printf("[INFO] Updating Policy with data:\n%+v\n", policy) + response, err := lacework.V2.Policy.Update(policy) + if err != nil { + return err + } + + d.SetId(response.Data.PolicyID) + d.Set("owner", response.Data.Owner) + d.Set("updated_time", response.Data.LastUpdateTime) + d.Set("updated_by", response.Data.LastUpdateUser) + d.Set("computed_tags", strings.Join(response.Data.Tags, ",")) + + log.Printf("[INFO] Updated Policy with guid %s\n", response.Data.PolicyID) + return nil +} + +func resourceLaceworkPolicyComplianceDelete(d *schema.ResourceData, meta interface{}) error { + lacework := meta.(*api.Client) + + log.Printf("[INFO] Deleting Policy with guid %s\n", d.Id()) + _, err := lacework.V2.Policy.Delete(d.Id()) + if err != nil { + return err + } + + log.Printf("[INFO] Deleted Policy with guid %s\n", d.Id()) + return nil +} + +func importLaceworkPolicyCompliance(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + lacework := meta.(*api.Client) + + log.Printf("[INFO] Importing Lacework Policy with guid: %s\n", d.Id()) + + response, err := lacework.V2.Policy.Get(d.Id()) + if err != nil { + return nil, fmt.Errorf( + "Unable to import Lacework resource. Policy with guid '%s' was not found", + d.Id(), + ) + } + if response.Data.PolicyType != api.PolicyTypeCompliance.String() { + return nil, fmt.Errorf( + "Unable to import Lacework resource. Policy with guid '%s' is not a compliance policy", + d.Id(), + ) + } + log.Printf("[INFO] Policy found with guid: %s\n", response.Data.PolicyID) + return []*schema.ResourceData{d}, nil +} diff --git a/website/docs/r/policy.html.markdown b/website/docs/r/policy.html.markdown index 70b876894..04c17a149 100644 --- a/website/docs/r/policy.html.markdown +++ b/website/docs/r/policy.html.markdown @@ -11,7 +11,7 @@ description: |- Lacework provides a highly scalable platform for creating, customizing, and managing custom policies against any datasource that is exposed via the Lacework Query Language (LQL). -For more information, see the [Policy Overview Documentation](https://docs.lacework.com/custom-policies-overview). +For more information, see the [Policy Overview Documentation](https://docs.lacework.net/console/custom-policy-overview). ## Example Usage @@ -41,7 +41,7 @@ resource "lacework_query" "AWS_CTA_AuroraPasswordChange" { } EOT } - + resource "lacework_policy" "example" { title = "Aurora Password Change" description = "Password for an Aurora RDS cluster was changed" diff --git a/website/docs/r/policy_compliance.html.markdown b/website/docs/r/policy_compliance.html.markdown new file mode 100644 index 000000000..cb90eb2d1 --- /dev/null +++ b/website/docs/r/policy_compliance.html.markdown @@ -0,0 +1,86 @@ +--- +subcategory: "Policies" +layout: "lacework" +page_title: "Lacework: lacework_policy_compliance" +description: |- + Create and manage Lacework Compliance Policies +--- + +# lacework\_policy\_compliance + +Lacework provides a highly scalable platform for creating, customizing, and managing custom policies +against any datasource that is exposed via the Lacework Query Language (LQL). + +For more information, see the [Policy Overview Documentation](https://docs.lacework.net/console/custom-policy-overview). + +## Example Usage + +Create a Lacework Compliance Policy to check for unenabled CloudTrail log file validation. + +```hcl +resource "lacework_query" "AWS_Config_CloudTrailLogFileValidationNotEnabled" { + query_id = "LW_Global_AWS_Config_CloudTrailLogFileValidationNotEnabled" + query = < **Note:** Lacework automatically generates a policy id when you create a policy, which is the recommended workflow. +Optionally, you can define your own policy id using the `policy_id_suffix`, this suffix must be all lowercase letters, +optionally followed by `-` and numbers, for example, `abcd-1234`. When you define your own policy id, Lacework prepends +the account name. The final policy id would then be `lwaccountname-abcd-1234`. + +## Argument Reference + +The following arguments are supported: + +* `title` - (Required) The policy title. +* `description` - (Required) The description of the policy. +* `query_id` - (Required) The query id. +* `severity` - (Required) The list of the severities. Valid severities include: + `Critical`, `High`, `Medium`, `Low` and `Info`. +* `remediation` - (Optional) The remediation message to display. +* `enabled` - (Optional) Whether the policy is enabled or disabled. Defaults to `true`. +* `policy_id_suffix` - (Optional) The string appended to the end of the policy id. +* `tags` - (Optional) A list of policy tags. +* `alerting_enabled` - (Optional) Whether the alerting profile is enabled or disabled. Defaults to `true`. + +## Import + +A Lacework compliance policy can be imported using a `POLICY_ID`, e.g. + +``` +$ terraform import lacework_policy_compliance.example YourLQLPolicyID +``` + +-> **Note:** To retrieve the `POLICY_ID` from existing policies in your account, use the +Lacework CLI command `lacework policy list`. To install this tool follow +[this documentation](https://docs.lacework.net/cli/).