From 93f7969c5b2d4f9978417b7c435af2608795b828 Mon Sep 17 00:00:00 2001 From: Fede Barcelona Date: Fri, 12 Sep 2025 16:07:00 +0200 Subject: [PATCH 01/11] ci: add tests for the new severities_and_threats rule type --- ...g_secure_vulnerability_rule_bundle_test.go | 283 ++++++++++++++++-- 1 file changed, 266 insertions(+), 17 deletions(-) diff --git a/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go index 9f200ced..e8c647e6 100644 --- a/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go +++ b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go @@ -10,7 +10,8 @@ import ( "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.comcom/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -40,15 +41,83 @@ func TestAccVulnerabilityRuleBundle(t *testing.T) { ExpectError: regexp.MustCompile(`no predicate has been specified for image label rule`), }, { - Config: minimalVulnerabilityRuleBundleConfig(random()), + Config: minimalVulnerabilityRuleBundleConfig_ImageLabel(random()), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.image_label.0.label_must_exist", "required-label"), + ), }, { - Config: fullVulnerabilityRuleBundleConfig(random()), + Config: minimalVulnerabilityRuleBundleConfig_Severities(random()), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.severity_at_least", "critical"), + ), + }, + { + Config: fullVulnerabilityRuleBundleConfig_ImageLabel(random()), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.image_label.0.label_must_not_exist", "forbidden-label"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.1.image_label.0.label_must_exist", "another-required-label"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.2.image_label.0.label_must_exist_and_contain_value.0.required_label", "required-label"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.2.image_label.0.label_must_exist_and_contain_value.0.required_value", "required-value"), + ), + }, + { + Config: fullVulnerabilityRuleBundleConfig_Severities(random()), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.severity_at_least", "high"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.fix_available", "true"), + ), + }, + { + Config: fullVulnerabilityRuleBundleConfig_AllTypes(random()), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.image_label.0.label_must_not_exist", "forbidden-label"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.1.image_label.0.label_must_exist", "another-required-label"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.2.image_label.0.label_must_exist_and_contain_value.0.required_label", "required-label"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.2.image_label.0.label_must_exist_and_contain_value.0.required_value", "required-value"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.3.severities_and_threats.0.severity_at_least", "high"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.3.severities_and_threats.0.fix_available", "true"), + ), + }, + { + Config: variantVulnerabilityRuleBundleConfig_SeverityEquals(random()), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.severity_equals", "medium"), + ), + }, + { + Config: variantVulnerabilityRuleBundleConfig_Cvss(random()), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.cvss_at_least", "7.5"), + ), + }, + { + Config: errorVulnerabilityRuleBundleConfig_Conflict1(random()), + ExpectError: regexp.MustCompile(`only one of.+severity_at_least.+severity_equals.+can be set`), + }, + { + Config: errorVulnerabilityRuleBundleConfig_Conflict2(random()), + ExpectError: regexp.MustCompile(`only one of.+severity_at_least.+cvss_at_least.+can be set`), + }, + { + Config: errorVulnerabilityRuleBundleConfig_Conflict3(random()), + ExpectError: regexp.MustCompile(`only one of.+severity_equals.+cvss_at_least.+can be set`), + }, + { + Config: errorVulnerabilityRuleBundleConfig_DisclosureConflict(random()), + ExpectError: regexp.MustCompile("`disclosure_older_than_days` and `disclosure_date` are mutually exclusive"), }, { ResourceName: "sysdig_secure_vulnerability_rule_bundle.sample", ImportState: true, ImportStateVerify: true, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources["sysdig_secure_vulnerability_rule_bundle.sample"] + if !ok { + return "", fmt.Errorf("Not found: sysdig_secure_vulnerability_rule_bundle.sample") + } + return rs.Primary.ID, nil + }, }, }, }) @@ -86,7 +155,7 @@ resource "sysdig_secure_vulnerability_rule_bundle" "sample" { `, suffix) } -func minimalVulnerabilityRuleBundleConfig(suffix string) string { +func minimalVulnerabilityRuleBundleConfig_ImageLabel(suffix string) string { return fmt.Sprintf(` resource "sysdig_secure_vulnerability_rule_bundle" "sample" { name = "TERRAFORM TEST %s" @@ -99,30 +168,210 @@ resource "sysdig_secure_vulnerability_rule_bundle" "sample" { `, suffix) } -func fullVulnerabilityRuleBundleConfig(suffix string) string { +func minimalVulnerabilityRuleBundleConfig_Severities(suffix string) string { return fmt.Sprintf(` resource "sysdig_secure_vulnerability_rule_bundle" "sample" { name = "TERRAFORM TEST %s" + rule { + severities_and_threats { + severity_at_least = "critical" + } + } +} +`, suffix) +} + +func fullVulnerabilityRuleBundleConfig_ImageLabel(suffix string) string { + return fmt.Sprintf(` +resource "sysdig_secure_vulnerability_rule_bundle" "sample" { + name = "TERRAFORM TEST %s" + description = "Full bundle with image_label rules" rule { - image_label { - label_must_exist = "required-label" - } + image_label { + label_must_not_exist = "forbidden-label" + } } rule { - image_label { - label_must_not_exist = "forbidden-label" - } + image_label { + label_must_exist = "another-required-label" + } } rule { - image_label { - label_must_exist_and_contain_value { - required_label = "required-label" - required_value = "required-value" - } - } + image_label { + label_must_exist_and_contain_value { + required_label = "required-label" + required_value = "required-value" + } + } + } +} +`, suffix) +} + +func fullVulnerabilityRuleBundleConfig_Severities(suffix string) string { + return fmt.Sprintf(` +resource "sysdig_secure_vulnerability_rule_bundle" "sample" { + name = "TERRAFORM TEST %s" + description = "Full bundle with severities_and_threats rules" + + rule { + severities_and_threats { + severity_at_least = "high" + disclosure_older_than_days = 90 + package_type = "os" + in_use = true + fix_available = true + fix_available_since_days = 30 + public_exploit_available = true + public_exploit_available_since_days = 15 + exploit_no_admin_privileges = true + exploit_no_user_interaction = true + exploit_network_attack_vector = true + cisa_kev_in_ransomware_campaign = true + cisa_kev_available_since_days = 10 + cisa_kev_due_date_in_days = 21 + epss_score_at_least_percentage = 80 + epss_percentile_at_least_percentage = 90 + } + } +} +`, suffix) +} + +func fullVulnerabilityRuleBundleConfig_AllTypes(suffix string) string { + return fmt.Sprintf(` +resource "sysdig_secure_vulnerability_rule_bundle" "sample" { + name = "TERRAFORM TEST %s" + description = "Full bundle with all rule types" + + rule { + image_label { + label_must_not_exist = "forbidden-label" + } + } + + rule { + image_label { + label_must_exist = "another-required-label" + } + } + + rule { + image_label { + label_must_exist_and_contain_value { + required_label = "required-label" + required_value = "required-value" + } + } + } + + rule { + severities_and_threats { + severity_at_least = "high" + disclosure_older_than_days = 90 + package_type = "os" + in_use = true + fix_available = true + fix_available_since_days = 30 + public_exploit_available = true + public_exploit_available_since_days = 15 + exploit_no_admin_privileges = true + exploit_no_user_interaction = true + exploit_network_attack_vector = true + cisa_kev_in_ransomware_campaign = true + cisa_kev_available_since_days = 10 + cisa_kev_due_date_in_days = 21 + epss_score_at_least_percentage = 80 + epss_percentile_at_least_percentage = 90 + } + } +} +`, suffix) +} + +func variantVulnerabilityRuleBundleConfig_SeverityEquals(suffix string) string { + return fmt.Sprintf(` +resource "sysdig_secure_vulnerability_rule_bundle" "sample" { + name = "TERRAFORM TEST %s" + rule { + severities_and_threats { + severity_equals = "medium" + } + } +} +`, suffix) +} + +func variantVulnerabilityRuleBundleConfig_Cvss(suffix string) string { + return fmt.Sprintf(` +resource "sysdig_secure_vulnerability_rule_bundle" "sample" { + name = "TERRAFORM TEST %s" + rule { + severities_and_threats { + cvss_at_least = 7.5 + } + } +} +`, suffix) +} + +func errorVulnerabilityRuleBundleConfig_Conflict1(suffix string) string { + return fmt.Sprintf(` +resource "sysdig_secure_vulnerability_rule_bundle" "sample" { + name = "TERRAFORM TEST %s" + rule { + severities_and_threats { + severity_at_least = "high" + severity_equals = "high" + } + } +} +`, suffix) +} + +func errorVulnerabilityRuleBundleConfig_Conflict2(suffix string) string { + return fmt.Sprintf(` +resource "sysdig_secure_vulnerability_rule_bundle" "sample" { + name = "TERRAFORM TEST %s" + rule { + severities_and_threats { + severity_at_least = "high" + cvss_at_least = 5.0 + } + } +} +`, suffix) +} + +func errorVulnerabilityRuleBundleConfig_Conflict3(suffix string) string { + return fmt.Sprintf(` +resource "sysdig_secure_vulnerability_rule_bundle" "sample" { + name = "TERRAFORM TEST %s" + rule { + severities_and_threats { + severity_equals = "medium" + cvss_at_least = 5.0 + } + } +} +`, suffix) +} + +func errorVulnerabilityRuleBundleConfig_DisclosureConflict(suffix string) string { + return fmt.Sprintf(` +resource "sysdig_secure_vulnerability_rule_bundle" "sample" { + name = "TERRAFORM TEST %s" + rule { + severities_and_threats { + disclosure_older_than_days = 90 + disclosure_date { + from = "2023-01-01" + to = "2023-01-31" + } + } } } `, suffix) From 0c39969a40ebffa13714391216d9b66e0fac7375 Mon Sep 17 00:00:00 2001 From: Fede Barcelona Date: Fri, 12 Sep 2025 16:41:37 +0200 Subject: [PATCH 02/11] feat: implement predicate values for the model --- .../v2/vulnerability_rule_bundle_model.go | 26 +++++++++++++------ ...g_secure_vulnerability_rule_bundle_test.go | 2 +- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/sysdig/internal/client/v2/vulnerability_rule_bundle_model.go b/sysdig/internal/client/v2/vulnerability_rule_bundle_model.go index c2f0c693..83795a73 100644 --- a/sysdig/internal/client/v2/vulnerability_rule_bundle_model.go +++ b/sysdig/internal/client/v2/vulnerability_rule_bundle_model.go @@ -20,14 +20,24 @@ type VulnerabilityRulePredicate struct { } 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"` + // Common fields for different predicate types + Level *Level `json:"level,omitempty"` + Age *int `json:"age,omitempty"` + Days *int `json:"days,omitempty"` + PkgType *string `json:"pkgType,omitempty"` + VulnIDS []string `json:"vulnIds,omitempty"` + Packages []Package `json:"packages,omitempty"` + Key *string `json:"key,omitempty"` + User *string `json:"user,omitempty"` + Value interface{} `json:"value,omitempty"` // For image labels or CVSS score + + // Disclosure Date Range + StartDate *string `json:"startDate,omitempty"` + EndDate *string `json:"endDate,omitempty"` + + // EPSS + Score *int `json:"score,omitempty"` + Percentile *int `json:"percentile,omitempty"` } type Package struct { diff --git a/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go index e8c647e6..b48d004d 100644 --- a/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go +++ b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go @@ -10,7 +10,7 @@ import ( "github.com/draios/terraform-provider-sysdig/sysdig" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" - "github.comcom/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) From 7303700544e3ca9ab2381830d6f5dd53a4645048 Mon Sep 17 00:00:00 2001 From: Fede Barcelona Date: Fri, 12 Sep 2025 17:30:41 +0200 Subject: [PATCH 03/11] feat: Add severities_and_threats rule schema --- .../v2/vulnerability_rule_bundle_model.go | 5 ++ ...sysdig_secure_vulnerability_rule_bundle.go | 47 ++++++++++++++++--- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/sysdig/internal/client/v2/vulnerability_rule_bundle_model.go b/sysdig/internal/client/v2/vulnerability_rule_bundle_model.go index 83795a73..c07a1f39 100644 --- a/sysdig/internal/client/v2/vulnerability_rule_bundle_model.go +++ b/sysdig/internal/client/v2/vulnerability_rule_bundle_model.go @@ -67,3 +67,8 @@ const ( VulnerabilityRuleTypeVulnDenyList VulnerabilityRuleType = "vulnDenyList" VulnerabilityRuleTypeVulnSeverityAndThreats VulnerabilityRuleType = "vulnSeverityAndThreats" ) + +type DateRange struct { + From string `json:"from"` + To string `json:"to"` +} diff --git a/sysdig/resource_sysdig_secure_vulnerability_rule_bundle.go b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle.go index 00cefc26..5ad9d9eb 100644 --- a/sysdig/resource_sysdig_secure_vulnerability_rule_bundle.go +++ b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle.go @@ -4,8 +4,6 @@ import ( "context" "errors" "fmt" - "maps" - "slices" "strconv" "time" @@ -106,6 +104,7 @@ func resourceSysdigSecureVulnerabilityRuleBundle() *schema.Resource { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "image_label": vulnerabilityRuleSchemaImageConfigLabel(), + "severities_and_threats": vulnerabilityRuleSchemaSeveritiesAndThreats(), }, }, }, @@ -113,6 +112,18 @@ func resourceSysdigSecureVulnerabilityRuleBundle() *schema.Resource { } } +func vulnerabilityRuleSchemaSeveritiesAndThreats() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + Description: "Defines rules based on vulnerability severity and threat intelligence.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{}, + }, + } +} + func getSecureVulnerabilityRuleBundleClient(c SysdigClients) (v2.VulnerabilityRuleBundleClient, error) { return c.sysdigSecureClientV2() } @@ -329,14 +340,22 @@ func vulnerabilityRulesFromList(list []any) ([]v2.VulnerabilityRule, error) { } func validateRuleMap(ruleMap map[string]any) error { - if len(ruleMap) == 1 { - return nil + var activeRuleNames []string + for key, val := range ruleMap { + if val.(*schema.Set).Len() > 0 { + activeRuleNames = append(activeRuleNames, key) + } } - if len(ruleMap) == 0 { + + if len(activeRuleNames) == 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) + + if len(activeRuleNames) > 1 { + return fmt.Errorf("you can only specify one rule per rule block, specify more rule blocks if you need more rules, you specified: %s", activeRuleNames) + } + + return nil } func vulnerabilityRuleFromMap(ruleMap map[string]any) (v2.VulnerabilityRule, error) { @@ -349,6 +368,8 @@ func vulnerabilityRuleFromMap(ruleMap map[string]any) (v2.VulnerabilityRule, err switch v2.VulnerabilityRuleType(ruleType) { case "image_label": return vulnerabilityRuleImageConfigLabelFromMap(ruleBody.(*schema.Set).List()[0].(map[string]any)) + case "severities_and_threats": + return vulnerabilityRuleSeveritiesAndThreatsFromMap(ruleBody.(*schema.Set).List()[0].(map[string]any)) } return v2.VulnerabilityRule{}, fmt.Errorf("unsupported rule type: %s", ruleType) @@ -356,6 +377,18 @@ func vulnerabilityRuleFromMap(ruleMap map[string]any) (v2.VulnerabilityRule, err panic("unreachable") } +func vulnerabilityRuleSeveritiesAndThreatsFromMap(ruleBody map[string]any) (v2.VulnerabilityRule, error) { + rule := v2.VulnerabilityRule{ + ID: toPtr(ruleBody["id"].(string)), + Type: v2.VulnerabilityRuleTypeVulnSeverityAndThreats, + } + + // This is where the translation logic will go. + // For now, it returns an empty rule. + + return rule, nil +} + func vulnerabilityRuleImageConfigLabelFromMap(ruleBody map[string]any) (v2.VulnerabilityRule, error) { rule := v2.VulnerabilityRule{ ID: toPtr(ruleBody["id"].(string)), From 9be7491d6dd75024989e6dec207f453c4be0235d Mon Sep 17 00:00:00 2001 From: Fede Barcelona Date: Mon, 15 Sep 2025 09:29:07 +0200 Subject: [PATCH 04/11] ci: add tests to verify that fields are mutually exclusive --- ...g_secure_vulnerability_rule_bundle_test.go | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go index b48d004d..fbc534ac 100644 --- a/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go +++ b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go @@ -107,6 +107,14 @@ func TestAccVulnerabilityRuleBundle(t *testing.T) { Config: errorVulnerabilityRuleBundleConfig_DisclosureConflict(random()), ExpectError: regexp.MustCompile("`disclosure_older_than_days` and `disclosure_date` are mutually exclusive"), }, + { + Config: errorVulnerabilityRuleBundleConfig_FixConflict(random()), + ExpectError: regexp.MustCompile("`fix_available` and `fix_available_since_days` are mutually exclusive"), + }, + { + Config: errorVulnerabilityRuleBundleConfig_ExploitConflict(random()), + ExpectError: regexp.MustCompile("`public_exploit_available` and `public_exploit_available_since_days` are mutually exclusive"), + }, { ResourceName: "sysdig_secure_vulnerability_rule_bundle.sample", ImportState: true, @@ -223,9 +231,7 @@ resource "sysdig_secure_vulnerability_rule_bundle" "sample" { disclosure_older_than_days = 90 package_type = "os" in_use = true - fix_available = true fix_available_since_days = 30 - public_exploit_available = true public_exploit_available_since_days = 15 exploit_no_admin_privileges = true exploit_no_user_interaction = true @@ -274,9 +280,7 @@ resource "sysdig_secure_vulnerability_rule_bundle" "sample" { disclosure_older_than_days = 90 package_type = "os" in_use = true - fix_available = true fix_available_since_days = 30 - public_exploit_available = true public_exploit_available_since_days = 15 exploit_no_admin_privileges = true exploit_no_user_interaction = true @@ -376,3 +380,31 @@ resource "sysdig_secure_vulnerability_rule_bundle" "sample" { } `, suffix) } + +func errorVulnerabilityRuleBundleConfig_FixConflict(suffix string) string { + return fmt.Sprintf(` +resource "sysdig_secure_vulnerability_rule_bundle" "sample" { + name = "TERRAFORM TEST %s" + rule { + severities_and_threats { + fix_available = true + fix_available_since_days = 30 + } + } +} +`, suffix) +} + +func errorVulnerabilityRuleBundleConfig_ExploitConflict(suffix string) string { + return fmt.Sprintf(` +resource "sysdig_secure_vulnerability_rule_bundle" "sample" { + name = "TERRAFORM TEST %s" + rule { + severities_and_threats { + public_exploit_available = true + public_exploit_available_since_days = 15 + } + } +} +`, suffix) +} From 7c59f5caf65ebbe375c058cd243b446005e9b3ef Mon Sep 17 00:00:00 2001 From: Fede Barcelona Date: Mon, 15 Sep 2025 10:25:37 +0200 Subject: [PATCH 05/11] fix(ci): solve tests checks --- ...e_sysdig_secure_vulnerability_rule_bundle_test.go | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go index fbc534ac..13d7dcde 100644 --- a/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go +++ b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go @@ -11,7 +11,6 @@ import ( "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/terraform" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -65,7 +64,7 @@ func TestAccVulnerabilityRuleBundle(t *testing.T) { Config: fullVulnerabilityRuleBundleConfig_Severities(random()), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.severity_at_least", "high"), - resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.fix_available", "true"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.fix_available_since_days", "30"), ), }, { @@ -76,7 +75,7 @@ func TestAccVulnerabilityRuleBundle(t *testing.T) { resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.2.image_label.0.label_must_exist_and_contain_value.0.required_label", "required-label"), resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.2.image_label.0.label_must_exist_and_contain_value.0.required_value", "required-value"), resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.3.severities_and_threats.0.severity_at_least", "high"), - resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.3.severities_and_threats.0.fix_available", "true"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.3.severities_and_threats.0.fix_available_since_days", "30"), ), }, { @@ -119,13 +118,6 @@ func TestAccVulnerabilityRuleBundle(t *testing.T) { ResourceName: "sysdig_secure_vulnerability_rule_bundle.sample", ImportState: true, ImportStateVerify: true, - ImportStateIdFunc: func(s *terraform.State) (string, error) { - rs, ok := s.RootModule().Resources["sysdig_secure_vulnerability_rule_bundle.sample"] - if !ok { - return "", fmt.Errorf("Not found: sysdig_secure_vulnerability_rule_bundle.sample") - } - return rs.Primary.ID, nil - }, }, }, }) From 11320ee82879c874210beaa6b600aacee7b4de6c Mon Sep 17 00:00:00 2001 From: Fede Barcelona Date: Mon, 15 Sep 2025 10:58:40 +0200 Subject: [PATCH 06/11] feat: implement conversion between model and api --- ...sysdig_secure_vulnerability_rule_bundle.go | 357 +++++++++++++++++- ...g_secure_vulnerability_rule_bundle_test.go | 48 +-- 2 files changed, 372 insertions(+), 33 deletions(-) diff --git a/sysdig/resource_sysdig_secure_vulnerability_rule_bundle.go b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle.go index 5ad9d9eb..632f97ef 100644 --- a/sysdig/resource_sysdig_secure_vulnerability_rule_bundle.go +++ b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle.go @@ -103,7 +103,7 @@ func resourceSysdigSecureVulnerabilityRuleBundle() *schema.Resource { Description: "Rules for this bundle", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "image_label": vulnerabilityRuleSchemaImageConfigLabel(), + "image_label": vulnerabilityRuleSchemaImageConfigLabel(), "severities_and_threats": vulnerabilityRuleSchemaSeveritiesAndThreats(), }, }, @@ -119,7 +119,123 @@ func vulnerabilityRuleSchemaSeveritiesAndThreats() *schema.Schema { MaxItems: 1, Description: "Defines rules based on vulnerability severity and threat intelligence.", Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{}, + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + Description: "Internal identifier for the severities and threats rule block.", + }, + "severity_at_least": { + Type: schema.TypeString, + Optional: true, + Description: "Vulnerability severity must be at least this level (critical, high, medium, low, negligible).", + }, + "severity_equals": { + Type: schema.TypeString, + Optional: true, + Description: "Vulnerability severity must be exactly this level.", + }, + "cvss_at_least": { + Type: schema.TypeFloat, + Optional: true, + Description: "Vulnerability CVSS score must be at least this value.", + }, + "disclosure_older_than_days": { + Type: schema.TypeInt, + Optional: true, + Description: "Vulnerability was disclosed more than this number of days ago.", + }, + "disclosure_date": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: "Vulnerability was disclosed within this date range.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "from": { + Type: schema.TypeString, + Required: true, + Description: "Start of the date range (YYYY-MM-DD).", + }, + "to": { + Type: schema.TypeString, + Required: true, + Description: "End of the date range (YYYY-MM-DD).", + }, + }, + }, + }, + "package_type": { + Type: schema.TypeString, + Optional: true, + Description: "Type of the package (e.g., 'os', 'npm', 'maven').", + }, + "in_use": { + Type: schema.TypeBool, + Optional: true, + Description: "Package is currently in use at runtime.", + }, + "fix_available": { + Type: schema.TypeBool, + Optional: true, + Description: "A fix is available for the vulnerability.", + }, + "fix_available_since_days": { + Type: schema.TypeInt, + Optional: true, + Description: "A fix has been available for at least this number of days.", + }, + "public_exploit_available": { + Type: schema.TypeBool, + Optional: true, + Description: "A public exploit is available for the vulnerability.", + }, + "public_exploit_available_since_days": { + Type: schema.TypeInt, + Optional: true, + Description: "A public exploit has been available for at least this number of days.", + }, + "exploit_no_admin_privileges": { + Type: schema.TypeBool, + Optional: true, + Description: "Exploit does not require admin privileges.", + }, + "exploit_no_user_interaction": { + Type: schema.TypeBool, + Optional: true, + Description: "Exploit does not require user interaction.", + }, + "exploit_network_attack_vector": { + Type: schema.TypeBool, + Optional: true, + Description: "Exploit has a network attack vector.", + }, + "cisa_kev_in_ransomware_campaign": { + Type: schema.TypeBool, + Optional: true, + Description: "Vulnerability is in a CISA KEV ransomware campaign.", + }, + "cisa_kev_available_since_days": { + Type: schema.TypeInt, + Optional: true, + Description: "Vulnerability has been in CISA KEV for at least this number of days.", + }, + "cisa_kev_due_date_in_days": { + Type: schema.TypeInt, + Optional: true, + Description: "CISA KEV due date is within this number of days.", + }, + "epss_score_at_least_percentage": { + Type: schema.TypeInt, + Optional: true, + Description: "EPSS score is at least this percentage.", + }, + "epss_percentile_at_least_percentage": { + Type: schema.TypeInt, + Optional: true, + Description: "EPSS percentile is at least this percentage.", + }, + }, }, } } @@ -245,11 +361,189 @@ func vulnerabilityRuleToData(ruleBundle v2.VulnerabilityRule) (map[string]any, e switch ruleBundle.Type { case v2.VulnerabilityRuleTypeImageConfigLabel: return vulnerabilityRuleImageConfigLabelToData(ruleBundle) + case v2.VulnerabilityRuleTypeVulnSeverityAndThreats: + return vulnerabilityRuleSeveritiesAndThreatsToData(ruleBundle) default: return nil, fmt.Errorf("unsupported rule bundle type: %s", ruleBundle.Type) } } +func vulnerabilityRuleSeveritiesAndThreatsToData(ruleBundle v2.VulnerabilityRule) (map[string]any, error) { + ruleData := map[string]any{ + "id": ruleBundle.ID, + } + + for _, predicate := range ruleBundle.Predicates { + switch predicate.Type { + case "vulnSeverity": + ruleData["severity_at_least"] = predicate.Extra.Level + case "vulnSeverityEquals": + ruleData["severity_equals"] = predicate.Extra.Level + case "vulnCVSS": + // The value comes as float64 from the API, which is the type in the schema + if v, ok := predicate.Extra.Value.(float64); ok { + ruleData["cvss_at_least"] = v + } + case "vulnAge": + ruleData["disclosure_older_than_days"] = predicate.Extra.Age + case "vulnDisclosureRange": + ruleData["disclosure_date"] = []map[string]any{ + { + "from": predicate.Extra.StartDate, + "to": predicate.Extra.EndDate, + }, + } + case "vulnPkgType": + ruleData["package_type"] = predicate.Extra.PkgType + case "vulnIsInUse": + ruleData["in_use"] = true + case "vulnIsFixable": + ruleData["fix_available"] = true + case "vulnIsFixableWithAge": + ruleData["fix_available_since_days"] = predicate.Extra.Age + case "vulnIsExploitable": + ruleData["public_exploit_available"] = true + case "vulnExploitableWithAge": + ruleData["public_exploit_available_since_days"] = predicate.Extra.Age + case "vulnExploitableNoAdmin": + ruleData["exploit_no_admin_privileges"] = true + case "vulnExploitableNoUser": + ruleData["exploit_no_user_interaction"] = true + case "vulnExploitableViaNetwork": + ruleData["exploit_network_attack_vector"] = true + case "cisaKevKnownRansomwareCampaignUse": + ruleData["cisa_kev_in_ransomware_campaign"] = true + case "cisaKevAvailableSince": + ruleData["cisa_kev_available_since_days"] = predicate.Extra.Days + case "cisaKevDueDateIn": + ruleData["cisa_kev_due_date_in_days"] = predicate.Extra.Days + case "vulnEpssScoreGte": + ruleData["epss_score_at_least_percentage"] = predicate.Extra.Score + case "vulnEpssPercentileGte": + ruleData["epss_percentile_at_least_percentage"] = predicate.Extra.Percentile + } + } + + return map[string]any{ + "severities_and_threats": []any{ruleData}, + }, nil +} + +func vulnerabilityRuleSeveritiesAndThreatsFromMap(ruleBody map[string]any) (v2.VulnerabilityRule, error) { + if err := validateSeveritiesAndThreatsRule(ruleBody); err != nil { + return v2.VulnerabilityRule{}, err + } + + rule := v2.VulnerabilityRule{ + ID: toPtr(ruleBody["id"].(string)), + Type: v2.VulnerabilityRuleTypeVulnSeverityAndThreats, + Predicates: []v2.VulnerabilityRulePredicate{}, + } + + addPredicate := func(predicateType string, extra *v2.VulnerabilityRulePredicateExtra) { + rule.Predicates = append(rule.Predicates, v2.VulnerabilityRulePredicate{ + Type: predicateType, + Extra: extra, + }) + } + + if val, ok := ruleBody["severity_at_least"]; ok && val.(string) != "" { + level := v2.Level(val.(string)) + addPredicate("vulnSeverity", &v2.VulnerabilityRulePredicateExtra{Level: &level}) + } + + if val, ok := ruleBody["severity_equals"]; ok && val.(string) != "" { + level := v2.Level(val.(string)) + addPredicate("vulnSeverityEquals", &v2.VulnerabilityRulePredicateExtra{Level: &level}) + } + + if val, ok := ruleBody["cvss_at_least"]; ok && val.(float64) > 0 { + score := val.(float64) + addPredicate("vulnCVSS", &v2.VulnerabilityRulePredicateExtra{Value: &score}) + } + + if val, ok := ruleBody["disclosure_older_than_days"]; ok && val.(int) > 0 { + days := val.(int) + addPredicate("vulnAge", &v2.VulnerabilityRulePredicateExtra{Age: &days}) + } + + if val, ok := ruleBody["disclosure_date"]; ok && len(val.([]any)) > 0 { + dates := val.([]any)[0].(map[string]any) + startDate := dates["from"].(string) + endDate := dates["to"].(string) + addPredicate("vulnDisclosureRange", &v2.VulnerabilityRulePredicateExtra{StartDate: &startDate, EndDate: &endDate}) + } + + if val, ok := ruleBody["package_type"]; ok && val.(string) != "" { + pkgType := val.(string) + addPredicate("vulnPkgType", &v2.VulnerabilityRulePredicateExtra{PkgType: &pkgType}) + } + + if val, ok := ruleBody["in_use"]; ok && val.(bool) { + addPredicate("vulnIsInUse", nil) + } + + if val, ok := ruleBody["fix_available"]; ok && val.(bool) { + addPredicate("vulnIsFixable", nil) + } + + if val, ok := ruleBody["fix_available_since_days"]; ok && val.(int) > 0 { + days := val.(int) + addPredicate("vulnIsFixableWithAge", &v2.VulnerabilityRulePredicateExtra{Age: &days}) + } + + if val, ok := ruleBody["public_exploit_available"]; ok && val.(bool) { + addPredicate("vulnIsExploitable", nil) + } + + if val, ok := ruleBody["public_exploit_available_since_days"]; ok && val.(int) > 0 { + days := val.(int) + addPredicate("vulnExploitableWithAge", &v2.VulnerabilityRulePredicateExtra{Age: &days}) + } + + if val, ok := ruleBody["exploit_no_admin_privileges"]; ok && val.(bool) { + addPredicate("vulnExploitableNoAdmin", nil) + } + + if val, ok := ruleBody["exploit_no_user_interaction"]; ok && val.(bool) { + addPredicate("vulnExploitableNoUser", nil) + } + + if val, ok := ruleBody["exploit_network_attack_vector"]; ok && val.(bool) { + addPredicate("vulnExploitableViaNetwork", nil) + } + + if val, ok := ruleBody["cisa_kev_in_ransomware_campaign"]; ok && val.(bool) { + addPredicate("cisaKevKnownRansomwareCampaignUse", nil) + } + + if val, ok := ruleBody["cisa_kev_available_since_days"]; ok && val.(int) > 0 { + days := val.(int) + addPredicate("cisaKevAvailableSince", &v2.VulnerabilityRulePredicateExtra{Days: &days}) + } + + if val, ok := ruleBody["cisa_kev_due_date_in_days"]; ok && val.(int) > 0 { + days := val.(int) + addPredicate("cisaKevDueDateIn", &v2.VulnerabilityRulePredicateExtra{Days: &days}) + } + + if val, ok := ruleBody["epss_score_at_least_percentage"]; ok && val.(int) > 0 { + score := val.(int) + addPredicate("vulnEpssScoreGte", &v2.VulnerabilityRulePredicateExtra{Score: &score}) + } + + if val, ok := ruleBody["epss_percentile_at_least_percentage"]; ok && val.(int) > 0 { + percentile := val.(int) + addPredicate("vulnEpssPercentileGte", &v2.VulnerabilityRulePredicateExtra{Percentile: &percentile}) + } + + if len(rule.Predicates) == 0 { + return v2.VulnerabilityRule{}, errors.New("no predicate has been specified for severities_and_threats rule") + } + + return rule, nil +} + func vulnerabilityRuleImageConfigLabelToData(ruleBundle v2.VulnerabilityRule) (map[string]any, error) { switch ruleBundle.Predicates[0].Type { case "imageConfigLabelNotExists": @@ -365,6 +659,9 @@ func vulnerabilityRuleFromMap(ruleMap map[string]any) (v2.VulnerabilityRule, err } for ruleType, ruleBody := range ruleMap { + if ruleBody.(*schema.Set).Len() == 0 { + continue + } switch v2.VulnerabilityRuleType(ruleType) { case "image_label": return vulnerabilityRuleImageConfigLabelFromMap(ruleBody.(*schema.Set).List()[0].(map[string]any)) @@ -377,18 +674,60 @@ func vulnerabilityRuleFromMap(ruleMap map[string]any) (v2.VulnerabilityRule, err panic("unreachable") } -func vulnerabilityRuleSeveritiesAndThreatsFromMap(ruleBody map[string]any) (v2.VulnerabilityRule, error) { - rule := v2.VulnerabilityRule{ - ID: toPtr(ruleBody["id"].(string)), - Type: v2.VulnerabilityRuleTypeVulnSeverityAndThreats, +func validateSeveritiesAndThreatsRule(ruleBody map[string]any) error { + // Programmatic validation for mutually exclusive fields. + severityConflictCount := 0 + if val, ok := ruleBody["severity_at_least"]; ok && val.(string) != "" { + severityConflictCount++ + } + if val, ok := ruleBody["severity_equals"]; ok && val.(string) != "" { + severityConflictCount++ + } + if val, ok := ruleBody["cvss_at_least"]; ok && val.(float64) > 0 { + severityConflictCount++ + } + if severityConflictCount > 1 { + return errors.New("only one of 'severity_at_least', 'severity_equals', or 'cvss_at_least' can be set") } - // This is where the translation logic will go. - // For now, it returns an empty rule. + disclosureConflictCount := 0 + if val, ok := ruleBody["disclosure_older_than_days"]; ok && val.(int) > 0 { + disclosureConflictCount++ + } + if val, ok := ruleBody["disclosure_date"]; ok && len(val.([]any)) > 0 { + disclosureConflictCount++ + } + if disclosureConflictCount > 1 { + return errors.New("`disclosure_older_than_days` and `disclosure_date` are mutually exclusive") + } - return rule, nil + exploitConflictCount := 0 + if val, ok := ruleBody["public_exploit_available"]; ok && val.(bool) { + exploitConflictCount++ + } + if val, ok := ruleBody["public_exploit_available_since_days"]; ok && val.(int) > 0 { + exploitConflictCount++ + } + if exploitConflictCount > 1 { + return errors.New("`public_exploit_available` and `public_exploit_available_since_days` are mutually exclusive") + } + + fixConflictCount := 0 + if val, ok := ruleBody["fix_available"]; ok && val.(bool) { + fixConflictCount++ + } + if val, ok := ruleBody["fix_available_since_days"]; ok && val.(int) > 0 { + fixConflictCount++ + } + if fixConflictCount > 1 { + return errors.New("`fix_available` and `fix_available_since_days` are mutually exclusive") + } + + return nil } + + func vulnerabilityRuleImageConfigLabelFromMap(ruleBody map[string]any) (v2.VulnerabilityRule, error) { rule := v2.VulnerabilityRule{ ID: toPtr(ruleBody["id"].(string)), diff --git a/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go index 13d7dcde..9d7d6ff4 100644 --- a/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go +++ b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go @@ -39,6 +39,30 @@ func TestAccVulnerabilityRuleBundle(t *testing.T) { Config: incorrectVulnerabilityRuleBundleConfig3(random()), ExpectError: regexp.MustCompile(`no predicate has been specified for image label rule`), }, + { + Config: errorVulnerabilityRuleBundleConfig_Conflict1(random()), + ExpectError: regexp.MustCompile(`only one of.+severity_at_least.+severity_equals.+can be set`), + }, + { + Config: errorVulnerabilityRuleBundleConfig_Conflict2(random()), + ExpectError: regexp.MustCompile(`only one of.+severity_at_least.+cvss_at_least.+can be set`), + }, + { + Config: errorVulnerabilityRuleBundleConfig_Conflict3(random()), + ExpectError: regexp.MustCompile(`only one of.+severity_equals.+cvss_at_least.+can be set`), + }, + { + Config: errorVulnerabilityRuleBundleConfig_DisclosureConflict(random()), + ExpectError: regexp.MustCompile("`disclosure_older_than_days` and `disclosure_date` are mutually exclusive"), + }, + { + Config: errorVulnerabilityRuleBundleConfig_FixConflict(random()), + ExpectError: regexp.MustCompile("`fix_available` and `fix_available_since_days` are mutually exclusive"), + }, + { + Config: errorVulnerabilityRuleBundleConfig_ExploitConflict(random()), + ExpectError: regexp.MustCompile("`public_exploit_available` and `public_exploit_available_since_days` are mutually exclusive"), + }, { Config: minimalVulnerabilityRuleBundleConfig_ImageLabel(random()), Check: resource.ComposeTestCheckFunc( @@ -90,30 +114,6 @@ func TestAccVulnerabilityRuleBundle(t *testing.T) { resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.cvss_at_least", "7.5"), ), }, - { - Config: errorVulnerabilityRuleBundleConfig_Conflict1(random()), - ExpectError: regexp.MustCompile(`only one of.+severity_at_least.+severity_equals.+can be set`), - }, - { - Config: errorVulnerabilityRuleBundleConfig_Conflict2(random()), - ExpectError: regexp.MustCompile(`only one of.+severity_at_least.+cvss_at_least.+can be set`), - }, - { - Config: errorVulnerabilityRuleBundleConfig_Conflict3(random()), - ExpectError: regexp.MustCompile(`only one of.+severity_equals.+cvss_at_least.+can be set`), - }, - { - Config: errorVulnerabilityRuleBundleConfig_DisclosureConflict(random()), - ExpectError: regexp.MustCompile("`disclosure_older_than_days` and `disclosure_date` are mutually exclusive"), - }, - { - Config: errorVulnerabilityRuleBundleConfig_FixConflict(random()), - ExpectError: regexp.MustCompile("`fix_available` and `fix_available_since_days` are mutually exclusive"), - }, - { - Config: errorVulnerabilityRuleBundleConfig_ExploitConflict(random()), - ExpectError: regexp.MustCompile("`public_exploit_available` and `public_exploit_available_since_days` are mutually exclusive"), - }, { ResourceName: "sysdig_secure_vulnerability_rule_bundle.sample", ImportState: true, From 70dd0762d46ac94f97f2eacbf13be2158b1ad76d Mon Sep 17 00:00:00 2001 From: Fede Barcelona Date: Mon, 15 Sep 2025 12:01:18 +0200 Subject: [PATCH 07/11] ci: add more tests on other fields --- ...sysdig_secure_vulnerability_rule_bundle.go | 6 +- ...g_secure_vulnerability_rule_bundle_test.go | 61 +++++++++++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/sysdig/resource_sysdig_secure_vulnerability_rule_bundle.go b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle.go index 632f97ef..a9d31410 100644 --- a/sysdig/resource_sysdig_secure_vulnerability_rule_bundle.go +++ b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle.go @@ -401,7 +401,7 @@ func vulnerabilityRuleSeveritiesAndThreatsToData(ruleBundle v2.VulnerabilityRule ruleData["fix_available"] = true case "vulnIsFixableWithAge": ruleData["fix_available_since_days"] = predicate.Extra.Age - case "vulnIsExploitable": + case "vulnExploitable": ruleData["public_exploit_available"] = true case "vulnExploitableWithAge": ruleData["public_exploit_available_since_days"] = predicate.Extra.Age @@ -493,7 +493,7 @@ func vulnerabilityRuleSeveritiesAndThreatsFromMap(ruleBody map[string]any) (v2.V } if val, ok := ruleBody["public_exploit_available"]; ok && val.(bool) { - addPredicate("vulnIsExploitable", nil) + addPredicate("vulnExploitable", nil) } if val, ok := ruleBody["public_exploit_available_since_days"]; ok && val.(int) > 0 { @@ -726,8 +726,6 @@ func validateSeveritiesAndThreatsRule(ruleBody map[string]any) error { return nil } - - func vulnerabilityRuleImageConfigLabelFromMap(ruleBody map[string]any) (v2.VulnerabilityRule, error) { rule := v2.VulnerabilityRule{ ID: toPtr(ruleBody["id"].(string)), diff --git a/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go index 9d7d6ff4..b9be3463 100644 --- a/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go +++ b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go @@ -114,6 +114,25 @@ func TestAccVulnerabilityRuleBundle(t *testing.T) { resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.cvss_at_least", "7.5"), ), }, + { + Config: variantVulnerabilityRuleBundleConfig_DisclosureDate(random()), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.disclosure_date.0.from", "2022-01-01"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.disclosure_date.0.to", "2022-12-31"), + ), + }, + { + Config: variantVulnerabilityRuleBundleConfig_PublicExploit(random()), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.public_exploit_available", "true"), + ), + }, + { + Config: variantVulnerabilityRuleBundleConfig_FixAvailable(random()), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.fix_available", "true"), + ), + }, { ResourceName: "sysdig_secure_vulnerability_rule_bundle.sample", ImportState: true, @@ -400,3 +419,45 @@ resource "sysdig_secure_vulnerability_rule_bundle" "sample" { } `, suffix) } + +func variantVulnerabilityRuleBundleConfig_DisclosureDate(suffix string) string { + return fmt.Sprintf(` +resource "sysdig_secure_vulnerability_rule_bundle" "sample" { + name = "TERRAFORM TEST %s" + rule { + severities_and_threats { + disclosure_date { + from = "2022-01-01" + to = "2022-12-31" + } + } + } +} +`, suffix) +} + +func variantVulnerabilityRuleBundleConfig_PublicExploit(suffix string) string { + return fmt.Sprintf(` +resource "sysdig_secure_vulnerability_rule_bundle" "sample" { + name = "TERRAFORM TEST %s" + rule { + severities_and_threats { + public_exploit_available = true + } + } +} +`, suffix) +} + +func variantVulnerabilityRuleBundleConfig_FixAvailable(suffix string) string { + return fmt.Sprintf(` +resource "sysdig_secure_vulnerability_rule_bundle" "sample" { + name = "TERRAFORM TEST %s" + rule { + severities_and_threats { + fix_available = true + } + } +} +`, suffix) +} From 4018d4ff746c0c839983a89c20a242f39e1573cd Mon Sep 17 00:00:00 2001 From: Fede Barcelona Date: Mon, 15 Sep 2025 12:38:52 +0200 Subject: [PATCH 08/11] ci: add more checks --- ...g_secure_vulnerability_rule_bundle_test.go | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go index b9be3463..0dc91bc9 100644 --- a/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go +++ b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go @@ -88,7 +88,19 @@ func TestAccVulnerabilityRuleBundle(t *testing.T) { Config: fullVulnerabilityRuleBundleConfig_Severities(random()), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.severity_at_least", "high"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.disclosure_older_than_days", "90"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.package_type", "os"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.in_use", "true"), resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.fix_available_since_days", "30"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.public_exploit_available_since_days", "15"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.exploit_no_admin_privileges", "true"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.exploit_no_user_interaction", "true"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.exploit_network_attack_vector", "true"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.cisa_kev_in_ransomware_campaign", "true"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.cisa_kev_available_since_days", "10"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.cisa_kev_due_date_in_days", "21"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.epss_score_at_least_percentage", "80"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.severities_and_threats.0.epss_percentile_at_least_percentage", "90"), ), }, { @@ -99,7 +111,19 @@ func TestAccVulnerabilityRuleBundle(t *testing.T) { resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.2.image_label.0.label_must_exist_and_contain_value.0.required_label", "required-label"), resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.2.image_label.0.label_must_exist_and_contain_value.0.required_value", "required-value"), resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.3.severities_and_threats.0.severity_at_least", "high"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.3.severities_and_threats.0.disclosure_older_than_days", "90"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.3.severities_and_threats.0.package_type", "os"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.3.severities_and_threats.0.in_use", "true"), resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.3.severities_and_threats.0.fix_available_since_days", "30"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.3.severities_and_threats.0.public_exploit_available_since_days", "15"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.3.severities_and_threats.0.exploit_no_admin_privileges", "true"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.3.severities_and_threats.0.exploit_no_user_interaction", "true"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.3.severities_and_threats.0.exploit_network_attack_vector", "true"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.3.severities_and_threats.0.cisa_kev_in_ransomware_campaign", "true"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.3.severities_and_threats.0.cisa_kev_available_since_days", "10"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.3.severities_and_threats.0.cisa_kev_due_date_in_days", "21"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.3.severities_and_threats.0.epss_score_at_least_percentage", "80"), + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.3.severities_and_threats.0.epss_percentile_at_least_percentage", "90"), ), }, { From c5c285dfdb7629a8d3ceca08d8fc7b869460fdc0 Mon Sep 17 00:00:00 2001 From: Fede Barcelona Date: Mon, 15 Sep 2025 12:50:25 +0200 Subject: [PATCH 09/11] docs: update documentation --- ...sysdig_secure_vulnerability_rule_bundle.go | 22 ++-- .../r/secure_vulnerability_rule_bundle.md | 123 +++++++++++++++--- 2 files changed, 117 insertions(+), 28 deletions(-) diff --git a/sysdig/resource_sysdig_secure_vulnerability_rule_bundle.go b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle.go index a9d31410..3d9d0a27 100644 --- a/sysdig/resource_sysdig_secure_vulnerability_rule_bundle.go +++ b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle.go @@ -11,6 +11,7 @@ import ( "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 vulnerabilityRuleSchemaImageConfigLabel() *schema.Schema { @@ -126,14 +127,16 @@ func vulnerabilityRuleSchemaSeveritiesAndThreats() *schema.Schema { Description: "Internal identifier for the severities and threats rule block.", }, "severity_at_least": { - Type: schema.TypeString, - Optional: true, - Description: "Vulnerability severity must be at least this level (critical, high, medium, low, negligible).", + Type: schema.TypeString, + Optional: true, + Description: "Vulnerability severity must be at least this level (critical, high, medium, low, negligible).", + ValidateFunc: validation.StringInSlice([]string{"critical", "high", "medium", "low", "negligible"}, false), }, "severity_equals": { - Type: schema.TypeString, - Optional: true, - Description: "Vulnerability severity must be exactly this level.", + Type: schema.TypeString, + Optional: true, + Description: "Vulnerability severity must be exactly this level.", + ValidateFunc: validation.StringInSlice([]string{"critical", "high", "medium", "low", "negligible"}, false), }, "cvss_at_least": { Type: schema.TypeFloat, @@ -166,9 +169,10 @@ func vulnerabilityRuleSchemaSeveritiesAndThreats() *schema.Schema { }, }, "package_type": { - Type: schema.TypeString, - Optional: true, - Description: "Type of the package (e.g., 'os', 'npm', 'maven').", + Type: schema.TypeString, + Optional: true, + Description: "Type of the package.", + ValidateFunc: validation.StringInSlice([]string{"os", "nonOs"}, false), }, "in_use": { Type: schema.TypeBool, diff --git a/website/docs/r/secure_vulnerability_rule_bundle.md b/website/docs/r/secure_vulnerability_rule_bundle.md index fd2e2b25..7e595340 100644 --- a/website/docs/r/secure_vulnerability_rule_bundle.md +++ b/website/docs/r/secure_vulnerability_rule_bundle.md @@ -3,33 +3,42 @@ subcategory: "Sysdig Secure" layout: "sysdig" page_title: "Sysdig: sysdig_secure_vulnerability_rule_bundle" description: |- - Creates a Sysdig Secure Vulnerability Rule Bundle. + Creates a Sysdig Secure Vulnerability Rule Bundle for defining custom vulnerability management rules. --- # 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. +Creates a Sysdig Secure Vulnerability Rule Bundle. + +A **Rule Bundle** is a collection of rules that can be reused across multiple [Vulnerability Policies](https://docs.sysdig.com/en/docs/sysdig-secure/policies/vulnerability_policies/). Rule bundles allow you to define a standardized set of conditions for evaluating vulnerabilities, which can then be applied consistently to different policies. For more details, see the official documentation on [Rule Bundles](https://docs.sysdig.com/en/sysdig-secure/vm_policies/rule_bundles/). -> **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 +### Image Label Example + +This example defines a rule bundle that checks for the presence or absence of specific image labels. + ```terraform -resource "sysdig_secure_vulnerability_rule_bundle" "example" { - name = "Example Rule Bundle" +resource "sysdig_secure_vulnerability_rule_bundle" "example_image_label" { + name = "Example Rule Bundle - Image Label" + # Rule to ensure a specific label exists rule { image_label { label_must_exist = "required-label" } } + # Rule to ensure a specific label does not exist rule { image_label { label_must_not_exist = "forbidden-label" } } + # Rule to ensure a label exists and has a specific value rule { image_label { label_must_exist_and_contain_value { @@ -41,38 +50,114 @@ resource "sysdig_secure_vulnerability_rule_bundle" "example" { } ``` -## Argument Reference +### Severities and Threats Example -* `name` - (Required) The name of the vulnerability rule bundle. +This example creates a comprehensive rule bundle that evaluates vulnerabilities based on severity, threat intelligence, and other risk factors. -* `description` - (Optional) A description for the rule bundle. +```terraform +resource "sysdig_secure_vulnerability_rule_bundle" "example_severities" { + name = "Example Rule Bundle - Severities & Threats" + description = "Bundle with rules for high-priority vulnerabilities" + + rule { + severities_and_threats { + # Severity and disclosure criteria + severity_at_least = "high" + disclosure_older_than_days = 90 + + # Package and runtime context + package_type = "os" + in_use = true # Only trigger if the package is loaded in memory + + # Fix and exploitability status + fix_available_since_days = 30 + public_exploit_available_since_days = 15 + + # Exploit characteristics (CVSS vector) + exploit_no_admin_privileges = true + exploit_no_user_interaction = true + exploit_network_attack_vector = true + + # CISA KEV (Known Exploited Vulnerabilities) intelligence + cisa_kev_in_ransomware_campaign = true + cisa_kev_available_since_days = 10 + cisa_kev_due_date_in_days = 21 + + # EPSS (Exploit Prediction Scoring System) scores + epss_score_at_least_percentage = 80 + epss_percentile_at_least_percentage = 90 + } + } +} +``` -* `rule` - (Required) List of rule definitions. Each rule supports multiple types (e.g., `image_label`). Each type may have different required attributes: +## Argument Reference -### Rule Types +* `name` - (Required) The name of the vulnerability rule bundle. -#### image_label +* `description` - (Optional) A description for the rule bundle. -Defines label-based matching rules for image configuration. Only one of the following attributes must be specified: +* `rule` - (Required) A list of rule definitions. Each `rule` block must define exactly one of the available rule types. For more details on rule types, see the [Rules documentation](https://docs.sysdig.com/en/sysdig-secure/policies/vulnerability_policies/rules). -* `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. +### `rule` block + +Each `rule` block defines a single condition. A bundle can contain multiple rules. + +#### `image_label` + +Defines rules based on image labels to evaluate image configuration. Only one of the following attributes can be specified within a single `image_label` block. + +* `label_must_exist` - (Optional) The rule matches if an image contains a label with this key. +* `label_must_not_exist` - (Optional) The rule matches if an image does not contain a label with this key. +* `label_must_exist_and_contain_value` - (Optional) A block specifying a label key and value that must exist in the image configuration. + * `required_label` - (Required) The label key that must exist. + * `required_value` - (Required) The expected value for the given label key. + +#### `severities_and_threats` + +Defines rules based on vulnerability severity, threat intelligence, and other risk factors. + +* `severity_at_least` - (Optional) Matches if the vulnerability severity is at least this level. Valid values: `critical`, `high`, `medium`, `low`, `negligible`. +* `severity_equals` - (Optional) Matches if the vulnerability severity is exactly this level. Valid values: `critical`, `high`, `medium`, `low`, `negligible`. +* `cvss_at_least` - (Optional) Matches if the vulnerability's CVSS score is at least this value (e.g., `7.5`). +* `disclosure_older_than_days` - (Optional) Matches if the vulnerability was publicly disclosed more than this number of days ago. +* `disclosure_date` - (Optional) A block specifying that the vulnerability was disclosed within a specific date range. + * `from` - (Required) Start of the date range in `YYYY-MM-DD` format. + * `to` - (Required) End of the date range in `YYYY-MM-DD` format. +* `package_type` - (Optional) Matches if the vulnerability is in a package of this type. Valid values: `os`, `nonOs`. +* `in_use` - (Optional) If `true`, the rule matches only if the vulnerable package is loaded in memory at runtime. +* `fix_available` - (Optional) If `true`, a fix is available for the vulnerability. +* `fix_available_since_days` - (Optional) Matches if a fix has been available for at least this number of days. +* `public_exploit_available` - (Optional) If `true`, a public exploit is known to exist for the vulnerability. +* `public_exploit_available_since_days` - (Optional) Matches if a public exploit has been available for at least this number of days. +* `exploit_no_admin_privileges` - (Optional) If `true`, the exploit does not require administrator privileges. +* `exploit_no_user_interaction` - (Optional) If `true`, the exploit does not require user interaction. +* `exploit_network_attack_vector` - (Optional) If `true`, the exploit has a network attack vector. +* `cisa_kev_in_ransomware_campaign` - (Optional) If `true`, the vulnerability is part of a CISA KEV (Known Exploited Vulnerabilities) ransomware campaign. +* `cisa_kev_available_since_days` - (Optional) Matches if the vulnerability has been in the CISA KEV catalog for at least this number of days. +* `cisa_kev_due_date_in_days` - (Optional) Matches if the CISA KEV remediation due date is within this number of days. +* `epss_score_at_least_percentage` - (Optional) Matches if the EPSS (Exploit Prediction Scoring System) score is at least this percentage (0-100). +* `epss_percentile_at_least_percentage` - (Optional) Matches if the EPSS percentile is at least this percentage (0-100). + +-> **Note on mutually exclusive fields:** +> - Within a `severities_and_threats` block, only one of `severity_at_least`, `severity_equals`, or `cvss_at_least` can be set. +> - `disclosure_older_than_days` and `disclosure_date` are mutually exclusive. +> - `public_exploit_available` and `public_exploit_available_since_days` are mutually exclusive. +> - `fix_available` and `fix_available_since_days` are mutually exclusive. ## 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. +* `id` - The internal identifier of the vulnerability rule bundle. This is the ID to be used in the `sysdig_secure_vulnerability_policy.bundles` field. +* `identifier` - The external identifier of the vulnerability rule bundle. ## Import Vulnerability rule bundles can be imported using their bundle ID, for example: ```shell -$ terraform import sysdig_secure_vulnerability_rule_bundle.example bundle_123456 +$ terraform import sysdig_secure_vulnerability_rule_bundle.example 12345 ``` - From 579368a40296ae42b605e163e6c74be2ea9822a7 Mon Sep 17 00:00:00 2001 From: Fede Barcelona Date: Mon, 15 Sep 2025 14:20:48 +0200 Subject: [PATCH 10/11] ci: add linter build flags to validate --- GNUmakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GNUmakefile b/GNUmakefile index fcbb6a5b..516cade6 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -64,7 +64,7 @@ fmtcheck: @sh -c "'$(CURDIR)/scripts/gofmtcheck.sh'" lint: - golangci-lint run --timeout 1h ./... + golangci-lint run --build-tags "$(TEST_SUITE)" --timeout 1h ./... errcheck: @sh -c "'$(CURDIR)/scripts/errcheck.sh'" From 57743788e13a1b38b2781f78db82b056cba86af6 Mon Sep 17 00:00:00 2001 From: Fede Barcelona Date: Mon, 15 Sep 2025 15:06:10 +0200 Subject: [PATCH 11/11] style: format file --- .../v2/vulnerability_rule_bundle_model.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/sysdig/internal/client/v2/vulnerability_rule_bundle_model.go b/sysdig/internal/client/v2/vulnerability_rule_bundle_model.go index c07a1f39..a962c7ab 100644 --- a/sysdig/internal/client/v2/vulnerability_rule_bundle_model.go +++ b/sysdig/internal/client/v2/vulnerability_rule_bundle_model.go @@ -21,15 +21,15 @@ type VulnerabilityRulePredicate struct { type VulnerabilityRulePredicateExtra struct { // Common fields for different predicate types - Level *Level `json:"level,omitempty"` - Age *int `json:"age,omitempty"` - Days *int `json:"days,omitempty"` - PkgType *string `json:"pkgType,omitempty"` - VulnIDS []string `json:"vulnIds,omitempty"` - Packages []Package `json:"packages,omitempty"` - Key *string `json:"key,omitempty"` - User *string `json:"user,omitempty"` - Value interface{} `json:"value,omitempty"` // For image labels or CVSS score + Level *Level `json:"level,omitempty"` + Age *int `json:"age,omitempty"` + Days *int `json:"days,omitempty"` + PkgType *string `json:"pkgType,omitempty"` + VulnIDS []string `json:"vulnIds,omitempty"` + Packages []Package `json:"packages,omitempty"` + Key *string `json:"key,omitempty"` + User *string `json:"user,omitempty"` + Value interface{} `json:"value,omitempty"` // For image labels or CVSS score // Disclosure Date Range StartDate *string `json:"startDate,omitempty"`