diff --git a/sysdig/common.go b/sysdig/common.go index b5eef924..6687f83b 100644 --- a/sysdig/common.go +++ b/sysdig/common.go @@ -48,6 +48,7 @@ const ( SchemaTargetKey = "target" SchemaZonesKey = "zones" SchemaZonesIDsKey = "zone_ids" + SchemaZoneIDKey = "zone_id" SchemaAllZones = "all_zones" SchemaScopeKey = "scope" SchemaScopesKey = "scopes" diff --git a/sysdig/internal/client/v2/client.go b/sysdig/internal/client/v2/client.go index e7a05b05..5271b7ba 100644 --- a/sysdig/internal/client/v2/client.go +++ b/sysdig/internal/client/v2/client.go @@ -63,6 +63,7 @@ type SecureCommon interface { PostureVulnerabilityAcceptRiskInterface ZoneInterface ZoneV2Interface + ZonePolicyAssignmentInterface } type Requester interface { diff --git a/sysdig/internal/client/v2/model.go b/sysdig/internal/client/v2/model.go index 18dbc333..20069e86 100644 --- a/sysdig/internal/client/v2/model.go +++ b/sysdig/internal/client/v2/model.go @@ -1123,6 +1123,15 @@ type PostureZoneResponse struct { Data PostureZone `json:"data"` } +type ZonePolicyAssignment struct { + ZoneID int `json:"zoneId"` + PolicyIDs []int `json:"policyIds"` +} + +type ZonePolicyAssignmentRequest struct { + PolicyIDs []int `json:"policyIds"` +} + type IdentityContext struct { IdentityType string `json:"identityType"` CustomerID int `json:"customerId"` diff --git a/sysdig/internal/client/v2/zone_policy_assignment.go b/sysdig/internal/client/v2/zone_policy_assignment.go new file mode 100644 index 00000000..ca8bd40a --- /dev/null +++ b/sysdig/internal/client/v2/zone_policy_assignment.go @@ -0,0 +1,103 @@ +package v2 + +import ( + "context" + "fmt" + "net/http" +) + +const zonePolicyAssignmentPath = "%s/api/cspm/v1/zones/%d/policies" + +type ZonePolicyAssignmentInterface interface { + Base + GetZonePolicyAssignment(ctx context.Context, zoneID int) (*ZonePolicyAssignment, error) + CreateZonePolicyAssignment(ctx context.Context, zoneID int, req *ZonePolicyAssignmentRequest) (*ZonePolicyAssignment, error) + UpdateZonePolicyAssignment(ctx context.Context, zoneID int, req *ZonePolicyAssignmentRequest) (*ZonePolicyAssignment, error) + DeleteZonePolicyAssignment(ctx context.Context, zoneID int) error +} + +func (c *Client) GetZonePolicyAssignment(ctx context.Context, zoneID int) (result *ZonePolicyAssignment, err error) { + response, err := c.requester.Request(ctx, http.MethodGet, c.getZonePolicyAssignmentURL(zoneID), nil) + if err != nil { + return nil, err + } + defer func() { + if dErr := response.Body.Close(); dErr != nil { + err = fmt.Errorf("unable to close response body: %w", dErr) + } + }() + + if response.StatusCode != http.StatusOK { + return nil, c.APIErrorFromResponse(response) + } + + return Unmarshal[*ZonePolicyAssignment](response.Body) +} + +func (c *Client) CreateZonePolicyAssignment(ctx context.Context, zoneID int, req *ZonePolicyAssignmentRequest) (result *ZonePolicyAssignment, err error) { + payload, err := Marshal(req) + if err != nil { + return nil, err + } + + response, err := c.requester.Request(ctx, http.MethodPost, c.getZonePolicyAssignmentURL(zoneID), payload) + if err != nil { + return nil, err + } + defer func() { + if dErr := response.Body.Close(); dErr != nil { + err = fmt.Errorf("unable to close response body: %w", dErr) + } + }() + + if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusCreated { + return nil, c.APIErrorFromResponse(response) + } + + return Unmarshal[*ZonePolicyAssignment](response.Body) +} + +func (c *Client) UpdateZonePolicyAssignment(ctx context.Context, zoneID int, req *ZonePolicyAssignmentRequest) (result *ZonePolicyAssignment, err error) { + payload, err := Marshal(req) + if err != nil { + return nil, err + } + + response, err := c.requester.Request(ctx, http.MethodPut, c.getZonePolicyAssignmentURL(zoneID), payload) + if err != nil { + return nil, err + } + defer func() { + if dErr := response.Body.Close(); dErr != nil { + err = fmt.Errorf("unable to close response body: %w", dErr) + } + }() + + if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusCreated { + return nil, c.APIErrorFromResponse(response) + } + + return Unmarshal[*ZonePolicyAssignment](response.Body) +} + +func (c *Client) DeleteZonePolicyAssignment(ctx context.Context, zoneID int) (err error) { + response, err := c.requester.Request(ctx, http.MethodDelete, c.getZonePolicyAssignmentURL(zoneID), nil) + if err != nil { + return err + } + defer func() { + if dErr := response.Body.Close(); dErr != nil { + err = fmt.Errorf("unable to close response body: %w", dErr) + } + }() + + if response.StatusCode != http.StatusNoContent && response.StatusCode != http.StatusOK && response.StatusCode != http.StatusNotFound { + return c.APIErrorFromResponse(response) + } + + return nil +} + +func (c *Client) getZonePolicyAssignmentURL(zoneID int) string { + return fmt.Sprintf(zonePolicyAssignmentPath, c.config.url, zoneID) +} diff --git a/sysdig/provider.go b/sysdig/provider.go index df2a83c3..38467bf3 100644 --- a/sysdig/provider.go +++ b/sysdig/provider.go @@ -195,6 +195,7 @@ func (p *SysdigProvider) Provider() *schema.Provider { "sysdig_secure_vulnerability_policy": resourceSysdigSecureVulnerabilityPolicy(), "sysdig_secure_vulnerability_rule_bundle": resourceSysdigSecureVulnerabilityRuleBundle(), "sysdig_secure_zone": resourceSysdigSecureZone(), + "sysdig_secure_zone_posture_policy_assignment": resourceSysdigSecureZonePosturePolicyAssignment(), }, DataSourcesMap: map[string]*schema.Resource{ "sysdig_agent_access_key": dataSourceSysdigAgentAccessKey(), diff --git a/sysdig/resource_sysdig_secure_zone_posture_policy_assignment.go b/sysdig/resource_sysdig_secure_zone_posture_policy_assignment.go new file mode 100644 index 00000000..66eeabdc --- /dev/null +++ b/sysdig/resource_sysdig_secure_zone_posture_policy_assignment.go @@ -0,0 +1,154 @@ +package sysdig + +import ( + "context" + "fmt" + "strconv" + "time" + + v2 "github.com/draios/terraform-provider-sysdig/sysdig/internal/client/v2" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceSysdigSecureZonePosturePolicyAssignment() *schema.Resource { + timeout := 5 * time.Minute + + return &schema.Resource{ + CreateContext: resourceSysdigSecureZonePosturePolicyAssignmentCreate, + ReadContext: resourceSysdigSecureZonePosturePolicyAssignmentRead, + UpdateContext: resourceSysdigSecureZonePosturePolicyAssignmentUpdate, + DeleteContext: resourceSysdigSecureZonePosturePolicyAssignmentDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(timeout), + Update: schema.DefaultTimeout(timeout), + Read: schema.DefaultTimeout(timeout), + Delete: schema.DefaultTimeout(timeout), + }, + Schema: map[string]*schema.Schema{ + SchemaZoneIDKey: { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + SchemaPolicyIDsKey: { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeInt}, + }, + }, + } +} + +func getZonePolicyAssignmentClient(clients SysdigClients) (v2.ZonePolicyAssignmentInterface, error) { + var client v2.ZonePolicyAssignmentInterface + var err error + switch clients.GetClientType() { + case IBMSecure: + client, err = clients.ibmSecureClient() + default: + client, err = clients.sysdigSecureClientV2() + } + if err != nil { + return nil, err + } + return client, nil +} + +func resourceSysdigSecureZonePosturePolicyAssignmentCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + client, err := getZonePolicyAssignmentClient(m.(SysdigClients)) + if err != nil { + return diag.FromErr(err) + } + + zoneID := d.Get(SchemaZoneIDKey).(int) + req := &v2.ZonePolicyAssignmentRequest{ + PolicyIDs: expandIntSet(d.Get(SchemaPolicyIDsKey).(*schema.Set)), + } + + _, err = client.CreateZonePolicyAssignment(ctx, zoneID, req) + if err != nil { + return diag.FromErr(fmt.Errorf("error creating zone policy assignment: %w", err)) + } + + d.SetId(strconv.Itoa(zoneID)) + return resourceSysdigSecureZonePosturePolicyAssignmentRead(ctx, d, m) +} + +func resourceSysdigSecureZonePosturePolicyAssignmentRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + client, err := getZonePolicyAssignmentClient(m.(SysdigClients)) + if err != nil { + return diag.FromErr(err) + } + + zoneID, err := strconv.Atoi(d.Id()) + if err != nil { + return diag.FromErr(fmt.Errorf("invalid zone id %q: %w", d.Id(), err)) + } + + assignment, err := client.GetZonePolicyAssignment(ctx, zoneID) + if err != nil { + if isNotFound(err) { + d.SetId("") + return nil + } + return diag.FromErr(fmt.Errorf("error reading zone policy assignment for zone %d: %w", zoneID, err)) + } + + _ = d.Set(SchemaZoneIDKey, zoneID) + _ = d.Set(SchemaPolicyIDsKey, assignment.PolicyIDs) + return nil +} + +func resourceSysdigSecureZonePosturePolicyAssignmentUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + client, err := getZonePolicyAssignmentClient(m.(SysdigClients)) + if err != nil { + return diag.FromErr(err) + } + + zoneID, err := strconv.Atoi(d.Id()) + if err != nil { + return diag.FromErr(fmt.Errorf("invalid zone id %q: %w", d.Id(), err)) + } + + req := &v2.ZonePolicyAssignmentRequest{ + PolicyIDs: expandIntSet(d.Get(SchemaPolicyIDsKey).(*schema.Set)), + } + + _, err = client.UpdateZonePolicyAssignment(ctx, zoneID, req) + if err != nil { + return diag.FromErr(fmt.Errorf("error updating zone policy assignment for zone %d: %w", zoneID, err)) + } + + return resourceSysdigSecureZonePosturePolicyAssignmentRead(ctx, d, m) +} + +func resourceSysdigSecureZonePosturePolicyAssignmentDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + client, err := getZonePolicyAssignmentClient(m.(SysdigClients)) + if err != nil { + return diag.FromErr(err) + } + + zoneID, err := strconv.Atoi(d.Id()) + if err != nil { + return diag.FromErr(fmt.Errorf("invalid zone id %q: %w", d.Id(), err)) + } + + if err := client.DeleteZonePolicyAssignment(ctx, zoneID); err != nil { + return diag.FromErr(fmt.Errorf("error deleting zone policy assignment for zone %d: %w", zoneID, err)) + } + + d.SetId("") + return nil +} + +func expandIntSet(set *schema.Set) []int { + result := make([]int, 0, set.Len()) + for _, v := range set.List() { + result = append(result, v.(int)) + } + return result +} diff --git a/sysdig/resource_sysdig_secure_zone_posture_policy_assignment_test.go b/sysdig/resource_sysdig_secure_zone_posture_policy_assignment_test.go new file mode 100644 index 00000000..6e04e56e --- /dev/null +++ b/sysdig/resource_sysdig_secure_zone_posture_policy_assignment_test.go @@ -0,0 +1,97 @@ +//go:build tf_acc_sysdig_secure || tf_acc_ibm_secure + +package sysdig_test + +import ( + "fmt" + "testing" + + "github.com/draios/terraform-provider-sysdig/sysdig" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func TestAccSecureZonePosturePolicyAssignment_basic(t *testing.T) { + zoneName := "ZonePolicyAssign_TF_" + randomText(5) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: preCheckAnyEnv(t, SysdigSecureApiTokenEnv, SysdigIBMSecureAPIKeyEnv), + ProviderFactories: map[string]func() (*schema.Provider, error){ + "sysdig": func() (*schema.Provider, error) { + return sysdig.Provider(), nil + }, + }, + Steps: []resource.TestStep{ + // Step 1: Create zone + assignment with 1 policy + { + Config: testAccZonePolicyAssignmentWith1Policy(zoneName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("sysdig_secure_zone_posture_policy_assignment.test", "zone_id"), + resource.TestCheckResourceAttr("sysdig_secure_zone_posture_policy_assignment.test", "policy_ids.#", "1"), + ), + }, + // Step 2: Update to 2 policies + { + Config: testAccZonePolicyAssignmentWith2Policies(zoneName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("sysdig_secure_zone_posture_policy_assignment.test", "policy_ids.#", "2"), + ), + }, + // Step 3: Import by zone_id + { + ResourceName: "sysdig_secure_zone_posture_policy_assignment.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccZonePolicyAssignmentWith1Policy(zoneName string) string { + return fmt.Sprintf(` +resource "sysdig_secure_zone" "test" { + name = "%s" + scope { + target_type = "aws" + rules = "account in (\"111111111111\")" + } +} + +data "sysdig_secure_posture_policy" "p1" { + name = "Sysdig Kubernetes" +} + +resource "sysdig_secure_zone_posture_policy_assignment" "test" { + zone_id = sysdig_secure_zone.test.id + policy_ids = [data.sysdig_secure_posture_policy.p1.id] +} +`, zoneName) +} + +func testAccZonePolicyAssignmentWith2Policies(zoneName string) string { + return fmt.Sprintf(` +resource "sysdig_secure_zone" "test" { + name = "%s" + scope { + target_type = "aws" + rules = "account in (\"111111111111\")" + } +} + +data "sysdig_secure_posture_policy" "p1" { + name = "Sysdig Kubernetes" +} + +data "sysdig_secure_posture_policy" "p2" { + id = 1 +} + +resource "sysdig_secure_zone_posture_policy_assignment" "test" { + zone_id = sysdig_secure_zone.test.id + policy_ids = [ + data.sysdig_secure_posture_policy.p1.id, + data.sysdig_secure_posture_policy.p2.id, + ] +} +`, zoneName) +} diff --git a/website/docs/r/secure_zone_posture_policy_assignment.md b/website/docs/r/secure_zone_posture_policy_assignment.md new file mode 100644 index 00000000..161e1ac7 --- /dev/null +++ b/website/docs/r/secure_zone_posture_policy_assignment.md @@ -0,0 +1,57 @@ +--- +subcategory: "Sysdig Secure" +layout: "sysdig" +page_title: "Sysdig: sysdig_secure_zone_posture_policy_assignment" +description: |- + Manages the association between a Sysdig Secure Zone and a set of posture policies. +--- + +# Resource: sysdig_secure_zone_posture_policy_assignment + +Manages the association between a [`sysdig_secure_zone`](secure_zone.html) and a set of posture policy IDs. + +Each zone can have at most one assignment. Updating the resource replaces the entire policy list (PUT semantics). + +-> **Note:** Sysdig Terraform Provider is under rapid development at this point. If you experience any issue or discrepancy while using it, please make sure you have the latest version. If the issue persists, or you have a Feature Request to support an additional set of resources, please open a [new issue](https://github.com/sysdiglabs/terraform-provider-sysdig/issues/new) in the GitHub repository. + +## Example Usage + +```terraform +resource "sysdig_secure_zone" "production" { + name = "Production" + scope { + target_type = "aws" + expression { + field = "account" + operator = "in" + values = ["111111111111"] + } + } +} + +data "sysdig_secure_posture_policy" "cis_k8s" { + name = "CIS Kubernetes V1.24 Benchmark" +} + +resource "sysdig_secure_zone_posture_policy_assignment" "production" { + zone_id = sysdig_secure_zone.production.id + policy_ids = [data.sysdig_secure_posture_policy.cis_k8s.id] +} +``` + +## Argument Reference + +- `zone_id` - (Required, ForceNew) The ID of the zone to associate policies with. Changing this forces a new resource. +- `policy_ids` - (Required) Set of posture policy IDs to associate with the zone. Updates replace the entire list. + +## Attributes Reference + +No additional attributes are exported beyond the arguments. + +## Import + +The resource can be imported using the zone ID: + +``` +$ terraform import sysdig_secure_zone_posture_policy_assignment.example 12345 +```