From f9bdeec996e5357a22989c4bcff8406d51931234 Mon Sep 17 00:00:00 2001 From: Axton Grams Date: Fri, 14 Oct 2022 11:01:15 -0500 Subject: [PATCH] New Data Source: wiz_host_config_rules (#30) Added a new data source to retrieve Wiz host configuration rules. closes #24 --- README.md | 2 +- docs/data-sources/host_config_rules.md | 56 ++++ .../wiz_host_config_rules/data-source.tf | 5 + .../data_source_host_configuration_rules.go | 316 ++++++++++++++++++ ...ta_source_host_configuration_rules_test.go | 40 +++ internal/provider/provider.go | 1 + internal/vendor/wiz.go | 27 ++ 7 files changed, 446 insertions(+), 1 deletion(-) create mode 100644 docs/data-sources/host_config_rules.md create mode 100644 examples/data-sources/wiz_host_config_rules/data-source.tf create mode 100644 internal/provider/data_source_host_configuration_rules.go create mode 100644 internal/provider/data_source_host_configuration_rules_test.go diff --git a/README.md b/README.md index b8bb6fb..38a96db 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Binaries are available for tagged releases in this repository. Once you have the provider installed, follow the instructions in the docs folder to understand what options are available. The documentation includes examples. ## Using the Provider -See the [provider docs](docs/index.md) +See the [provider docs](https://registry.terraform.io/providers/AxtonGrams/wiz/latest/docs) ## Contributing We welcome your contribution. Please understand that the experimental nature of this repository means that contributing code may be a bit of a moving target. If you have an idea for an enhancement or bug fix, and want to take on the work yourself, please first create an issue so that we can discuss the implementation with you before you proceed with the work. diff --git a/docs/data-sources/host_config_rules.md b/docs/data-sources/host_config_rules.md new file mode 100644 index 0000000..c250d3d --- /dev/null +++ b/docs/data-sources/host_config_rules.md @@ -0,0 +1,56 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "wiz_host_config_rules Data Source - terraform-provider-wiz" +subcategory: "" +description: |- + Query cloud configuration rules. +--- + +# wiz_host_config_rules (Data Source) + +Query cloud configuration rules. + +## Example Usage + +```terraform +# get the first five host configuration rules for access keys +data "wiz_host_config_rules" "access" { + first = 5 + search = "access" +} +``` + + +## Schema + +### Optional + +- `enabled` (Boolean) Host Configuration Rule enabled status. +- `first` (Number) How many results to return + - Defaults to `500`. +- `framework_category` (List of String) Search rules by any of securityFramework | securitySubCategory | securityCategory. +- `search` (String) Free text search on id, name, externalId. +- `target_platform` (List of String) Search by target platforms. + +### Read-Only + +- `host_configuration_rules` (Set of Object) The returned cloud configuration rules. (see [below for nested schema](#nestedatt--host_configuration_rules)) +- `id` (String) Internal identifier for the data. + + +### Nested Schema for `host_configuration_rules` + +Read-Only: + +- `builtin` (Boolean) +- `description` (String) +- `direct_oval` (String) +- `enabled` (Boolean) +- `external_id` (String) +- `id` (String) +- `name` (String) +- `security_sub_category_ids` (List of String) +- `short_name` (String) +- `target_platform_ids` (List of String) + + diff --git a/examples/data-sources/wiz_host_config_rules/data-source.tf b/examples/data-sources/wiz_host_config_rules/data-source.tf new file mode 100644 index 0000000..0547894 --- /dev/null +++ b/examples/data-sources/wiz_host_config_rules/data-source.tf @@ -0,0 +1,5 @@ +# get the first five host configuration rules for access keys +data "wiz_host_config_rules" "access" { + first = 5 + search = "access" +} diff --git a/internal/provider/data_source_host_configuration_rules.go b/internal/provider/data_source_host_configuration_rules.go new file mode 100644 index 0000000..5e8aa0a --- /dev/null +++ b/internal/provider/data_source_host_configuration_rules.go @@ -0,0 +1,316 @@ +package provider + +import ( + "bytes" + "context" + "crypto/sha1" + "encoding/hex" + "fmt" + "sort" + + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "wiz.io/hashicorp/terraform-provider-wiz/internal" + "wiz.io/hashicorp/terraform-provider-wiz/internal/client" + "wiz.io/hashicorp/terraform-provider-wiz/internal/utils" + "wiz.io/hashicorp/terraform-provider-wiz/internal/vendor" +) + +func dataSourceWizHostConfigurationRules() *schema.Resource { + return &schema.Resource{ + Description: "Query cloud configuration rules.", + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + Description: "Internal identifier for the data.", + }, + "first": { + Type: schema.TypeInt, + Optional: true, + Default: 500, + Description: "How many results to return", + }, + "search": { + Type: schema.TypeString, + Optional: true, + Description: "Free text search on id, name, externalId.", + }, + "enabled": { + Type: schema.TypeBool, + Optional: true, + Description: "Host Configuration Rule enabled status.", + }, + "framework_category": { + Type: schema.TypeList, + Optional: true, + Description: "Search rules by any of securityFramework | securitySubCategory | securityCategory.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "target_platform": { + Type: schema.TypeList, + Optional: true, + Description: "Search by target platforms.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "host_configuration_rules": { + Type: schema.TypeSet, + Computed: true, + Description: "The returned cloud configuration rules.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + Description: "Wiz UUID.", + }, + "external_id": { + Type: schema.TypeString, + Computed: true, + Description: "An external id for the rule.", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "The name of the rule.", + }, + "short_name": { + Type: schema.TypeString, + Computed: true, + Description: "A short name that identifies the rule.", + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "enabled": { + Type: schema.TypeBool, + Computed: true, + Description: "Rule enabled status.", + }, + "security_sub_category_ids": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "builtin": { + Type: schema.TypeBool, + Computed: true, + Description: "Indication whether the rule is built-in or custom.", + }, + "direct_oval": { + Type: schema.TypeString, + Computed: true, + Description: "Direct OVAL definition assessed on hosts during disk scanning.", + }, + "target_platform_ids": { + Type: schema.TypeList, + Optional: true, + Description: "The platforms the rule is targeting. e.g Ubuntu, RedHat, NGINX.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, + }, + ReadContext: dataSourceWizHostConfigurationRuleRead, + } +} + +// ReadHostConfigurationRules struct +type ReadHostConfigurationRules struct { + HostConfigurationRules vendor.HostConfigurationRuleConnection `json:"hostConfigurationRules"` +} + +func dataSourceWizHostConfigurationRuleRead(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { + tflog.Info(ctx, "dataSourceWizHostConfigurationRuleRead called...") + + // generate the id for this resource + // id must be deterministic, so the id is based on a hash of the search parameters + var identifier bytes.Buffer + + a, b := d.GetOk("first") + if b { + identifier.WriteString(utils.PrettyPrint(a)) + } + a, b = d.GetOk("search") + if b { + identifier.WriteString(utils.PrettyPrint(a)) + } + a, b = d.GetOk("enabled") + if b { + identifier.WriteString(utils.PrettyPrint(a)) + } + a, b = d.GetOk("framework_category") + if b { + identifier.WriteString(utils.PrettyPrint(a)) + } + a, b = d.GetOk("target_platform") + if b { + identifier.WriteString(utils.PrettyPrint(a)) + } + + h := sha1.New() + h.Write([]byte(identifier.String())) + hashID := hex.EncodeToString(h.Sum(nil)) + + // Set the id + d.SetId(hashID) + + // define the graphql query + query := `query hostConfigurationRules( + $filterBy: HostConfigurationRuleFilters + $first: Int + $after: String + $orderBy: HostConfigurationRuleOrder + ) { + hostConfigurationRules( + filterBy: $filterBy + first: $first + after: $after + orderBy: $orderBy + ) { + nodes { + id + externalId + name + shortName + description + enabled + securitySubCategories { + id + } + builtin + directOVAL + targetPlatforms { + id + } + } + pageInfo { + hasNextPage + endCursor + } + totalCount + } + }` + + // set the resource parameters + err := d.Set("first", d.Get("first").(int)) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("search", d.Get("search").(string)) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + a, b = d.GetOk("enabled") + if b { + err = d.Set("enabled", a.(bool)) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + } + err = d.Set("framework_category", d.Get("framework_category").([]interface{})) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("target_platform", d.Get("target_platform").([]interface{})) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + + // populate the graphql variables + vars := &internal.QueryVariables{} + vars.First = d.Get("first").(int) + filterBy := &vendor.HostConfigurationRuleFilters{} + a, b = d.GetOk("search") + if b { + filterBy.Search = a.(string) + } + a, b = d.GetOk("enabled") + if b { + filterBy.Enabled = utils.ConvertBoolToPointer(a.(bool)) + } + a, b = d.GetOk("framework_category") + if b { + filterBy.FrameworkCategory = utils.ConvertListToString(a.([]interface{})) + } + a, b = d.GetOk("target_platform") + if b { + filterBy.TargetPlatforms = utils.ConvertListToString(a.([]interface{})) + } + + vars.FilterBy = filterBy + + // process the request + data := &ReadHostConfigurationRules{} + requestDiags := client.ProcessRequest(ctx, m, vars, data, query, "host_config_rules", "read") + diags = append(diags, requestDiags...) + if len(diags) > 0 { + return diags + } + + hostConfigurationRules := flattenHostConfigurationRules(ctx, &data.HostConfigurationRules.Nodes) + if err := d.Set("host_configuration_rules", hostConfigurationRules); err != nil { + return append(diags, diag.FromErr(err)...) + } + + return diags +} + +func flattenHostConfigurationRules(ctx context.Context, nodes *[]*vendor.HostConfigurationRule) []interface{} { + tflog.Info(ctx, "flattenHostConfigurationRules called...") + tflog.Debug(ctx, fmt.Sprintf("HostConfigurationRules: %s", utils.PrettyPrint(nodes))) + + // walk the slice and construct the list + var output = make([]interface{}, 0, 0) + for _, b := range *nodes { + tflog.Debug(ctx, fmt.Sprintf("b: %T %s", b, utils.PrettyPrint(b))) + ruleMap := make(map[string]interface{}) + ruleMap["id"] = b.ID + ruleMap["name"] = b.Name + ruleMap["short_name"] = b.ShortName + ruleMap["builtin"] = b.Builtin + ruleMap["description"] = b.Description + ruleMap["direct_oval"] = b.DirectOVAL + ruleMap["external_id"] = b.ExternalID + ruleMap["security_sub_category_ids"] = flattenSecuritySubCategoryIDs(ctx, &b.SecuritySubCategories) + ruleMap["target_platform_ids"] = flattenTargetPlatformIDs(ctx, b.TargetPlatforms) + + output = append(output, ruleMap) + } + + tflog.Debug(ctx, fmt.Sprintf("flattenCloudConfigurationRules output: %s", utils.PrettyPrint(output))) + + return output +} + +func flattenTargetPlatformIDs(ctx context.Context, plats []vendor.Technology) []interface{} { + tflog.Info(ctx, "flattenTargetPlatformIDs called...") + tflog.Debug(ctx, fmt.Sprintf("TargetPlatforms: %s", utils.PrettyPrint(plats))) + + // walk the slice and construct the list + var output = make([]interface{}, 0, 0) + for _, b := range plats { + tflog.Debug(ctx, fmt.Sprintf("b: %T %s", b, utils.PrettyPrint(b))) + output = append(output, b.ID) + } + + // sort the return slice to avoid unwanted diffs + sort.Slice(output, func(i, j int) bool { + return output[i].(string) < output[j].(string) + }) + + tflog.Debug(ctx, fmt.Sprintf("flattenTargetPlatformIDs output: %s", utils.PrettyPrint(output))) + + return output +} diff --git a/internal/provider/data_source_host_configuration_rules_test.go b/internal/provider/data_source_host_configuration_rules_test.go new file mode 100644 index 0000000..e6f56d1 --- /dev/null +++ b/internal/provider/data_source_host_configuration_rules_test.go @@ -0,0 +1,40 @@ +package provider + +import ( + "context" + "reflect" + "testing" + + "wiz.io/hashicorp/terraform-provider-wiz/internal/vendor" +) + +func TestFlattenTargetPlatformIDs(t *testing.T) { + ctx := context.Background() + expected := []interface{}{ + "02ef6af4-f2fe-45ea-a119-a4e4150ffb6c", + "b488610a-6846-404c-8e5b-28ee04846dda", + "cdd2c255-921d-4ea9-b348-5660a7b9d459", + } + + var plats = []vendor.Technology{ + { + ID: "cdd2c255-921d-4ea9-b348-5660a7b9d459", + }, + { + ID: "02ef6af4-f2fe-45ea-a119-a4e4150ffb6c", + }, + { + ID: "b488610a-6846-404c-8e5b-28ee04846dda", + }, + } + + flattened := flattenTargetPlatformIDs(ctx, plats) + + if !reflect.DeepEqual(flattened, expected) { + t.Fatalf( + "Got:\n\n%#v\n\nExpected:\n\n%#v\n", + flattened, + expected, + ) + } +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 6243402..1eb6cac 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -251,6 +251,7 @@ yLyKQXhw2W2Xs0qLeC1etA+jTGDK4UfLeC0SF7FSi8o5LL21L8IzApar2pR/ DataSourcesMap: map[string]*schema.Resource{ "wiz_cloud_accounts": dataSourceWizCloudAccounts(), "wiz_cloud_config_rules": dataSourceWizCloudConfigurationRules(), + "wiz_host_config_rules": dataSourceWizHostConfigurationRules(), "wiz_organizations": dataSourceWizOrganizations(), }, ResourcesMap: map[string]*schema.Resource{ diff --git a/internal/vendor/wiz.go b/internal/vendor/wiz.go index af70605..faf7ad1 100644 --- a/internal/vendor/wiz.go +++ b/internal/vendor/wiz.go @@ -1933,6 +1933,7 @@ type HostConfigurationRule struct { Analytics HostConfigurationRuleAnalytics `json:"analytics"` Builtin bool `json:"builtin"` Description string `json:"description,omitempty"` + DirectOVAL string `json:"directOVAL,omitempty"` Enabled bool `json:"enabled"` ExternalID string `json:"externalId,omitempty"` ID string `json:"id"` @@ -2160,3 +2161,29 @@ type CloudConfigurationRuleExternalReference struct { ID string `json:"id"` Name string `json:"name"` } + +// HostConfigurationRuleOrder struct +type HostConfigurationRuleOrder struct { + Direction string `json:"direction"` // enum OrderDirection + Field string `json:"field"` // enum HostConfigurationRuleOrderField +} + +// HostConfigurationRuleOrderField enum +var HostConfigurationRuleOrderField = []string{ + "FAILED_CHECK_COUNT", + "NAME", +} + +// HostConfigurationRuleConnection struct +type HostConfigurationRuleConnection struct { + Edges []*HostConfigurationRuleEdge `json:"edges,omitempty"` + Nodes []*HostConfigurationRule `json:"nodes,omitempty"` + PageInfo PageInfo `json:"pageInfo"` + TotalCount int `json:"totalCount"` +} + +// HostConfigurationRuleEdge struct +type HostConfigurationRuleEdge struct { + Cursor string `json:"cursor"` + Node HostConfigurationRule `json:"node"` +}