diff --git a/sysdig/internal/client/v2/sysdig.go b/sysdig/internal/client/v2/sysdig.go index beb55e97..65b092eb 100644 --- a/sysdig/internal/client/v2/sysdig.go +++ b/sysdig/internal/client/v2/sysdig.go @@ -18,39 +18,43 @@ type SysdigRequest struct { type SysdigCommon interface { Common - GroupMappingInterface - GroupMappingConfigInterface + CustomRoleInterface CustomRolePermissionInterface - TeamServiceAccountInterface - IPFiltersInterface + GroupMappingConfigInterface + GroupMappingInterface IPFilteringSettingsInterface + IPFiltersInterface + TeamServiceAccountInterface } type SysdigMonitor interface { SysdigCommon MonitorCommon + CloudAccountMonitorInterface } type SysdigSecure interface { SysdigCommon SecureCommon - PolicyInterface - CompositePolicyInterface - RuleInterface - ListInterface - MacroInterface - DeprecatedScanningPolicyInterface - DeprecatedScanningPolicyAssignmentInterface - DeprecatedVulnerabilityExceptionListInterface - DeprecatedVulnerabilityExceptionInterface + CloudAccountSecureInterface - CloudauthAccountSecureInterface - OrganizationSecureInterface CloudauthAccountComponentSecureInterface CloudauthAccountFeatureSecureInterface + CloudauthAccountSecureInterface + CompositePolicyInterface + DeprecatedScanningPolicyAssignmentInterface + DeprecatedScanningPolicyInterface + DeprecatedVulnerabilityExceptionInterface + DeprecatedVulnerabilityExceptionListInterface + ListInterface + MacroInterface OnboardingSecureInterface + OrganizationSecureInterface + PolicyInterface + RuleInterface + VulnerabilityPolicyClient } func (sr *SysdigRequest) Request(ctx context.Context, method string, url string, payload io.Reader) (*http.Response, error) { diff --git a/sysdig/internal/client/v2/vulnerability_policy.go b/sysdig/internal/client/v2/vulnerability_policy.go new file mode 100644 index 00000000..621980d1 --- /dev/null +++ b/sysdig/internal/client/v2/vulnerability_policy.go @@ -0,0 +1,116 @@ +package v2 + +import ( + "context" + "errors" + "fmt" + "net/http" + "strconv" +) + +const ( + vulnerabilityPoliciesPath = "%s/secure/vulnerability/v1/policies" + vulnerabilityPolicyPath = "%s/secure/vulnerability/v1/policies/%s" +) + +type VulnerabilityPolicyClient interface { + CreateVulnerabilityPolicy(ctx context.Context, vulnerabilityPolicy VulnerabilityPolicy) (VulnerabilityPolicy, error) + GetVulnerabilityPolicyByID(ctx context.Context, vulnerabilityPolicyID string) (VulnerabilityPolicy, error) + UpdateVulnerabilityPolicy(ctx context.Context, vulnerabilityPolicy VulnerabilityPolicy) (VulnerabilityPolicy, error) + DeleteVulnerabilityPolicyByID(ctx context.Context, vulnerabilityPolicyID string) error +} + +func (c *Client) CreateVulnerabilityPolicy(ctx context.Context, vulnerabilityPolicy VulnerabilityPolicy) (policy VulnerabilityPolicy, err error) { + payload, err := Marshal(vulnerabilityPolicy) + if err != nil { + return VulnerabilityPolicy{}, err + } + + response, err := c.requester.Request(ctx, http.MethodPost, c.vulnerabilityPoliciesURL(), payload) + if err != nil { + return VulnerabilityPolicy{}, 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 VulnerabilityPolicy{}, c.ErrorFromResponse(response) + } + + return Unmarshal[VulnerabilityPolicy](response.Body) +} + +func (c *Client) GetVulnerabilityPolicyByID(ctx context.Context, vulnerabilityPolicyID string) (policy VulnerabilityPolicy, err error) { + response, err := c.requester.Request(ctx, http.MethodGet, c.vulnerabilityPolicyURL(vulnerabilityPolicyID), nil) + if err != nil { + return VulnerabilityPolicy{}, 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 VulnerabilityPolicy{}, c.ErrorFromResponse(response) + } + + return Unmarshal[VulnerabilityPolicy](response.Body) +} + +func (c *Client) UpdateVulnerabilityPolicy(ctx context.Context, vulnerabilityPolicy VulnerabilityPolicy) (policy VulnerabilityPolicy, err error) { + if vulnerabilityPolicy.ID == nil { + return VulnerabilityPolicy{}, errors.New("policy id was null") + } + + payload, err := Marshal(vulnerabilityPolicy) + if err != nil { + return VulnerabilityPolicy{}, err + } + + idAsStr := strconv.Itoa(int(*vulnerabilityPolicy.ID)) + response, err := c.requester.Request(ctx, http.MethodPut, c.vulnerabilityPolicyURL(idAsStr), payload) + if err != nil { + return VulnerabilityPolicy{}, 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 VulnerabilityPolicy{}, c.ErrorFromResponse(response) + } + + return Unmarshal[VulnerabilityPolicy](response.Body) +} + +func (c *Client) DeleteVulnerabilityPolicyByID(ctx context.Context, vulnerabilityPolicyID string) (err error) { + response, err := c.requester.Request(ctx, http.MethodDelete, c.vulnerabilityPolicyURL(vulnerabilityPolicyID), 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 { + return c.ErrorFromResponse(response) + } + + return err +} + +func (c *Client) vulnerabilityPoliciesURL() string { + return fmt.Sprintf(vulnerabilityPoliciesPath, c.config.url) +} + +func (c *Client) vulnerabilityPolicyURL(vulnerabilityPolicyID string) string { + return fmt.Sprintf(vulnerabilityPolicyPath, c.config.url, vulnerabilityPolicyID) +} diff --git a/sysdig/internal/client/v2/vulnerability_policy_model.go b/sysdig/internal/client/v2/vulnerability_policy_model.go new file mode 100644 index 00000000..dffbaa38 --- /dev/null +++ b/sysdig/internal/client/v2/vulnerability_policy_model.go @@ -0,0 +1,23 @@ +package v2 + +type VulnerabilityPolicy struct { + Bundles []Bundle `json:"bundles"` + Description string `json:"description"` + Name string `json:"name"` + Stages []Stage `json:"stages,omitempty"` + ID *int32 `json:"id,omitempty"` + Identifier *string `json:"identifier,omitempty"` +} + +type Bundle struct { + ID int64 `json:"id"` +} + +type Stage struct { + Name string `json:"name"` + Configuration []Configuration `json:"configuration,omitempty"` +} + +type Configuration struct { + Scope string `json:"scope"` +} diff --git a/sysdig/provider.go b/sysdig/provider.go index 7e52960e..8d0623b4 100644 --- a/sysdig/provider.go +++ b/sysdig/provider.go @@ -199,6 +199,7 @@ func (p *SysdigProvider) Provider() *schema.Provider { "sysdig_secure_rule_syscall": resourceSysdigSecureRuleSyscall(), "sysdig_secure_team": resourceSysdigSecureTeam(), "sysdig_secure_vulnerability_accept_risk": resourceSysdigSecureVulnerabilityAcceptRisk(), + "sysdig_secure_vulnerability_policy": resourceSysdigSecureVulnerabilityPolicy(), "sysdig_secure_zone": resourceSysdigSecureZone(), }, DataSourcesMap: map[string]*schema.Resource{ diff --git a/sysdig/resource_sysdig_secure_vulnerability_policy.go b/sysdig/resource_sysdig_secure_vulnerability_policy.go new file mode 100644 index 00000000..af2d7f4d --- /dev/null +++ b/sysdig/resource_sysdig_secure_vulnerability_policy.go @@ -0,0 +1,304 @@ +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 resourceSysdigSecureVulnerabilityPolicy() *schema.Resource { + timeout := 5 * time.Minute + + return &schema.Resource{ + CreateContext: resourceSysdigVulnerabilityPolicyCreate, + ReadContext: resourceSysdigVulnerabilityPolicyRead, + UpdateContext: resourceSysdigVulnerabilityPolicyUpdate, + DeleteContext: resourceSysdigVulnerabilityPolicyDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(timeout), + Delete: schema.DefaultTimeout(timeout), + Update: schema.DefaultTimeout(timeout), + Read: schema.DefaultTimeout(timeout), + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Policy name", + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: "Policy description", + }, + "identifier": { + Type: schema.TypeString, + Computed: true, + Description: "External identifier", + }, + "bundles": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + + "stages": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Stage name: pipeline, registry, admission_control o runtime", + ValidateDiagFunc: validation.ToDiagFunc( + validation.StringInSlice([]string{ + "pipeline", + "registry", + "runtime", + }, false)), + }, + "configuration": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "scope": { + Type: schema.TypeString, + Required: true, + Description: "Scope expression for this stage", + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func getSecureVulnerabilityPolicyClient(c SysdigClients) (v2.VulnerabilityPolicyClient, error) { + return c.sysdigSecureClientV2() +} + +func resourceSysdigVulnerabilityPolicyCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client, err := getSecureVulnerabilityPolicyClient(meta.(SysdigClients)) + if err != nil { + return diag.FromErr(err) + } + + scanningPolicy, err := vulnerabilityPolicyFromResourceData(d) + if err != nil { + return diag.FromErr(err) + } + + scanningPolicy, err = client.CreateVulnerabilityPolicy(ctx, scanningPolicy) + if err != nil { + return diag.FromErr(err) + } + + vulnerabilityPolicyToResourceData(&scanningPolicy, d) + + return nil +} + +func resourceSysdigVulnerabilityPolicyUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client, err := getSecureVulnerabilityPolicyClient(meta.(SysdigClients)) + if err != nil { + return diag.FromErr(err) + } + + scanningPolicy, err := vulnerabilityPolicyFromResourceData(d) + if err != nil { + return diag.FromErr(err) + } + + _, err = client.UpdateVulnerabilityPolicy(ctx, scanningPolicy) + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceSysdigVulnerabilityPolicyRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client, err := getSecureVulnerabilityPolicyClient(meta.(SysdigClients)) + if err != nil { + return diag.FromErr(err) + } + + scanningPolicy, err := client.GetVulnerabilityPolicyByID(ctx, d.Id()) + if err != nil { + return diag.FromErr(err) + } + + vulnerabilityPolicyToResourceData(&scanningPolicy, d) + + return nil +} + +func resourceSysdigVulnerabilityPolicyDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client, err := getSecureVulnerabilityPolicyClient(meta.(SysdigClients)) + if err != nil { + return diag.FromErr(err) + } + + err = client.DeleteVulnerabilityPolicyByID(ctx, d.Id()) + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +func vulnerabilityPolicyToResourceData(scanningPolicy *v2.VulnerabilityPolicy, d *schema.ResourceData) { + if scanningPolicy.ID == nil { + d.SetId("") // id is nil, let's destroy the resource + return + } + + d.SetId(strconv.Itoa(int(*scanningPolicy.ID))) + _ = d.Set("name", scanningPolicy.Name) + _ = d.Set("description", scanningPolicy.Description) + + if scanningPolicy.Identifier != nil { + _ = d.Set("identifier", *scanningPolicy.Identifier) + } + + _ = d.Set("bundles", vulnerabilityPolicyBundlesToData(scanningPolicy)) + + stages := vulnerabilityPolicyStagesToMap(scanningPolicy.Stages) + _ = d.Set("stages", stages) +} + +func vulnerabilityPolicyStagesToMap(policyStages []v2.Stage) []map[string]any { + var stages []map[string]any + + for _, stage := range policyStages { + var configsMap []map[string]any + + for _, stageconfig := range stage.Configuration { + newConfig := map[string]any{ + "scope": stageconfig.Scope, + } + configsMap = append(configsMap, newConfig) + } + + stages = append(stages, map[string]any{ + "name": stage.Name, + "configuration": configsMap, + }) + } + + return stages +} + +func vulnerabilityPolicyBundlesToData(scanningPolicy *v2.VulnerabilityPolicy) []string { + var bundles []string + + for _, policyBundle := range scanningPolicy.Bundles { + bundle := strconv.Itoa(int(policyBundle.ID)) + + bundles = append(bundles, bundle) + } + + return bundles +} + +func vulnerabilityPolicyFromResourceData(d *schema.ResourceData) (v2.VulnerabilityPolicy, error) { + stringPtr := func(d *schema.ResourceData, key string) *string { + if value, ok := d.GetOk(key); ok && value.(string) != "" { + valueAsString := value.(string) + return &valueAsString + } + return nil + } + + int32PtrFromID := func(d *schema.ResourceData) *int32 { + id := d.Id() + if id == "" { + return nil + } + + idAsInt, err := strconv.ParseInt(id, 10, 32) + if err != nil { + return nil + } + + i32 := int32(idAsInt) + return &i32 + } + + bundles, err := vulnerabilityPolicyBundlesFromList(d.Get("bundles").(*schema.Set)) + if err != nil { + return v2.VulnerabilityPolicy{}, err + } + + stages, err := vulnerabilityPolicyStagesFromSet(d.Get("stages").(*schema.Set)) + if err != nil { + return v2.VulnerabilityPolicy{}, err + } + + return v2.VulnerabilityPolicy{ + ID: int32PtrFromID(d), + Identifier: stringPtr(d, "identifier"), + Name: d.Get("name").(string), + Description: d.Get("description").(string), + Bundles: bundles, + Stages: stages, + }, nil +} + +func vulnerabilityPolicyBundlesFromList(list *schema.Set) ([]v2.Bundle, error) { + var out []v2.Bundle + + for _, idRaw := range list.List() { + id, err := strconv.Atoi(idRaw.(string)) + if err != nil { + return nil, err + } + + out = append(out, v2.Bundle{ID: int64(id)}) + } + + return out, nil +} + +func vulnerabilityPolicyStagesFromSet(set *schema.Set) ([]v2.Stage, error) { + var out []v2.Stage + + for _, raw := range set.List() { + rawMap := raw.(map[string]any) + + out = append(out, v2.Stage{ + Name: rawMap["name"].(string), + Configuration: vulnerabilityPolicyConfigsFromSet(rawMap["configuration"].(*schema.Set)), + }) + } + + return out, nil +} + +func vulnerabilityPolicyConfigsFromSet(set *schema.Set) []v2.Configuration { + var out []v2.Configuration + + for _, raw := range set.List() { + rawMap := raw.(map[string]any) + + out = append(out, v2.Configuration{Scope: rawMap["scope"].(string)}) + } + + return out +} diff --git a/sysdig/resource_sysdig_secure_vulnerability_policy_test.go b/sysdig/resource_sysdig_secure_vulnerability_policy_test.go new file mode 100644 index 00000000..0ed667e5 --- /dev/null +++ b/sysdig/resource_sysdig_secure_vulnerability_policy_test.go @@ -0,0 +1,86 @@ +//go:build tf_acc_sysdig_secure || tf_acc_vulnerability_scanning + +package sysdig_test + +import ( + "fmt" + "os" + "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 TestAccVulnerabilityPolicy(t *testing.T) { + random := func() string { return acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + if v := os.Getenv("SYSDIG_SECURE_API_TOKEN"); v == "" { + t.Fatal("SYSDIG_SECURE_API_TOKEN must be set for acceptance tests") + } + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "sysdig": func() (*schema.Provider, error) { return sysdig.Provider(), nil }, + }, + Steps: []resource.TestStep{ + { + Config: minimalVulnerabilityPolicyConfig(random()), + }, + { + Config: vulnerabilityPolicyConfig(random()), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_policy.sample", "bundles.#", "1"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_policy.sample", "bundles.0", "1"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_policy.sample", "stages.#", "3"), + ), + }, + { + ResourceName: "sysdig_secure_vulnerability_policy.sample", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func minimalVulnerabilityPolicyConfig(suffix string) string { + return fmt.Sprintf(` +resource "sysdig_secure_vulnerability_policy" "sample" { + name = "TERRAFORM TEST %s" + bundles = [ "1" ] +} +`, suffix) +} + +func vulnerabilityPolicyConfig(suffix string) string { + return fmt.Sprintf(` +resource "sysdig_secure_vulnerability_policy" "sample" { + name = "TERRAFORM TEST %s" + description = "Acceptance test for bundles as ordered list %s" + + bundles = [ "1" ] + + stages { + name = "pipeline" + configuration { + scope = "pullstring = \"foobar\"" + } + } + stages { + name = "registry" + configuration { + scope = "pullstring != \"foobar\"" + } + } + stages { + name = "runtime" + configuration { + scope = "agent.tag.cluster = \"my-cluster\"" + } + } +} +`, suffix, suffix) +} diff --git a/website/docs/r/secure_vulnerability_policy.md b/website/docs/r/secure_vulnerability_policy.md new file mode 100644 index 00000000..52c7a27f --- /dev/null +++ b/website/docs/r/secure_vulnerability_policy.md @@ -0,0 +1,61 @@ +--- +subcategory: "Sysdig Secure" +layout: "sysdig" +page_title: "Sysdig: sysdig_secure_vulnerability_policy" +description: |- + Creates a Sysdig Secure Vulnerability Policy. +--- + +# Resource: sysdig_secure_vulnerability_policy + +Creates a Sysdig Secure Vulnerability Policy. + +-> **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_vulnerability_policy" "vulnerability_policy_example" { + name = "Vulnerability Policy Name" + description = "Vulnerability Policy Description" + bundles = ["1"] + + stages { + name = "pipeline" + configuration { + scope = "container.image != ''" + } + } +} +``` + +## Argument Reference + +* `name` - (Required) The unique name of the vulnerability policy. +* `description` - (Optional) A description of the vulnerability policy. +* `bundles` - (Required) Set of bundle IDs associated with the policy. +* `stages` - (Optional) Set defining stages of vulnerability detection. + +### Stages block + +* `name` - (Required) Must be one of `pipeline`, `registry`, or `runtime`. +* `configuration` - (Optional) Configuration block for the stage. If no configuration is provided, it will apply to any workload in this stage. + +### Configuration block + +* `scope` - (Required) Scope expression defining the stage applicability. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `identifier` - The external identifier computed after creation. + +## Import + +Secure vulnerability policies can be imported using the policy ID, e.g.: + +``` +$ terraform import sysdig_secure_vulnerability_policy.example policy_123456 +``` +