diff --git a/examples/resource_lacework_integration_aws_org_agentless_scanning/main.tf b/examples/resource_lacework_integration_aws_org_agentless_scanning/main.tf index b76a6f7ab..2833a604e 100644 --- a/examples/resource_lacework_integration_aws_org_agentless_scanning/main.tf +++ b/examples/resource_lacework_integration_aws_org_agentless_scanning/main.tf @@ -13,7 +13,7 @@ provider "lacework" { resource "lacework_integration_aws_org_agentless_scanning" "example" { name = var.name query_text = var.query_text - scan_frequency = 24 + scan_frequency = var.scan_frequency scan_containers = true scan_host_vulnerabilities = true scan_multi_volume = false @@ -50,6 +50,11 @@ variable "account_id" { default = "" } +variable "scan_frequency" { + type = number + default = 24 +} + variable "bucket_arn" { type = string default = "" diff --git a/examples/resource_lacework_integration_gcp_org_agentless_scanning/main.tf b/examples/resource_lacework_integration_gcp_org_agentless_scanning/main.tf new file mode 100644 index 000000000..361790a93 --- /dev/null +++ b/examples/resource_lacework_integration_gcp_org_agentless_scanning/main.tf @@ -0,0 +1,152 @@ +terraform { + required_providers { + lacework = { + source = "lacework/lacework" + } + } +} + +provider "lacework" { + organization = true +} + +variable "integration_name" { + type = string + default = "GCP Agentless Scanning Example" +} + +variable "client_id" { + type = string + default = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} + +variable "client_email" { + type = string + default = "email@some-project-name.iam.gserviceaccount.com" +} + +variable "private_key_id" { + type = string + default = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} + +variable "private_key" { + type = string + default = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +} + +variable "token_uri" { + type = string + default = "https://oauth2.googleapis.com/token" +} + +variable "integration_type" { + type = string + default = "PROJECT" +} + +variable "project_id" { + type = string + default = "example-project-id" +} + +variable "bucket_name" { + type = string + default = "storage bucket id" +} + +variable "scanning-project-id" { + type = string + default = "scanning-project-id" +} + +variable "query_text" { + type = string + default = "" +} + +variable "filter_list" { + type = list(string) + default = ["proj1", "proj2"] +} + +variable "org_account_mappings" { + type = list(object({ + default_lacework_account = string + mapping = list(object({ + lacework_account = string + gcp_projects = list(string) + })) + })) + default = [] + description = "Mapping of GCP projects to Lacework accounts within a Lacework organization" +} + +resource "lacework_integration_gcp_agentless_scanning" "org_example" { + name = var.integration_name + credentials { + client_id = var.client_id + client_email = var.client_email + private_key_id = var.private_key_id + private_key = var.private_key + token_uri = var.token_uri + } + resource_level = "ORGANIZATION" + resource_id = "294451184225" + scanning_project_id = "techally-test" + scan_frequency = 24 + scan_containers = true + scan_host_vulnerabilities = true + scan_multi_volume = false + scan_stopped_instances = true + bucket_name = var.bucket_name + query_text = var.query_text + filter_list = var.filter_list + + dynamic "org_account_mappings" { + for_each = var.org_account_mappings + content { + default_lacework_account = org_account_mappings.value["default_lacework_account"] + + dynamic "mapping" { + for_each = org_account_mappings.value["mapping"] + content { + lacework_account = mapping.value["lacework_account"] + gcp_projects = mapping.value["gcp_projects"] + } + } + } + } +} + +output "name" { + value = lacework_integration_gcp_agentless_scanning.org_example.name +} + +output "client_id" { + value = lacework_integration_gcp_agentless_scanning.org_example.credentials[0].client_id +} + +output "client_email" { + value = lacework_integration_gcp_agentless_scanning.org_example.credentials[0].client_email +} + +output "bucket_name" { + value = lacework_integration_gcp_agentless_scanning.org_example.bucket_name +} + +output "scanning_project_id" { + value = lacework_integration_gcp_agentless_scanning.org_example.scanning_project_id +} + +output "scan_frequency" { + value = lacework_integration_gcp_agentless_scanning.org_example.scan_frequency +} + +output "server_token" { + value = lacework_integration_gcp_agentless_scanning.org_example.server_token +} + +output "org_account_mappings" { + value = lacework_integration_gcp_agentless_scanning.org_example.org_account_mappings +} diff --git a/integration/integration.go b/integration/integration.go index d353f4fe9..fb58f76e6 100644 --- a/integration/integration.go +++ b/integration/integration.go @@ -189,7 +189,6 @@ func GetContainerRegisteryGar(result string) api.GcpGarIntegrationResponse { func GetGcpAgentlessScanningResponse(result string) api.GcpSidekickIntegrationResponse { id := GetIDFromTerraResults(result) - res, err := LwClient.V2.CloudAccounts.GetGcpSidekick(id) if err != nil { @@ -199,6 +198,17 @@ func GetGcpAgentlessScanningResponse(result string) api.GcpSidekickIntegrationRe return res } +func GetGcpAgentlessOrgScanningResponse(result string) api.GcpSidekickIntegrationResponse { + id := GetIDFromTerraResults(result) + res, err := LwOrgClient.V2.CloudAccounts.GetGcpSidekick(id) + + if err != nil { + log.Fatalf("Unable to find integration id: %s\n Response: %v", id, res) + } + + return res +} + func GetContainerRegisteryGcr(result string) api.GcpGcrIntegrationResponse { id := GetIDFromTerraResults(result) diff --git a/integration/resource_lacework_integration_aws_org_agentless_scanning_test.go b/integration/resource_lacework_integration_aws_org_agentless_scanning_test.go index 9bf573115..5e571bae4 100644 --- a/integration/resource_lacework_integration_aws_org_agentless_scanning_test.go +++ b/integration/resource_lacework_integration_aws_org_agentless_scanning_test.go @@ -40,7 +40,6 @@ func TestIntegrationAwsOrgAgentlessScanningLog(t *testing.T) { // Create new AWS Agentless Scanning Integration create := terraform.InitAndApplyAndIdempotent(t, terraformOptions) createData := GetAwsAgentlessOrgScanningResponse(create) - println(create) actualName := terraform.Output(t, terraformOptions, "name") assert.Equal( t, diff --git a/integration/resource_lacework_integration_gcp_org_agentless_scanning_test.go b/integration/resource_lacework_integration_gcp_org_agentless_scanning_test.go new file mode 100644 index 000000000..26035fa20 --- /dev/null +++ b/integration/resource_lacework_integration_gcp_org_agentless_scanning_test.go @@ -0,0 +1,72 @@ +package integration + +import ( + "fmt" + "testing" + + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/stretchr/testify/assert" +) + +func TestIntegrationGcpAgentlessOrgScanningCreateAndUpdate(t *testing.T) { + gcreds, err := googleLoadDefaultCredentials() + integration_name := "GCP Org Agentless Scanning Example Integration Test" + update_integration_name := fmt.Sprintf("%s Updated", integration_name) + if assert.Nil(t, err, "this test requires you to set GOOGLE_CREDENTIALS environment variable") { + terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ + TerraformDir: "../examples/resource_lacework_integration_gcp_org_agentless_scanning", + Vars: map[string]interface{}{ + "integration_name": integration_name, + "client_id": gcreds.ClientID, + "client_email": gcreds.ClientEmail, + "private_key_id": gcreds.PrivateKeyID, + "bucket_name": "storage bucket id", + "org_account_mappings": []map[string]interface{}{ + { + "default_lacework_account": "customerdemo", + "mapping": []map[string]interface{}{ + { + "lacework_account": "tech-ally", + "gcp_projects": []string{"techally-test"}, + }, + }, + }, + }, + }, + EnvVars: map[string]string{ + "TF_VAR_private_key": gcreds.PrivateKey, + "LW_API_TOKEN": LwApiToken, + }, + }) + defer terraform.Destroy(t, terraformOptions) + + // Create new Google Agentless Scanning integration + create := terraform.InitAndApplyAndIdempotent(t, terraformOptions) + createData := GetGcpAgentlessOrgScanningResponse(create) + assert.Equal(t, integration_name, createData.Data.Name) + + // Update Gcp integration + terraformOptions.Vars = map[string]interface{}{ + "integration_name": update_integration_name, + "client_id": gcreds.ClientID, + "client_email": gcreds.ClientEmail, + "private_key_id": gcreds.PrivateKeyID, + "bucket_name": "storage bucket id", + "org_account_mappings": []map[string]interface{}{ + { + "default_lacework_account": "customerdemo", + "mapping": []map[string]interface{}{ + { + "lacework_account": "abc", + "gcp_projects": []string{"techally-test"}, + }, + }, + }, + }, + } + + update := terraform.ApplyAndIdempotent(t, terraformOptions) + updateData := GetGcpAgentlessOrgScanningResponse(update) + assert.Equal(t, update_integration_name, updateData.Data.Name) + } +} diff --git a/lacework/account_mapping_helper.go b/lacework/account_mapping_helper.go index 697d74cb8..d3f3d1583 100644 --- a/lacework/account_mapping_helper.go +++ b/lacework/account_mapping_helper.go @@ -5,7 +5,7 @@ import ( ) type accountMappingsFile struct { - DefaultLaceworkAccount string `json:"defaultLaceworkAccountAws"` + DefaultLaceworkAccount string `json:"defaultLaceworkAccount"` Mappings map[string]interface{} `json:"integration_mappings"` } @@ -13,7 +13,10 @@ func (f *accountMappingsFile) Empty() bool { return f.DefaultLaceworkAccount == "" } -func getResourceOrgAccountMappings(d *schema.ResourceData) *accountMappingsFile { +var awsMappingType string = "aws_accounts" +var gcpMappingType string = "gcp_projects" + +func getResourceOrgAccountMappings(d *schema.ResourceData, mappingsType string) *accountMappingsFile { accountMapFile := new(accountMappingsFile) accMapsInt := d.Get("org_account_mappings").([]interface{}) if len(accMapsInt) != 0 && accMapsInt[0] != nil { @@ -27,17 +30,22 @@ func getResourceOrgAccountMappings(d *schema.ResourceData) *accountMappingsFile mappingSet := accountMappings["mapping"].(*schema.Set) for _, m := range mappingSet.List() { mapping := m.(map[string]interface{}) - accountMapFile.Mappings[mapping["lacework_account"].(string)] = map[string]interface{}{ - "aws_accounts": castStringSlice(mapping["aws_accounts"].(*schema.Set).List()), + if mappingsType == "gcp_projects" { + accountMapFile.Mappings[mapping["lacework_account"].(string)] = map[string]interface{}{ + "gcp_projects": castStringSlice(mapping[mappingsType].(*schema.Set).List()), + } + } else { + accountMapFile.Mappings[mapping["lacework_account"].(string)] = map[string]interface{}{ + "aws_accounts": castStringSlice(mapping[mappingsType].(*schema.Set).List()), + } } } } - return accountMapFile } -func flattenOrgAccountMappings(mappingFile *accountMappingsFile) []map[string]interface{} { +func flattenOrgAccountMappings(mappingFile *accountMappingsFile, mappingsType string) []map[string]interface{} { orgAccMappings := make([]map[string]interface{}, 0, 1) if mappingFile.Empty() { @@ -46,23 +54,24 @@ func flattenOrgAccountMappings(mappingFile *accountMappingsFile) []map[string]in mappings := map[string]interface{}{ "default_lacework_account": mappingFile.DefaultLaceworkAccount, - "mapping": flattenMappings(mappingFile.Mappings), + "mapping": flattenMappings(mappingFile.Mappings, mappingsType), } orgAccMappings = append(orgAccMappings, mappings) return orgAccMappings } -func flattenMappings(mappings map[string]interface{}) *schema.Set { +func flattenMappings(mappings map[string]interface{}, mappingsType string) *schema.Set { var ( - orgAccountMappingsSchema = awsCloudTrailIntegrationSchema["org_account_mappings"].Elem.(*schema.Resource) - mappingSchema = orgAccountMappingsSchema.Schema["mapping"].Elem.(*schema.Resource) - awsAccountsSchema = mappingSchema.Schema["aws_accounts"].Elem.(*schema.Schema) - res = schema.NewSet(schema.HashResource(mappingSchema), []interface{}{}) + awsOrgAccountMappingsSchema = awsCloudTrailIntegrationSchema["org_account_mappings"].Elem.(*schema.Resource) + awsMappingSchema = awsOrgAccountMappingsSchema.Schema["mapping"].Elem.(*schema.Resource) + awsAccountsSchema = awsMappingSchema.Schema[mappingsType].Elem.(*schema.Schema) + awsRes = schema.NewSet(schema.HashResource(awsMappingSchema), []interface{}{}) ) + for laceworkAccount, m := range mappings { mappingValue := m.(map[string]interface{}) - res.Add(map[string]interface{}{ + awsRes.Add(map[string]interface{}{ "lacework_account": laceworkAccount, "aws_accounts": schema.NewSet(schema.HashSchema(awsAccountsSchema), mappingValue["aws_accounts"].([]interface{}), @@ -70,5 +79,41 @@ func flattenMappings(mappings map[string]interface{}) *schema.Set { }) } - return res + return awsRes +} + +func flattenOrgGcpAccountMappings(mappingFile *accountMappingsFile) []map[string]interface{} { + orgAccMappings := make([]map[string]interface{}, 0, 1) + + if mappingFile.Empty() { + return orgAccMappings + } + + mappings := map[string]interface{}{ + "default_lacework_account": mappingFile.DefaultLaceworkAccount, + "mapping": flattenGcpMappings(mappingFile.Mappings), + } + + orgAccMappings = append(orgAccMappings, mappings) + return orgAccMappings +} + +func flattenGcpMappings(mappings map[string]interface{}) *schema.Set { + var ( + gcpOrgAccountMappingsSchema = gcpAgentlessScanningIntegrationSchema["org_account_mappings"].Elem.(*schema.Resource) + gcpMappingSchema = gcpOrgAccountMappingsSchema.Schema["mapping"].Elem.(*schema.Resource) + gcpAccountsSchema = gcpMappingSchema.Schema["mapping"].Elem.(*schema.Schema) + gcpRes = schema.NewSet(schema.HashResource(gcpMappingSchema), []interface{}{}) + ) + + for laceworkAccount, m := range mappings { + mappingValue := m.(map[string]interface{}) + gcpRes.Add(map[string]interface{}{ + "lacework_account": laceworkAccount, + "gcp_projects": schema.NewSet(schema.HashSchema(gcpAccountsSchema), + mappingValue["gcp_projects"].([]interface{}), + ), + }) + } + return gcpRes } diff --git a/lacework/resource_lacework_integration_aws_ct.go b/lacework/resource_lacework_integration_aws_ct.go index 30654b1ae..9f908acf5 100644 --- a/lacework/resource_lacework_integration_aws_ct.go +++ b/lacework/resource_lacework_integration_aws_ct.go @@ -135,7 +135,7 @@ func resourceLaceworkIntegrationAwsCloudTrailCreate(d *schema.ResourceData, meta } ) // verify if the user provided an account mapping - accountMapFile := getResourceOrgAccountMappings(d) + accountMapFile := getResourceOrgAccountMappings(d, awsMappingType) if !accountMapFile.Empty() { accountMapFileBytes, err := json.Marshal(accountMapFile) if err != nil { @@ -234,7 +234,7 @@ func resourceLaceworkIntegrationAwsCloudTrailRead(d *schema.ResourceData, meta i } - err = d.Set("org_account_mappings", flattenOrgAccountMappings(accountMapFile)) + err = d.Set("org_account_mappings", flattenOrgAccountMappings(accountMapFile, awsMappingType)) if err != nil { return fmt.Errorf("Error flattening organization account mapping: %s", err) } @@ -262,7 +262,7 @@ func resourceLaceworkIntegrationAwsCloudTrailUpdate(d *schema.ResourceData, meta ) // verify if the user provided an account mapping - accountMapFile := getResourceOrgAccountMappings(d) + accountMapFile := getResourceOrgAccountMappings(d, awsMappingType) if !accountMapFile.Empty() { accountMapFileBytes, err := json.Marshal(accountMapFile) if err != nil { diff --git a/lacework/resource_lacework_integration_aws_org_agentless_scanning.go b/lacework/resource_lacework_integration_aws_org_agentless_scanning.go index 648445fd9..a12ac1b57 100644 --- a/lacework/resource_lacework_integration_aws_org_agentless_scanning.go +++ b/lacework/resource_lacework_integration_aws_org_agentless_scanning.go @@ -175,7 +175,7 @@ var awsOrgAgentlessScanningIntegrationSchema = map[string]*schema.Schema{ "lacework_account": { Type: schema.TypeString, Required: true, - Description: "The Lacework account name where the CloudTrail activity from the selected AWS accounts will appear.", + Description: "The Lacework account name where the Agentless activity from the selected AWS accounts will appear.", }, "aws_accounts": { Type: schema.TypeSet, @@ -214,7 +214,7 @@ func resourceLaceworkIntegrationAwsOrgAgentlessScanningCreate(d *schema.Resource } // verify if the user provided an account mapping - accountMapFile := getResourceOrgAccountMappings(d) + accountMapFile := getResourceOrgAccountMappings(d, awsMappingType) if !accountMapFile.Empty() { accountMapFileBytes, err := json.Marshal(accountMapFile) if err != nil { @@ -318,7 +318,7 @@ func resourceLaceworkIntegrationAwsOrgAgentlessScanningRead(d *schema.ResourceDa } - err = d.Set("org_account_mappings", flattenOrgAccountMappings(accountMapFile)) + err = d.Set("org_account_mappings", flattenOrgAccountMappings(accountMapFile, awsMappingType)) if err != nil { return fmt.Errorf("Error flattening organization account mapping: %s", err) } @@ -354,7 +354,7 @@ func resourceLaceworkIntegrationAwsOrgAgentlessScanningUpdate(d *schema.Resource } // verify if the user provided an account mapping - accountMapFile := getResourceOrgAccountMappings(d) + accountMapFile := getResourceOrgAccountMappings(d, awsMappingType) if !accountMapFile.Empty() { accountMapFileBytes, err := json.Marshal(accountMapFile) if err != nil { diff --git a/lacework/resource_lacework_integration_gcp_agentless_scanning.go b/lacework/resource_lacework_integration_gcp_agentless_scanning.go index 3e7c84829..1ccbfb0a6 100644 --- a/lacework/resource_lacework_integration_gcp_agentless_scanning.go +++ b/lacework/resource_lacework_integration_gcp_agentless_scanning.go @@ -2,6 +2,7 @@ package lacework import ( "context" + "encoding/json" "fmt" "log" "strings" @@ -18,198 +19,233 @@ func resourceLaceworkIntegrationGcpAgentlessScanning() *schema.Resource { Read: resourceLaceworkIntegrationGcpAgentlessScanningRead, Update: resourceLaceworkIntegrationGcpAgentlessScanningUpdate, Delete: resourceLaceworkIntegrationGcpAgentlessScanningDelete, - + Schema: gcpAgentlessScanningIntegrationSchema, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, + } +} - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - Description: "The integration name.", - }, - "intg_guid": { - Type: schema.TypeString, - Computed: true, - }, - "enabled": { - Type: schema.TypeBool, - Optional: true, - Default: true, - Description: "The state of the external integration.", - }, - "retries": { - Type: schema.TypeInt, - Optional: true, - Default: 5, - Description: "The number of attempts to create the external integration.", - }, - "credentials": { - Type: schema.TypeList, - MaxItems: 1, - Required: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "client_id": { - Type: schema.TypeString, - Required: true, - Description: "Client Id from credentials file.", - }, - "private_key_id": { - Type: schema.TypeString, - Required: true, - DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - return !d.HasChanges( - "name", "resource_level", "resource_id", "org_level", "enabled", - "credentials.0.client_id", - "credentials.0.client_email", - ) - }, - Description: "Private Key Id from credentials file.", - }, - "client_email": { - Type: schema.TypeString, - Required: true, - Description: "Client email from credentials file.", - }, - "private_key": { - Type: schema.TypeString, - Required: true, - Sensitive: true, - DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - // @afiune we can't compare this element since our API, for security reasons, - // does NOT return the private key configured in the Lacework server. So if - // any other element changed from the credentials then we trigger a diff - return !d.HasChanges( - "name", "resource_level", "resource_id", "org_level", "enabled", - "credentials.0.client_id", - "credentials.0.client_email", - ) - }, - Description: "Private Key from credentials file.", - }, - "token_uri": { - Type: schema.TypeString, - Optional: true, - Default: "https://oauth2.googleapis.com/token", - Description: "Token URI from credentials file.", - }, +var gcpAgentlessScanningIntegrationSchema = map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "The integration name.", + }, + "intg_guid": { + Type: schema.TypeString, + Computed: true, + }, + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "The state of the external integration.", + }, + "retries": { + Type: schema.TypeInt, + Optional: true, + Default: 5, + Description: "The number of attempts to create the external integration.", + }, + "credentials": { + Type: schema.TypeList, + MaxItems: 1, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "client_id": { + Type: schema.TypeString, + Required: true, + Description: "Client Id from credentials file.", + }, + "private_key_id": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + return !d.HasChanges( + "name", "resource_level", "resource_id", "org_level", "enabled", + "credentials.0.client_id", + "credentials.0.client_email", + ) }, + Description: "Private Key Id from credentials file.", }, - }, - "resource_level": { - Type: schema.TypeString, - Optional: true, - Default: api.GcpProjectIntegration.String(), - StateFunc: func(val interface{}) string { - return strings.ToUpper(val.(string)) + "client_email": { + Type: schema.TypeString, + Required: true, + Description: "Client email from credentials file.", }, - ValidateFunc: func(value interface{}, key string) ([]string, []error) { - switch strings.ToUpper(value.(string)) { - case api.GcpProjectIntegration.String(), - api.GcpOrganizationIntegration.String(): - return nil, nil - default: - return nil, []error{ - fmt.Errorf("%s: can only be either '%s' or '%s'", - key, - api.GcpProjectIntegration.String(), - api.GcpOrganizationIntegration.String()), - } - } + "private_key": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + // @afiune we can't compare this element since our API, for security reasons, + // does NOT return the private key configured in the Lacework server. So if + // any other element changed from the credentials then we trigger a diff + return !d.HasChanges( + "name", "resource_level", "resource_id", "org_level", "enabled", + "credentials.0.client_id", + "credentials.0.client_email", + ) + }, + Description: "Private Key from credentials file.", + }, + "token_uri": { + Type: schema.TypeString, + Optional: true, + Default: "https://oauth2.googleapis.com/token", + Description: "Token URI from credentials file.", }, - Description: "Integration level - ORGANIZATION / PROJECT.", - }, - "resource_id": { - Type: schema.TypeString, - Required: true, - Description: "Organization Id or Project Id.", - }, - "created_or_updated_time": { - Type: schema.TypeString, - Computed: true, - }, - "created_or_updated_by": { - Type: schema.TypeString, - Computed: true, - }, - "type_name": { - Type: schema.TypeString, - Computed: true, - }, - "org_level": { - Type: schema.TypeBool, - Computed: true, - }, - "server_token": { - Type: schema.TypeString, - Computed: true, - }, - "uri": { - Type: schema.TypeString, - Computed: true, - }, - "bucket_name": { - Type: schema.TypeString, - Required: true, - Description: "Bucket containing analysis results shared with Lacework platform.", - }, - "scanning_project_id": { - Type: schema.TypeString, - Required: true, - Description: "Project ID where scanner is deployed.", - }, - "scan_frequency": { - Type: schema.TypeInt, - Optional: true, - Default: 24, - Description: "How often in hours the scan will run in hours.", - }, - "scan_containers": { - Type: schema.TypeBool, - Optional: true, - Default: true, - Description: "Whether to includes scanning for containers.", - }, - "scan_host_vulnerabilities": { - Type: schema.TypeBool, - Optional: true, - Default: true, - Description: "Whether to includes scanning for host vulnerabilities.", - }, - "scan_multi_volume": { - Type: schema.TypeBool, - Optional: true, - Default: false, - Description: "Whether to scan secondary volumes (true) or only root volumes (false)", - }, - "scan_stopped_instances": { - Type: schema.TypeBool, - Optional: true, - Default: true, - Description: "Whether to scan stopped instances (true)", }, - "query_text": { - Type: schema.TypeString, - Optional: true, - Default: "", - Description: "The LQL query text.", + }, + }, + "resource_level": { + Type: schema.TypeString, + Optional: true, + Default: api.GcpProjectIntegration.String(), + StateFunc: func(val interface{}) string { + return strings.ToUpper(val.(string)) + }, + ValidateFunc: func(value interface{}, key string) ([]string, []error) { + switch strings.ToUpper(value.(string)) { + case api.GcpProjectIntegration.String(), + api.GcpOrganizationIntegration.String(): + return nil, nil + default: + return nil, []error{ + fmt.Errorf("%s: can only be either '%s' or '%s'", + key, + api.GcpProjectIntegration.String(), + api.GcpOrganizationIntegration.String()), + } + } + }, + Description: "Integration level - ORGANIZATION / PROJECT.", + }, + "resource_id": { + Type: schema.TypeString, + Required: true, + Description: "Organization Id or Project Id.", + }, + "created_or_updated_time": { + Type: schema.TypeString, + Computed: true, + }, + "created_or_updated_by": { + Type: schema.TypeString, + Computed: true, + }, + "type_name": { + Type: schema.TypeString, + Computed: true, + }, + "org_level": { + Type: schema.TypeBool, + Computed: true, + }, + "server_token": { + Type: schema.TypeString, + Computed: true, + }, + "uri": { + Type: schema.TypeString, + Computed: true, + }, + "bucket_name": { + Type: schema.TypeString, + Required: true, + Description: "Bucket containing analysis results shared with Lacework platform.", + }, + "scanning_project_id": { + Type: schema.TypeString, + Required: true, + Description: "Project ID where scanner is deployed.", + }, + "scan_frequency": { + Type: schema.TypeInt, + Optional: true, + Default: 24, + Description: "How often in hours the scan will run in hours.", + }, + "scan_containers": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Whether to includes scanning for containers.", + }, + "scan_host_vulnerabilities": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Whether to includes scanning for host vulnerabilities.", + }, + "scan_multi_volume": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Whether to scan secondary volumes (true) or only root volumes (false)", + }, + "scan_stopped_instances": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Whether to scan stopped instances (true)", + }, + "query_text": { + Type: schema.TypeString, + Optional: true, + Default: "", + Description: "The LQL query text.", + }, + "filter_list": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + StateFunc: func(val interface{}) string { + return strings.TrimSpace(val.(string)) }, - "filter_list": { - Type: schema.TypeList, - Optional: true, - Elem: &schema.Schema{ - Type: schema.TypeString, - StateFunc: func(val interface{}) string { - return strings.TrimSpace(val.(string)) + }, + Default: nil, + Description: "List of Projects to specifically include/exclude.", + }, + "org_account_mappings": { + Type: schema.TypeList, + Optional: true, + Description: "Mapping of GCP projects to Lacework accounts within a Lacework organization.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "default_lacework_account": { + Type: schema.TypeString, + Required: true, + Description: "The default Lacework account name where any non-mapped GCP project will appear", + }, + "mapping": { + Type: schema.TypeSet, + Required: true, + Description: "A map of GCP projects to Lacework account. This can be specified multiple times to map multiple Lacework accounts.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "lacework_account": { + Type: schema.TypeString, + Required: true, + Description: "The Lacework account name where the Agentless activity from the selected gcp projects will appear.", + }, + "gcp_projects": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + MinItems: 1, + Required: true, + Description: "The list of GCP project IDs to map.", + }, + }, }, }, - Default: nil, - Description: "List of Projects to specifically include/exclude.", }, }, - } + }, } func resourceLaceworkIntegrationGcpAgentlessScanningCreate(d *schema.ResourceData, meta interface{}) error { @@ -219,35 +255,47 @@ func resourceLaceworkIntegrationGcpAgentlessScanningCreate(d *schema.ResourceDat resourceLevel = api.GcpProjectIntegration ) - if strings.ToUpper( - d.Get("resource_level").(string), - ) == api.GcpOrganizationIntegration.String() { + if strings.ToUpper(d.Get("resource_level").(string)) == api.GcpOrganizationIntegration.String() { resourceLevel = api.GcpOrganizationIntegration } + log.Printf("[INFO] Creating %s integration\n", api.GcpSidekickCloudAccount.String()) + gcpSidekickData := api.GcpSidekickData{ + ID: d.Get("resource_id").(string), + IDType: resourceLevel.String(), + Credentials: api.GcpSidekickCredentials{ + ClientID: d.Get("credentials.0.client_id").(string), + ClientEmail: d.Get("credentials.0.client_email").(string), + PrivateKeyID: d.Get("credentials.0.private_key_id").(string), + PrivateKey: d.Get("credentials.0.private_key").(string), + TokenUri: d.Get("credentials.0.token_uri").(string), + }, + SharedBucket: d.Get("bucket_name").(string), + ScanningProjectId: d.Get("scanning_project_id").(string), + ScanFrequency: d.Get("scan_frequency").(int), + ScanContainers: d.Get("scan_containers").(bool), + ScanHostVulnerabilities: d.Get("scan_host_vulnerabilities").(bool), + ScanMultiVolume: d.Get("scan_multi_volume").(bool), + ScanStoppedInstances: d.Get("scan_stopped_instances").(bool), + QueryText: d.Get("query_text").(string), + FilterList: strings.Join(castAttributeToStringSlice(d, "filter_list"), ", "), + } + + // verify if the user provided an account mapping + accountMapFile := getResourceOrgAccountMappings(d, gcpMappingType) + if !accountMapFile.Empty() { + accountMapFileBytes, err := json.Marshal(accountMapFile) + if err != nil { + return err + } + + gcpSidekickData.EncodeAccountMappingFile(accountMapFileBytes) + } + data := api.NewCloudAccount(d.Get("name").(string), api.GcpSidekickCloudAccount, - api.GcpSidekickData{ - ID: d.Get("resource_id").(string), - IDType: resourceLevel.String(), - Credentials: api.GcpSidekickCredentials{ - ClientID: d.Get("credentials.0.client_id").(string), - ClientEmail: d.Get("credentials.0.client_email").(string), - PrivateKeyID: d.Get("credentials.0.private_key_id").(string), - PrivateKey: d.Get("credentials.0.private_key").(string), - TokenUri: d.Get("credentials.0.token_uri").(string), - }, - SharedBucket: d.Get("bucket_name").(string), - ScanningProjectId: d.Get("scanning_project_id").(string), - ScanFrequency: d.Get("scan_frequency").(int), - ScanContainers: d.Get("scan_containers").(bool), - ScanHostVulnerabilities: d.Get("scan_host_vulnerabilities").(bool), - ScanMultiVolume: d.Get("scan_multi_volume").(bool), - ScanStoppedInstances: d.Get("scan_stopped_instances").(bool), - QueryText: d.Get("query_text").(string), - FilterList: strings.Join(castAttributeToStringSlice(d, "filter_list"), ", "), - }, + gcpSidekickData, ) if !d.Get("enabled").(bool) { @@ -340,6 +388,27 @@ func resourceLaceworkIntegrationGcpAgentlessScanningRead(d *schema.ResourceData, d.Set("filter_list", trimmed_filter_list) } + accountMapFileBytes, err := integration.Data.DecodeAccountMappingFile() + if err != nil { + return err + } + + accountMapFile := new(accountMappingsFile) + if len(accountMapFileBytes) != 0 { + // The integration has an account mapping file + // unmarshal its content into the account mapping struct + err := json.Unmarshal(accountMapFileBytes, accountMapFile) + if err != nil { + return fmt.Errorf("Error decoding organization account mapping: %s", err) + } + + } + + err = d.Set("org_account_mappings", flattenOrgGcpAccountMappings(accountMapFile)) + if err != nil { + return fmt.Errorf("Error flattening organization account mapping: %s", err) + } + log.Printf("[INFO] Read %s integration with guid: %v\n", api.GcpSidekickCloudAccount.String(), integration.IntgGuid) return nil @@ -359,28 +428,41 @@ func resourceLaceworkIntegrationGcpAgentlessScanningUpdate(d *schema.ResourceDat resourceLevel = api.GcpOrganizationIntegration } + gcpSidekickData := api.GcpSidekickData{ + ID: d.Get("resource_id").(string), + IDType: resourceLevel.String(), + Credentials: api.GcpSidekickCredentials{ + ClientID: d.Get("credentials.0.client_id").(string), + ClientEmail: d.Get("credentials.0.client_email").(string), + PrivateKeyID: d.Get("credentials.0.private_key_id").(string), + PrivateKey: d.Get("credentials.0.private_key").(string), + TokenUri: d.Get("credentials.0.token_uri").(string), + }, + SharedBucket: d.Get("bucket_name").(string), + ScanningProjectId: d.Get("scanning_project_id").(string), + ScanFrequency: d.Get("scan_frequency").(int), + ScanContainers: d.Get("scan_containers").(bool), + ScanHostVulnerabilities: d.Get("scan_host_vulnerabilities").(bool), + ScanMultiVolume: d.Get("scan_multi_volume").(bool), + ScanStoppedInstances: d.Get("scan_stopped_instances").(bool), + QueryText: d.Get("query_text").(string), + FilterList: strings.Join(castAttributeToStringSlice(d, "filter_list"), ", "), + } + + // verify if the user provided an account mapping + accountMapFile := getResourceOrgAccountMappings(d, gcpMappingType) + if !accountMapFile.Empty() { + accountMapFileBytes, err := json.Marshal(accountMapFile) + if err != nil { + return err + } + + gcpSidekickData.EncodeAccountMappingFile(accountMapFileBytes) + } + data := api.NewCloudAccount(d.Get("name").(string), api.GcpSidekickCloudAccount, - api.GcpSidekickData{ - ID: d.Get("resource_id").(string), - IDType: resourceLevel.String(), - Credentials: api.GcpSidekickCredentials{ - ClientID: d.Get("credentials.0.client_id").(string), - ClientEmail: d.Get("credentials.0.client_email").(string), - PrivateKeyID: d.Get("credentials.0.private_key_id").(string), - PrivateKey: d.Get("credentials.0.private_key").(string), - TokenUri: d.Get("credentials.0.token_uri").(string), - }, - SharedBucket: d.Get("bucket_name").(string), - ScanningProjectId: d.Get("scanning_project_id").(string), - ScanFrequency: d.Get("scan_frequency").(int), - ScanContainers: d.Get("scan_containers").(bool), - ScanHostVulnerabilities: d.Get("scan_host_vulnerabilities").(bool), - ScanMultiVolume: d.Get("scan_multi_volume").(bool), - ScanStoppedInstances: d.Get("scan_stopped_instances").(bool), - QueryText: d.Get("query_text").(string), - FilterList: strings.Join(castAttributeToStringSlice(d, "filter_list"), ", "), - }, + gcpSidekickData, ) if !d.Get("enabled").(bool) {