diff --git a/sysdig/internal/client/v2/sysdig.go b/sysdig/internal/client/v2/sysdig.go index 65b092eb..d0fd5bdb 100644 --- a/sysdig/internal/client/v2/sysdig.go +++ b/sysdig/internal/client/v2/sysdig.go @@ -55,6 +55,7 @@ type SysdigSecure interface { PolicyInterface RuleInterface VulnerabilityPolicyClient + VulnerabilityRuleBundleClient } 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_rule_bundle.go b/sysdig/internal/client/v2/vulnerability_rule_bundle.go new file mode 100644 index 00000000..92c5f6bd --- /dev/null +++ b/sysdig/internal/client/v2/vulnerability_rule_bundle.go @@ -0,0 +1,116 @@ +package v2 + +import ( + "context" + "errors" + "fmt" + "net/http" + "strconv" +) + +const ( + vulnerabilityRuleBundlesPath = "%s/secure/vulnerability/v1/bundles" + vulnerabilityRuleBundlePath = "%s/secure/vulnerability/v1/bundles/%s" +) + +type VulnerabilityRuleBundleClient interface { + CreateVulnerabilityRuleBundle(ctx context.Context, vulnerabilityRuleBundle VulnerabilityRuleBundle) (VulnerabilityRuleBundle, error) + GetVulnerabilityRuleBundleByID(ctx context.Context, vulnerabilityRuleBundleID string) (VulnerabilityRuleBundle, error) + UpdateVulnerabilityRuleBundle(ctx context.Context, vulnerabilityRuleBundle VulnerabilityRuleBundle) (VulnerabilityRuleBundle, error) + DeleteVulnerabilityRuleBundleByID(ctx context.Context, vulnerabilityRuleBundleID string) error +} + +func (c *Client) CreateVulnerabilityRuleBundle(ctx context.Context, vulnerabilityRuleBundle VulnerabilityRuleBundle) (ruleBundle VulnerabilityRuleBundle, err error) { + payload, err := Marshal(vulnerabilityRuleBundle) + if err != nil { + return VulnerabilityRuleBundle{}, err + } + + response, err := c.requester.Request(ctx, http.MethodPost, c.vulnerabilityRuleBundlesURL(), payload) + if err != nil { + return VulnerabilityRuleBundle{}, 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 VulnerabilityRuleBundle{}, c.ErrorFromResponse(response) + } + + return Unmarshal[VulnerabilityRuleBundle](response.Body) +} + +func (c *Client) GetVulnerabilityRuleBundleByID(ctx context.Context, vulnerabilityRuleBundleID string) (ruleBundle VulnerabilityRuleBundle, err error) { + response, err := c.requester.Request(ctx, http.MethodGet, c.vulnerabilityRuleBundleURL(vulnerabilityRuleBundleID), nil) + if err != nil { + return VulnerabilityRuleBundle{}, 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 VulnerabilityRuleBundle{}, c.ErrorFromResponse(response) + } + + return Unmarshal[VulnerabilityRuleBundle](response.Body) +} + +func (c *Client) UpdateVulnerabilityRuleBundle(ctx context.Context, vulnerabilityRuleBundle VulnerabilityRuleBundle) (ruleBundle VulnerabilityRuleBundle, err error) { + if vulnerabilityRuleBundle.ID == nil { + return VulnerabilityRuleBundle{}, errors.New("rule bundle id was null") + } + + payload, err := Marshal(vulnerabilityRuleBundle) + if err != nil { + return VulnerabilityRuleBundle{}, err + } + + idAsStr := strconv.Itoa(int(*vulnerabilityRuleBundle.ID)) + response, err := c.requester.Request(ctx, http.MethodPut, c.vulnerabilityRuleBundleURL(idAsStr), payload) + if err != nil { + return VulnerabilityRuleBundle{}, 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 VulnerabilityRuleBundle{}, c.ErrorFromResponse(response) + } + + return Unmarshal[VulnerabilityRuleBundle](response.Body) +} + +func (c *Client) DeleteVulnerabilityRuleBundleByID(ctx context.Context, vulnerabilityRuleBundleID string) (err error) { + response, err := c.requester.Request(ctx, http.MethodDelete, c.vulnerabilityRuleBundleURL(vulnerabilityRuleBundleID), 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) vulnerabilityRuleBundlesURL() string { + return fmt.Sprintf(vulnerabilityRuleBundlesPath, c.config.url) +} + +func (c *Client) vulnerabilityRuleBundleURL(vulnerabilityRuleBundleID string) string { + return fmt.Sprintf(vulnerabilityRuleBundlePath, c.config.url, vulnerabilityRuleBundleID) +} diff --git a/sysdig/internal/client/v2/vulnerability_rule_bundle_model.go b/sysdig/internal/client/v2/vulnerability_rule_bundle_model.go new file mode 100644 index 00000000..c2f0c693 --- /dev/null +++ b/sysdig/internal/client/v2/vulnerability_rule_bundle_model.go @@ -0,0 +1,59 @@ +package v2 + +type VulnerabilityRuleBundle struct { + ID *int `json:"id,omitempty"` + Name string `json:"name"` + Identifier *string `json:"identifier,omitempty"` + Description *string `json:"description,omitempty"` + Rules []VulnerabilityRule `json:"rules"` +} + +type VulnerabilityRule struct { + ID *string `json:"ruleId,omitempty"` + Type VulnerabilityRuleType `json:"ruleType"` + Predicates []VulnerabilityRulePredicate `json:"predicates"` +} + +type VulnerabilityRulePredicate struct { + Type string `json:"type"` + Extra *VulnerabilityRulePredicateExtra `json:"extra,omitempty"` +} + +type VulnerabilityRulePredicateExtra struct { + Level *Level `json:"level,omitempty"` + Age *int `json:"age,omitempty"` + VulnIDS []string `json:"vulnIds,omitempty"` + Value *string `json:"value,omitempty"` + Packages []Package `json:"packages,omitempty"` + Key *string `json:"key,omitempty"` + User *string `json:"user,omitempty"` + PkgType *string `json:"pkgType,omitempty"` +} + +type Package struct { + Name string `json:"name"` + Version string `json:"version"` +} + +type Level string + +const ( + Critical Level = "critical" + High Level = "high" + Medium Level = "medium" +) + +type VulnerabilityRuleType string + +const ( + VulnerabilityRuleTypeImageConfigCreationDate VulnerabilityRuleType = "imageConfigCreationDate" + VulnerabilityRuleTypeImageConfigDefaultUser VulnerabilityRuleType = "imageConfigDefaultUser" + VulnerabilityRuleTypeImageConfigEnvVariable VulnerabilityRuleType = "imageConfigEnvVariable" + VulnerabilityRuleTypeImageConfigInstructionIsPkgManager VulnerabilityRuleType = "imageConfigInstructionIsPkgManager" + VulnerabilityRuleTypeImageConfigInstructionNotRecommended VulnerabilityRuleType = "imageConfigInstructionNotRecommended" + VulnerabilityRuleTypeImageConfigLabel VulnerabilityRuleType = "imageConfigLabel" + VulnerabilityRuleTypeImageConfigSensitiveInformationAndSecrets VulnerabilityRuleType = "imageConfigSensitiveInformationAndSecrets" + VulnerabilityRuleTypePkgDenyList VulnerabilityRuleType = "pkgDenyList" + VulnerabilityRuleTypeVulnDenyList VulnerabilityRuleType = "vulnDenyList" + VulnerabilityRuleTypeVulnSeverityAndThreats VulnerabilityRuleType = "vulnSeverityAndThreats" +) diff --git a/sysdig/provider.go b/sysdig/provider.go index 8d0623b4..8ef0ef4e 100644 --- a/sysdig/provider.go +++ b/sysdig/provider.go @@ -200,6 +200,7 @@ func (p *SysdigProvider) Provider() *schema.Provider { "sysdig_secure_team": resourceSysdigSecureTeam(), "sysdig_secure_vulnerability_accept_risk": resourceSysdigSecureVulnerabilityAcceptRisk(), "sysdig_secure_vulnerability_policy": resourceSysdigSecureVulnerabilityPolicy(), + "sysdig_secure_vulnerability_rule_bundle": resourceSysdigSecureVulnerabilityRuleBundle(), "sysdig_secure_zone": resourceSysdigSecureZone(), }, DataSourcesMap: map[string]*schema.Resource{ diff --git a/sysdig/resource_sysdig_secure_vulnerability_policy_test.go b/sysdig/resource_sysdig_secure_vulnerability_policy_test.go index 0ed667e5..2fbad888 100644 --- a/sysdig/resource_sysdig_secure_vulnerability_policy_test.go +++ b/sysdig/resource_sysdig_secure_vulnerability_policy_test.go @@ -32,7 +32,7 @@ func TestAccVulnerabilityPolicy(t *testing.T) { { Config: vulnerabilityPolicyConfig(random()), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("sysdig_secure_vulnerability_policy.sample", "bundles.#", "1"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_policy.sample", "bundles.#", "2"), resource.TestCheckResourceAttr("sysdig_secure_vulnerability_policy.sample", "bundles.0", "1"), resource.TestCheckResourceAttr("sysdig_secure_vulnerability_policy.sample", "stages.#", "3"), ), @@ -57,11 +57,20 @@ resource "sysdig_secure_vulnerability_policy" "sample" { func vulnerabilityPolicyConfig(suffix string) string { return fmt.Sprintf(` +resource "sysdig_secure_vulnerability_rule_bundle" "sample" { + name = "TERRAFORM TEST %s" + rule { + image_label { + label_must_exist = "required-label" + } + } +} + resource "sysdig_secure_vulnerability_policy" "sample" { name = "TERRAFORM TEST %s" description = "Acceptance test for bundles as ordered list %s" - bundles = [ "1" ] + bundles = [ "1", sysdig_secure_vulnerability_rule_bundle.sample.id ] stages { name = "pipeline" @@ -82,5 +91,5 @@ resource "sysdig_secure_vulnerability_policy" "sample" { } } } -`, suffix, suffix) +`, suffix, suffix, suffix) } diff --git a/sysdig/resource_sysdig_secure_vulnerability_rule_bundle.go b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle.go new file mode 100644 index 00000000..00cefc26 --- /dev/null +++ b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle.go @@ -0,0 +1,400 @@ +package sysdig + +import ( + "context" + "errors" + "fmt" + "maps" + "slices" + "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 vulnerabilityRuleSchemaImageConfigLabel() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + Description: "Defines label-based matching rules for image configuration.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + Description: "Internal identifier for the label rule block.", + }, + "label_must_exist": { + Type: schema.TypeString, + Optional: true, + Description: "A label key that must exist in the image configuration for the rule to match.", + }, + "label_must_not_exist": { + Type: schema.TypeString, + Optional: true, + Description: "A label key that must not exist in the image configuration for the rule to match.", + }, + "label_must_exist_and_contain_value": { + Type: schema.TypeList, + Optional: true, + Description: "List of label-value pairs that must exist in the image configuration for the rule to match.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "required_label": { + Type: schema.TypeString, + Required: true, + Description: "Label key that must exist in the image configuration.", + }, + "required_value": { + Type: schema.TypeString, + Required: true, + Description: "Expected value for the given label key.", + }, + }, + }, + }, + }, + }, + } +} + +func resourceSysdigSecureVulnerabilityRuleBundle() *schema.Resource { + timeout := 5 * time.Minute + + return &schema.Resource{ + CreateContext: resourceSysdigVulnerabilityRuleBundleCreate, + ReadContext: resourceSysdigVulnerabilityRuleBundleRead, + UpdateContext: resourceSysdigVulnerabilityRuleBundleUpdate, + DeleteContext: resourceSysdigVulnerabilityRuleBundleDelete, + 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: "Rule Bundle name", + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: "Rule Bundle description", + }, + "identifier": { + Type: schema.TypeString, + Computed: true, + Description: "External identifier", + }, + + "rule": { + Type: schema.TypeList, + Required: true, + MinItems: 1, + Description: "Rules for this bundle", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "image_label": vulnerabilityRuleSchemaImageConfigLabel(), + }, + }, + }, + }, + } +} + +func getSecureVulnerabilityRuleBundleClient(c SysdigClients) (v2.VulnerabilityRuleBundleClient, error) { + return c.sysdigSecureClientV2() +} + +func resourceSysdigVulnerabilityRuleBundleCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client, err := getSecureVulnerabilityRuleBundleClient(meta.(SysdigClients)) + if err != nil { + return diag.FromErr(err) + } + + scanningRuleBundle, err := vulnerabilityRuleBundleFromResourceData(d) + if err != nil { + return diag.FromErr(err) + } + + scanningRuleBundle, err = client.CreateVulnerabilityRuleBundle(ctx, scanningRuleBundle) + if err != nil { + return diag.FromErr(err) + } + + err = vulnerabilityRuleBundleToResourceData(&scanningRuleBundle, d) + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceSysdigVulnerabilityRuleBundleUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client, err := getSecureVulnerabilityRuleBundleClient(meta.(SysdigClients)) + if err != nil { + return diag.FromErr(err) + } + + scanningRuleBundle, err := vulnerabilityRuleBundleFromResourceData(d) + if err != nil { + return diag.FromErr(err) + } + + _, err = client.UpdateVulnerabilityRuleBundle(ctx, scanningRuleBundle) + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceSysdigVulnerabilityRuleBundleRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client, err := getSecureVulnerabilityRuleBundleClient(meta.(SysdigClients)) + if err != nil { + return diag.FromErr(err) + } + + scanningRuleBundle, err := client.GetVulnerabilityRuleBundleByID(ctx, d.Id()) + if err != nil { + return diag.FromErr(err) + } + + err = vulnerabilityRuleBundleToResourceData(&scanningRuleBundle, d) + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceSysdigVulnerabilityRuleBundleDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client, err := getSecureVulnerabilityRuleBundleClient(meta.(SysdigClients)) + if err != nil { + return diag.FromErr(err) + } + + err = client.DeleteVulnerabilityRuleBundleByID(ctx, d.Id()) + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +func vulnerabilityRuleBundleToResourceData(scanningRuleBundle *v2.VulnerabilityRuleBundle, d *schema.ResourceData) error { + if scanningRuleBundle.ID == nil { + d.SetId("") // id is nil, let's destroy the resource + return nil + } + + d.SetId(strconv.Itoa(int(*scanningRuleBundle.ID))) + _ = d.Set("name", scanningRuleBundle.Name) + _ = d.Set("description", scanningRuleBundle.Description) + + if scanningRuleBundle.Identifier != nil { + _ = d.Set("identifier", *scanningRuleBundle.Identifier) + } + + ruleData, err := vulnerabilityRulesToData(scanningRuleBundle.Rules) + if err != nil { + return err + } + _ = d.Set("rule", ruleData) + + return nil +} + +func vulnerabilityRulesToData(scanningRuleBundle []v2.VulnerabilityRule) ([]map[string]any, error) { + var rules []map[string]any + + for _, ruleBundle := range scanningRuleBundle { + data, err := vulnerabilityRuleToData(ruleBundle) + if err != nil { + return nil, err + } + rules = append(rules, data) + } + + return rules, nil +} + +func vulnerabilityRuleToData(ruleBundle v2.VulnerabilityRule) (map[string]any, error) { + switch ruleBundle.Type { + case v2.VulnerabilityRuleTypeImageConfigLabel: + return vulnerabilityRuleImageConfigLabelToData(ruleBundle) + default: + return nil, fmt.Errorf("unsupported rule bundle type: %s", ruleBundle.Type) + } +} + +func vulnerabilityRuleImageConfigLabelToData(ruleBundle v2.VulnerabilityRule) (map[string]any, error) { + switch ruleBundle.Predicates[0].Type { + case "imageConfigLabelNotExists": + return map[string]any{ + "image_label": []map[string]any{{ + "id": ruleBundle.ID, + "label_must_exist": ruleBundle.Predicates[0].Extra.Key, + }}, + }, nil + case "imageConfigLabelExists": + return map[string]any{ + "image_label": []map[string]any{{ + "id": ruleBundle.ID, + "label_must_not_exist": ruleBundle.Predicates[0].Extra.Key, + }}, + }, nil + case "imageConfigLabelNotContains": + return map[string]any{ + "image_label": []map[string]any{{ + "id": ruleBundle.ID, + "label_must_exist_and_contain_value": []map[string]any{{ + "required_label": ruleBundle.Predicates[0].Extra.Key, + "required_value": ruleBundle.Predicates[0].Extra.Value, + }}, + }}, + }, nil + } + + return nil, fmt.Errorf("unsupported image config label rule for predicate: %s", ruleBundle.Predicates[0].Type) +} + +func toPtr[T any](any T) *T { + return &any +} + +func vulnerabilityRuleBundleFromResourceData(d *schema.ResourceData) (v2.VulnerabilityRuleBundle, 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) *int { + id := d.Id() + if id == "" { + return nil + } + + idAsInt, err := strconv.Atoi(id) + if err != nil { + return nil + } + + return &idAsInt + } + + rules, err := vulnerabilityRulesFromList(d.Get("rule").([]any)) + if err != nil { + return v2.VulnerabilityRuleBundle{}, err + } + + return v2.VulnerabilityRuleBundle{ + ID: int32PtrFromID(d), + Identifier: stringPtr(d, "identifier"), + Name: d.Get("name").(string), + Description: toPtr(d.Get("description").(string)), + Rules: rules, + }, nil +} + +func vulnerabilityRulesFromList(list []any) ([]v2.VulnerabilityRule, error) { + var out []v2.VulnerabilityRule + + for _, ruleRaw := range list { + if ruleRaw == nil { + return nil, errors.New("empty rule detected, you need to specify one") + } + rule, err := vulnerabilityRuleFromMap(ruleRaw.(map[string]any)) + if err != nil { + return nil, err + } + out = append(out, rule) + } + + return out, nil +} + +func validateRuleMap(ruleMap map[string]any) error { + if len(ruleMap) == 1 { + return nil + } + if len(ruleMap) == 0 { + return errors.New("you must specify one rule") + } + keys := slices.Collect(maps.Keys(ruleMap)) + return fmt.Errorf("you can only specify one rule per rule block, specify more rule blocks if you need more rules, you specified: %s", keys) +} + +func vulnerabilityRuleFromMap(ruleMap map[string]any) (v2.VulnerabilityRule, error) { + err := validateRuleMap(ruleMap) + if err != nil { + return v2.VulnerabilityRule{}, err + } + + for ruleType, ruleBody := range ruleMap { + switch v2.VulnerabilityRuleType(ruleType) { + case "image_label": + return vulnerabilityRuleImageConfigLabelFromMap(ruleBody.(*schema.Set).List()[0].(map[string]any)) + } + + return v2.VulnerabilityRule{}, fmt.Errorf("unsupported rule type: %s", ruleType) + } + panic("unreachable") +} + +func vulnerabilityRuleImageConfigLabelFromMap(ruleBody map[string]any) (v2.VulnerabilityRule, error) { + rule := v2.VulnerabilityRule{ + ID: toPtr(ruleBody["id"].(string)), + Type: v2.VulnerabilityRuleTypeImageConfigLabel, + Predicates: []v2.VulnerabilityRulePredicate{}, + } + + if label, ok := ruleBody["label_must_exist"]; ok && label.(string) != "" { + rule.Predicates = append(rule.Predicates, v2.VulnerabilityRulePredicate{ + Type: "imageConfigLabelNotExists", + Extra: &v2.VulnerabilityRulePredicateExtra{ + Key: toPtr(label.(string)), + }, + }) + } + + if label, ok := ruleBody["label_must_not_exist"]; ok && label.(string) != "" { + rule.Predicates = append(rule.Predicates, v2.VulnerabilityRulePredicate{ + Type: "imageConfigLabelExists", + Extra: &v2.VulnerabilityRulePredicateExtra{ + Key: toPtr(label.(string)), + }, + }) + } + + if label, ok := ruleBody["label_must_exist_and_contain_value"]; ok && len(label.([]any)) > 0 { + contents := label.([]any)[0].(map[string]any) + rule.Predicates = append(rule.Predicates, v2.VulnerabilityRulePredicate{ + Type: "imageConfigLabelNotContains", + Extra: &v2.VulnerabilityRulePredicateExtra{ + Key: toPtr(contents["required_label"].(string)), + Value: toPtr(contents["required_value"].(string)), + }, + }) + } + + if len(rule.Predicates) == 0 { + return v2.VulnerabilityRule{}, errors.New("no predicate has been specified for image label rule") + } + + return rule, nil +} diff --git a/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go new file mode 100644 index 00000000..9f200ced --- /dev/null +++ b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go @@ -0,0 +1,129 @@ +//go:build tf_acc_sysdig_secure || tf_acc_vulnerability_scanning + +package sysdig_test + +import ( + "fmt" + "os" + "regexp" + "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 TestAccVulnerabilityRuleBundle(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: incorrectVulnerabilityRuleBundleConfig(random()), + ExpectError: regexp.MustCompile("empty rule detected, you need to specify one"), + }, + { + Config: incorrectVulnerabilityRuleBundleConfig2(random()), + ExpectError: regexp.MustCompile(`No more than 1 "image_label" blocks are allowed`), + }, + { + Config: incorrectVulnerabilityRuleBundleConfig3(random()), + ExpectError: regexp.MustCompile(`no predicate has been specified for image label rule`), + }, + { + Config: minimalVulnerabilityRuleBundleConfig(random()), + }, + { + Config: fullVulnerabilityRuleBundleConfig(random()), + }, + { + ResourceName: "sysdig_secure_vulnerability_rule_bundle.sample", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func incorrectVulnerabilityRuleBundleConfig(suffix string) string { + return fmt.Sprintf(` +resource "sysdig_secure_vulnerability_rule_bundle" "sample" { + name = "TERRAFORM TEST %s" + rule {} +} +`, suffix) +} + +func incorrectVulnerabilityRuleBundleConfig2(suffix string) string { + return fmt.Sprintf(` +resource "sysdig_secure_vulnerability_rule_bundle" "sample" { + name = "TERRAFORM TEST %s" + rule { + image_label {} + image_label {} + } +} +`, suffix) +} + +func incorrectVulnerabilityRuleBundleConfig3(suffix string) string { + return fmt.Sprintf(` +resource "sysdig_secure_vulnerability_rule_bundle" "sample" { + name = "TERRAFORM TEST %s" + rule { + image_label {} + } +} +`, suffix) +} + +func minimalVulnerabilityRuleBundleConfig(suffix string) string { + return fmt.Sprintf(` +resource "sysdig_secure_vulnerability_rule_bundle" "sample" { + name = "TERRAFORM TEST %s" + rule { + image_label { + label_must_exist = "required-label" + } + } +} +`, suffix) +} + +func fullVulnerabilityRuleBundleConfig(suffix string) string { + return fmt.Sprintf(` +resource "sysdig_secure_vulnerability_rule_bundle" "sample" { + name = "TERRAFORM TEST %s" + + rule { + image_label { + label_must_exist = "required-label" + } + } + + rule { + image_label { + label_must_not_exist = "forbidden-label" + } + } + + rule { + image_label { + label_must_exist_and_contain_value { + required_label = "required-label" + required_value = "required-value" + } + } + } +} +`, suffix) +} diff --git a/website/docs/r/secure_vulnerability_rule_bundle.md b/website/docs/r/secure_vulnerability_rule_bundle.md new file mode 100644 index 00000000..fd2e2b25 --- /dev/null +++ b/website/docs/r/secure_vulnerability_rule_bundle.md @@ -0,0 +1,78 @@ +--- +subcategory: "Sysdig Secure" +layout: "sysdig" +page_title: "Sysdig: sysdig_secure_vulnerability_rule_bundle" +description: |- + Creates a Sysdig Secure Vulnerability Rule Bundle. +--- + +# Resource: sysdig_secure_vulnerability_rule_bundle + +Creates a Sysdig Secure Vulnerability Rule Bundle to define custom rules for vulnerability management, supporting various types of rules. + +-> **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_rule_bundle" "example" { + name = "Example Rule Bundle" + + rule { + image_label { + label_must_exist = "required-label" + } + } + + rule { + image_label { + label_must_not_exist = "forbidden-label" + } + } + + rule { + image_label { + label_must_exist_and_contain_value { + required_label = "required-label" + required_value = "required-value" + } + } + } +} +``` + +## Argument Reference + +* `name` - (Required) The name of the vulnerability rule bundle. + +* `description` - (Optional) A description for the rule bundle. + +* `rule` - (Required) List of rule definitions. Each rule supports multiple types (e.g., `image_label`). Each type may have different required attributes: + +### Rule Types + +#### image_label + +Defines label-based matching rules for image configuration. Only one of the following attributes must be specified: + +* `label_must_exist` - (Optional) Label key that must exist in the image configuration. +* `label_must_not_exist` - (Optional) Label key that must not exist in the image configuration. +* `label_must_exist_and_contain_value` - (Optional) List of required label-value pairs, each containing: + + * `required_label` - (Required) Label key required in the image configuration. + * `required_value` - (Required) Value that the label must contain. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `identifier` - External identifier computed after creation. Not to be used with the `secure_vulnerability_policy.bundles` field, use `id` for that. + +## Import + +Vulnerability rule bundles can be imported using their bundle ID, for example: + +```shell +$ terraform import sysdig_secure_vulnerability_rule_bundle.example bundle_123456 +``` +