diff --git a/sysdig/internal/client/v2/vulnerability_rule_bundle_model.go b/sysdig/internal/client/v2/vulnerability_rule_bundle_model.go index c2f0c693..a962c7ab 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 { @@ -57,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..3d9d0a27 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" @@ -13,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 { @@ -105,7 +104,8 @@ 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(), }, }, }, @@ -113,6 +113,137 @@ 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{ + "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).", + 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.", + ValidateFunc: validation.StringInSlice([]string{"critical", "high", "medium", "low", "negligible"}, false), + }, + "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.", + ValidateFunc: validation.StringInSlice([]string{"os", "nonOs"}, false), + }, + "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.", + }, + }, + }, + } +} + func getSecureVulnerabilityRuleBundleClient(c SysdigClients) (v2.VulnerabilityRuleBundleClient, error) { return c.sysdigSecureClientV2() } @@ -234,11 +365,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 "vulnExploitable": + 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("vulnExploitable", 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": @@ -329,14 +638,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) { @@ -346,9 +663,14 @@ 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)) + 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 +678,58 @@ func vulnerabilityRuleFromMap(ruleMap map[string]any) (v2.VulnerabilityRule, err panic("unreachable") } +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") + } + + 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") + } + + 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 9f200ced..0dc91bc9 100644 --- a/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go +++ b/sysdig/resource_sysdig_secure_vulnerability_rule_bundle_test.go @@ -40,10 +40,122 @@ func TestAccVulnerabilityRuleBundle(t *testing.T) { ExpectError: regexp.MustCompile(`no predicate has been specified for image label rule`), }, { - Config: minimalVulnerabilityRuleBundleConfig(random()), + Config: errorVulnerabilityRuleBundleConfig_Conflict1(random()), + ExpectError: regexp.MustCompile(`only one of.+severity_at_least.+severity_equals.+can be set`), }, { - Config: fullVulnerabilityRuleBundleConfig(random()), + 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( + resource.TestCheckResourceAttr("sysdig_secure_vulnerability_rule_bundle.sample", "rule.0.image_label.0.label_must_exist", "required-label"), + ), + }, + { + 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.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"), + ), + }, + { + 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.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"), + ), + }, + { + 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: 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", @@ -86,7 +198,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 +211,276 @@ 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_since_days = 30 + 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_since_days = 30 + 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) +} + +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) +} + +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) 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 ``` -