diff --git a/sysdig/internal/client/v2/client.go b/sysdig/internal/client/v2/client.go index f3cfefce..1971c37a 100644 --- a/sysdig/internal/client/v2/client.go +++ b/sysdig/internal/client/v2/client.go @@ -60,6 +60,7 @@ type SecureCommon interface { PostureControlInterface PostureAcceptRiskInterface PostureVulnerabilityAcceptRiskInterface + ZoneInterface } type Requester interface { diff --git a/sysdig/internal/client/v2/client_test.go b/sysdig/internal/client/v2/client_test.go index 907c594d..d03e0986 100644 --- a/sysdig/internal/client/v2/client_test.go +++ b/sysdig/internal/client/v2/client_test.go @@ -57,7 +57,6 @@ func TestUnmarshal(t *testing.T) { } func TestClient_ErrorFromResponse_non_json(t *testing.T) { - givenPayload := "non json body" expected := "401 Unauthorized" c := Client{} @@ -111,7 +110,6 @@ func TestClient_ErrorFromResponse_standard_error_format(t *testing.T) { } func TestClient_ErrorFromResponse_standard_error_format_2(t *testing.T) { - givenPayload := ` { "timestamp" : 1715255725613, diff --git a/sysdig/internal/client/v2/model.go b/sysdig/internal/client/v2/model.go index a7b6183c..649bc22e 100644 --- a/sysdig/internal/client/v2/model.go +++ b/sysdig/internal/client/v2/model.go @@ -1223,3 +1223,31 @@ type AgentAccessKeyWriteWrapper struct { type OrganizationSecure struct { cloudauth.CloudOrganization } + +type ZonesWrapper struct { + Zones []Zone `json:"zones"` +} + +type ZoneRequest struct { + ID int `json:"id,omitempty"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Scopes []ZoneScope `json:"scopes"` +} + +type Zone struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Author string `json:"author"` + LastModifiedBy string `json:"lastModifiedBy,omitempty"` + LastUpdated int64 `json:"lastUpdated,omitempty"` + IsSystem bool `json:"isSystem"` + Scopes []ZoneScope `json:"scopes"` +} + +type ZoneScope struct { + ID int `json:"id,omitempty"` + TargetType string `json:"targetType"` + Rules string `json:"rules"` +} diff --git a/sysdig/internal/client/v2/posture_zones.go b/sysdig/internal/client/v2/posture_zones.go index a463ec87..263d83b1 100644 --- a/sysdig/internal/client/v2/posture_zones.go +++ b/sysdig/internal/client/v2/posture_zones.go @@ -7,8 +7,8 @@ import ( ) const ( - ZonesPath = "%s/api/cspm/v1/policy/zones" - ZonePath = "%s/api/cspm/v1/policy/zones/%d" + PostureZonesPath = "%s/api/cspm/v1/policy/zones" + PostureZonePath = "%s/api/cspm/v1/policy/zones/%d" ) type PostureZoneInterface interface { @@ -28,7 +28,7 @@ func (client *Client) CreateOrUpdatePostureZone(ctx context.Context, r *PostureZ return nil, "", err } - response, err := client.requester.Request(ctx, http.MethodPost, client.createZoneURL(), payload) + response, err := client.requester.Request(ctx, http.MethodPost, client.createPostureZoneURL(), payload) if err != nil { return nil, "", err } @@ -48,7 +48,7 @@ func (client *Client) CreateOrUpdatePostureZone(ctx context.Context, r *PostureZ } func (client *Client) GetPostureZone(ctx context.Context, id int) (*PostureZone, error) { - response, err := client.requester.Request(ctx, http.MethodGet, client.getZoneURL(id), nil) + response, err := client.requester.Request(ctx, http.MethodGet, client.getPostureZoneURL(id), nil) if err != nil { return nil, err } @@ -63,7 +63,7 @@ func (client *Client) GetPostureZone(ctx context.Context, id int) (*PostureZone, } func (client *Client) DeletePostureZone(ctx context.Context, id int) error { - response, err := client.requester.Request(ctx, http.MethodDelete, client.getZoneURL(id), nil) + response, err := client.requester.Request(ctx, http.MethodDelete, client.getPostureZoneURL(id), nil) if err != nil { return err } @@ -76,10 +76,10 @@ func (client *Client) DeletePostureZone(ctx context.Context, id int) error { return nil } -func (client *Client) createZoneURL() string { - return fmt.Sprintf(ZonesPath, client.config.url) +func (client *Client) createPostureZoneURL() string { + return fmt.Sprintf(PostureZonesPath, client.config.url) } -func (client *Client) getZoneURL(id int) string { - return fmt.Sprintf(ZonePath, client.config.url, id) +func (client *Client) getPostureZoneURL(id int) string { + return fmt.Sprintf(PostureZonePath, client.config.url, id) } diff --git a/sysdig/internal/client/v2/zones.go b/sysdig/internal/client/v2/zones.go new file mode 100644 index 00000000..c2b132c7 --- /dev/null +++ b/sysdig/internal/client/v2/zones.go @@ -0,0 +1,121 @@ +package v2 + +import ( + "context" + "fmt" + "net/http" +) + +const ( + PlatformZonesPath = "%s/platform/v1/zones" + PlatformZonePath = "%s/platform/v1/zones/%d" +) + +type ZoneInterface interface { + Base + GetZones(ctx context.Context) ([]Zone, error) + GetZoneById(ctx context.Context, id int) (*Zone, error) + CreateZone(ctx context.Context, zone *ZoneRequest) (*Zone, error) + UpdateZone(ctx context.Context, zone *ZoneRequest) (*Zone, error) + DeleteZone(ctx context.Context, id int) error +} + +func (client *Client) GetZones(ctx context.Context) ([]Zone, error) { + response, err := client.requester.Request(ctx, http.MethodGet, client.getZonesURL(), nil) + if err != nil { + return nil, err + } + defer response.Body.Close() + + wrapper, err := Unmarshal[ZonesWrapper](response.Body) + if err != nil { + return nil, err + } + + return wrapper.Zones, nil +} + +func (client *Client) GetZoneById(ctx context.Context, id int) (*Zone, error) { + response, err := client.requester.Request(ctx, http.MethodGet, client.getZoneURL(id), nil) + if err != nil { + return nil, err + } + defer response.Body.Close() + + zone, err := Unmarshal[Zone](response.Body) + if err != nil { + return nil, err + } + + return &zone, nil +} + +func (client *Client) CreateZone(ctx context.Context, zone *ZoneRequest) (*Zone, error) { + payload, err := Marshal(zone) + if err != nil { + return nil, err + } + + response, err := client.requester.Request(ctx, http.MethodPost, client.getZonesURL(), payload) + if err != nil { + return nil, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusCreated { + return nil, client.ErrorFromResponse(response) + } + + createdZone, err := Unmarshal[Zone](response.Body) + if err != nil { + return nil, err + } + + return &createdZone, nil +} + +func (client *Client) UpdateZone(ctx context.Context, zone *ZoneRequest) (*Zone, error) { + payload, err := Marshal(zone) + if err != nil { + return nil, err + } + + response, err := client.requester.Request(ctx, http.MethodPut, client.getZoneURL(zone.ID), payload) + if err != nil { + return nil, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusCreated { + return nil, client.ErrorFromResponse(response) + } + + updatedZone, err := Unmarshal[Zone](response.Body) + if err != nil { + return nil, err + } + + return &updatedZone, nil +} + +func (client *Client) DeleteZone(ctx context.Context, id int) error { + response, err := client.requester.Request(ctx, http.MethodDelete, client.getZoneURL(id), nil) + if err != nil { + return err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusNoContent && response.StatusCode != http.StatusOK && response.StatusCode != http.StatusNotFound { + return client.ErrorFromResponse(response) + } + + return nil +} + +func (client *Client) getZonesURL() string { + return fmt.Sprintf(PlatformZonesPath, client.config.url) +} + +func (client *Client) getZoneURL(id int) string { + return fmt.Sprintf(PlatformZonePath, client.config.url, id) +} diff --git a/sysdig/provider.go b/sysdig/provider.go index 11de1d1c..4a3f4a3f 100644 --- a/sysdig/provider.go +++ b/sysdig/provider.go @@ -199,6 +199,7 @@ func (p *SysdigProvider) Provider() *schema.Provider { "sysdig_secure_posture_control": resourceSysdigSecurePostureControl(), "sysdig_secure_posture_accept_risk": resourceSysdigSecureAcceptPostureRisk(), "sysdig_secure_vulnerability_accept_risk": resourceSysdigSecureVulnerabilityAcceptRisk(), + "sysdig_secure_zone": resourceSysdigSecureZone(), }, DataSourcesMap: map[string]*schema.Resource{ "sysdig_secure_agentless_scanning_assets": dataSourceSysdigSecureAgentlessScanningAssets(), diff --git a/sysdig/resource_sysdig_secure_zone.go b/sysdig/resource_sysdig_secure_zone.go new file mode 100644 index 00000000..399c4527 --- /dev/null +++ b/sysdig/resource_sysdig_secure_zone.go @@ -0,0 +1,195 @@ +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 resourceSysdigSecureZone() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceSysdigSecureZoneCreate, + ReadContext: resourceSysdigSecureZoneRead, + UpdateContext: resourceSysdigSecureZoneUpdate, + DeleteContext: resourceSysdigSecureZoneDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + SchemaNameKey: { + Type: schema.TypeString, + Required: true, + }, + SchemaDescriptionKey: { + Type: schema.TypeString, + Optional: true, + }, + SchemaIsSystemKey: { + Type: schema.TypeBool, + Computed: true, + }, + SchemaAuthorKey: { + Type: schema.TypeString, + Computed: true, + }, + SchemaLastModifiedBy: { + Type: schema.TypeString, + Computed: true, + }, + SchemaLastUpdated: { + Type: schema.TypeString, + Computed: true, + }, + SchemaScopeKey: { + Type: schema.TypeSet, + MinItems: 1, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + SchemaIDKey: { + Type: schema.TypeInt, + Computed: true, + }, + SchemaTargetTypeKey: { + Type: schema.TypeString, + Required: true, + }, + SchemaRulesKey: { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + } +} + +func resourceSysdigSecureZoneCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client, err := getZoneClient(m.(SysdigClients)) + if err != nil { + return diag.FromErr(err) + } + + zoneRequest := zoneRequestFromResourceData(d) + + createdZone, err := client.CreateZone(ctx, zoneRequest) + if err != nil { + return diag.FromErr(fmt.Errorf("error creating Sysdig Zone: %s", err)) + } + + d.SetId(fmt.Sprintf("%d", createdZone.ID)) + return resourceSysdigSecureZoneRead(ctx, d, m) +} + +func resourceSysdigSecureZoneRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client, err := getZoneClient(m.(SysdigClients)) + if err != nil { + return diag.FromErr(err) + } + + id, _ := strconv.Atoi(d.Id()) + + zone, err := client.GetZoneById(ctx, id) + if err != nil { + d.SetId("") + return diag.FromErr(err) + } + + _ = d.Set("name", zone.Name) + _ = d.Set("description", zone.Description) + _ = d.Set("is_system", zone.IsSystem) + _ = d.Set("author", zone.Author) + _ = d.Set("last_modified_by", zone.LastModifiedBy) + _ = d.Set("last_updated", time.UnixMilli(zone.LastUpdated).Format(time.RFC3339)) + + if err := d.Set(SchemaScopeKey, fromZoneScopesResponse(zone.Scopes)); err != nil { + return diag.FromErr(fmt.Errorf("error setting scope: %s", err)) + } + + return nil +} + +func resourceSysdigSecureZoneUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client, err := getZoneClient(m.(SysdigClients)) + if err != nil { + return diag.FromErr(err) + } + + zoneRequest := zoneRequestFromResourceData(d) + + _, err = client.UpdateZone(ctx, zoneRequest) + if err != nil { + return diag.FromErr(fmt.Errorf("error updating Sysdig Zone: %s", err)) + } + + return resourceSysdigSecureZoneRead(ctx, d, m) +} + +func resourceSysdigSecureZoneDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client, err := getZoneClient(m.(SysdigClients)) + if err != nil { + return diag.FromErr(err) + } + + id, _ := strconv.Atoi(d.Id()) + err = client.DeleteZone(ctx, id) + if err != nil { + return diag.FromErr(fmt.Errorf("error deleting Sysdig Zone: %s", err)) + } + + d.SetId("") + return nil +} + +func zoneRequestFromResourceData(d *schema.ResourceData) *v2.ZoneRequest { + zoneRequest := &v2.ZoneRequest{ + Name: d.Get("name").(string), + Description: d.Get("description").(string), + Scopes: toZoneScopesRequest(d.Get(SchemaScopeKey).(*schema.Set)), + } + + if d.Id() != "" { + id, err := strconv.Atoi(d.Id()) + if err == nil { + zoneRequest.ID = id + } + } + + return zoneRequest +} + +func toZoneScopesRequest(scopes *schema.Set) []v2.ZoneScope { + var zoneScopes []v2.ZoneScope + for _, attr := range scopes.List() { + s := attr.(map[string]interface{}) + zoneScopes = append(zoneScopes, v2.ZoneScope{ + ID: s[SchemaIDKey].(int), + TargetType: s[SchemaTargetTypeKey].(string), + Rules: s[SchemaRulesKey].(string), + }) + } + return zoneScopes +} + +func fromZoneScopesResponse(scopes []v2.ZoneScope) []interface{} { + var flattenedScopes []interface{} + for _, scope := range scopes { + flattenedScopes = append(flattenedScopes, map[string]interface{}{ + SchemaIDKey: scope.ID, + SchemaTargetTypeKey: scope.TargetType, + SchemaRulesKey: scope.Rules, + }) + } + return flattenedScopes +} + +func getZoneClient(clients SysdigClients) (v2.ZoneInterface, error) { + return clients.sysdigSecureClientV2() +} diff --git a/sysdig/resource_sysdig_secure_zone_test.go b/sysdig/resource_sysdig_secure_zone_test.go new file mode 100644 index 00000000..405ee424 --- /dev/null +++ b/sysdig/resource_sysdig_secure_zone_test.go @@ -0,0 +1,67 @@ +//go:build tf_acc_sysdig_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 TestAccSysdigZone_basic(t *testing.T) { + zoneName := "Zone_TF_" + randomText(5) + zoneDescription := "Test Zone Description" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: preCheckAnyEnv(t, SysdigSecureApiTokenEnv), + ProviderFactories: map[string]func() (*schema.Provider, error){ + "sysdig": func() (*schema.Provider, error) { + return sysdig.Provider(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: zoneConfig(zoneName, zoneDescription), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("sysdig_secure_zone.test", "name", zoneName), + resource.TestCheckResourceAttr("sysdig_secure_zone.test", "description", zoneDescription), + resource.TestCheckTypeSetElemNestedAttrs( + "sysdig_secure_zone.test", + "scope.*", + map[string]string{ + "target_type": "aws", + "rules": "organization in (\"o1\", \"o2\") and account in (\"a1\", \"a2\")", + }, + ), + ), + }, + { + ResourceName: "sysdig_secure_zone.test", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: zoneConfig(zoneName, "Updated Description"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("sysdig_secure_zone.test", "description", "Updated Description"), + ), + }, + }, + }) +} + +func zoneConfig(name, description string) string { + return fmt.Sprintf(` +resource "sysdig_secure_zone" "test" { + name = "%s" + description = "%s" + scope { + target_type = "aws" + rules = "organization in (\"o1\", \"o2\") and account in (\"a1\", \"a2\")" + } +} +`, name, description) +} diff --git a/website/docs/r/secure_zone.md b/website/docs/r/secure_zone.md new file mode 100644 index 00000000..6b57ea1d --- /dev/null +++ b/website/docs/r/secure_zone.md @@ -0,0 +1,179 @@ +--- +subcategory: "Sysdig Secure" +layout: "sysdig" +page_title: "Sysdig: sysdig_secure_zone" +description: |- + Creates a Sysdig Secure Zone. +--- + +# Resource: sysdig_secure_zone + +Creates a Sysdig Secure Zone. + +-> **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" "example" { + name = "example-zone" + description = "An example Sysdig zone" + + scope { + target_type = "aws" + rules = "organization in (\"o1\", \"o2\") and account in (\"a1\", \"a2\")" + } + + scope { + target_type = "azure" + rules = "organization contains \"o1\"" + } +} +``` + +## Argument Reference + +- `name` - (Required) The name of the Zone. +- `description` - (Optional) The description of the Zone. +- `scopes` - (Required) Scopes block defines list of scopes attached to Zone. + +### Scopes block + +- `id` - (Computed) The ID of the scope. + +- `target_type` - (Required) The target type for the scope. Supported types: + + - AWS - `aws` + - GCP - `gcp` + - Azure - `azure` + - Kubernetes - `kubernetes` + - Image - `image` + - Host - `host` + - Git - `git` + +- `rules` - (Optional) Query language expression for filtering results. Empty rules means no filtering. + + Operators: + + - `and`, `or` logical operators + - `in` + - `contains` to check partial values of attributes + + List of supported fields by target type: + + - `aws`: + - `account` + - Type: string + - Description: AWS account ID + - Example query: `account in ("123456789012")` + - `organization` + - Type: string + - Description: AWS organization ID + - Example query: `organization in ("o-1234567890")` + - `labels` + - Type: string + - Description: AWS account labels + - Example query: `labels in ("label1")` + - `location` + - Type: string + - Description: AWS account location + - Example query: `location in ("us-east-1")` + - `gcp`: + - `account` + - Type: string + - Description: GCP account ID + - Example query: `account in ("123456789012")` + - `organization` + - Type: string + - Description: GCP organization ID + - Example query: `organization in ("1234567890")` + - `labels` + - Type: string + - Description: GCP account labels + - Example query: `labels in ("label1")` + - `location` + - Type: string + - Description: GCP account location + - Example query: `location in ("us-east-1")` + - `azure`: + - `account` + - Type: string + - Description: Azure account ID + - Example query: `account in ("123456789012")` + - `organization` + - Type: string + - Description: Azure organization ID + - Example query: `organization in ("1234567890")` + - `labels` + - Type: string + - Description: Azure account labels + - Example query: `labels in ("label1")` + - `location` + - Type: string + - Description: Azure account location + - Example query: `location in ("us-east-1")` + - `kubernetes`: + - `clusterId` + - Type: string + - Description: Kubernetes cluster ID + - Example query: `clusterId in ("cluster")` + - `namespace` + - Type: string + - Description: Kubernetes namespace + - Example query: `namespace in ("namespace")` + - `labelValues` + - Type: string + - Description: Kubernetes label values + - Example query: `labelValues in ("label1")` + - `distribution` + - Type: string + - Description: Kubernetes distribution + - Example query: `distribution in ("eks")` + - `host`: + - `clusterId` + - Type: string + - Description: Kubernetes cluster ID + - Example query: `clusterId in ("cluster")` + - `name` + - Type: string + - Description: Host name + - Example query: `name in ("host")` + - `image`: + - `registry` + - Type: string + - Description: Image registry + - Example query: `registry in ("registry")` + - `repository` + - Type: string + - Description: Image repository + - Example query: `repository in ("repository")` + - `git`: + - `gitIntegrationId` + - Type: string + - Description: Git integration ID + - Example query: `gitIntegrationId in ("gitIntegrationId")` + - `gitSourceId` + - Type: string + - Description: Git source ID + - Example query: `gitSourceId in ("gitSourceId")` + + **Note**: Whenever filtering for values with special characters, the values need to be encoded. + When “ or \ are the special characters, they need to be escaped with \ and then encoded. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +- `id` - (Computed) The ID of the Zone. +- `is_system` - (Computed) Whether the Zone is a system zone. +- `author` - (Computed) The zone author. +- `last_modified_by` - (Computed) By whom is last modification made. +- `last_updated` - (Computed) Timestamp of last modification of zone. + +## Import + +Zone can be imported using the ID, e.g. + +``` +$ terraform import sysdig_secure_zone.example 12345 +```