From 9626096e9c86397d7cf6a054435f28e3610c0e45 Mon Sep 17 00:00:00 2001 From: hila-krut-sysdig <115632909+hila-krut-sysdig@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:55:58 +0300 Subject: [PATCH] feat(policy): add custom control (#530) * add crud for posture control * add docs * fix * add test * remove * fix * fix test * add acceptance --------- Co-authored-by: hila1608 <115632909+hila1608@users.noreply.github.com> --- sysdig/common.go | 140 +++++++------- sysdig/internal/client/v2/client.go | 1 + .../client/v2/model_posture_control.go | 25 +++ sysdig/internal/client/v2/posture_controls.go | 73 ++++++++ sysdig/provider.go | 1 + .../resource_sysdig_secure_posture_control.go | 177 ++++++++++++++++++ ...urce_sysdig_secure_posture_control_test.go | 59 ++++++ website/docs/r/secure_posture_control.md | 69 +++++++ website/docs/r/secure_posture_policy.md | 2 +- 9 files changed, 478 insertions(+), 69 deletions(-) create mode 100644 sysdig/internal/client/v2/model_posture_control.go create mode 100644 sysdig/internal/client/v2/posture_controls.go create mode 100644 sysdig/resource_sysdig_secure_posture_control.go create mode 100644 sysdig/resource_sysdig_secure_posture_control_test.go create mode 100644 website/docs/r/secure_posture_control.md diff --git a/sysdig/common.go b/sysdig/common.go index 3b287efb..9ef82056 100644 --- a/sysdig/common.go +++ b/sysdig/common.go @@ -1,72 +1,76 @@ package sysdig const ( - SchemaIDKey = "id" - SchemaTeamIDKey = "team_id" - SchemaPoliciesKey = "policies" - SchemaPolicyIDsKey = "policy_ids" - SchemaAuthorsKey = "authors" - SchemaAuthorKey = "author" - SchemaNameKey = "name" - SchemaEnabledKey = "enabled" - SchemaStatusKey = "status" - SchemaTypeKey = "type" - SchemaKindKey = "kind" - SchemaDescriptionKey = "description" - SchemaVersionKey = "version" - SchemaLinkKey = "link" - SchemaGroupKey = "group" - SchemaLastModifiedBy = "last_modified_by" - SchemaLastUpdated = "last_updated" - SchemaExpirationDateKey = "expiration_date" - SchemaPublishedDateKey = "published_date" - SchemaCreatedDateKey = "date_created" - SchemaMinKubeVersionKey = "min_kube_version" - SchemaMaxKubeVersionKey = "max_kube_version" - SchemaIsCustomKey = "is_custom" - SchemaIsActiveKey = "is_active" - SchemaPlatformKey = "platform" - SchemaZonesKey = "zones" - SchemaZonesIDsKey = "zone_ids" - SchemaAllZones = "all_zones" - SchemaScopeKey = "scope" - SchemaScopesKey = "scopes" - SchemaTargetTypeKey = "target_type" - SchemaRoleKey = "role" - SchemaSystemRoleKey = "system_role" - SchemaRulesKey = "rules" - SchemaApiKeyKey = "api_key" - SchemaPermissionsKey = "permissions" - SchemaMonitorPermKey = "monitor_permissions" - SchemaSecurePermKey = "secure_permissions" - SchemaRequestedPermKey = "requested_permissions" - SchemaEnrichedPermKey = "enriched_permissions" - SchemaSecureThreatDetection = "secure_threat_detection" - SchemaSecureConfigPosture = "secure_config_posture" - SchemaSecureIdentityEntitlement = "secure_identity_entitlement" - SchemaSecureAgentlessScanning = "secure_agentless_scanning" - SchemaMonitorCloudMetrics = "monitor_cloud_metrics" - SchemaType = "type" - SchemaInstance = "instance" - SchemaVersion = "version" - SchemaCloudConnectorMetadata = "cloud_connector_metadata" - SchemaTrustedRoleMetadata = "trusted_role_metadata" - SchemaEventBridgeMetadata = "event_bridge_metadata" - SchemaServicePrincipalMetadata = "service_principal_metadata" - SchemaWebhookDatasourceMetadata = "webhook_datasource_metadata" - SchemaCryptoKeyMetadata = "crypto_key_metadata" - SchemaCloudLogsMetadata = "cloud_logs_metadata" - SchemaEnabled = "enabled" - SchemaComponents = "components" - SchemaComponent = "component" - SchemaCloudProviderId = "provider_id" - SchemaCloudProviderType = "provider_type" - SchemaFeature = "feature" - SchemaManagementAccountId = "management_account_id" - SchemaOrganizationIDKey = "organization_id" - SchemaOrganizationalUnitIds = "organizational_unit_ids" - SchemaCloudProviderTenantId = "provider_tenant_id" - SchemaCloudProviderAlias = "provider_alias" - SchemaAccountId = "account_id" - SchemaFeatureFlags = "flags" + SchemaIDKey = "id" + SchemaTeamIDKey = "team_id" + SchemaPoliciesKey = "policies" + SchemaPolicyIDsKey = "policy_ids" + SchemaAuthorsKey = "authors" + SchemaAuthorKey = "author" + SchemaNameKey = "name" + SchemaEnabledKey = "enabled" + SchemaStatusKey = "status" + SchemaTypeKey = "type" + SchemaResourceKindKey = "resource_kind" + SchemaResourceRegoKey = "rego" + SchemaResourceSeverityKey = "severity" + SchemaResourceRemediationDetailsKey = "remediation_details" + SchemaKindKey = "kind" + SchemaDescriptionKey = "description" + SchemaVersionKey = "version" + SchemaLinkKey = "link" + SchemaGroupKey = "group" + SchemaLastModifiedBy = "last_modified_by" + SchemaLastUpdated = "last_updated" + SchemaExpirationDateKey = "expiration_date" + SchemaPublishedDateKey = "published_date" + SchemaCreatedDateKey = "date_created" + SchemaMinKubeVersionKey = "min_kube_version" + SchemaMaxKubeVersionKey = "max_kube_version" + SchemaIsCustomKey = "is_custom" + SchemaIsActiveKey = "is_active" + SchemaPlatformKey = "platform" + SchemaZonesKey = "zones" + SchemaZonesIDsKey = "zone_ids" + SchemaAllZones = "all_zones" + SchemaScopeKey = "scope" + SchemaScopesKey = "scopes" + SchemaTargetTypeKey = "target_type" + SchemaRoleKey = "role" + SchemaSystemRoleKey = "system_role" + SchemaRulesKey = "rules" + SchemaApiKeyKey = "api_key" + SchemaPermissionsKey = "permissions" + SchemaMonitorPermKey = "monitor_permissions" + SchemaSecurePermKey = "secure_permissions" + SchemaRequestedPermKey = "requested_permissions" + SchemaEnrichedPermKey = "enriched_permissions" + SchemaSecureThreatDetection = "secure_threat_detection" + SchemaSecureConfigPosture = "secure_config_posture" + SchemaSecureIdentityEntitlement = "secure_identity_entitlement" + SchemaSecureAgentlessScanning = "secure_agentless_scanning" + SchemaMonitorCloudMetrics = "monitor_cloud_metrics" + SchemaType = "type" + SchemaInstance = "instance" + SchemaVersion = "version" + SchemaCloudConnectorMetadata = "cloud_connector_metadata" + SchemaTrustedRoleMetadata = "trusted_role_metadata" + SchemaEventBridgeMetadata = "event_bridge_metadata" + SchemaServicePrincipalMetadata = "service_principal_metadata" + SchemaWebhookDatasourceMetadata = "webhook_datasource_metadata" + SchemaCryptoKeyMetadata = "crypto_key_metadata" + SchemaCloudLogsMetadata = "cloud_logs_metadata" + SchemaEnabled = "enabled" + SchemaComponents = "components" + SchemaComponent = "component" + SchemaCloudProviderId = "provider_id" + SchemaCloudProviderType = "provider_type" + SchemaFeature = "feature" + SchemaManagementAccountId = "management_account_id" + SchemaOrganizationIDKey = "organization_id" + SchemaOrganizationalUnitIds = "organizational_unit_ids" + SchemaCloudProviderTenantId = "provider_tenant_id" + SchemaCloudProviderAlias = "provider_alias" + SchemaAccountId = "account_id" + SchemaFeatureFlags = "flags" ) diff --git a/sysdig/internal/client/v2/client.go b/sysdig/internal/client/v2/client.go index eca1b506..c820e983 100644 --- a/sysdig/internal/client/v2/client.go +++ b/sysdig/internal/client/v2/client.go @@ -57,6 +57,7 @@ type MonitorCommon interface { type SecureCommon interface { PosturePolicyInterface PostureZoneInterface + PostureControlInterface } type Requester interface { diff --git a/sysdig/internal/client/v2/model_posture_control.go b/sysdig/internal/client/v2/model_posture_control.go new file mode 100644 index 00000000..edbc934b --- /dev/null +++ b/sysdig/internal/client/v2/model_posture_control.go @@ -0,0 +1,25 @@ +package v2 + +type SaveControlRequest struct { + ID string `json:"id,omitempty"` + Name string `json:"name"` + Description string `json:"description"` + ResourceKind string `json:"resourceKind"` + Severity string `json:"severity"` + Rego string `json:"rego"` + RemediationDetails string `json:"remediationDetails"` +} + +type SaveControlResponse struct { + Data PostureControl `json:"data"` +} + +type PostureControl struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + ResourceKind string `json:"resourceKind"` + Severity string `json:"severity"` + Rego string `json:"rego"` + RemediationDetails string `json:"remediationDetails"` +} diff --git a/sysdig/internal/client/v2/posture_controls.go b/sysdig/internal/client/v2/posture_controls.go new file mode 100644 index 00000000..42883c27 --- /dev/null +++ b/sysdig/internal/client/v2/posture_controls.go @@ -0,0 +1,73 @@ +package v2 + +import ( + "context" + "fmt" + "net/http" +) + +const ( + PostureControlSavePath = "%s/api/cspm/v1/policy/controls" + PostureControlGetPath = "%s/api/cspm/v1/policy/controls/view/%d" + PostureControlDeletePath = "%s/api/cspm/v1/policy/controls/%d" +) + +type PostureControlInterface interface { + Base + CreateOrUpdatePostureControl(ctx context.Context, p *SaveControlRequest) (*PostureControl, string, error) + GetPostureControl(ctx context.Context, id int64) (*PostureControl, error) + DeletePostureControl(ctx context.Context, id int64) error +} + +func (c *Client) CreateOrUpdatePostureControl(ctx context.Context, p *SaveControlRequest) (*PostureControl, string, error) { + payload, err := Marshal(p) + if err != nil { + return nil, "", err + } + response, err := c.requester.Request(ctx, http.MethodPost, c.getPostureControlURL(PostureControlSavePath), payload) + if err != nil { + return nil, "", err + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusCreated { + errStatus, err := c.ErrorAndStatusFromResponse(response) + return nil, errStatus, err + } + resp, err := Unmarshal[SaveControlResponse](response.Body) + if err != nil { + return nil, "", err + } + return &resp.Data, "", nil + +} + +func (c *Client) GetPostureControl(ctx context.Context, id int64) (*PostureControl, error) { + response, err := c.requester.Request(ctx, http.MethodGet, fmt.Sprintf(PostureControlGetPath, c.config.url, id), nil) + if err != nil { + return nil, err + } + defer response.Body.Close() + + wrapper, err := Unmarshal[SaveControlResponse](response.Body) + if err != nil { + return nil, err + } + return &wrapper.Data, nil +} + +func (c *Client) DeletePostureControl(ctx context.Context, id int64) error { + response, err := c.requester.Request(ctx, http.MethodDelete, fmt.Sprintf(PostureControlDeletePath, c.config.url, 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 c.ErrorFromResponse(response) + } + + return nil +} +func (c *Client) getPostureControlURL(path string) string { + return fmt.Sprintf(path, c.config.url) +} diff --git a/sysdig/provider.go b/sysdig/provider.go index 7867e1f0..639ef94b 100644 --- a/sysdig/provider.go +++ b/sysdig/provider.go @@ -193,6 +193,7 @@ func (p *SysdigProvider) Provider() *schema.Provider { "sysdig_secure_posture_zone": resourceSysdigSecurePostureZone(), "sysdig_secure_organization": resourceSysdigSecureOrganization(), "sysdig_secure_posture_policy": resourceSysdigSecurePosturePolicy(), + "sysdig_secure_posture_control": resourceSysdigSecurePostureControl(), }, DataSourcesMap: map[string]*schema.Resource{ "sysdig_secure_agentless_scanning_assets": dataSourceSysdigSecureAgentlessScanningAssets(), diff --git a/sysdig/resource_sysdig_secure_posture_control.go b/sysdig/resource_sysdig_secure_posture_control.go new file mode 100644 index 00000000..a4304695 --- /dev/null +++ b/sysdig/resource_sysdig_secure_posture_control.go @@ -0,0 +1,177 @@ +package sysdig + +import ( + "context" + "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" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceSysdigSecurePostureControl() *schema.Resource { + timeout := 5 * time.Minute + + return &schema.Resource{ + CreateContext: resourceSysdigSecurePostureControlCreateOrUpdate, + ReadContext: resourceSysdigSecurePostureContorlRead, + DeleteContext: resourceSysdigSecurePostureControlDelete, + UpdateContext: resourceSysdigSecurePostureControlCreateOrUpdate, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(timeout), + }, + Schema: map[string]*schema.Schema{ + SchemaIDKey: { + Type: schema.TypeString, + Computed: true, + }, + SchemaNameKey: { + Type: schema.TypeString, + Required: true, + }, + SchemaDescriptionKey: { + Type: schema.TypeString, + Required: true, + }, + SchemaResourceKindKey: { + Type: schema.TypeString, + Required: true, + }, + SchemaResourceRegoKey: { + Type: schema.TypeString, + Required: true, + }, + SchemaResourceRemediationDetailsKey: { + Type: schema.TypeString, + Required: true, + }, + SchemaResourceSeverityKey: { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{"High", "Medium", "Low"}, false), + }, + }, + } +} + +func resourceSysdigSecurePostureControlCreateOrUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + // Extract 'group' field from Terraform configuration + client, err := getPostureControlClient(meta.(SysdigClients)) + if err != nil { + return diag.FromErr(err) + } + + req := &v2.SaveControlRequest{ + ID: getStringValue(d, SchemaIDKey), + Name: getStringValue(d, SchemaNameKey), + Description: getStringValue(d, SchemaDescriptionKey), + ResourceKind: getStringValue(d, SchemaResourceKindKey), + Rego: getStringValue(d, SchemaResourceRegoKey), + Severity: getStringValue(d, SchemaResourceSeverityKey), + RemediationDetails: getStringValue(d, SchemaResourceRemediationDetailsKey), + } + + control, errStatus, err := client.CreateOrUpdatePostureControl(ctx, req) + if err != nil { + return diag.Errorf("Error saving control. error status: %s err: %s", errStatus, err) + } + + d.SetId(control.ID) + resourceSysdigSecurePostureContorlRead(ctx, d, meta) + return nil +} + +func resourceSysdigSecurePostureContorlRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, err := getPostureControlClient(meta.(SysdigClients)) + if err != nil { + return diag.FromErr(err) + } + + id, err := strconv.ParseInt(d.Id(), 10, 64) + if err != nil { + return diag.FromErr(err) + } + + control, err := client.GetPostureControl(ctx, id) + if err != nil { + return diag.FromErr(err) + } + err = d.Set(SchemaIDKey, control.ID) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set(SchemaNameKey, control.Name) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set(SchemaDescriptionKey, control.Description) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set(SchemaResourceRemediationDetailsKey, control.RemediationDetails) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set(SchemaResourceKindKey, control.ResourceKind) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set(SchemaResourceRegoKey, control.Rego) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set(SchemaResourceSeverityKey, control.Severity) + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceSysdigSecurePostureControlDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, err := getPostureControlClient(meta.(SysdigClients)) + if err != nil { + return diag.FromErr(err) + } + + id, err := strconv.ParseInt(d.Id(), 10, 64) + if err != nil { + return diag.FromErr(err) + } + + err = client.DeletePostureControl(ctx, id) + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +func getPostureControlClient(c SysdigClients) (v2.PostureControlInterface, error) { + var client v2.PostureControlInterface + var err error + switch c.GetClientType() { + case IBMSecure: + client, err = c.ibmSecureClient() + if err != nil { + return nil, err + } + default: + client, err = c.sysdigSecureClientV2() + if err != nil { + return nil, err + } + } + return client, nil +} diff --git a/sysdig/resource_sysdig_secure_posture_control_test.go b/sysdig/resource_sysdig_secure_posture_control_test.go new file mode 100644 index 00000000..9eb5d300 --- /dev/null +++ b/sysdig/resource_sysdig_secure_posture_control_test.go @@ -0,0 +1,59 @@ +//go:build tf_acc_sysdig_secure || tf_acc_policies || tf_acc_onprem_secure + +package sysdig_test + +import ( + "fmt" + "testing" + + "github.com/draios/terraform-provider-sysdig/sysdig" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func TestCreateCustomControlResource(t *testing.T) { + rText := func() string { return acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) } + 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: createControlResource(rText()), + }, + }, + }) +} + +func createControlResource(name string) string { + return fmt.Sprintf(`resource "sysdig_secure_posture_control" "test" { + name = "S3 - Enabled Versioning-test-%s" + description = "S3 - Enabled Versioning" + resource_kind = "AWS_S3_BUCKET" + severity = "Low" + rego = <<-EOF + + package sysdig + + import future.keywords.if + import future.keywords.in + + default risky := false + + risky if { + count(input.Versioning) == 0 + } + + risky if { + some version in input.Versioning + lower(version.Status) != "enabled" + } + EOF + + remediation_details = "Enable versioning on the S3 bucket" + }`, name) +} diff --git a/website/docs/r/secure_posture_control.md b/website/docs/r/secure_posture_control.md new file mode 100644 index 00000000..90d3745b --- /dev/null +++ b/website/docs/r/secure_posture_control.md @@ -0,0 +1,69 @@ +--- +subcategory: "Sysdig Secure" +layout: "sysdig" +page_title: "Sysdig: sysdig_secure_posture_control" +description: |- + Creates Sysdig Secure Posture Control. +--- + +# Resource: sysdig_secure_posture_control + +Creates a Sysdig Secure Posture Control. + +-> **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_posture_control" "c"{ + name = "S3 - Enabled Versioning" + description = "S3 - Enabled Versioning" + resource_kind = "AWS_S3_BUCKET" + severity = "Low" + rego = <<-EOF + + package sysdig + + import future.keywords.if + import future.keywords.in + + default risky := false + + risky if { + count(input.Versioning) == 0 + } + + risky if { + some version in input.Versioning + lower(version.Status) != "enabled" + } + EOF + + remediation_details = <<-EOF + **Using AWS CLI**\n1. Run **put-bucket-versioning** command (OSX/Linux/UNIX) using the name of the Amazon S3 bucket that you want to reconfigure as the identifier parameter, to enable S3 object versioning for the selected bucket. If the request is successful, the **put-bucket-versioning** command should not return an output:\n```bash\nawsaws s3api put-bucket-versioning\n --bucket cc-prod-web-data\n --versioning-configuration Status=Enabled\n```\n2. Repeat step no. 1 to enable S3 object versioning for other Amazon S3 buckets available within your AWS cloud account. + + EOF +} +``` + +## Argument Reference + +- `name` - (Required) The name of the Posture Control. The name must be unique, e.g. `EC2 - Instances should not have a public IP address` +- `description` - (Required) The description of the Posture Control, eg. `EC2 - Instances should not have a public IP address` +- `rego` - (Required) The Posture control Rego. `package sysdig\ndefault risky = false\nrisky {\n input.NetworkInterfaces[_].Association.PublicIp\n input. NetworkInterfaces[_].Association.PublicIp != \"\"\n}` +- `remediation_details`- (Required) The Posture control Remediation details. `Use a non-default VPC so that your instance is not assigned a public IP address by default` +- `resource_kind` - (Required) The Posture Control Resource kind. It should be a supported resource kind, eg. `AWS_S3_BUCKET` +- `severity` - (Required) The Posture Control Severity [`High`, `Medium`, `Low`], case sensitive, e.g., `High`. +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +- `author` - (Computed) The custom control author. + +## Import + +Posture custom control can be imported using the ID, e.g. + +``` +$ terraform import sysdig_secure_posture_control.example c 12345 +``` diff --git a/website/docs/r/secure_posture_policy.md b/website/docs/r/secure_posture_policy.md index 9698e413..79a5551e 100644 --- a/website/docs/r/secure_posture_policy.md +++ b/website/docs/r/secure_posture_policy.md @@ -105,5 +105,5 @@ In addition to all arguments above, the following attributes are exported: Posture policy can be imported using the ID, e.g. ``` -$ terraform import sysdig_secure_posture_policy.example p +$ terraform import sysdig_secure_posture_policy.example p 12345 ```