From c5df5a46928e2f1a2369ee87bafdb93fc6910048 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Thu, 5 Sep 2024 13:12:02 -0700 Subject: [PATCH 01/11] Fix resource type name not being set correctly for telemetry --- pkg/xray/resource/resource_xray_binary_manager_builds.go | 7 ++++--- pkg/xray/resource/resource_xray_binary_manager_repos.go | 7 ++++--- pkg/xray/resource/resource_xray_ignore_rule.go | 7 ++++--- pkg/xray/resource/resource_xray_settings.go | 7 ++++--- pkg/xray/resource/resource_xray_watch.go | 7 ++++--- pkg/xray/resource/resource_xray_webhook.go | 7 ++++--- pkg/xray/resource/resource_xray_workers_count.go | 7 ++++--- 7 files changed, 28 insertions(+), 21 deletions(-) diff --git a/pkg/xray/resource/resource_xray_binary_manager_builds.go b/pkg/xray/resource/resource_xray_binary_manager_builds.go index 4ee25938..75a52751 100644 --- a/pkg/xray/resource/resource_xray_binary_manager_builds.go +++ b/pkg/xray/resource/resource_xray_binary_manager_builds.go @@ -25,7 +25,9 @@ const BinaryManagerBuildsEndpoint = "xray/api/v1/binMgr/{id}/builds" var _ resource.Resource = &BinaryManagerBuildsResource{} func NewBinaryManagerBuildsResource() resource.Resource { - return &BinaryManagerBuildsResource{} + return &BinaryManagerBuildsResource{ + TypeName: "xray_binary_manager_builds", + } } type BinaryManagerBuildsResource struct { @@ -34,8 +36,7 @@ type BinaryManagerBuildsResource struct { } func (r *BinaryManagerBuildsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_binary_manager_builds" - r.TypeName = resp.TypeName + resp.TypeName = r.TypeName } type BinaryManagerBuildsResourceModel struct { diff --git a/pkg/xray/resource/resource_xray_binary_manager_repos.go b/pkg/xray/resource/resource_xray_binary_manager_repos.go index e8aead22..308c9fbb 100644 --- a/pkg/xray/resource/resource_xray_binary_manager_repos.go +++ b/pkg/xray/resource/resource_xray_binary_manager_repos.go @@ -29,7 +29,9 @@ const BinaryManagerReposEndpoint = "xray/api/v1/binMgr/{id}/repos" var _ resource.Resource = &BinaryManagerReposResource{} func NewBinaryManagerReposResource() resource.Resource { - return &BinaryManagerReposResource{} + return &BinaryManagerReposResource{ + TypeName: "xray_binary_manager_repos", + } } type BinaryManagerReposResource struct { @@ -38,8 +40,7 @@ type BinaryManagerReposResource struct { } func (r *BinaryManagerReposResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_binary_manager_repos" - r.TypeName = resp.TypeName + resp.TypeName = r.TypeName } type BinaryManagerReposResourceModel struct { diff --git a/pkg/xray/resource/resource_xray_ignore_rule.go b/pkg/xray/resource/resource_xray_ignore_rule.go index b1dab5a5..ce2e844a 100644 --- a/pkg/xray/resource/resource_xray_ignore_rule.go +++ b/pkg/xray/resource/resource_xray_ignore_rule.go @@ -33,7 +33,9 @@ const ( var _ resource.Resource = &IgnoreRuleResource{} func NewIgnoreRuleResource() resource.Resource { - return &IgnoreRuleResource{} + return &IgnoreRuleResource{ + TypeName: "xray_ignore_rule", + } } type IgnoreRuleResource struct { @@ -42,8 +44,7 @@ type IgnoreRuleResource struct { } func (r *IgnoreRuleResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_ignore_rule" - r.TypeName = resp.TypeName + resp.TypeName = r.TypeName } type IgnoreRuleResourceModel struct { diff --git a/pkg/xray/resource/resource_xray_settings.go b/pkg/xray/resource/resource_xray_settings.go index 41b9adb5..14abfc5c 100644 --- a/pkg/xray/resource/resource_xray_settings.go +++ b/pkg/xray/resource/resource_xray_settings.go @@ -27,7 +27,9 @@ const ( var _ resource.Resource = &SettingsResource{} func NewSettingsResource() resource.Resource { - return &SettingsResource{} + return &SettingsResource{ + TypeName: "xray_settings", + } } type SettingsResource struct { @@ -36,8 +38,7 @@ type SettingsResource struct { } func (r *SettingsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_settings" - r.TypeName = resp.TypeName + resp.TypeName = r.TypeName } type SettingsResourceModel struct { diff --git a/pkg/xray/resource/resource_xray_watch.go b/pkg/xray/resource/resource_xray_watch.go index 92d87478..bde12416 100644 --- a/pkg/xray/resource/resource_xray_watch.go +++ b/pkg/xray/resource/resource_xray_watch.go @@ -48,7 +48,9 @@ var supportedResourceTypes = []string{ var _ resource.Resource = &WatchResource{} func NewWatchResource() resource.Resource { - return &WatchResource{} + return &WatchResource{ + TypeName: "xray_watch", + } } type WatchResource struct { @@ -57,8 +59,7 @@ type WatchResource struct { } func (r *WatchResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_watch" - r.TypeName = resp.TypeName + resp.TypeName = r.TypeName } type WatchResourceModel struct { diff --git a/pkg/xray/resource/resource_xray_webhook.go b/pkg/xray/resource/resource_xray_webhook.go index 5aa3323d..d63f6cfd 100644 --- a/pkg/xray/resource/resource_xray_webhook.go +++ b/pkg/xray/resource/resource_xray_webhook.go @@ -27,7 +27,9 @@ const ( var _ resource.Resource = &WebhookResource{} func NewWebhookResource() resource.Resource { - return &WebhookResource{} + return &WebhookResource{ + TypeName: "xray_webhook", + } } type WebhookResource struct { @@ -36,8 +38,7 @@ type WebhookResource struct { } func (r *WebhookResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_webhook" - r.TypeName = resp.TypeName + resp.TypeName = r.TypeName } type WebhookResourceModel struct { diff --git a/pkg/xray/resource/resource_xray_workers_count.go b/pkg/xray/resource/resource_xray_workers_count.go index e3c81f0f..63a6193f 100644 --- a/pkg/xray/resource/resource_xray_workers_count.go +++ b/pkg/xray/resource/resource_xray_workers_count.go @@ -27,7 +27,9 @@ const WorkersCountEndpoint = "xray/api/v1/configuration/workersCount" var _ resource.Resource = &WorkersCountResource{} func NewWorkersCountResource() resource.Resource { - return &WorkersCountResource{} + return &WorkersCountResource{ + TypeName: "xray_workers_count", + } } type WorkersCountResource struct { @@ -36,8 +38,7 @@ type WorkersCountResource struct { } func (r *WorkersCountResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_workers_count" - r.TypeName = resp.TypeName + resp.TypeName = r.TypeName } type WorkersCountResourceModelV0 struct { From 62a784a64e04bad62118ada825a0ac65048d33b2 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Mon, 9 Sep 2024 11:44:35 -0700 Subject: [PATCH 02/11] Migrate security policy to Plugin Framework --- pkg/xray/provider/framework.go | 1 + pkg/xray/provider/sdkv2.go | 1 - pkg/xray/resource/policies.go | 183 ++- .../resource/resource_xray_security_policy.go | 1448 +++++++++++++++-- .../resource_xray_security_policy_test.go | 47 +- 5 files changed, 1410 insertions(+), 270 deletions(-) diff --git a/pkg/xray/provider/framework.go b/pkg/xray/provider/framework.go index c5debb64..11166c52 100644 --- a/pkg/xray/provider/framework.go +++ b/pkg/xray/provider/framework.go @@ -181,6 +181,7 @@ func (p *XrayProvider) Resources(ctx context.Context) []func() resource.Resource xray_resource.NewCustomIssueResource, xray_resource.NewIgnoreRuleResource, xray_resource.NewRepositoryConfigResource, + xray_resource.NewSecurityPolicyV2Resource, xray_resource.NewSettingsResource, xray_resource.NewWatchResource, xray_resource.NewWebhookResource, diff --git a/pkg/xray/provider/sdkv2.go b/pkg/xray/provider/sdkv2.go index b8a34bc1..eb6675e6 100644 --- a/pkg/xray/provider/sdkv2.go +++ b/pkg/xray/provider/sdkv2.go @@ -54,7 +54,6 @@ func SdkV2() *schema.Provider { ResourcesMap: sdk.AddTelemetry( productId, map[string]*schema.Resource{ - "xray_security_policy": xray.ResourceXraySecurityPolicyV2(), "xray_license_policy": xray.ResourceXrayLicensePolicyV2(), "xray_operational_risk_policy": xray.ResourceXrayOperationalRiskPolicy(), "xray_vulnerabilities_report": xray.ResourceXrayVulnerabilitiesReport(), diff --git a/pkg/xray/resource/policies.go b/pkg/xray/resource/policies.go index 71e7d618..aa7fe038 100644 --- a/pkg/xray/resource/policies.go +++ b/pkg/xray/resource/policies.go @@ -11,8 +11,14 @@ import ( "github.com/jfrog/terraform-provider-shared/validator" ) +const ( + PoliciesEndpoint = "xray/api/v2/policies" + PolicyEndpoint = "xray/api/v2/policies/{name}" +) + var validPackageTypesSupportedXraySecPolicies = []string{ "alpine", + "bower", "cargo", "composer", "conan", @@ -30,6 +36,7 @@ var validPackageTypesSupportedXraySecPolicies = []string{ "pypi", "rpm", "rubygems", + "terraformbe", } var commonActionsSchema = map[string]*schema.Schema{ @@ -197,12 +204,12 @@ var getPolicySchema = func(criteriaSchema map[string]*schema.Schema, actionsSche ) } -type PolicyCVSSRange struct { +type PolicyCVSSRangeAPIModel struct { To *float64 `json:"to,omitempty"` From *float64 `json:"from,omitempty"` } -type PolicyExposures struct { +type PolicyExposuresAPIModel struct { MinSeverity *string `json:"min_severity,omitempty"` Secrets *bool `json:"secrets,omitempty"` Applications *bool `json:"applications,omitempty"` @@ -210,30 +217,30 @@ type PolicyExposures struct { Iac *bool `json:"iac,omitempty"` } -type OperationalRiskCriteria struct { +type OperationalRiskCriteriaAPIModel struct { UseAndCondition bool `json:"use_and_condition"` IsEOL bool `json:"is_eol"` - ReleaseDateGreaterThanMonths int `json:"release_date_greater_than_months,omitempty"` - NewerVersionsGreaterThan int `json:"newer_versions_greater_than,omitempty"` - ReleaseCadencePerYearLessThan int `json:"release_cadence_per_year_less_than,omitempty"` - CommitsLessThan int `json:"commits_less_than,omitempty"` - CommittersLessThan int `json:"committers_less_than,omitempty"` + ReleaseDateGreaterThanMonths int64 `json:"release_date_greater_than_months,omitempty"` + NewerVersionsGreaterThan int64 `json:"newer_versions_greater_than,omitempty"` + ReleaseCadencePerYearLessThan int64 `json:"release_cadence_per_year_less_than,omitempty"` + CommitsLessThan int64 `json:"commits_less_than,omitempty"` + CommittersLessThan int64 `json:"committers_less_than,omitempty"` Risk string `json:"risk,omitempty"` } -type PolicyRuleCriteria struct { +type PolicyRuleCriteriaAPIModel struct { // Security Criteria - MinimumSeverity string `json:"min_severity,omitempty"` // Omitempty is used because the empty field is conflicting with CVSSRange - CVSSRange *PolicyCVSSRange `json:"cvss_range,omitempty"` + MinimumSeverity string `json:"min_severity,omitempty"` // Omitempty is used because the empty field is conflicting with CVSSRange + CVSSRange *PolicyCVSSRangeAPIModel `json:"cvss_range,omitempty"` // Omitempty is used in FixVersionDependant because an empty field throws an error in Xray below 3.44.3 - FixVersionDependant bool `json:"fix_version_dependant,omitempty"` - ApplicableCVEsOnly bool `json:"applicable_cves_only,omitempty"` - MaliciousPackage bool `json:"malicious_package,omitempty"` - VulnerabilityIds []string `json:"vulnerability_ids,omitempty"` - Exposures *PolicyExposures `json:"exposures,omitempty"` - PackageName string `json:"package_name,omitempty"` - PackageType string `json:"package_type,omitempty"` - PackageVersions []string `json:"package_versions,omitempty"` + FixVersionDependant bool `json:"fix_version_dependant,omitempty"` + ApplicableCVEsOnly bool `json:"applicable_cves_only,omitempty"` + MaliciousPackage bool `json:"malicious_package,omitempty"` + VulnerabilityIds []string `json:"vulnerability_ids,omitempty"` + Exposures *PolicyExposuresAPIModel `json:"exposures,omitempty"` + PackageName string `json:"package_name,omitempty"` + PackageType string `json:"package_type,omitempty"` + PackageVersions []string `json:"package_versions,omitempty"` // We use pointer for CVSSRange to address nil-verification for non-primitive types. // Unlike primitive types, when the non-primitive type in the struct is set // to nil, the empty key will be created in the JSON body anyway. @@ -250,54 +257,54 @@ type PolicyRuleCriteria struct { AllowedLicenses []string `json:"allowed_licenses,omitempty"` // Operational Risk custom criteria - OperationalRiskCustom *OperationalRiskCriteria `json:"op_risk_custom,omitempty"` - OperationalRiskMinRisk string `json:"op_risk_min_risk,omitempty"` + OperationalRiskCustom *OperationalRiskCriteriaAPIModel `json:"op_risk_custom,omitempty"` + OperationalRiskMinRisk string `json:"op_risk_min_risk,omitempty"` } -type BlockDownloadSettings struct { +type BlockDownloadSettingsAPIModel struct { Unscanned bool `json:"unscanned"` Active bool `json:"active"` } -type PolicyRuleActions struct { - Webhooks []string `json:"webhooks,omitempty"` - Mails []string `json:"mails,omitempty"` - FailBuild bool `json:"fail_build"` - BlockDownload BlockDownloadSettings `json:"block_download"` - BlockReleaseBundleDistribution bool `json:"block_release_bundle_distribution"` - BlockReleaseBundlePromotion bool `json:"block_release_bundle_promotion"` - NotifyWatchRecipients bool `json:"notify_watch_recipients"` - NotifyDeployer bool `json:"notify_deployer"` - CreateJiraTicketEnabled bool `json:"create_ticket_enabled"` - FailureGracePeriodDays int `json:"build_failure_grace_period_in_days,omitempty"` +type PolicyRuleActionsAPIModel struct { + Webhooks []string `json:"webhooks,omitempty"` + Mails []string `json:"mails,omitempty"` + FailBuild bool `json:"fail_build"` + BlockDownload BlockDownloadSettingsAPIModel `json:"block_download"` + BlockReleaseBundleDistribution bool `json:"block_release_bundle_distribution"` + BlockReleaseBundlePromotion bool `json:"block_release_bundle_promotion"` + NotifyWatchRecipients bool `json:"notify_watch_recipients"` + NotifyDeployer bool `json:"notify_deployer"` + CreateJiraTicketEnabled bool `json:"create_ticket_enabled"` + FailureGracePeriodDays int64 `json:"build_failure_grace_period_in_days,omitempty"` // License Actions CustomSeverity string `json:"custom_severity,omitempty"` } -type PolicyRule struct { - Name string `json:"name"` - Priority int `json:"priority"` - Criteria *PolicyRuleCriteria `json:"criteria"` - Actions PolicyRuleActions `json:"actions"` +type PolicyRuleAPIModel struct { + Name string `json:"name"` + Priority int64 `json:"priority"` + Criteria *PolicyRuleCriteriaAPIModel `json:"criteria"` + Actions PolicyRuleActionsAPIModel `json:"actions"` } -type Policy struct { - Name string `json:"name"` - Type string `json:"type"` - ProjectKey string `json:"-"` - Author string `json:"author,omitempty"` // Omitempty is used because the field is computed - Description string `json:"description"` - Rules *[]PolicyRule `json:"rules"` - Created string `json:"created,omitempty"` // Omitempty is used because the field is computed - Modified string `json:"modified,omitempty"` // Omitempty is used because the field is computed +type PolicyAPIModel struct { + Name string `json:"name"` + Type string `json:"type"` + ProjectKey string `json:"-"` + Author string `json:"author,omitempty"` // Omitempty is used because the field is computed + Description string `json:"description"` + Rules *[]PolicyRuleAPIModel `json:"rules"` + Created string `json:"created,omitempty"` // Omitempty is used because the field is computed + Modified string `json:"modified,omitempty"` // Omitempty is used because the field is computed } type PolicyError struct { Error string `json:"error"` } -func unpackPolicy(d *schema.ResourceData) (*Policy, error) { - policy := new(Policy) +func unpackPolicy(d *schema.ResourceData) (*PolicyAPIModel, error) { + policy := new(PolicyAPIModel) policy.Name = d.Get("name").(string) if v, ok := d.GetOk("type"); ok { @@ -318,14 +325,14 @@ func unpackPolicy(d *schema.ResourceData) (*Policy, error) { return policy, err } -func unpackRules(configured *schema.Set, policyType string) (policyRules []PolicyRule, err error) { - var rules []PolicyRule +func unpackRules(configured *schema.Set, policyType string) (policyRules []PolicyRuleAPIModel, err error) { + var rules []PolicyRuleAPIModel for _, raw := range configured.List() { - rule := new(PolicyRule) + rule := new(PolicyRuleAPIModel) data := raw.(map[string]interface{}) rule.Name = data["name"].(string) - rule.Priority = data["priority"].(int) + rule.Priority = data["priority"].(int64) rule.Criteria, err = unpackCriteria(data["criteria"].(*schema.Set), policyType) if v, ok := data["actions"]; ok { @@ -337,8 +344,8 @@ func unpackRules(configured *schema.Set, policyType string) (policyRules []Polic return rules, err } -func unpackSecurityCriteria(tfCriteria map[string]interface{}) *PolicyRuleCriteria { - criteria := new(PolicyRuleCriteria) +func unpackSecurityCriteria(tfCriteria map[string]interface{}) *PolicyRuleCriteriaAPIModel { + criteria := new(PolicyRuleCriteriaAPIModel) if v, ok := tfCriteria["fix_version_dependant"]; ok { criteria.FixVersionDependant = v.(bool) @@ -375,8 +382,8 @@ func unpackSecurityCriteria(tfCriteria map[string]interface{}) *PolicyRuleCriter return criteria } -func unpackLicenseCriteria(tfCriteria map[string]interface{}) *PolicyRuleCriteria { - criteria := new(PolicyRuleCriteria) +func unpackLicenseCriteria(tfCriteria map[string]interface{}) *PolicyRuleCriteriaAPIModel { + criteria := new(PolicyRuleCriteriaAPIModel) if v, ok := tfCriteria["allow_unknown"]; ok { criteria.AllowUnknown = sdk.BoolPtr(v.(bool)) } @@ -393,8 +400,8 @@ func unpackLicenseCriteria(tfCriteria map[string]interface{}) *PolicyRuleCriteri return criteria } -func unpackOperationalRiskCustomCriteria(tfCriteria map[string]interface{}) *OperationalRiskCriteria { - criteria := OperationalRiskCriteria{} +func unpackOperationalRiskCustomCriteria(tfCriteria map[string]interface{}) *OperationalRiskCriteriaAPIModel { + criteria := OperationalRiskCriteriaAPIModel{} if v, ok := tfCriteria["use_and_condition"]; ok { criteria.UseAndCondition = v.(bool) } @@ -402,19 +409,19 @@ func unpackOperationalRiskCustomCriteria(tfCriteria map[string]interface{}) *Ope criteria.IsEOL = v.(bool) } if v, ok := tfCriteria["release_date_greater_than_months"]; ok { - criteria.ReleaseDateGreaterThanMonths = v.(int) + criteria.ReleaseDateGreaterThanMonths = v.(int64) } if v, ok := tfCriteria["newer_versions_greater_than"]; ok { - criteria.NewerVersionsGreaterThan = v.(int) + criteria.NewerVersionsGreaterThan = v.(int64) } if v, ok := tfCriteria["release_cadence_per_year_less_than"]; ok { - criteria.ReleaseCadencePerYearLessThan = v.(int) + criteria.ReleaseCadencePerYearLessThan = v.(int64) } if v, ok := tfCriteria["commits_less_than"]; ok { - criteria.CommitsLessThan = v.(int) + criteria.CommitsLessThan = v.(int64) } if v, ok := tfCriteria["committers_less_than"]; ok { - criteria.CommittersLessThan = v.(int) + criteria.CommittersLessThan = v.(int64) } if v, ok := tfCriteria["risk"]; ok { criteria.Risk = v.(string) @@ -423,8 +430,8 @@ func unpackOperationalRiskCustomCriteria(tfCriteria map[string]interface{}) *Ope return &criteria } -func unpackOperationalRiskCriteria(tfCriteria map[string]interface{}) *PolicyRuleCriteria { - criteria := new(PolicyRuleCriteria) +func unpackOperationalRiskCriteria(tfCriteria map[string]interface{}) *PolicyRuleCriteriaAPIModel { + criteria := new(PolicyRuleCriteriaAPIModel) if v, ok := tfCriteria["op_risk_custom"]; ok { custom := v.([]interface{}) if len(custom) > 0 { @@ -438,14 +445,14 @@ func unpackOperationalRiskCriteria(tfCriteria map[string]interface{}) *PolicyRul return criteria } -func unpackCriteria(d *schema.Set, policyType string) (*PolicyRuleCriteria, error) { +func unpackCriteria(d *schema.Set, policyType string) (*PolicyRuleCriteriaAPIModel, error) { tfCriteria := d.List() if len(tfCriteria) == 0 { return nil, nil } m := tfCriteria[0].(map[string]interface{}) // We made this a list of one to make schema validation easier - var criteria *PolicyRuleCriteria + var criteria *PolicyRuleCriteriaAPIModel // criteria := new(PolicyRuleCriteria) // The API doesn't allow both severity and license criteria to be _set_, even if they have empty values // So we have to figure out which group is actually empty and not even set it @@ -464,26 +471,26 @@ func Float64Ptr(v float64) *float64 { return &v } func StringPtr(v string) *string { return &v } -func unpackCVSSRange(l []interface{}) *PolicyCVSSRange { +func unpackCVSSRange(l []interface{}) *PolicyCVSSRangeAPIModel { if len(l) == 0 { return nil } m := l[0].(map[string]interface{}) - cvssrange := &PolicyCVSSRange{ + cvssrange := &PolicyCVSSRangeAPIModel{ From: Float64Ptr(m["from"].(float64)), To: Float64Ptr(m["to"].(float64)), } return cvssrange } -func unpackExposures(l []interface{}) *PolicyExposures { +func unpackExposures(l []interface{}) *PolicyExposuresAPIModel { if len(l) == 0 { return nil } m := l[0].(map[string]interface{}) - exposures := &PolicyExposures{ + exposures := &PolicyExposuresAPIModel{ MinSeverity: StringPtr(m["min_severity"].(string)), Secrets: sdk.BoolPtr(m["secrets"].(bool)), Applications: sdk.BoolPtr(m["applications"].(bool)), @@ -501,8 +508,8 @@ func unpackLicenses(d *schema.Set) []string { return licenses } -func unpackActions(l *schema.Set) PolicyRuleActions { - actions := PolicyRuleActions{} +func unpackActions(l *schema.Set) PolicyRuleActionsAPIModel { + actions := PolicyRuleActionsAPIModel{} policyActions := l.List() if len(policyActions) > 0 { @@ -532,12 +539,12 @@ func unpackActions(l *schema.Set) PolicyRuleActions { vList := v.(*schema.Set).List() vMap := vList[0].(map[string]interface{}) - actions.BlockDownload = BlockDownloadSettings{ + actions.BlockDownload = BlockDownloadSettingsAPIModel{ Unscanned: vMap["unscanned"].(bool), Active: vMap["active"].(bool), } } else { - actions.BlockDownload = BlockDownloadSettings{ + actions.BlockDownload = BlockDownloadSettingsAPIModel{ Unscanned: false, Active: false, } @@ -565,7 +572,7 @@ func unpackActions(l *schema.Set) PolicyRuleActions { actions.CreateJiraTicketEnabled = v.(bool) } if v, ok := m["build_failure_grace_period_in_days"]; ok { - actions.FailureGracePeriodDays = v.(int) + actions.FailureGracePeriodDays = v.(int64) } if v, ok := m["custom_severity"]; ok { actions.CustomSeverity = v.(string) @@ -576,7 +583,7 @@ func unpackActions(l *schema.Set) PolicyRuleActions { return actions } -func packRules(rules []PolicyRule, policyType string) []interface{} { +func packRules(rules []PolicyRuleAPIModel, policyType string) []interface{} { var rs []interface{} for _, rule := range rules { @@ -608,7 +615,7 @@ func packRules(rules []PolicyRule, policyType string) []interface{} { return rs } -func packOperationalRiskCriteria(criteria *PolicyRuleCriteria) []interface{} { +func packOperationalRiskCriteria(criteria *PolicyRuleCriteriaAPIModel) []interface{} { m := map[string]interface{}{} if len(criteria.OperationalRiskMinRisk) > 0 { @@ -621,7 +628,7 @@ func packOperationalRiskCriteria(criteria *PolicyRuleCriteria) []interface{} { return []interface{}{m} } -func packOperationalRiskCustom(custom *OperationalRiskCriteria) []interface{} { +func packOperationalRiskCustom(custom *OperationalRiskCriteriaAPIModel) []interface{} { m := map[string]interface{}{ "use_and_condition": custom.UseAndCondition, "is_eol": custom.IsEOL, @@ -636,7 +643,7 @@ func packOperationalRiskCustom(custom *OperationalRiskCriteria) []interface{} { return []interface{}{m} } -func packLicenseCriteria(criteria *PolicyRuleCriteria) []interface{} { +func packLicenseCriteria(criteria *PolicyRuleCriteriaAPIModel) []interface{} { m := map[string]interface{}{} @@ -652,7 +659,7 @@ func packLicenseCriteria(criteria *PolicyRuleCriteria) []interface{} { return []interface{}{m} } -func packSecurityCriteria(criteria *PolicyRuleCriteria) []interface{} { +func packSecurityCriteria(criteria *PolicyRuleCriteriaAPIModel) []interface{} { m := map[string]interface{}{} // cvss_range and min_severity are conflicting, only one can be present in the JSON m["cvss_range"] = packCVSSRange(criteria.CVSSRange) @@ -676,7 +683,7 @@ func packSecurityCriteria(criteria *PolicyRuleCriteria) []interface{} { return []interface{}{m} } -func packCVSSRange(cvss *PolicyCVSSRange) []interface{} { +func packCVSSRange(cvss *PolicyCVSSRangeAPIModel) []interface{} { if cvss == nil { return []interface{}{} } @@ -687,7 +694,7 @@ func packCVSSRange(cvss *PolicyCVSSRange) []interface{} { return []interface{}{m} } -func packExposures(exposures *PolicyExposures) []interface{} { +func packExposures(exposures *PolicyExposuresAPIModel) []interface{} { if exposures == nil { return []interface{}{} } @@ -701,7 +708,7 @@ func packExposures(exposures *PolicyExposures) []interface{} { return []interface{}{m} } -func packActions(actions PolicyRuleActions, license bool) []interface{} { +func packActions(actions PolicyRuleActionsAPIModel, license bool) []interface{} { m := map[string]interface{}{ "block_download": packBlockDownload(actions.BlockDownload), "webhooks": actions.Webhooks, @@ -722,14 +729,14 @@ func packActions(actions PolicyRuleActions, license bool) []interface{} { return []interface{}{m} } -func packBlockDownload(bd BlockDownloadSettings) []interface{} { +func packBlockDownload(bd BlockDownloadSettingsAPIModel) []interface{} { m := map[string]interface{}{} m["unscanned"] = bd.Unscanned m["active"] = bd.Active return []interface{}{m} } -func packPolicy(policy Policy, d *schema.ResourceData) diag.Diagnostics { +func packPolicy(policy PolicyAPIModel, d *schema.ResourceData) diag.Diagnostics { if err := d.Set("name", policy.Name); err != nil { return diag.FromErr(err) } @@ -788,7 +795,7 @@ func resourceXrayPolicyCreate(ctx context.Context, d *schema.ResourceData, m int } func resourceXrayPolicyRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - var policy Policy + var policy PolicyAPIModel projectKey := d.Get("project_key").(string) req, err := getRestyRequest(m.(util.ProviderMetadata).Client, projectKey) diff --git a/pkg/xray/resource/resource_xray_security_policy.go b/pkg/xray/resource/resource_xray_security_policy.go index 397d046b..fd9f8aca 100644 --- a/pkg/xray/resource/resource_xray_security_policy.go +++ b/pkg/xray/resource/resource_xray_security_policy.go @@ -3,209 +3,1339 @@ package xray import ( "context" "fmt" + "net/http" "regexp" + "strings" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - "github.com/jfrog/terraform-provider-shared/validator" + "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/float64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/jfrog/terraform-provider-shared/util" + utilfw "github.com/jfrog/terraform-provider-shared/util/fw" + validatorfw_string "github.com/jfrog/terraform-provider-shared/validator/fw/string" + "github.com/samber/lo" ) -func ResourceXraySecurityPolicyV2() *schema.Resource { - var criteriaSchema = map[string]*schema.Schema{ - "min_severity": { - Type: schema.TypeString, - Optional: true, - Description: "The minimum security vulnerability severity that will be impacted by the policy. Valid values: `All Severities`, `Critical`, `High`, `Medium`, `Low`", - ValidateDiagFunc: validator.StringInSlice(true, "All Severities", "Critical", "High", "Medium", "Low"), +var _ resource.Resource = &SecurityPolicyV2Resource{} + +func NewSecurityPolicyV2Resource() resource.Resource { + return &SecurityPolicyV2Resource{ + TypeName: "xray_security_policy", + } +} + +type SecurityPolicyV2Resource struct { + ProviderData util.ProviderMetadata + TypeName string +} + +func (r *SecurityPolicyV2Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = r.TypeName +} + +type SecurityPolicyV2ResourceModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + ProjectKey types.String `tfsdk:"project_key"` + Type types.String `tfsdk:"type"` + Rules types.Set `tfsdk:"rule"` + Author types.String `tfsdk:"author"` + Created types.String `tfsdk:"created"` + Modified types.String `tfsdk:"modified"` +} + +func (m SecurityPolicyV2ResourceModel) toAPIModel(ctx context.Context, apiModel *PolicyAPIModel) diag.Diagnostics { + diags := diag.Diagnostics{} + + rules := lo.Map( + m.Rules.Elements(), + func(elem attr.Value, _ int) PolicyRuleAPIModel { + attrs := elem.(types.Object).Attributes() + + var criteria *PolicyRuleCriteriaAPIModel + criteriaElems := attrs["criteria"].(types.Set).Elements() + if len(criteriaElems) > 0 { + attrs := criteriaElems[0].(types.Object).Attributes() + + var vulnerabilityIds []string + d := attrs["vulnerability_ids"].(types.Set).ElementsAs(ctx, &vulnerabilityIds, false) + if d.HasError() { + diags.Append(d...) + } + + var cvssRange *PolicyCVSSRangeAPIModel + cvssRangeElems := attrs["cvss_range"].(types.List).Elements() + if len(cvssRangeElems) > 0 { + attrs := cvssRangeElems[0].(types.Object).Attributes() + + cvssRange = &PolicyCVSSRangeAPIModel{ + From: attrs["from"].(types.Float64).ValueFloat64Pointer(), + To: attrs["to"].(types.Float64).ValueFloat64Pointer(), + } + } + + var exposures *PolicyExposuresAPIModel + exposuresElem := attrs["exposures"].(types.List).Elements() + if len(exposuresElem) > 0 { + attrs := exposuresElem[0].(types.Object).Attributes() + + exposures = &PolicyExposuresAPIModel{ + MinSeverity: attrs["min_severity"].(types.String).ValueStringPointer(), + Secrets: attrs["secrets"].(types.Bool).ValueBoolPointer(), + Applications: attrs["applications"].(types.Bool).ValueBoolPointer(), + Services: attrs["services"].(types.Bool).ValueBoolPointer(), + Iac: attrs["iac"].(types.Bool).ValueBoolPointer(), + } + } + + var packageVersions []string + d = attrs["package_versions"].(types.Set).ElementsAs(ctx, &packageVersions, false) + if d.HasError() { + diags.Append(d...) + } + + criteria = &PolicyRuleCriteriaAPIModel{ + MinimumSeverity: attrs["min_severity"].(types.String).ValueString(), + CVSSRange: cvssRange, + FixVersionDependant: attrs["fix_version_dependant"].(types.Bool).ValueBool(), + ApplicableCVEsOnly: attrs["applicable_cves_only"].(types.Bool).ValueBool(), + MaliciousPackage: attrs["malicious_package"].(types.Bool).ValueBool(), + VulnerabilityIds: vulnerabilityIds, + Exposures: exposures, + PackageName: attrs["package_name"].(types.String).ValueString(), + PackageType: attrs["package_type"].(types.String).ValueString(), + PackageVersions: packageVersions, + } + } + + actions := PolicyRuleActionsAPIModel{} + actionsElems := attrs["actions"].(types.Set).Elements() + if len(actionsElems) > 0 { + attrs := actionsElems[0].(types.Object).Attributes() + + var webhooks []string + d := attrs["webhooks"].(types.Set).ElementsAs(ctx, &webhooks, false) + if d.HasError() { + diags.Append(d...) + } + + var mails []string + d = attrs["mails"].(types.Set).ElementsAs(ctx, &mails, false) + if d.HasError() { + diags.Append(d...) + } + + blockDownload := BlockDownloadSettingsAPIModel{} + blockDownloadElems := attrs["block_download"].(types.Set).Elements() + if len(blockDownloadElems) > 0 { + attrs := blockDownloadElems[0].(types.Object).Attributes() + + blockDownload.Unscanned = attrs["unscanned"].(types.Bool).ValueBool() + blockDownload.Active = attrs["active"].(types.Bool).ValueBool() + } + + actions.Webhooks = webhooks + actions.Mails = mails + actions.FailBuild = attrs["fail_build"].(types.Bool).ValueBool() + actions.BlockDownload = blockDownload + actions.BlockReleaseBundleDistribution = attrs["block_release_bundle_distribution"].(types.Bool).ValueBool() + actions.BlockReleaseBundlePromotion = attrs["block_release_bundle_promotion"].(types.Bool).ValueBool() + actions.NotifyWatchRecipients = attrs["notify_watch_recipients"].(types.Bool).ValueBool() + actions.NotifyDeployer = attrs["notify_deployer"].(types.Bool).ValueBool() + actions.CreateJiraTicketEnabled = attrs["create_ticket_enabled"].(types.Bool).ValueBool() + actions.FailureGracePeriodDays = attrs["build_failure_grace_period_in_days"].(types.Int64).ValueInt64() + // actions.CustomSeverity = attrs["custom_severity"].(types.String).ValueString() + } + + return PolicyRuleAPIModel{ + Name: attrs["name"].(types.String).ValueString(), + Priority: attrs["priority"].(types.Int64).ValueInt64(), + Criteria: criteria, + Actions: actions, + } }, - "fix_version_dependant": { - Type: schema.TypeBool, - Optional: true, - Default: false, - Description: "Default value is `false`. Issues that do not have a fixed version are not generated until a fixed version is available. Must be `false` with `malicious_package` enabled.", + ) + + *apiModel = PolicyAPIModel{ + Name: m.Name.ValueString(), + Description: m.Description.ValueString(), + Type: m.Type.ValueString(), + Rules: &rules, + } + + return diags +} + +var cvssRangeAttrType = map[string]attr.Type{ + "from": types.Float64Type, + "to": types.Float64Type, +} + +var cvssRangeElementType = types.ObjectType{ + AttrTypes: cvssRangeAttrType, +} + +var exposuresAttrType = map[string]attr.Type{ + "min_severity": types.StringType, + "secrets": types.BoolType, + "applications": types.BoolType, + "services": types.BoolType, + "iac": types.BoolType, +} + +var exposuresElementType = types.ObjectType{ + AttrTypes: exposuresAttrType, +} + +var criteriaAttrTypes = map[string]attr.Type{ + "min_severity": types.StringType, + "fix_version_dependant": types.BoolType, + "applicable_cves_only": types.BoolType, + "malicious_package": types.BoolType, + "cvss_range": types.ListType{ElemType: cvssRangeElementType}, + "vulnerability_ids": types.SetType{ElemType: types.StringType}, + "exposures": types.ListType{ElemType: exposuresElementType}, + "package_name": types.StringType, + "package_type": types.StringType, + "package_versions": types.SetType{ElemType: types.StringType}, +} + +var criteriaSetElementType = types.ObjectType{ + AttrTypes: criteriaAttrTypes, +} + +var blockDownloadAttrTypes = map[string]attr.Type{ + "unscanned": types.BoolType, + "active": types.BoolType, +} + +var blockDownloadElementType = types.ObjectType{ + AttrTypes: blockDownloadAttrTypes, +} + +var actionsAttrTypes = map[string]attr.Type{ + "webhooks": types.SetType{ElemType: types.StringType}, + "mails": types.SetType{ElemType: types.StringType}, + "block_download": types.SetType{ElemType: blockDownloadElementType}, + "block_release_bundle_distribution": types.BoolType, + "block_release_bundle_promotion": types.BoolType, + "fail_build": types.BoolType, + "notify_deployer": types.BoolType, + "notify_watch_recipients": types.BoolType, + "create_ticket_enabled": types.BoolType, + "build_failure_grace_period_in_days": types.Int64Type, +} + +var actionsSetElementType = types.ObjectType{ + AttrTypes: actionsAttrTypes, +} + +var ruleAttrTypes = map[string]attr.Type{ + "name": types.StringType, + "priority": types.Int64Type, + "criteria": types.SetType{ElemType: criteriaSetElementType}, + "actions": types.SetType{ElemType: actionsSetElementType}, +} + +var ruleSetElementType = types.ObjectType{ + AttrTypes: ruleAttrTypes, +} + +func (m *SecurityPolicyV2ResourceModel) fromAPIModel(ctx context.Context, apiModel PolicyAPIModel) diag.Diagnostics { + diags := diag.Diagnostics{} + + rules := lo.Map( + *apiModel.Rules, + func(rule PolicyRuleAPIModel, _ int) attr.Value { + criteriaSet := types.SetNull(criteriaSetElementType) + if rule.Criteria != nil { + minimumSeverity := types.StringNull() + if rule.Criteria.MinimumSeverity != "" { + minimumSeverity = types.StringValue(rule.Criteria.MinimumSeverity) + } + + cvssRangeList := types.ListNull(cvssRangeElementType) + if rule.Criteria.CVSSRange != nil { + cvssRange, d := types.ObjectValue( + cvssRangeAttrType, + map[string]attr.Value{ + "from": types.Float64PointerValue(rule.Criteria.CVSSRange.From), + "to": types.Float64PointerValue(rule.Criteria.CVSSRange.To), + }, + ) + if d.HasError() { + diags.Append(d...) + } + + cr, d := types.ListValue( + cvssRangeElementType, + []attr.Value{cvssRange}, + ) + if d.HasError() { + diags.Append(d...) + } + + cvssRangeList = cr + } + + vulnerabilityIDs, d := types.SetValueFrom(ctx, types.StringType, rule.Criteria.VulnerabilityIds) + if d.HasError() { + diags.Append(d...) + } + + exposuresList := types.ListNull(exposuresElementType) + if rule.Criteria.Exposures != nil { + exposures, d := types.ObjectValue( + exposuresAttrType, + map[string]attr.Value{ + "min_severity": types.StringPointerValue(rule.Criteria.Exposures.MinSeverity), + "secrets": types.BoolPointerValue(rule.Criteria.Exposures.Secrets), + "applications": types.BoolPointerValue(rule.Criteria.Exposures.Applications), + "services": types.BoolPointerValue(rule.Criteria.Exposures.Services), + "iac": types.BoolPointerValue(rule.Criteria.Exposures.Iac), + }, + ) + if d.HasError() { + diags.Append(d...) + } + + es, d := types.ListValue( + exposuresElementType, + []attr.Value{exposures}, + ) + if d.HasError() { + diags.Append(d...) + } + + exposuresList = es + } + + packageName := types.StringNull() + if rule.Criteria.PackageName != "" { + packageName = types.StringValue(rule.Criteria.PackageName) + } + + packageType := types.StringNull() + if rule.Criteria.PackageType != "" { + packageType = types.StringValue(rule.Criteria.PackageType) + } + + packageVersions, d := types.SetValueFrom(ctx, types.StringType, rule.Criteria.PackageVersions) + if d.HasError() { + diags.Append(d...) + } + + criteria, d := types.ObjectValue( + criteriaAttrTypes, + map[string]attr.Value{ + "min_severity": minimumSeverity, + "fix_version_dependant": types.BoolValue(rule.Criteria.FixVersionDependant), + "applicable_cves_only": types.BoolValue(rule.Criteria.ApplicableCVEsOnly), + "malicious_package": types.BoolValue(rule.Criteria.MaliciousPackage), + "cvss_range": cvssRangeList, + "vulnerability_ids": vulnerabilityIDs, + "exposures": exposuresList, + "package_name": packageName, + "package_type": packageType, + "package_versions": packageVersions, + }, + ) + if d.HasError() { + diags.Append(d...) + } + cs, d := types.SetValue( + criteriaSetElementType, + []attr.Value{criteria}, + ) + if d.HasError() { + diags.Append(d...) + } + + criteriaSet = cs + } + + webhooks, d := types.SetValueFrom(ctx, types.StringType, rule.Actions.Webhooks) + if d.HasError() { + diags.Append(d...) + } + + mails, d := types.SetValueFrom(ctx, types.StringType, rule.Actions.Mails) + if d.HasError() { + diags.Append(d...) + } + + blockDownload, d := types.ObjectValue( + blockDownloadAttrTypes, + map[string]attr.Value{ + "unscanned": types.BoolValue(rule.Actions.BlockDownload.Unscanned), + "active": types.BoolValue(rule.Actions.BlockDownload.Active), + }, + ) + if d.HasError() { + diags.Append(d...) + } + blockDownloadSet, d := types.SetValue( + blockDownloadElementType, + []attr.Value{blockDownload}, + ) + if d.HasError() { + diags.Append(d...) + } + + actions, d := types.ObjectValue( + actionsAttrTypes, + map[string]attr.Value{ + "webhooks": webhooks, + "mails": mails, + "block_download": blockDownloadSet, + "block_release_bundle_distribution": types.BoolValue(rule.Actions.BlockReleaseBundleDistribution), + "block_release_bundle_promotion": types.BoolValue(rule.Actions.BlockReleaseBundlePromotion), + "fail_build": types.BoolValue(rule.Actions.FailBuild), + "notify_deployer": types.BoolValue(rule.Actions.NotifyDeployer), + "notify_watch_recipients": types.BoolValue(rule.Actions.NotifyWatchRecipients), + "create_ticket_enabled": types.BoolValue(rule.Actions.CreateJiraTicketEnabled), + "build_failure_grace_period_in_days": types.Int64Value(rule.Actions.FailureGracePeriodDays), + }, + ) + if d.HasError() { + diags.Append(d...) + } + actionsSet, d := types.SetValue( + actionsSetElementType, + []attr.Value{actions}, + ) + if d.HasError() { + diags.Append(d...) + } + + r, d := types.ObjectValue( + ruleAttrTypes, + map[string]attr.Value{ + "name": types.StringValue(rule.Name), + "priority": types.Int64Value(rule.Priority), + "criteria": criteriaSet, + "actions": actionsSet, + }, + ) + if d.HasError() { + diags.Append(d...) + } + + return r }, - "applicable_cves_only": { - Type: schema.TypeBool, - Optional: true, - Default: false, - Description: "Default value is `false`. Mark to skip CVEs that are not applicable in the context of the artifact. The contextual analysis operation might be long and affect build time if the `fail_build` action is set.\n\n~>Only supported by JFrog Advanced Security", + ) + + rulesSet, d := types.SetValue( + ruleSetElementType, + rules, + ) + if d.HasError() { + diags.Append(d...) + } + + m.ID = types.StringValue(apiModel.Name) + m.Name = types.StringValue(apiModel.Name) + m.Description = types.StringValue(apiModel.Description) + m.Type = types.StringValue(apiModel.Type) + m.Author = types.StringValue(apiModel.Author) + m.Created = types.StringValue(apiModel.Created) + m.Modified = types.StringValue(apiModel.Modified) + + m.Rules = rulesSet + + return diags +} + +var projectKeySchemaAttrs = func(isForceNew bool, additionalDescription string) map[string]schema.Attribute { + description := fmt.Sprintf("Project key for assigning this resource to. Must be 2 - 10 lowercase alphanumeric and hyphen characters. %s", additionalDescription) + planModifiers := []planmodifier.String{} + + if isForceNew { + planModifiers = append(planModifiers, stringplanmodifier.RequiresReplace()) + } + + return map[string]schema.Attribute{ + "project_key": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validatorfw_string.ProjectKey(), + }, + PlanModifiers: planModifiers, + Description: description, }, - "malicious_package": { - Type: schema.TypeBool, - Optional: true, - Default: false, - Description: "Default value is `false`. Generating a violation on a malicious package.", + } +} + +var policySchemaAttrs = lo.Assign( + projectKeySchemaAttrs(false, ""), + map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, }, - "cvss_range": { - Type: schema.TypeList, + "name": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Description: "Name of the policy (must be unique)", + }, + "description": schema.StringAttribute{ Optional: true, - MaxItems: 1, - Description: "The CVSS score range to apply to the rule. This is used for a fine-grained control, rather than using the predefined severities. The score range is based on CVSS v3 scoring, and CVSS v2 score is CVSS v3 score is not available.", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "from": { - Type: schema.TypeFloat, - Required: true, - Description: "The beginning of the range of CVS scores (from 1-10, float) to flag.", - ValidateDiagFunc: validation.ToDiagFunc(validation.FloatBetween(0, 10)), + Description: "More verbose description of the policy", + }, + "type": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf("security", "license", "operational_risk"), + }, + Description: "Type of the policy", + }, + "author": schema.StringAttribute{ + Computed: true, + Description: "User, who created the policy", + }, + "created": schema.StringAttribute{ + Computed: true, + Description: "Creation timestamp", + }, + "modified": schema.StringAttribute{ + Computed: true, + Description: "Modification timestamp", + }, + }, +) + +var commonActionsBlocks = map[string]schema.Block{ + "block_download": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "unscanned": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + Description: "Whether or not to block download of artifacts that meet the artifact `filters` for the associated `xray_watch` resource but have not been scanned yet. Can not be set to `true` if attribute `active` is `false`. Default value is `false`.", + }, + "active": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + Description: "Whether or not to block download of artifacts that meet the artifact and severity `filters` for the associated `xray_watch` resource. Default value is `false`.", + }, + }, + }, + Validators: []validator.Set{ + setvalidator.IsRequired(), + setvalidator.SizeAtMost(1), + }, + Description: "Block download of artifacts that meet the Artifact Filter and Severity Filter specifications for this watch", + }, +} + +var commonActionsAttrs = map[string]schema.Attribute{ + "webhooks": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + Description: "A list of Xray-configured webhook URLs to be invoked if a violation is triggered.", + }, + "mails": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + Description: "A list of email addressed that will get emailed when a violation is triggered.", + }, + "block_release_bundle_distribution": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + Description: "Blocks Release Bundle distribution to Edge nodes if a violation is found. Default value is `false`.", + }, + "block_release_bundle_promotion": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + Description: "Blocks Release Bundle promotion if a violation is found. Default value is `false`.", + }, + "fail_build": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + Description: "Whether or not the related CI build should be marked as failed if a violation is triggered. This option is only available when the policy is applied to an `xray_watch` resource with a `type` of `builds`. Default value is `false`.", + }, + "notify_deployer": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + Description: "Sends an email message to component deployer with details about the generated Violations. Default value is `false`.", + }, + "notify_watch_recipients": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + Description: "Sends an email message to all configured recipients inside a specific watch with details about the generated Violations. Default value is `false`.", + }, + "create_ticket_enabled": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + Description: "Create Jira Ticket for this Policy Violation. Requires configured Jira integration. Default value is `false`.", + }, + "build_failure_grace_period_in_days": schema.Int64Attribute{ + Optional: true, + Validators: []validator.Int64{ + int64validator.AtLeast(0), + }, + Description: "Allow grace period for certain number of days. All violations will be ignored during this time. To be used only if `fail_build` is enabled.", + }, +} + +var securityPolicyCriteriaBlocks = map[string]schema.Block{ + "cvss_range": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "from": schema.Float64Attribute{ + Required: true, + Validators: []validator.Float64{ + float64validator.Between(0, 10), }, - "to": { - Type: schema.TypeFloat, - Required: true, - Description: "The end of the range of CVS scores (from 1-10, float) to flag. ", - ValidateDiagFunc: validation.ToDiagFunc(validation.FloatBetween(0, 10)), + Description: "The beginning of the range of CVS scores (from 1-10, float) to flag.", + }, + "to": schema.Float64Attribute{ + Required: true, + Validators: []validator.Float64{ + float64validator.Between(0, 10), }, + Description: "The end of the range of CVS scores (from 1-10, float) to flag. ", }, }, }, - "vulnerability_ids": { - Type: schema.TypeSet, - Optional: true, - MaxItems: 100, - MinItems: 1, - Description: "Creates policy rules for specific vulnerability IDs that you input. You can add multiple vulnerabilities IDs up to 100. CVEs and Xray IDs are supported. Example - CVE-2015-20107, XRAY-2344", - Elem: &schema.Schema{ - Type: schema.TypeString, - ValidateDiagFunc: validation.ToDiagFunc( - validation.StringMatch(regexp.MustCompile(`(CVE\W*\d{4}\W+\d{4,}|XRAY-\d{4,})`), "invalid Vulnerability, must be a valid CVE or Xray ID, example CVE-2021-12345, XRAY-1234"), - ), - }, + Validators: []validator.List{ + listvalidator.SizeAtMost(1), }, - "exposures": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Description: "Creates policy rules for specific exposures.\n\n~>Only supported by JFrog Advanced Security", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "min_severity": { - Type: schema.TypeString, - Optional: true, - Default: "All Severities", - Description: "The minimum security vulnerability severity that will be impacted by the policy. Valid values: `All Severities`, `Critical`, `High`, `Medium`, `Low`", - ValidateDiagFunc: validator.StringInSlice(true, "All Severities", "Critical", "High", "Medium", "Low"), + Description: "The CVSS score range to apply to the rule. This is used for a fine-grained control, rather than using the predefined severities. The score range is based on CVSS v3 scoring, and CVSS v2 score is CVSS v3 score is not available.", + }, + "exposures": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "min_severity": schema.StringAttribute{ + Optional: true, + Computed: true, + Default: stringdefault.StaticString("All Severities"), + Validators: []validator.String{ + stringvalidator.OneOfCaseInsensitive("All Severities", "Critical", "High", "Medium", "Low"), }, - "secrets": { - Type: schema.TypeBool, - Optional: true, - Default: true, - Description: "Secrets exposures.", + MarkdownDescription: "The minimum security vulnerability severity that will be impacted by the policy. Valid values: `All Severities`, `Critical`, `High`, `Medium`, `Low`", + }, + "secrets": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + Description: "Secrets exposures.", + }, + "applications": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + Description: "Applications exposures.", + }, + "services": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + Description: "Services exposures.", + }, + "iac": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + Description: "Iac exposures.", + }, + }, + }, + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + listvalidator.ConflictsWith( + path.MatchRelative().AtParent().AtName("cvss_range"), + ), + listvalidator.ConflictsWith( + path.MatchRelative().AtParent().AtName("min_severity"), + ), + listvalidator.ConflictsWith( + path.MatchRelative().AtParent().AtName("malicious_package"), + ), + listvalidator.ConflictsWith( + path.MatchRelative().AtParent().AtName("vulnerability_ids"), + ), + }, + Description: "Creates policy rules for specific exposures.\n\n~>Only supported by JFrog Advanced Security", + }, +} + +var securityPolicyCriteriaAttrs = map[string]schema.Attribute{ + "min_severity": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.OneOfCaseInsensitive("All Severities", "Critical", "High", "Medium", "Low"), + stringvalidator.ConflictsWith( + path.MatchRelative().AtParent().AtName("cvss_range"), + ), + }, + Description: "The minimum security vulnerability severity that will be impacted by the policy. Valid values: `All Severities`, `Critical`, `High`, `Medium`, `Low`", + }, + "fix_version_dependant": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + Description: "Default value is `false`. Issues that do not have a fixed version are not generated until a fixed version is available. Must be `false` with `malicious_package` enabled.", + }, + "applicable_cves_only": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + Description: "Default value is `false`. Mark to skip CVEs that are not applicable in the context of the artifact. The contextual analysis operation might be long and affect build time if the `fail_build` action is set.\n\n~>Only supported by JFrog Advanced Security", + }, + "malicious_package": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + Validators: []validator.Bool{ + boolvalidator.ConflictsWith( + path.MatchRelative().AtParent().AtName("min_severity"), + ), + boolvalidator.ConflictsWith( + path.MatchRelative().AtParent().AtName("cvss_range"), + ), + }, + Description: "Default value is `false`. Generating a violation on a malicious package.", + }, + "vulnerability_ids": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + Validators: []validator.Set{ + setvalidator.SizeBetween(1, 100), + setvalidator.ValueStringsAre( + stringvalidator.RegexMatches(regexp.MustCompile(`(CVE\W*\d{4}\W+\d{4,}|XRAY-\d{4,})`), "invalid Vulnerability, must be a valid CVE or Xray ID, example CVE-2021-12345, XRAY-1234"), + ), + setvalidator.ConflictsWith( + path.MatchRelative().AtParent().AtName("malicious_package"), + ), + setvalidator.ConflictsWith( + path.MatchRelative().AtParent().AtName("min_severity"), + ), + setvalidator.ConflictsWith( + path.MatchRelative().AtParent().AtName("cvss_range"), + ), + setvalidator.ConflictsWith( + path.MatchRelative().AtParent().AtName("exposures"), + ), + setvalidator.ConflictsWith( + path.MatchRelative().AtParent().AtName("package_name"), + path.MatchRelative().AtParent().AtName("package_type"), + path.MatchRelative().AtParent().AtName("package_versions"), + ), + }, + Description: "Creates policy rules for specific vulnerability IDs that you input. You can add multiple vulnerabilities IDs up to 100. CVEs and Xray IDs are supported. Example - CVE-2015-20107, XRAY-2344", + }, + "package_name": schema.StringAttribute{ + Optional: true, + Description: "The package name to create a rule for", + Validators: []validator.String{ + stringvalidator.AlsoRequires( + path.MatchRelative().AtParent().AtName("package_type"), + ), + }, + }, + "package_type": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.OneOfCaseInsensitive(validPackageTypesSupportedXraySecPolicies...), + stringvalidator.AlsoRequires( + path.MatchRelative().AtParent().AtName("package_name"), + ), + }, + Description: "The package type to create a rule for", + }, + "package_versions": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + Validators: []validator.Set{ + setvalidator.ValueStringsAre( + stringvalidator.RegexMatches(regexp.MustCompile(`((^(\(|\[)((\d+\.)?(\d+\.)?(\*|\d+)|(\s*))\,((\d+\.)?(\d+\.)?(\*|\d+)|(\s*))(\)|\])$|^\[(\d+\.)?(\d+\.)?(\*|\d+)\]$))`), "invalid Range, must be one of the follows: Any Version: (,) or Specific Version: [1.2], [3] or Range: (1,), [,1.2.3], (4.5.0,6.5.2]"), + ), + }, + Description: "package versions to apply the rule on can be (,) for any version or an open range (1,4) or closed [1,4] or one version [1]", + }, +} + +var policyBlocks = func(criteriaAttrs map[string]schema.Attribute, criteriaBlocks map[string]schema.Block, actionsAttrs map[string]schema.Attribute, actionsBlocks map[string]schema.Block) map[string]schema.Block { + return map[string]schema.Block{ + "rule": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + Description: "Name of the rule", }, - "applications": { - Type: schema.TypeBool, - Optional: true, - Default: true, - Description: "Applications exposures.", + "priority": schema.Int64Attribute{ + Required: true, + Validators: []validator.Int64{ + int64validator.AtLeast(1), + }, + Description: "Integer describing the rule priority. Must be at least 1", }, - "services": { - Type: schema.TypeBool, - Optional: true, - Default: true, - Description: "Services exposures.", + }, + Blocks: map[string]schema.Block{ + "criteria": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: criteriaAttrs, + Blocks: criteriaBlocks, + }, + Validators: []validator.Set{ + setvalidator.IsRequired(), + setvalidator.SizeBetween(1, 1), + }, + Description: "The set of security conditions to examine when an scanned artifact is scanned.", }, - "iac": { - Type: schema.TypeBool, - Optional: true, - Default: true, - Description: "Iac exposures.", + "actions": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: actionsAttrs, + Blocks: actionsBlocks, + }, + Validators: []validator.Set{ + setvalidator.IsRequired(), + setvalidator.SizeBetween(1, 1), + }, + Description: "Specifies the actions to take once a security policy violation has been triggered.", }, }, }, - }, - "package_name": { - Type: schema.TypeString, - Optional: true, - Description: "The package name to create a rule for", - }, - "package_type": { - Type: schema.TypeString, - Optional: true, - Description: "The package type to create a rule for", - ValidateDiagFunc: validator.StringInSlice(true, validPackageTypesSupportedXraySecPolicies...), - }, - "package_versions": { - Type: schema.TypeSet, - Optional: true, - Description: "package versions to apply the rule on can be (,) for any version or an open range (1,4) or closed [1,4] or one version [1]", - Elem: &schema.Schema{ - Type: schema.TypeString, - ValidateDiagFunc: validation.ToDiagFunc( - validation.StringMatch(regexp.MustCompile(`((^(\(|\[)((\d+\.)?(\d+\.)?(\*|\d+)|(\s*))\,((\d+\.)?(\d+\.)?(\*|\d+)|(\s*))(\)|\])$|^\[(\d+\.)?(\d+\.)?(\*|\d+)\]$))`), "invalid Range, must be one of the follows: Any Version: (,) or Specific Version: [1.2], [3] or Range: (1,), [,1.2.3], (4.5.0,6.5.2]"), - ), + Validators: []validator.Set{ + setvalidator.IsRequired(), + setvalidator.SizeAtLeast(1), }, + Description: "A list of user-defined rules allowing you to trigger violations for specific vulnerability or license breaches by setting a license or security criteria, with a corresponding set of automatic actions according to your needs. Rules are processed according to the ascending order in which they are placed in the Rules list on the Policy. If a rule is met, the subsequent rules in the list will not be applied.", }, } +} - return &schema.Resource{ - SchemaVersion: 1, - CreateContext: resourceXrayPolicyCreate, - ReadContext: resourceXrayPolicyRead, - UpdateContext: resourceXrayPolicyUpdate, - DeleteContext: resourceXrayPolicyDelete, +func (r *SecurityPolicyV2Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Version: 1, + Attributes: policySchemaAttrs, + Blocks: policyBlocks(securityPolicyCriteriaAttrs, securityPolicyCriteriaBlocks, commonActionsAttrs, commonActionsBlocks), Description: "Creates an Xray policy using V2 of the underlying APIs. Please note: " + "It's only compatible with Bearer token auth method (Identity and Access => Access Tokens)", + } +} - Importer: &schema.ResourceImporter{ - StateContext: resourceImporterForProjectKey, - }, - CustomizeDiff: criteriaMaliciousPkgDiff, - Schema: getPolicySchema(criteriaSchema, commonActionsSchema), +func (r *SecurityPolicyV2Resource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return } + r.ProviderData = req.ProviderData.(util.ProviderMetadata) } -var criteriaMaliciousPkgDiff = func(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { - rules := diff.Get("rule").(*schema.Set).List() - if len(rules) == 0 { - return nil +func (r SecurityPolicyV2Resource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + var data SecurityPolicyV2ResourceModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return } - criteria := rules[0].(map[string]interface{})["criteria"].(*schema.Set).List() - if len(criteria) == 0 { - return nil + + // If rule is not configured, return without warning. + if data.Rules.IsNull() || data.Rules.IsUnknown() { + return + } + + for _, rule := range data.Rules.Elements() { + ruleAttrs := rule.(types.Object).Attributes() + criteria := ruleAttrs["criteria"].(types.Set) + attrs := criteria.Elements()[0].(types.Object).Attributes() + + fixVersionDependant := attrs["fix_version_dependant"].(types.Bool).ValueBool() + maliciousPackage := attrs["malicious_package"].(types.Bool).ValueBool() + + packageName := attrs["package_name"].(types.String) + packagType := attrs["package_type"].(types.String) + packageVersions := attrs["package_versions"].(types.Set) + + if maliciousPackage && fixVersionDependant { + resp.Diagnostics.AddAttributeError( + path.Root("rules").AtSetValue(rule).AtName("criteria").AtSetValue(criteria.Elements()[0]).AtName("fix_version_dependant"), + "Invalid Attribute Configuration", + "fix_version_dependant must be set to 'false' if malicious_package is 'true'", + ) + return + } + + if fixVersionDependant && (!packageName.IsNull() || !packagType.IsNull() || !packageVersions.IsNull()) { + resp.Diagnostics.AddAttributeError( + path.Root("rules").AtSetValue(rule).AtName("criteria").AtSetValue(criteria.Elements()[0]).AtName("fix_version_dependant"), + "Invalid Attribute Configuration", + "fix_version_dependant must be set to 'false' if any package attribute is set", + ) + return + } } +} + +func (r *SecurityPolicyV2Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + go util.SendUsageResourceCreate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) - criterion := criteria[0].(map[string]interface{}) - // fixVersionDependant can't be set with malicious_package - fixVersionDependant := criterion["fix_version_dependant"].(bool) - // Only one of the following: - minSeverity := criterion["min_severity"].(string) - cvssRange := criterion["cvss_range"].([]interface{}) - vulnerabilityIDs := criterion["vulnerability_ids"].(*schema.Set).List() - maliciousPackage := criterion["malicious_package"].(bool) - exposures := criterion["exposures"].([]interface{}) - package_name := criterion["package_name"].(string) - package_type := criterion["package_type"].(string) - package_versions := criterion["package_versions"].(*schema.Set).List() - isPackageSet := len(package_name) > 0 || len(package_type) > 0 || len(package_versions) > 0 //if one of them is not defined the API will return an error guiding which one is missing + var plan SecurityPolicyV2ResourceModel - if len(exposures) > 0 && maliciousPackage || (len(exposures) > 0 && len(cvssRange) > 0) || - (len(exposures) > 0 && len(minSeverity) > 0) || (len(exposures) > 0 && len(vulnerabilityIDs) > 0) { - return fmt.Errorf("exsposures can't be set together with cvss_range, min_severity, malicious_package and vulnerability_ids") + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return } - // If `malicious_package` is enabled in the UI, `fix_version_dependant` is set to `false` in the UI call. - // UI itself doesn't have this checkbox at all. We are adding this check to avoid unexpected behavior. - if maliciousPackage && fixVersionDependant { - return fmt.Errorf("fix_version_dependant must be set to false if malicious_package is true") + + request, err := getRestyRequest(r.ProviderData.Client, plan.ProjectKey.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "failed to get Resty client", + err.Error(), + ) + return } - if (maliciousPackage && len(minSeverity) > 0) || (maliciousPackage && len(cvssRange) > 0) { - return fmt.Errorf("malicious_package can't be set together with min_severity and/or cvss_range") + + var policy PolicyAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, &policy)...) + if resp.Diagnostics.HasError() { + return + } + + var policyError PolicyError + response, err := request. + SetBody(policy). + SetError(&policyError). + Post(PoliciesEndpoint) + + if err != nil { + utilfw.UnableToCreateResourceError(resp, err.Error()) + return } - if len(minSeverity) > 0 && len(cvssRange) > 0 { - return fmt.Errorf("min_severity can't be set together with cvss_range") + + if response.IsError() { + utilfw.UnableToCreateResourceError(resp, policyError.Error) + return } - if (len(vulnerabilityIDs) > 0 && maliciousPackage) || (len(vulnerabilityIDs) > 0 && len(minSeverity) > 0) || - (len(vulnerabilityIDs) > 0 && len(cvssRange) > 0) || (len(vulnerabilityIDs) > 0 && len(exposures) > 0) { - return fmt.Errorf("vulnerability_ids can't be set together with with malicious_package, min_severity, cvss_range and exposures") + + response, err = request. + SetResult(&policy). + SetPathParam("name", plan.Name.ValueString()). + SetError(&policyError). + Get(PolicyEndpoint) + + if err != nil { + utilfw.UnableToCreateResourceError(resp, err.Error()) + return } - if (isPackageSet && len(vulnerabilityIDs) > 0) || (isPackageSet && maliciousPackage) || - (isPackageSet && len(cvssRange) > 0) || (isPackageSet && len(minSeverity) > 0) || - (isPackageSet && len(exposures) > 0) { - return fmt.Errorf("package_name, package_type and package versions can't be set together with with vulnerability_ids, malicious_package, min_severity, cvss_range and exposures") + if response.IsError() { + utilfw.UnableToCreateResourceError(resp, policyError.Error) + return } - if isPackageSet && fixVersionDependant { - return fmt.Errorf("fix_version_dependant must be set to false if package type policy is used") + resp.Diagnostics.Append(plan.fromAPIModel(ctx, policy)...) + if resp.Diagnostics.HasError() { + return } - return nil + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) } + +func (r *SecurityPolicyV2Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + go util.SendUsageResourceRead(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var state SecurityPolicyV2ResourceModel + + // Read Terraform state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + request, err := getRestyRequest(r.ProviderData.Client, state.ProjectKey.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "failed to get Resty client", + err.Error(), + ) + return + } + + var policy PolicyAPIModel + var policyError PolicyError + + response, err := request. + SetResult(&policy). + SetPathParam("name", state.Name.ValueString()). + SetError(&policyError). + Get(PolicyEndpoint) + + if err != nil { + utilfw.UnableToRefreshResourceError(resp, err.Error()) + return + } + + if response.StatusCode() == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + + if response.IsError() { + utilfw.UnableToRefreshResourceError(resp, policyError.Error) + return + } + + resp.Diagnostics.Append(state.fromAPIModel(ctx, policy)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *SecurityPolicyV2Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + go util.SendUsageResourceUpdate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var plan SecurityPolicyV2ResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + request, err := getRestyRequest(r.ProviderData.Client, plan.ProjectKey.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "failed to get Resty client", + err.Error(), + ) + return + } + + var policy PolicyAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, &policy)...) + if resp.Diagnostics.HasError() { + return + } + + var policyError PolicyError + + response, err := request. + SetPathParam("name", plan.Name.ValueString()). + SetBody(policy). + SetError(&policyError). + Put(PolicyEndpoint) + + if err != nil { + utilfw.UnableToUpdateResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToUpdateResourceError(resp, policyError.Error) + return + } + + response, err = request. + SetResult(&policy). + SetPathParam("name", plan.Name.ValueString()). + SetError(&policyError). + Get(PolicyEndpoint) + + if err != nil { + utilfw.UnableToUpdateResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToUpdateResourceError(resp, policyError.Error) + return + } + + resp.Diagnostics.Append(plan.fromAPIModel(ctx, policy)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *SecurityPolicyV2Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + go util.SendUsageResourceDelete(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var state SecurityPolicyV2ResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + request, err := getRestyRequest(r.ProviderData.Client, state.ProjectKey.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "failed to get Resty client", + err.Error(), + ) + return + } + + var policyError PolicyError + response, err := request. + SetPathParam("name", state.Name.ValueString()). + SetError(&policyError). + Delete(PolicyEndpoint) + + if err != nil { + utilfw.UnableToDeleteResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToDeleteResourceError(resp, policyError.Error) + return + } + + // If the logic reaches here, it implicitly succeeded and will remove + // the resource from state if there are no other errors. +} + +// ImportState imports the resource into the Terraform state. +func (r *SecurityPolicyV2Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + parts := strings.SplitN(req.ID, ":", 2) + + if len(parts) > 0 && parts[0] != "" { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), parts[0])...) + } + + if len(parts) == 2 && parts[1] != "" { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_key"), parts[1])...) + } +} + +// +// func ResourceXraySecurityPolicyV2() *schema.Resource { +// var criteriaSchema = map[string]*schema.Schema{ +// "min_severity": { +// Type: schema.TypeString, +// Optional: true, +// Description: "The minimum security vulnerability severity that will be impacted by the policy. Valid values: `All Severities`, `Critical`, `High`, `Medium`, `Low`", +// ValidateDiagFunc: validator.StringInSlice(true, "All Severities", "Critical", "High", "Medium", "Low"), +// }, +// "fix_version_dependant": { +// Type: schema.TypeBool, +// Optional: true, +// Default: false, +// Description: "Default value is `false`. Issues that do not have a fixed version are not generated until a fixed version is available. Must be `false` with `malicious_package` enabled.", +// }, +// "applicable_cves_only": { +// Type: schema.TypeBool, +// Optional: true, +// Default: false, +// Description: "Default value is `false`. Mark to skip CVEs that are not applicable in the context of the artifact. The contextual analysis operation might be long and affect build time if the `fail_build` action is set.\n\n~>Only supported by JFrog Advanced Security", +// }, +// "malicious_package": { +// Type: schema.TypeBool, +// Optional: true, +// Default: false, +// Description: "Default value is `false`. Generating a violation on a malicious package.", +// }, +// "cvss_range": { +// Type: schema.TypeList, +// Optional: true, +// MaxItems: 1, +// Description: "The CVSS score range to apply to the rule. This is used for a fine-grained control, rather than using the predefined severities. The score range is based on CVSS v3 scoring, and CVSS v2 score is CVSS v3 score is not available.", +// Elem: &schema.Resource{ +// Schema: map[string]*schema.Schema{ +// "from": { +// Type: schema.TypeFloat, +// Required: true, +// Description: "The beginning of the range of CVS scores (from 1-10, float) to flag.", +// ValidateDiagFunc: validation.ToDiagFunc(validation.FloatBetween(0, 10)), +// }, +// "to": { +// Type: schema.TypeFloat, +// Required: true, +// Description: "The end of the range of CVS scores (from 1-10, float) to flag. ", +// ValidateDiagFunc: validation.ToDiagFunc(validation.FloatBetween(0, 10)), +// }, +// }, +// }, +// }, +// "vulnerability_ids": { +// Type: schema.TypeSet, +// Optional: true, +// MaxItems: 100, +// MinItems: 1, +// Description: "Creates policy rules for specific vulnerability IDs that you input. You can add multiple vulnerabilities IDs up to 100. CVEs and Xray IDs are supported. Example - CVE-2015-20107, XRAY-2344", +// Elem: &schema.Schema{ +// Type: schema.TypeString, +// ValidateDiagFunc: validation.ToDiagFunc( +// validation.StringMatch(regexp.MustCompile(`(CVE\W*\d{4}\W+\d{4,}|XRAY-\d{4,})`), "invalid Vulnerability, must be a valid CVE or Xray ID, example CVE-2021-12345, XRAY-1234"), +// ), +// }, +// }, +// "exposures": { +// Type: schema.TypeList, +// Optional: true, +// MaxItems: 1, +// Description: "Creates policy rules for specific exposures.\n\n~>Only supported by JFrog Advanced Security", +// Elem: &schema.Resource{ +// Schema: map[string]*schema.Schema{ +// "min_severity": { +// Type: schema.TypeString, +// Optional: true, +// Default: "All Severities", +// Description: "The minimum security vulnerability severity that will be impacted by the policy. Valid values: `All Severities`, `Critical`, `High`, `Medium`, `Low`", +// ValidateDiagFunc: validator.StringInSlice(true, "All Severities", "Critical", "High", "Medium", "Low"), +// }, +// "secrets": { +// Type: schema.TypeBool, +// Optional: true, +// Default: true, +// Description: "Secrets exposures.", +// }, +// "applications": { +// Type: schema.TypeBool, +// Optional: true, +// Default: true, +// Description: "Applications exposures.", +// }, +// "services": { +// Type: schema.TypeBool, +// Optional: true, +// Default: true, +// Description: "Services exposures.", +// }, +// "iac": { +// Type: schema.TypeBool, +// Optional: true, +// Default: true, +// Description: "Iac exposures.", +// }, +// }, +// }, +// }, +// "package_name": { +// Type: schema.TypeString, +// Optional: true, +// Description: "The package name to create a rule for", +// }, +// "package_type": { +// Type: schema.TypeString, +// Optional: true, +// Description: "The package type to create a rule for", +// ValidateDiagFunc: validator.StringInSlice(true, validPackageTypesSupportedXraySecPolicies...), +// }, +// "package_versions": { +// Type: schema.TypeSet, +// Optional: true, +// Description: "package versions to apply the rule on can be (,) for any version or an open range (1,4) or closed [1,4] or one version [1]", +// Elem: &schema.Schema{ +// Type: schema.TypeString, +// ValidateDiagFunc: validation.ToDiagFunc( +// validation.StringMatch(regexp.MustCompile(`((^(\(|\[)((\d+\.)?(\d+\.)?(\*|\d+)|(\s*))\,((\d+\.)?(\d+\.)?(\*|\d+)|(\s*))(\)|\])$|^\[(\d+\.)?(\d+\.)?(\*|\d+)\]$))`), "invalid Range, must be one of the follows: Any Version: (,) or Specific Version: [1.2], [3] or Range: (1,), [,1.2.3], (4.5.0,6.5.2]"), +// ), +// }, +// }, +// } +// +// return &schema.Resource{ +// SchemaVersion: 1, +// CreateContext: resourceXrayPolicyCreate, +// ReadContext: resourceXrayPolicyRead, +// UpdateContext: resourceXrayPolicyUpdate, +// DeleteContext: resourceXrayPolicyDelete, +// Description: "Creates an Xray policy using V2 of the underlying APIs. Please note: " + +// "It's only compatible with Bearer token auth method (Identity and Access => Access Tokens)", +// +// Importer: &schema.ResourceImporter{ +// StateContext: resourceImporterForProjectKey, +// }, +// CustomizeDiff: criteriaMaliciousPkgDiff, +// Schema: getPolicySchema(criteriaSchema, commonActionsSchema), +// } +// } +// +// var criteriaMaliciousPkgDiff = func(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { +// rules := diff.Get("rule").(*schema.Set).List() +// if len(rules) == 0 { +// return nil +// } +// criteria := rules[0].(map[string]interface{})["criteria"].(*schema.Set).List() +// if len(criteria) == 0 { +// return nil +// } +// +// criterion := criteria[0].(map[string]interface{}) +// // fixVersionDependant can't be set with malicious_package +// fixVersionDependant := criterion["fix_version_dependant"].(bool) +// // Only one of the following: +// minSeverity := criterion["min_severity"].(string) +// cvssRange := criterion["cvss_range"].([]interface{}) +// vulnerabilityIDs := criterion["vulnerability_ids"].(*schema.Set).List() +// maliciousPackage := criterion["malicious_package"].(bool) +// exposures := criterion["exposures"].([]interface{}) +// package_name := criterion["package_name"].(string) +// package_type := criterion["package_type"].(string) +// package_versions := criterion["package_versions"].(*schema.Set).List() +// isPackageSet := len(package_name) > 0 || len(package_type) > 0 || len(package_versions) > 0 //if one of them is not defined the API will return an error guiding which one is missing +// +// if len(exposures) > 0 && maliciousPackage || (len(exposures) > 0 && len(cvssRange) > 0) || +// (len(exposures) > 0 && len(minSeverity) > 0) || (len(exposures) > 0 && len(vulnerabilityIDs) > 0) { +// return fmt.Errorf("exsposures can't be set together with cvss_range, min_severity, malicious_package and vulnerability_ids") +// } +// // If `malicious_package` is enabled in the UI, `fix_version_dependant` is set to `false` in the UI call. +// // UI itself doesn't have this checkbox at all. We are adding this check to avoid unexpected behavior. +// if maliciousPackage && fixVersionDependant { +// return fmt.Errorf("fix_version_dependant must be set to false if malicious_package is true") +// } +// if (maliciousPackage && len(minSeverity) > 0) || (maliciousPackage && len(cvssRange) > 0) { +// return fmt.Errorf("malicious_package can't be set together with min_severity and/or cvss_range") +// } +// if len(minSeverity) > 0 && len(cvssRange) > 0 { +// return fmt.Errorf("min_severity can't be set together with cvss_range") +// } +// if (len(vulnerabilityIDs) > 0 && maliciousPackage) || (len(vulnerabilityIDs) > 0 && len(minSeverity) > 0) || +// (len(vulnerabilityIDs) > 0 && len(cvssRange) > 0) || (len(vulnerabilityIDs) > 0 && len(exposures) > 0) { +// return fmt.Errorf("vulnerability_ids can't be set together with with malicious_package, min_severity, cvss_range and exposures") +// } +// +// if (isPackageSet && len(vulnerabilityIDs) > 0) || (isPackageSet && maliciousPackage) || +// (isPackageSet && len(cvssRange) > 0) || (isPackageSet && len(minSeverity) > 0) || +// (isPackageSet && len(exposures) > 0) { +// return fmt.Errorf("package_name, package_type and package versions can't be set together with with vulnerability_ids, malicious_package, min_severity, cvss_range and exposures") +// } +// +// if isPackageSet && fixVersionDependant { +// return fmt.Errorf("fix_version_dependant must be set to false if package type policy is used") +// } +// +// return nil +// } diff --git a/pkg/xray/resource/resource_xray_security_policy_test.go b/pkg/xray/resource/resource_xray_security_policy_test.go index d8ea00de..35cea7e8 100644 --- a/pkg/xray/resource/resource_xray_security_policy_test.go +++ b/pkg/xray/resource/resource_xray_security_policy_test.go @@ -299,9 +299,10 @@ func TestAccSecurityPolicy_createBlockDownloadTrueCVSS(t *testing.T) { Check: verifySecurityPolicy(fqrn, testData, criteriaTypeCvss), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, + ResourceName: fqrn, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"author", "created", "modified"}, }, }, }) @@ -440,7 +441,7 @@ func TestAccSecurityPolicy_createMaliciousPackageFail(t *testing.T) { Steps: []resource.TestStep{ { Config: util.ExecuteTemplate(fqrn, securityPolicyMaliciousPkgFixVersionDep, testData), - ExpectError: regexp.MustCompile("fix_version_dependant must be set to false if malicious_package is true"), + ExpectError: regexp.MustCompile("fix_version_dependant must be set to 'false' if malicious_package is 'true'"), }, }, }) @@ -463,7 +464,7 @@ func TestAccSecurityPolicy_createMaliciousPackageCvssMinSeverityFail(t *testing. Steps: []resource.TestStep{ { Config: util.ExecuteTemplate(fqrn, securityPolicyCVSSMinSeverityMaliciousPkg, testData), - ExpectError: regexp.MustCompile("malicious_package can't be set together with min_severity and/or cvss_range"), + ExpectError: regexp.MustCompile("(?s).*Invalid Attribute Combination.*cvss_range.*cannot be specified when.*malicious_package.*is specified.*"), }, }, }) @@ -486,7 +487,7 @@ func TestAccSecurityPolicy_createCvssMinSeverityFail(t *testing.T) { Steps: []resource.TestStep{ { Config: util.ExecuteTemplate(fqrn, securityPolicyCVSSMinSeverityMaliciousPkg, testData), - ExpectError: regexp.MustCompile("min_severity can't be set together with cvss_range"), + ExpectError: regexp.MustCompile("(?s).*Invalid Attribute Combination.*cvss_range.*cannot be specified when.*min_severity.*is specified.*"), }, }, }) @@ -542,9 +543,10 @@ func TestAccSecurityPolicy_createCVSSFloat(t *testing.T) { Check: verifySecurityPolicy(fqrn, testData, criteriaTypeCvss), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, + ResourceName: fqrn, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"author", "created", "modified"}, }, }, }) @@ -594,7 +596,7 @@ func TestAccSecurityPolicy_noActions(t *testing.T) { Steps: []resource.TestStep{ { Config: util.ExecuteTemplate(fqrn, securityPolicyNoActions, testData), - ExpectError: regexp.MustCompile("Insufficient actions blocks"), + ExpectError: regexp.MustCompile(".*must have a configuration value as the provider has marked it as required.*"), }, }, }) @@ -652,7 +654,7 @@ func TestAccSecurityPolicy_vulnerabilityIdsIncorrectCVEFails(t *testing.T) { Steps: []resource.TestStep{ { Config: util.ExecuteTemplate(fqrn, securityPolicyVulnIds, testData), - ExpectError: regexp.MustCompile("invalid value for vulnerability_ids"), + ExpectError: regexp.MustCompile(".*invalid Vulnerability, must be a valid CVE or Xray ID.*"), }, }, }) @@ -669,7 +671,6 @@ func TestAccSecurityPolicy_conflictingAttributesFail(t *testing.T) { "malicious_package = true", "min_severity = \"High\"", "exposures {\nmin_severity = \"High\" \nsecrets = true \n applications = true \n services = true \n iac = true\n}", - "package_name = \"nuget://RazorEngine\"", } for _, testAttribute := range testAttributes { @@ -693,7 +694,7 @@ func TestAccSecurityPolicy_conflictingAttributesFail(t *testing.T) { Steps: []resource.TestStep{ { Config: util.ExecuteTemplate(fqrn, securityPolicyVulnIdsConflict, testData), - ExpectError: regexp.MustCompile("can't be set together"), + ExpectError: regexp.MustCompile("(?s).*Invalid Attribute Combination.*cvss_range.*cannot be specified when.*vulnerability_ids.*is specified.*"), }, }, }) @@ -729,7 +730,7 @@ func TestAccSecurityPolicy_vulnerabilityIdsLimitFail(t *testing.T) { Steps: []resource.TestStep{ { Config: util.ExecuteTemplate(fqrn, securityPolicyVulnIdsLimit, testData), - ExpectError: regexp.MustCompile("Too many list items"), + ExpectError: regexp.MustCompile(".*set must contain at least 1 elements and at most 100 elements.*"), }, }, }) @@ -758,9 +759,10 @@ func TestAccSecurityPolicy_exposures(t *testing.T) { Check: verifySecurityPolicy(fqrn, testData, criteriaTypeExposures), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, + ResourceName: fqrn, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"author", "created", "modified"}, }, }, }) @@ -791,9 +793,10 @@ func TestAccSecurityPolicy_Packages(t *testing.T) { Check: verifySecurityPolicy(fqrn, testData, criteriaTypePackageName), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, + ResourceName: fqrn, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"author", "created", "modified"}, }, }, }) @@ -822,7 +825,7 @@ func TestAccSecurityPolicy_PackagesIncorrectVersionRangeFails(t *testing.T) { Steps: []resource.TestStep{ { Config: util.ExecuteTemplate(fqrn, securityPolicyPackages, testData), - ExpectError: regexp.MustCompile("invalid value for package_versions"), + ExpectError: regexp.MustCompile(`.*invalid Range, must be one of the follows: Any Version: \(,\) or Specific\n.*Version: \[1\.2\], \[3\] or Range: \(1,\), \[,1\.2\.3\], \(4\.5\.0,6\.5\.2\].*`), }, }, }) @@ -850,7 +853,7 @@ func TestAccSecurityPolicy_createPackagesFail(t *testing.T) { Steps: []resource.TestStep{ { Config: util.ExecuteTemplate(fqrn, securityPolicyPackagesFixVersionDep, testData), - ExpectError: regexp.MustCompile("fix_version_dependant must be set to false if package type policy is used"), + ExpectError: regexp.MustCompile("fix_version_dependant must be set to 'false' if any package attribute is set"), }, }, }) From 4ae434f14824e151f07246566437938bc3972f1f Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Tue, 10 Sep 2024 09:53:40 -0700 Subject: [PATCH 03/11] Migrate license policy to Plugin Framework --- pkg/xray/provider/framework.go | 5 +- pkg/xray/provider/sdkv2.go | 1 - pkg/xray/resource/policies.go | 856 +++++++++++-- .../resource/resource_xray_license_policy.go | 344 ++++- .../resource_xray_license_policy_test.go | 102 +- .../resource/resource_xray_security_policy.go | 1128 +++-------------- .../resource_xray_security_policy_test.go | 98 +- 7 files changed, 1439 insertions(+), 1095 deletions(-) diff --git a/pkg/xray/provider/framework.go b/pkg/xray/provider/framework.go index 11166c52..383ef4e2 100644 --- a/pkg/xray/provider/framework.go +++ b/pkg/xray/provider/framework.go @@ -175,13 +175,14 @@ func (p *XrayProvider) Configure(ctx context.Context, req provider.ConfigureRequ // Resources satisfies the provider.Provider interface for ArtifactoryProvider. func (p *XrayProvider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ - xray_resource.NewBinaryManagerReposResource, xray_resource.NewBinaryManagerBuildsResource, + xray_resource.NewBinaryManagerReposResource, xray_resource.NewBinaryManagerReleaseBundlesV2Resource, xray_resource.NewCustomIssueResource, xray_resource.NewIgnoreRuleResource, + xray_resource.NewLicensePolicyResource, xray_resource.NewRepositoryConfigResource, - xray_resource.NewSecurityPolicyV2Resource, + xray_resource.NewSecurityPolicyResource, xray_resource.NewSettingsResource, xray_resource.NewWatchResource, xray_resource.NewWebhookResource, diff --git a/pkg/xray/provider/sdkv2.go b/pkg/xray/provider/sdkv2.go index eb6675e6..f9771a34 100644 --- a/pkg/xray/provider/sdkv2.go +++ b/pkg/xray/provider/sdkv2.go @@ -54,7 +54,6 @@ func SdkV2() *schema.Provider { ResourcesMap: sdk.AddTelemetry( productId, map[string]*schema.Resource{ - "xray_license_policy": xray.ResourceXrayLicensePolicyV2(), "xray_operational_risk_policy": xray.ResourceXrayOperationalRiskPolicy(), "xray_vulnerabilities_report": xray.ResourceXrayVulnerabilitiesReport(), "xray_licenses_report": xray.ResourceXrayLicensesReport(), diff --git a/pkg/xray/resource/policies.go b/pkg/xray/resource/policies.go index aa7fe038..ea1d32a6 100644 --- a/pkg/xray/resource/policies.go +++ b/pkg/xray/resource/policies.go @@ -3,12 +3,26 @@ package xray import ( "context" "net/http" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + sdkv2_diag "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + sdkv2_schema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/jfrog/terraform-provider-shared/util" + utilfw "github.com/jfrog/terraform-provider-shared/util/fw" "github.com/jfrog/terraform-provider-shared/util/sdk" - "github.com/jfrog/terraform-provider-shared/validator" + shared_validator "github.com/jfrog/terraform-provider-shared/validator" + "github.com/samber/lo" ) const ( @@ -39,39 +53,391 @@ var validPackageTypesSupportedXraySecPolicies = []string{ "terraformbe", } -var commonActionsSchema = map[string]*schema.Schema{ +type PolicyResource struct { + ProviderData util.ProviderMetadata + TypeName string +} + +type PolicyResourceModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + ProjectKey types.String `tfsdk:"project_key"` + Type types.String `tfsdk:"type"` + Rules types.Set `tfsdk:"rule"` + Author types.String `tfsdk:"author"` + Created types.String `tfsdk:"created"` + Modified types.String `tfsdk:"modified"` +} + +var toActionsAPIModel = func(ctx context.Context, actionsElems []attr.Value) (PolicyRuleActionsAPIModel, diag.Diagnostics) { + diags := diag.Diagnostics{} + + actions := PolicyRuleActionsAPIModel{} + if len(actionsElems) > 0 { + attrs := actionsElems[0].(types.Object).Attributes() + + var webhooks []string + d := attrs["webhooks"].(types.Set).ElementsAs(ctx, &webhooks, false) + if d.HasError() { + diags.Append(d...) + } + + var mails []string + d = attrs["mails"].(types.Set).ElementsAs(ctx, &mails, false) + if d.HasError() { + diags.Append(d...) + } + + blockDownload := BlockDownloadSettingsAPIModel{} + blockDownloadElems := attrs["block_download"].(types.Set).Elements() + if len(blockDownloadElems) > 0 { + attrs := blockDownloadElems[0].(types.Object).Attributes() + + blockDownload.Unscanned = attrs["unscanned"].(types.Bool).ValueBool() + blockDownload.Active = attrs["active"].(types.Bool).ValueBool() + } + + actions.Webhooks = webhooks + actions.Mails = mails + actions.FailBuild = attrs["fail_build"].(types.Bool).ValueBool() + actions.BlockDownload = blockDownload + actions.BlockReleaseBundleDistribution = attrs["block_release_bundle_distribution"].(types.Bool).ValueBool() + actions.BlockReleaseBundlePromotion = attrs["block_release_bundle_promotion"].(types.Bool).ValueBool() + actions.NotifyWatchRecipients = attrs["notify_watch_recipients"].(types.Bool).ValueBool() + actions.NotifyDeployer = attrs["notify_deployer"].(types.Bool).ValueBool() + actions.CreateJiraTicketEnabled = attrs["create_ticket_enabled"].(types.Bool).ValueBool() + actions.FailureGracePeriodDays = attrs["build_failure_grace_period_in_days"].(types.Int64).ValueInt64() + } + + return actions, diags +} + +func (m PolicyResourceModel) toAPIModel( + ctx context.Context, + apiModel *PolicyAPIModel, + toCriteriaAPIModel func(ctx context.Context, criteriaElems []attr.Value) (*PolicyRuleCriteriaAPIModel, diag.Diagnostics), + toActionsAPIModel func(ctx context.Context, actionsElems []attr.Value) (PolicyRuleActionsAPIModel, diag.Diagnostics), +) diag.Diagnostics { + diags := diag.Diagnostics{} + + rules := lo.Map( + m.Rules.Elements(), + func(elem attr.Value, _ int) PolicyRuleAPIModel { + attrs := elem.(types.Object).Attributes() + + criteria, ds := toCriteriaAPIModel(ctx, attrs["criteria"].(types.Set).Elements()) + if ds.HasError() { + diags.Append(ds...) + } + + actions, ds := toActionsAPIModel(ctx, attrs["actions"].(types.Set).Elements()) + if ds.HasError() { + diags.Append(ds...) + } + + return PolicyRuleAPIModel{ + Name: attrs["name"].(types.String).ValueString(), + Priority: attrs["priority"].(types.Int64).ValueInt64(), + Criteria: criteria, + Actions: actions, + } + }, + ) + + *apiModel = PolicyAPIModel{ + Name: m.Name.ValueString(), + Description: m.Description.ValueString(), + Type: m.Type.ValueString(), + Rules: &rules, + } + + return diags +} + +var actionsAttrTypes = map[string]attr.Type{ + "webhooks": types.SetType{ElemType: types.StringType}, + "mails": types.SetType{ElemType: types.StringType}, + "block_download": types.SetType{ElemType: blockDownloadElementType}, + "block_release_bundle_distribution": types.BoolType, + "block_release_bundle_promotion": types.BoolType, + "fail_build": types.BoolType, + "notify_deployer": types.BoolType, + "notify_watch_recipients": types.BoolType, + "create_ticket_enabled": types.BoolType, + "build_failure_grace_period_in_days": types.Int64Type, +} + +var actionsSetElementType = types.ObjectType{ + AttrTypes: actionsAttrTypes, +} + +func (m *PolicyResourceModel) fromActionsAPIModel(ctx context.Context, actionsAPIModel PolicyRuleActionsAPIModel) (types.Set, diag.Diagnostics) { + diags := diag.Diagnostics{} + + webhooks := types.SetNull(types.StringType) + if len(actionsAPIModel.Webhooks) > 0 { + ws, d := types.SetValueFrom(ctx, types.StringType, actionsAPIModel.Webhooks) + if d.HasError() { + diags.Append(d...) + } + + webhooks = ws + } + + mails := types.SetNull(types.StringType) + if len(actionsAPIModel.Mails) > 0 { + ms, d := types.SetValueFrom(ctx, types.StringType, actionsAPIModel.Mails) + if d.HasError() { + diags.Append(d...) + } + + mails = ms + } + + blockDownload, d := types.ObjectValue( + blockDownloadAttrTypes, + map[string]attr.Value{ + "unscanned": types.BoolValue(actionsAPIModel.BlockDownload.Unscanned), + "active": types.BoolValue(actionsAPIModel.BlockDownload.Active), + }, + ) + if d.HasError() { + diags.Append(d...) + } + blockDownloadSet, d := types.SetValue( + blockDownloadElementType, + []attr.Value{blockDownload}, + ) + if d.HasError() { + diags.Append(d...) + } + + actions, d := types.ObjectValue( + actionsAttrTypes, + map[string]attr.Value{ + "webhooks": webhooks, + "mails": mails, + "block_download": blockDownloadSet, + "block_release_bundle_distribution": types.BoolValue(actionsAPIModel.BlockReleaseBundleDistribution), + "block_release_bundle_promotion": types.BoolValue(actionsAPIModel.BlockReleaseBundlePromotion), + "fail_build": types.BoolValue(actionsAPIModel.FailBuild), + "notify_deployer": types.BoolValue(actionsAPIModel.NotifyDeployer), + "notify_watch_recipients": types.BoolValue(actionsAPIModel.NotifyWatchRecipients), + "create_ticket_enabled": types.BoolValue(actionsAPIModel.CreateJiraTicketEnabled), + "build_failure_grace_period_in_days": types.Int64Value(actionsAPIModel.FailureGracePeriodDays), + }, + ) + if d.HasError() { + diags.Append(d...) + } + + actionsSet, d := types.SetValue( + actionsSetElementType, + []attr.Value{actions}, + ) + if d.HasError() { + diags.Append(d...) + } + + return actionsSet, diags +} + +func (m *PolicyResourceModel) fromAPIModel( + ctx context.Context, + apiModel PolicyAPIModel, + fromCriteriaAPIModel func(ctx context.Context, criteraAPIModel *PolicyRuleCriteriaAPIModel) (types.Set, diag.Diagnostics), + fromActionsAPIModel func(ctx context.Context, actionsAPIModel PolicyRuleActionsAPIModel) (types.Set, diag.Diagnostics), +) diag.Diagnostics { + diags := diag.Diagnostics{} + + var ruleAttrTypes map[string]attr.Type + var ruleSetElementType types.ObjectType + + switch apiModel.Type { + case "license": + ruleAttrTypes = licenseRuleAttrTypes + ruleSetElementType = licenseRuleSetElementType + case "security": + ruleAttrTypes = securityRuleAttrTypes + ruleSetElementType = securityRuleSetElementType + // case "operational_risk": + // ruleAttrTypes = opRiskRuleAttrTypes + // ruleSetElementType = opRiskRuleSetElementType + } + + rules := lo.Map( + *apiModel.Rules, + func(rule PolicyRuleAPIModel, _ int) attr.Value { + criteriaSet, d := fromCriteriaAPIModel(ctx, rule.Criteria) + if d.HasError() { + diags.Append(d...) + } + + actionsSet, d := fromActionsAPIModel(ctx, rule.Actions) + if d.HasError() { + diags.Append(d...) + } + + r, d := types.ObjectValue( + ruleAttrTypes, + map[string]attr.Value{ + "name": types.StringValue(rule.Name), + "priority": types.Int64Value(rule.Priority), + "criteria": criteriaSet, + "actions": actionsSet, + }, + ) + if d.HasError() { + diags.Append(d...) + } + + return r + }, + ) + + rulesSet, d := types.SetValue( + ruleSetElementType, + rules, + ) + if d.HasError() { + diags.Append(d...) + } + + m.ID = types.StringValue(apiModel.Name) + m.Name = types.StringValue(apiModel.Name) + m.Description = types.StringValue(apiModel.Description) + m.Type = types.StringValue(apiModel.Type) + m.Author = types.StringValue(apiModel.Author) + m.Created = types.StringValue(apiModel.Created) + m.Modified = types.StringValue(apiModel.Modified) + + m.Rules = rulesSet + + return diags +} + +var commonActionsBlocks = map[string]schema.Block{ + "block_download": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "unscanned": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + Description: "Whether or not to block download of artifacts that meet the artifact `filters` for the associated `xray_watch` resource but have not been scanned yet. Can not be set to `true` if attribute `active` is `false`. Default value is `false`.", + }, + "active": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + Description: "Whether or not to block download of artifacts that meet the artifact and severity `filters` for the associated `xray_watch` resource. Default value is `false`.", + }, + }, + }, + Validators: []validator.Set{ + setvalidator.IsRequired(), + setvalidator.SizeAtMost(1), + }, + Description: "Block download of artifacts that meet the Artifact Filter and Severity Filter specifications for this watch", + }, +} + +var commonActionsAttrs = map[string]schema.Attribute{ + "webhooks": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + }, + Description: "A list of Xray-configured webhook URLs to be invoked if a violation is triggered.", + }, + "mails": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + }, + Description: "A list of email addressed that will get emailed when a violation is triggered.", + }, + "block_release_bundle_distribution": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + Description: "Blocks Release Bundle distribution to Edge nodes if a violation is found. Default value is `false`.", + }, + "block_release_bundle_promotion": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + Description: "Blocks Release Bundle promotion if a violation is found. Default value is `false`.", + }, + "fail_build": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + Description: "Whether or not the related CI build should be marked as failed if a violation is triggered. This option is only available when the policy is applied to an `xray_watch` resource with a `type` of `builds`. Default value is `false`.", + }, + "notify_deployer": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + Description: "Sends an email message to component deployer with details about the generated Violations. Default value is `false`.", + }, + "notify_watch_recipients": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + Description: "Sends an email message to all configured recipients inside a specific watch with details about the generated Violations. Default value is `false`.", + }, + "create_ticket_enabled": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + Description: "Create Jira Ticket for this Policy Violation. Requires configured Jira integration. Default value is `false`.", + }, + "build_failure_grace_period_in_days": schema.Int64Attribute{ + Optional: true, + Validators: []validator.Int64{ + int64validator.AtLeast(0), + }, + Description: "Allow grace period for certain number of days. All violations will be ignored during this time. To be used only if `fail_build` is enabled.", + }, +} + +var commonActionsSchema = map[string]*sdkv2_schema.Schema{ "webhooks": { - Type: schema.TypeSet, + Type: sdkv2_schema.TypeSet, Optional: true, Description: "A list of Xray-configured webhook URLs to be invoked if a violation is triggered.", - Elem: &schema.Schema{ - Type: schema.TypeString, + Elem: &sdkv2_schema.Schema{ + Type: sdkv2_schema.TypeString, }, }, "mails": { - Type: schema.TypeSet, + Type: sdkv2_schema.TypeSet, Optional: true, Description: "A list of email addressed that will get emailed when a violation is triggered.", - Elem: &schema.Schema{ - Type: schema.TypeString, - ValidateDiagFunc: validator.IsEmail, + Elem: &sdkv2_schema.Schema{ + Type: sdkv2_schema.TypeString, + ValidateDiagFunc: shared_validator.IsEmail, }, }, "block_download": { - Type: schema.TypeSet, + Type: sdkv2_schema.TypeSet, Required: true, MaxItems: 1, Description: "Block download of artifacts that meet the Artifact Filter and Severity Filter specifications for this watch", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ + Elem: &sdkv2_schema.Resource{ + Schema: map[string]*sdkv2_schema.Schema{ "unscanned": { - Type: schema.TypeBool, + Type: sdkv2_schema.TypeBool, Optional: true, Default: false, Description: "Whether or not to block download of artifacts that meet the artifact `filters` for the associated `xray_watch` resource but have not been scanned yet. Can not be set to `true` if attribute `active` is `false`. Default value is `false`.", }, "active": { - Type: schema.TypeBool, + Type: sdkv2_schema.TypeBool, Optional: true, Default: false, Description: "Whether or not to block download of artifacts that meet the artifact and severity `filters` for the associated `xray_watch` resource. Default value is `false`.", @@ -80,120 +446,120 @@ var commonActionsSchema = map[string]*schema.Schema{ }, }, "block_release_bundle_distribution": { - Type: schema.TypeBool, + Type: sdkv2_schema.TypeBool, Optional: true, Default: false, Description: "Blocks Release Bundle distribution to Edge nodes if a violation is found. Default value is `false`.", }, "block_release_bundle_promotion": { - Type: schema.TypeBool, + Type: sdkv2_schema.TypeBool, Optional: true, Default: false, Description: "Blocks Release Bundle promotion if a violation is found. Default value is `false`.", }, "fail_build": { - Type: schema.TypeBool, + Type: sdkv2_schema.TypeBool, Optional: true, Default: false, Description: "Whether or not the related CI build should be marked as failed if a violation is triggered. This option is only available when the policy is applied to an `xray_watch` resource with a `type` of `builds`. Default value is `false`.", }, "notify_deployer": { - Type: schema.TypeBool, + Type: sdkv2_schema.TypeBool, Optional: true, Default: false, Description: "Sends an email message to component deployer with details about the generated Violations. Default value is `false`.", }, "notify_watch_recipients": { - Type: schema.TypeBool, + Type: sdkv2_schema.TypeBool, Optional: true, Default: false, Description: "Sends an email message to all configured recipients inside a specific watch with details about the generated Violations. Default value is `false`.", }, "create_ticket_enabled": { - Type: schema.TypeBool, + Type: sdkv2_schema.TypeBool, Optional: true, Default: false, Description: "Create Jira Ticket for this Policy Violation. Requires configured Jira integration. Default value is `false`.", }, "build_failure_grace_period_in_days": { - Type: schema.TypeInt, + Type: sdkv2_schema.TypeInt, Optional: true, Description: "Allow grace period for certain number of days. All violations will be ignored during this time. To be used only if `fail_build` is enabled.", - ValidateDiagFunc: validator.IntAtLeast(0), + ValidateDiagFunc: shared_validator.IntAtLeast(0), }, } -var getPolicySchema = func(criteriaSchema map[string]*schema.Schema, actionsSchema map[string]*schema.Schema) map[string]*schema.Schema { +var getPolicySchema = func(criteriaSchema map[string]*sdkv2_schema.Schema, actionsSchema map[string]*sdkv2_schema.Schema) map[string]*sdkv2_schema.Schema { return sdk.MergeMaps( getProjectKeySchema(false, ""), - map[string]*schema.Schema{ + map[string]*sdkv2_schema.Schema{ "name": { - Type: schema.TypeString, + Type: sdkv2_schema.TypeString, Required: true, ForceNew: true, Description: "Name of the policy (must be unique)", - ValidateDiagFunc: validator.StringIsNotEmpty, + ValidateDiagFunc: shared_validator.StringIsNotEmpty, }, "description": { - Type: schema.TypeString, + Type: sdkv2_schema.TypeString, Optional: true, Description: "More verbose description of the policy", }, "type": { - Type: schema.TypeString, + Type: sdkv2_schema.TypeString, Required: true, Description: "Type of the policy", - ValidateDiagFunc: validator.StringInSlice(false, "security", "license", "operational_risk"), + ValidateDiagFunc: shared_validator.StringInSlice(false, "security", "license", "operational_risk"), }, "author": { - Type: schema.TypeString, + Type: sdkv2_schema.TypeString, Computed: true, Description: "User, who created the policy", }, "created": { - Type: schema.TypeString, + Type: sdkv2_schema.TypeString, Computed: true, Description: "Creation timestamp", }, "modified": { - Type: schema.TypeString, + Type: sdkv2_schema.TypeString, Computed: true, Description: "Modification timestamp", }, "rule": { - Type: schema.TypeSet, + Type: sdkv2_schema.TypeSet, Required: true, Description: "A list of user-defined rules allowing you to trigger violations for specific vulnerability or license breaches by setting a license or security criteria, with a corresponding set of automatic actions according to your needs. Rules are processed according to the ascending order in which they are placed in the Rules list on the Policy. If a rule is met, the subsequent rules in the list will not be applied.", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ + Elem: &sdkv2_schema.Resource{ + Schema: map[string]*sdkv2_schema.Schema{ "name": { - Type: schema.TypeString, + Type: sdkv2_schema.TypeString, Required: true, Description: "Name of the rule", - ValidateDiagFunc: validator.StringIsNotEmpty, + ValidateDiagFunc: shared_validator.StringIsNotEmpty, }, "priority": { - Type: schema.TypeInt, + Type: sdkv2_schema.TypeInt, Required: true, - ValidateDiagFunc: validator.IntAtLeast(1), + ValidateDiagFunc: shared_validator.IntAtLeast(1), Description: "Integer describing the rule priority. Must be at least 1", }, "criteria": { - Type: schema.TypeSet, + Type: sdkv2_schema.TypeSet, Required: true, MinItems: 1, MaxItems: 1, Description: "The set of security conditions to examine when an scanned artifact is scanned.", - Elem: &schema.Resource{ + Elem: &sdkv2_schema.Resource{ Schema: criteriaSchema, }, }, "actions": { - Type: schema.TypeSet, + Type: sdkv2_schema.TypeSet, MaxItems: 1, Required: true, Description: "Specifies the actions to take once a security policy violation has been triggered.", - Elem: &schema.Resource{ + Elem: &sdkv2_schema.Resource{ Schema: actionsSchema, }, }, @@ -204,6 +570,60 @@ var getPolicySchema = func(criteriaSchema map[string]*schema.Schema, actionsSche ) } +var policyBlocks = func(criteriaAttrs map[string]schema.Attribute, criteriaBlocks map[string]schema.Block, actionsAttrs map[string]schema.Attribute, actionsBlocks map[string]schema.Block) map[string]schema.Block { + return map[string]schema.Block{ + "rule": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + Description: "Name of the rule", + }, + "priority": schema.Int64Attribute{ + Required: true, + Validators: []validator.Int64{ + int64validator.AtLeast(1), + }, + Description: "Integer describing the rule priority. Must be at least 1", + }, + }, + Blocks: map[string]schema.Block{ + "criteria": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: criteriaAttrs, + Blocks: criteriaBlocks, + }, + Validators: []validator.Set{ + setvalidator.IsRequired(), + setvalidator.SizeBetween(1, 1), + }, + Description: "The set of security conditions to examine when an scanned artifact is scanned.", + }, + "actions": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: actionsAttrs, + Blocks: actionsBlocks, + }, + Validators: []validator.Set{ + setvalidator.IsRequired(), + setvalidator.SizeBetween(1, 1), + }, + Description: "Specifies the actions to take once a security policy violation has been triggered.", + }, + }, + }, + Validators: []validator.Set{ + setvalidator.IsRequired(), + setvalidator.SizeAtLeast(1), + }, + Description: "A list of user-defined rules allowing you to trigger violations for specific vulnerability or license breaches by setting a license or security criteria, with a corresponding set of automatic actions according to your needs. Rules are processed according to the ascending order in which they are placed in the Rules list on the Policy. If a rule is met, the subsequent rules in the list will not be applied.", + }, + } +} + type PolicyCVSSRangeAPIModel struct { To *float64 `json:"to,omitempty"` From *float64 `json:"from,omitempty"` @@ -303,7 +723,263 @@ type PolicyError struct { Error string `json:"error"` } -func unpackPolicy(d *schema.ResourceData) (*PolicyAPIModel, error) { +func (r *PolicyResource) Create( + ctx context.Context, + toAPIModel func(context.Context, PolicyResourceModel, *PolicyAPIModel) diag.Diagnostics, + fromAPIModel func(context.Context, PolicyAPIModel, *PolicyResourceModel) diag.Diagnostics, + req resource.CreateRequest, + resp *resource.CreateResponse, +) { + go util.SendUsageResourceCreate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var plan PolicyResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + request, err := getRestyRequest(r.ProviderData.Client, plan.ProjectKey.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "failed to get Resty client", + err.Error(), + ) + return + } + + var policy PolicyAPIModel + resp.Diagnostics.Append(toAPIModel(ctx, plan, &policy)...) + if resp.Diagnostics.HasError() { + return + } + + var policyError PolicyError + response, err := request. + SetBody(policy). + SetError(&policyError). + Post(PoliciesEndpoint) + + if err != nil { + utilfw.UnableToCreateResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToCreateResourceError(resp, policyError.Error) + return + } + + response, err = request. + SetResult(&policy). + SetPathParam("name", plan.Name.ValueString()). + SetError(&policyError). + Get(PolicyEndpoint) + + if err != nil { + utilfw.UnableToCreateResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToCreateResourceError(resp, policyError.Error) + return + } + + resp.Diagnostics.Append(fromAPIModel(ctx, policy, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PolicyResource) Read( + ctx context.Context, + fromAPIModel func(context.Context, PolicyAPIModel, *PolicyResourceModel) diag.Diagnostics, + req resource.ReadRequest, + resp *resource.ReadResponse, +) { + go util.SendUsageResourceRead(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var state PolicyResourceModel + + // Read Terraform state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + request, err := getRestyRequest(r.ProviderData.Client, state.ProjectKey.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "failed to get Resty client", + err.Error(), + ) + return + } + + var policy PolicyAPIModel + var policyError PolicyError + + response, err := request. + SetResult(&policy). + SetPathParam("name", state.Name.ValueString()). + SetError(&policyError). + Get(PolicyEndpoint) + + if err != nil { + utilfw.UnableToRefreshResourceError(resp, err.Error()) + return + } + + if response.StatusCode() == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + + if response.IsError() { + utilfw.UnableToRefreshResourceError(resp, policyError.Error) + return + } + + resp.Diagnostics.Append(fromAPIModel(ctx, policy, &state)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *PolicyResource) Update( + ctx context.Context, + toAPIModel func(context.Context, PolicyResourceModel, *PolicyAPIModel) diag.Diagnostics, + fromAPIModel func(context.Context, PolicyAPIModel, *PolicyResourceModel) diag.Diagnostics, + req resource.UpdateRequest, + resp *resource.UpdateResponse, +) { + go util.SendUsageResourceUpdate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var plan PolicyResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + request, err := getRestyRequest(r.ProviderData.Client, plan.ProjectKey.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "failed to get Resty client", + err.Error(), + ) + return + } + + var policy PolicyAPIModel + resp.Diagnostics.Append(toAPIModel(ctx, plan, &policy)...) + if resp.Diagnostics.HasError() { + return + } + + var policyError PolicyError + + response, err := request. + SetPathParam("name", plan.Name.ValueString()). + SetBody(policy). + SetError(&policyError). + Put(PolicyEndpoint) + + if err != nil { + utilfw.UnableToUpdateResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToUpdateResourceError(resp, policyError.Error) + return + } + + response, err = request. + SetResult(&policy). + SetPathParam("name", plan.Name.ValueString()). + SetError(&policyError). + Get(PolicyEndpoint) + + if err != nil { + utilfw.UnableToUpdateResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToUpdateResourceError(resp, policyError.Error) + return + } + + resp.Diagnostics.Append(fromAPIModel(ctx, policy, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PolicyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + go util.SendUsageResourceDelete(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var state PolicyResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + request, err := getRestyRequest(r.ProviderData.Client, state.ProjectKey.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "failed to get Resty client", + err.Error(), + ) + return + } + + var policyError PolicyError + response, err := request. + SetPathParam("name", state.Name.ValueString()). + SetError(&policyError). + Delete(PolicyEndpoint) + + if err != nil { + utilfw.UnableToDeleteResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToDeleteResourceError(resp, policyError.Error) + return + } + + // If the logic reaches here, it implicitly succeeded and will remove + // the resource from state if there are no other errors. +} + +// ImportState imports the resource into the Terraform state. +func (r *PolicyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + parts := strings.SplitN(req.ID, ":", 2) + + if len(parts) > 0 && parts[0] != "" { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), parts[0])...) + } + + if len(parts) == 2 && parts[1] != "" { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_key"), parts[1])...) + } +} + +func unpackPolicy(d *sdkv2_schema.ResourceData) (*PolicyAPIModel, error) { policy := new(PolicyAPIModel) policy.Name = d.Get("name").(string) @@ -319,13 +995,13 @@ func unpackPolicy(d *schema.ResourceData) (*PolicyAPIModel, error) { if v, ok := d.GetOk("author"); ok { policy.Author = v.(string) } - policyRules, err := unpackRules(d.Get("rule").(*schema.Set), policy.Type) + policyRules, err := unpackRules(d.Get("rule").(*sdkv2_schema.Set), policy.Type) policy.Rules = &policyRules return policy, err } -func unpackRules(configured *schema.Set, policyType string) (policyRules []PolicyRuleAPIModel, err error) { +func unpackRules(configured *sdkv2_schema.Set, policyType string) (policyRules []PolicyRuleAPIModel, err error) { var rules []PolicyRuleAPIModel for _, raw := range configured.List() { @@ -334,9 +1010,9 @@ func unpackRules(configured *schema.Set, policyType string) (policyRules []Polic rule.Name = data["name"].(string) rule.Priority = data["priority"].(int64) - rule.Criteria, err = unpackCriteria(data["criteria"].(*schema.Set), policyType) + rule.Criteria, err = unpackCriteria(data["criteria"].(*sdkv2_schema.Set), policyType) if v, ok := data["actions"]; ok { - rule.Actions = unpackActions(v.(*schema.Set)) + rule.Actions = unpackActions(v.(*sdkv2_schema.Set)) } rules = append(rules, *rule) } @@ -357,7 +1033,7 @@ func unpackSecurityCriteria(tfCriteria map[string]interface{}) *PolicyRuleCriter criteria.MaliciousPackage = v.(bool) } if v, ok := tfCriteria["vulnerability_ids"]; ok { - criteria.VulnerabilityIds = sdk.CastToStringArr(v.(*schema.Set).List()) + criteria.VulnerabilityIds = sdk.CastToStringArr(v.(*sdkv2_schema.Set).List()) } if _, ok := tfCriteria["exposures"]; ok { criteria.Exposures = unpackExposures(tfCriteria["exposures"].([]interface{})) @@ -369,7 +1045,7 @@ func unpackSecurityCriteria(tfCriteria map[string]interface{}) *PolicyRuleCriter criteria.PackageType = v.(string) } if v, ok := tfCriteria["package_versions"]; ok { - criteria.PackageVersions = sdk.CastToStringArr(v.(*schema.Set).List()) + criteria.PackageVersions = sdk.CastToStringArr(v.(*sdkv2_schema.Set).List()) } // This is also picky about not allowing empty values to be set cvss := unpackCVSSRange(tfCriteria["cvss_range"].([]interface{})) @@ -388,10 +1064,10 @@ func unpackLicenseCriteria(tfCriteria map[string]interface{}) *PolicyRuleCriteri criteria.AllowUnknown = sdk.BoolPtr(v.(bool)) } if v, ok := tfCriteria["banned_licenses"]; ok { - criteria.BannedLicenses = unpackLicenses(v.(*schema.Set)) + criteria.BannedLicenses = unpackLicenses(v.(*sdkv2_schema.Set)) } if v, ok := tfCriteria["allowed_licenses"]; ok { - criteria.AllowedLicenses = unpackLicenses(v.(*schema.Set)) + criteria.AllowedLicenses = unpackLicenses(v.(*sdkv2_schema.Set)) } if v, ok := tfCriteria["multi_license_permissive"]; ok { criteria.MultiLicensePermissive = sdk.BoolPtr(v.(bool)) @@ -445,7 +1121,7 @@ func unpackOperationalRiskCriteria(tfCriteria map[string]interface{}) *PolicyRul return criteria } -func unpackCriteria(d *schema.Set, policyType string) (*PolicyRuleCriteriaAPIModel, error) { +func unpackCriteria(d *sdkv2_schema.Set, policyType string) (*PolicyRuleCriteriaAPIModel, error) { tfCriteria := d.List() if len(tfCriteria) == 0 { return nil, nil @@ -500,7 +1176,7 @@ func unpackExposures(l []interface{}) *PolicyExposuresAPIModel { return exposures } -func unpackLicenses(d *schema.Set) []string { +func unpackLicenses(d *sdkv2_schema.Set) []string { var licenses []string for _, license := range d.List() { licenses = append(licenses, license.(string)) @@ -508,14 +1184,14 @@ func unpackLicenses(d *schema.Set) []string { return licenses } -func unpackActions(l *schema.Set) PolicyRuleActionsAPIModel { +func unpackActions(l *sdkv2_schema.Set) PolicyRuleActionsAPIModel { actions := PolicyRuleActionsAPIModel{} policyActions := l.List() if len(policyActions) > 0 { m := policyActions[0].(map[string]interface{}) // We made this a list of one to make schema validation easier if v, ok := m["webhooks"]; ok { - m := v.(*schema.Set).List() + m := v.(*sdkv2_schema.Set).List() var webhooks []string for _, hook := range m { webhooks = append(webhooks, hook.(string)) @@ -523,7 +1199,7 @@ func unpackActions(l *schema.Set) PolicyRuleActionsAPIModel { actions.Webhooks = webhooks } if v, ok := m["mails"]; ok { - m := v.(*schema.Set).List() + m := v.(*sdkv2_schema.Set).List() var mails []string for _, mail := range m { mails = append(mails, mail.(string)) @@ -535,8 +1211,8 @@ func unpackActions(l *schema.Set) PolicyRuleActionsAPIModel { } if v, ok := m["block_download"]; ok { - if len(v.(*schema.Set).List()) > 0 { - vList := v.(*schema.Set).List() + if len(v.(*sdkv2_schema.Set).List()) > 0 { + vList := v.(*sdkv2_schema.Set).List() vMap := vList[0].(map[string]interface{}) actions.BlockDownload = BlockDownloadSettingsAPIModel{ @@ -736,46 +1412,46 @@ func packBlockDownload(bd BlockDownloadSettingsAPIModel) []interface{} { return []interface{}{m} } -func packPolicy(policy PolicyAPIModel, d *schema.ResourceData) diag.Diagnostics { +func packPolicy(policy PolicyAPIModel, d *sdkv2_schema.ResourceData) sdkv2_diag.Diagnostics { if err := d.Set("name", policy.Name); err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } if err := d.Set("type", policy.Type); err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } if len(policy.Description) > 0 { if err := d.Set("description", policy.Description); err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } } if err := d.Set("author", policy.Author); err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } if err := d.Set("created", policy.Created); err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } if err := d.Set("modified", policy.Modified); err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } if policy.Rules != nil { if err := d.Set("rule", packRules(*policy.Rules, policy.Type)); err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } } return nil } -func resourceXrayPolicyCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { +func resourceXrayPolicyCreate(ctx context.Context, d *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { policy, err := unpackPolicy(d) // Warning or errors can be collected in a slice type if err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } req, err := getRestyRequest(m.(util.ProviderMetadata).Client, policy.ProjectKey) if err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } var policyError PolicyError @@ -784,23 +1460,23 @@ func resourceXrayPolicyCreate(ctx context.Context, d *schema.ResourceData, m int SetError(&policyError). Post("xray/api/v2/policies") if err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } if resp.IsError() { - return diag.Errorf("%s", policyError.Error) + return sdkv2_diag.Errorf("%s", policyError.Error) } d.SetId(policy.Name) return resourceXrayPolicyRead(ctx, d, m) } -func resourceXrayPolicyRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { +func resourceXrayPolicyRead(ctx context.Context, d *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { var policy PolicyAPIModel projectKey := d.Get("project_key").(string) req, err := getRestyRequest(m.(util.ProviderMetadata).Client, projectKey) if err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } var policyError PolicyError @@ -810,28 +1486,28 @@ func resourceXrayPolicyRead(ctx context.Context, d *schema.ResourceData, m inter SetError(&policyError). Get("xray/api/v2/policies/{name}") if err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } if resp.StatusCode() == http.StatusNotFound { d.SetId("") - return diag.Errorf("policy (%s) not found, removing from state", d.Id()) + return sdkv2_diag.Errorf("policy (%s) not found, removing from state", d.Id()) } if resp.IsError() { - return diag.Errorf("%s", policyError.Error) + return sdkv2_diag.Errorf("%s", policyError.Error) } return packPolicy(policy, d) } -func resourceXrayPolicyUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { +func resourceXrayPolicyUpdate(ctx context.Context, d *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { policy, err := unpackPolicy(d) if err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } req, err := getRestyRequest(m.(util.ProviderMetadata).Client, policy.ProjectKey) if err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } var policyError PolicyError @@ -843,25 +1519,25 @@ func resourceXrayPolicyUpdate(ctx context.Context, d *schema.ResourceData, m int SetError(&policyError). Put("xray/api/v2/policies/{name}") if err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } if resp.IsError() { - return diag.Errorf("%s", policyError.Error) + return sdkv2_diag.Errorf("%s", policyError.Error) } d.SetId(policy.Name) return resourceXrayPolicyRead(ctx, d, m) } -func resourceXrayPolicyDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { +func resourceXrayPolicyDelete(ctx context.Context, d *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { policy, err := unpackPolicy(d) if err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } req, err := getRestyRequest(m.(util.ProviderMetadata).Client, policy.ProjectKey) if err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } var policyError PolicyError @@ -872,10 +1548,10 @@ func resourceXrayPolicyDelete(ctx context.Context, d *schema.ResourceData, m int SetError(&policyError). Delete("xray/api/v2/policies/{name}") if err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } if resp.IsError() { - return diag.Errorf("%s", policyError.Error) + return sdkv2_diag.Errorf("%s", policyError.Error) } d.SetId("") diff --git a/pkg/xray/resource/resource_xray_license_policy.go b/pkg/xray/resource/resource_xray_license_policy.go index 9059a1d5..208b8a17 100644 --- a/pkg/xray/resource/resource_xray_license_policy.go +++ b/pkg/xray/resource/resource_xray_license_policy.go @@ -1,69 +1,313 @@ package xray import ( - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/jfrog/terraform-provider-shared/util/sdk" - "github.com/jfrog/terraform-provider-shared/validator" + "context" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/jfrog/terraform-provider-shared/util" + "github.com/samber/lo" ) -func ResourceXrayLicensePolicyV2() *schema.Resource { - var criteriaSchema = map[string]*schema.Schema{ - "banned_licenses": { - Type: schema.TypeSet, - Optional: true, - Description: "A list of OSS license names that may not be attached to a component. Supports custom licenses added by the user, but there is no verification if the license exists on the Xray side. If the added license doesn't exist, the policy won't trigger the violation.", - Elem: &schema.Schema{ - Type: schema.TypeString, - }, +var _ resource.Resource = &LicensePolicyResource{} + +func NewLicensePolicyResource() resource.Resource { + return &LicensePolicyResource{ + PolicyResource: PolicyResource{ + TypeName: "xray_license_policy", }, - "allowed_licenses": { - Type: schema.TypeSet, - Optional: true, - Description: "A list of OSS license names that may be attached to a component. Supports custom licenses added by the user, but there is no verification if the license exists on the Xray side. If the added license doesn't exist, the policy won't trigger the violation.", - Elem: &schema.Schema{ - Type: schema.TypeString, + } +} + +type LicensePolicyResource struct { + PolicyResource +} + +func (r *LicensePolicyResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = r.TypeName +} + +func (r LicensePolicyResource) toCriteriaAPIModel(ctx context.Context, criteriaElems []attr.Value) (*PolicyRuleCriteriaAPIModel, diag.Diagnostics) { + diags := diag.Diagnostics{} + + var criteria *PolicyRuleCriteriaAPIModel + if len(criteriaElems) > 0 { + attrs := criteriaElems[0].(types.Object).Attributes() + + var allowedLicenses []string + d := attrs["allowed_licenses"].(types.Set).ElementsAs(ctx, &allowedLicenses, false) + if d.HasError() { + diags.Append(d...) + } + + var bannedLicenses []string + d = attrs["banned_licenses"].(types.Set).ElementsAs(ctx, &bannedLicenses, false) + if d.HasError() { + diags.Append(d...) + } + + criteria = &PolicyRuleCriteriaAPIModel{ + AllowedLicenses: allowedLicenses, + AllowUnknown: attrs["allow_unknown"].(types.Bool).ValueBoolPointer(), + BannedLicenses: bannedLicenses, + MultiLicensePermissive: attrs["multi_license_permissive"].(types.Bool).ValueBoolPointer(), + } + } + + return criteria, diags +} + +func (r LicensePolicyResource) toActionsAPIModel(ctx context.Context, actionsElems []attr.Value) (PolicyRuleActionsAPIModel, diag.Diagnostics) { + actions, ds := toActionsAPIModel(ctx, actionsElems) + + if len(actionsElems) > 0 { + attrs := actionsElems[0].(types.Object).Attributes() + actions.CustomSeverity = attrs["custom_severity"].(types.String).ValueString() + } + + return actions, ds +} + +func (r LicensePolicyResource) toAPIModel(ctx context.Context, plan PolicyResourceModel, policy *PolicyAPIModel) diag.Diagnostics { + return plan.toAPIModel(ctx, policy, r.toCriteriaAPIModel, r.toActionsAPIModel) +} + +var licenseCriteriaAttrTypes = lo.Assign( + map[string]attr.Type{ + "allow_unknown": types.BoolType, + "allowed_licenses": types.SetType{ElemType: types.StringType}, + "banned_licenses": types.SetType{ElemType: types.StringType}, + "multi_license_permissive": types.BoolType, + }, +) + +var licenseCriteriaSetElementType = types.ObjectType{ + AttrTypes: licenseCriteriaAttrTypes, +} + +func (r *LicensePolicyResource) fromCriteriaAPIModel(ctx context.Context, criteraAPIModel *PolicyRuleCriteriaAPIModel) (types.Set, diag.Diagnostics) { + diags := diag.Diagnostics{} + + criteriaSet := types.SetNull(licenseCriteriaSetElementType) + if criteraAPIModel != nil { + allowedLicenses, d := types.SetValueFrom(ctx, types.StringType, criteraAPIModel.AllowedLicenses) + if d.HasError() { + diags.Append(d...) + } + + bannedLicenses, d := types.SetValueFrom(ctx, types.StringType, criteraAPIModel.BannedLicenses) + if d.HasError() { + diags.Append(d...) + } + + criteria, d := types.ObjectValue( + licenseCriteriaAttrTypes, + map[string]attr.Value{ + "allow_unknown": types.BoolPointerValue(criteraAPIModel.AllowUnknown), + "allowed_licenses": allowedLicenses, + "banned_licenses": bannedLicenses, + "multi_license_permissive": types.BoolPointerValue(criteraAPIModel.MultiLicensePermissive), }, + ) + if d.HasError() { + diags.Append(d...) + } + cs, d := types.SetValue( + licenseCriteriaSetElementType, + []attr.Value{criteria}, + ) + if d.HasError() { + diags.Append(d...) + } + + criteriaSet = cs + } + + return criteriaSet, diags +} + +var licenseActionsAttrTypes = lo.Assign( + actionsAttrTypes, + map[string]attr.Type{ + "custom_severity": types.StringType, + }, +) + +var licenseActionsSetElementType = types.ObjectType{ + AttrTypes: licenseActionsAttrTypes, +} + +func (m *LicensePolicyResource) fromActionsAPIModel(ctx context.Context, actionsAPIModel PolicyRuleActionsAPIModel) (types.Set, diag.Diagnostics) { + diags := diag.Diagnostics{} + + webhooks := types.SetNull(types.StringType) + if len(actionsAPIModel.Webhooks) > 0 { + ws, d := types.SetValueFrom(ctx, types.StringType, actionsAPIModel.Webhooks) + if d.HasError() { + diags.Append(d...) + } + + webhooks = ws + } + + mails := types.SetNull(types.StringType) + if len(actionsAPIModel.Mails) > 0 { + ms, d := types.SetValueFrom(ctx, types.StringType, actionsAPIModel.Mails) + if d.HasError() { + diags.Append(d...) + } + + mails = ms + } + + blockDownload, d := types.ObjectValue( + blockDownloadAttrTypes, + map[string]attr.Value{ + "unscanned": types.BoolValue(actionsAPIModel.BlockDownload.Unscanned), + "active": types.BoolValue(actionsAPIModel.BlockDownload.Active), }, - "allow_unknown": { - Type: schema.TypeBool, - Optional: true, - Default: true, - Description: "A violation will be generated for artifacts with unknown licenses (`true` or `false`).", - }, - "multi_license_permissive": { - Type: schema.TypeBool, - Optional: true, - Default: false, - Description: "Do not generate a violation if at least one license is valid in cases whereby multiple licenses were detected on the component", + ) + if d.HasError() { + diags.Append(d...) + } + blockDownloadSet, d := types.SetValue( + blockDownloadElementType, + []attr.Value{blockDownload}, + ) + if d.HasError() { + diags.Append(d...) + } + + actions, d := types.ObjectValue( + licenseActionsAttrTypes, + map[string]attr.Value{ + "webhooks": webhooks, + "mails": mails, + "block_download": blockDownloadSet, + "block_release_bundle_distribution": types.BoolValue(actionsAPIModel.BlockReleaseBundleDistribution), + "block_release_bundle_promotion": types.BoolValue(actionsAPIModel.BlockReleaseBundlePromotion), + "fail_build": types.BoolValue(actionsAPIModel.FailBuild), + "notify_deployer": types.BoolValue(actionsAPIModel.NotifyDeployer), + "notify_watch_recipients": types.BoolValue(actionsAPIModel.NotifyWatchRecipients), + "create_ticket_enabled": types.BoolValue(actionsAPIModel.CreateJiraTicketEnabled), + "build_failure_grace_period_in_days": types.Int64Value(actionsAPIModel.FailureGracePeriodDays), + "custom_severity": types.StringValue(actionsAPIModel.CustomSeverity), }, + ) + if d.HasError() { + diags.Append(d...) } - var actionsSchema = sdk.MergeMaps( - commonActionsSchema, - map[string]*schema.Schema{ - "custom_severity": { - Type: schema.TypeString, - Optional: true, - Default: "High", - Description: "The severity of violation to be triggered if the `criteria` are met.", - ValidateDiagFunc: validator.StringInSlice(true, "Critical", "High", "Medium", "Low"), + actionsSet, d := types.SetValue( + licenseActionsSetElementType, + []attr.Value{actions}, + ) + if d.HasError() { + diags.Append(d...) + } + + return actionsSet, diags +} + +func (r LicensePolicyResource) fromAPIModel(ctx context.Context, policy PolicyAPIModel, plan *PolicyResourceModel) diag.Diagnostics { + return plan.fromAPIModel(ctx, policy, r.fromCriteriaAPIModel, r.fromActionsAPIModel) +} + +var licenseRuleAttrTypes = map[string]attr.Type{ + "name": types.StringType, + "priority": types.Int64Type, + "criteria": types.SetType{ElemType: licenseCriteriaSetElementType}, + "actions": types.SetType{ElemType: licenseActionsSetElementType}, +} + +var licenseRuleSetElementType = types.ObjectType{ + AttrTypes: licenseRuleAttrTypes, +} + +var licensePolicyCriteriaAttrs = map[string]schema.Attribute{ + "banned_licenses": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + Description: "A list of OSS license names that may not be attached to a component. Supports custom licenses added by the user, but there is no verification if the license exists on the Xray side. If the added license doesn't exist, the policy won't trigger the violation.", + }, + "allowed_licenses": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + Description: "A list of OSS license names that may be attached to a component. Supports custom licenses added by the user, but there is no verification if the license exists on the Xray side. If the added license doesn't exist, the policy won't trigger the violation.", + }, + "allow_unknown": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + Description: "A violation will be generated for artifacts with unknown licenses (`true` or `false`).", + }, + "multi_license_permissive": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + Description: "Do not generate a violation if at least one license is valid in cases whereby multiple licenses were detected on the component.", + }, +} + +var licensePolicyCriteriaBlocks = map[string]schema.Block{} + +var licensePolicyActionsAttrs = lo.Assign( + commonActionsAttrs, + map[string]schema.Attribute{ + "custom_severity": schema.StringAttribute{ + Optional: true, + Computed: true, + Default: stringdefault.StaticString("High"), + Validators: []validator.String{ + stringvalidator.OneOfCaseInsensitive("Critical", "High", "Medium", "Low"), }, + Description: "The severity of violation to be triggered if the `criteria` are met.", }, - ) + }, +) - return &schema.Resource{ - SchemaVersion: 1, - CreateContext: resourceXrayPolicyCreate, - ReadContext: resourceXrayPolicyRead, - UpdateContext: resourceXrayPolicyUpdate, - DeleteContext: resourceXrayPolicyDelete, +func (r *LicensePolicyResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Version: 1, + Attributes: policySchemaAttrs, + Blocks: policyBlocks(licensePolicyCriteriaAttrs, licensePolicyCriteriaBlocks, licensePolicyActionsAttrs, commonActionsBlocks), Description: "Creates an Xray policy using V2 of the underlying APIs. Please note: " + "It's only compatible with Bearer token auth method (Identity and Access => Access Tokens)", + } +} - Importer: &schema.ResourceImporter{ - StateContext: resourceImporterForProjectKey, - }, - - Schema: getPolicySchema(criteriaSchema, actionsSchema), +func (r *LicensePolicyResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return } + r.ProviderData = req.ProviderData.(util.ProviderMetadata) +} + +func (r *LicensePolicyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + r.PolicyResource.Create(ctx, r.toAPIModel, r.fromAPIModel, req, resp) +} + +func (r *LicensePolicyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + r.PolicyResource.Read(ctx, r.fromAPIModel, req, resp) +} + +func (r *LicensePolicyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + r.PolicyResource.Update(ctx, r.toAPIModel, r.fromAPIModel, req, resp) +} + +func (r *LicensePolicyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + r.PolicyResource.Delete(ctx, req, resp) +} + +// ImportState imports the resource into the Terraform state. +func (r *LicensePolicyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + r.PolicyResource.ImportState(ctx, req, resp) } diff --git a/pkg/xray/resource/resource_xray_license_policy_test.go b/pkg/xray/resource/resource_xray_license_policy_test.go index 9dcf7813..c70804d9 100644 --- a/pkg/xray/resource/resource_xray_license_policy_test.go +++ b/pkg/xray/resource/resource_xray_license_policy_test.go @@ -5,7 +5,6 @@ import ( "regexp" "testing" - "github.com/go-resty/resty/v2" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/jfrog/terraform-provider-shared/testutil" "github.com/jfrog/terraform-provider-shared/util" @@ -37,6 +36,77 @@ var testDataLicense = map[string]string{ "allowedOrBanned": "banned_licenses", } +func TestAccLicensePolicy_UpgradeFromSDKv2(t *testing.T) { + _, fqrn, resourceName := testutil.MkNames("policy-", "xray_license_policy") + + testData := sdk.MergeMaps(testDataLicense) + testData["resource_name"] = resourceName + testData["policy_name"] = fmt.Sprintf("terraform-license-policy-3-%d", testutil.RandomInt()) + testData["rule_name"] = fmt.Sprintf("test-license-rule-3-%d", testutil.RandomInt()) + testData["multi_license_permissive"] = "true" + testData["allowedOrBanned"] = "allowed_licenses" + + template := ` + resource "xray_license_policy" "{{ .resource_name }}" { + name = "{{ .policy_name }}" + description = "{{ .policy_description }}" + type = "license" + + rule { + name = "{{ .rule_name }}" + priority = 1 + criteria { + {{ .allowedOrBanned }} = ["{{ .license_0 }}","{{ .license_1 }}"] + allow_unknown = {{ .allow_unknown }} + multi_license_permissive = {{ .multi_license_permissive }} + } + actions { + mails = ["{{ .mails_0 }}", "{{ .mails_1 }}"] + block_download { + unscanned = {{ .block_unscanned }} + active = {{ .block_active }} + } + block_release_bundle_distribution = {{ .block_release_bundle_distribution }} + block_release_bundle_promotion = {{ .block_release_bundle_promotion }} + fail_build = {{ .fail_build }} + notify_watch_recipients = {{ .notify_watch_recipients }} + notify_deployer = {{ .notify_deployer }} + create_ticket_enabled = {{ .create_ticket_enabled }} + custom_severity = "{{ .custom_severity }}" + build_failure_grace_period_in_days = {{ .grace_period_days }} + } + } + }` + + config := util.ExecuteTemplate(fqrn, template, testData) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: acctest.VerifyDeleted(fqrn, "", acctest.CheckPolicy), + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "xray": { + Source: "jfrog/xray", + VersionConstraint: "2.11.0", + }, + }, + Config: config, + Check: resource.ComposeTestCheckFunc( + verifyLicensePolicy(fqrn, testData, testData["allowedOrBanned"]), + ), + }, + { + ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + ResourceName: fqrn, + ImportState: true, + ImportStateId: testData["policy_name"], + ImportStateVerify: true, + }, + }, + }) +} + // License policy criteria are different from the security policy criteria // Test will try to post a new license policy with incorrect body of security policy // with specified cvss_range. The function unpackLicenseCriteria will ignore all the @@ -97,11 +167,22 @@ func TestAccLicensePolicy_withProjectKey(t *testing.T) { testData["multi_license_permissive"] = "true" testData["allowedOrBanned"] = "allowed_licenses" - template := `resource "xray_license_policy" "{{ .resource_name }}" { + template := ` + resource "project" "{{ .project_key }}" { + key = "{{ .project_key }}" + display_name = "{{ .project_key }}" + admin_privileges { + manage_members = true + manage_resources = true + index_resources = true + } + } + + resource "xray_license_policy" "{{ .resource_name }}" { name = "{{ .policy_name }}" description = "{{ .policy_description }}" type = "license" - project_key = "{{ .project_key }}" + project_key = project.{{ .project_key }}.key rule { name = "{{ .rule_name }}" @@ -112,7 +193,6 @@ func TestAccLicensePolicy_withProjectKey(t *testing.T) { multi_license_permissive = {{ .multi_license_permissive }} } actions { - webhooks = [] mails = ["{{ .mails_0 }}", "{{ .mails_1 }}"] block_download { unscanned = {{ .block_unscanned }} @@ -137,14 +217,13 @@ func TestAccLicensePolicy_withProjectKey(t *testing.T) { updatedConfig := util.ExecuteTemplate(fqrn, template, updatedTestData) resource.Test(t, resource.TestCase{ - PreCheck: func() { - acctest.PreCheck(t) - acctest.CreateProject(t, projectKey) + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: acctest.VerifyDeleted(fqrn, "", acctest.CheckPolicy), + ExternalProviders: map[string]resource.ExternalProvider{ + "project": { + Source: "jfrog/project", + }, }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", func(id string, request *resty.Request) (*resty.Response, error) { - acctest.DeleteProject(t, projectKey) - return acctest.CheckPolicy(id, request) - }), ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, Steps: []resource.TestStep{ { @@ -402,7 +481,6 @@ const licensePolicyTemplate = `resource "xray_license_policy" "{{ .resource_name multi_license_permissive = {{ .multi_license_permissive }} } actions { - webhooks = [] mails = ["{{ .mails_0 }}", "{{ .mails_1 }}"] block_download { unscanned = {{ .block_unscanned }} diff --git a/pkg/xray/resource/resource_xray_security_policy.go b/pkg/xray/resource/resource_xray_security_policy.go index fd9f8aca..e2f68581 100644 --- a/pkg/xray/resource/resource_xray_security_policy.go +++ b/pkg/xray/resource/resource_xray_security_policy.go @@ -3,13 +3,10 @@ package xray import ( "context" "fmt" - "net/http" "regexp" - "strings" "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/float64validator" - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -25,160 +22,208 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/jfrog/terraform-provider-shared/util" - utilfw "github.com/jfrog/terraform-provider-shared/util/fw" validatorfw_string "github.com/jfrog/terraform-provider-shared/validator/fw/string" "github.com/samber/lo" ) -var _ resource.Resource = &SecurityPolicyV2Resource{} +var _ resource.Resource = &SecurityPolicyResource{} -func NewSecurityPolicyV2Resource() resource.Resource { - return &SecurityPolicyV2Resource{ - TypeName: "xray_security_policy", +func NewSecurityPolicyResource() resource.Resource { + return &SecurityPolicyResource{ + PolicyResource: PolicyResource{ + TypeName: "xray_security_policy", + }, } } -type SecurityPolicyV2Resource struct { - ProviderData util.ProviderMetadata - TypeName string +type SecurityPolicyResource struct { + PolicyResource } -func (r *SecurityPolicyV2Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { +func (r *SecurityPolicyResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = r.TypeName } -type SecurityPolicyV2ResourceModel struct { - ID types.String `tfsdk:"id"` - Name types.String `tfsdk:"name"` - Description types.String `tfsdk:"description"` - ProjectKey types.String `tfsdk:"project_key"` - Type types.String `tfsdk:"type"` - Rules types.Set `tfsdk:"rule"` - Author types.String `tfsdk:"author"` - Created types.String `tfsdk:"created"` - Modified types.String `tfsdk:"modified"` +func (r *SecurityPolicyResource) toCriteriaAPIModel(ctx context.Context, criteriaElems []attr.Value) (*PolicyRuleCriteriaAPIModel, diag.Diagnostics) { + diags := diag.Diagnostics{} + + var criteria *PolicyRuleCriteriaAPIModel + if len(criteriaElems) > 0 { + attrs := criteriaElems[0].(types.Object).Attributes() + + var vulnerabilityIds []string + d := attrs["vulnerability_ids"].(types.Set).ElementsAs(ctx, &vulnerabilityIds, false) + if d.HasError() { + diags.Append(d...) + } + + var cvssRange *PolicyCVSSRangeAPIModel + cvssRangeElems := attrs["cvss_range"].(types.List).Elements() + if len(cvssRangeElems) > 0 { + attrs := cvssRangeElems[0].(types.Object).Attributes() + + cvssRange = &PolicyCVSSRangeAPIModel{ + From: attrs["from"].(types.Float64).ValueFloat64Pointer(), + To: attrs["to"].(types.Float64).ValueFloat64Pointer(), + } + } + + var exposures *PolicyExposuresAPIModel + exposuresElem := attrs["exposures"].(types.List).Elements() + if len(exposuresElem) > 0 { + attrs := exposuresElem[0].(types.Object).Attributes() + + exposures = &PolicyExposuresAPIModel{ + MinSeverity: attrs["min_severity"].(types.String).ValueStringPointer(), + Secrets: attrs["secrets"].(types.Bool).ValueBoolPointer(), + Applications: attrs["applications"].(types.Bool).ValueBoolPointer(), + Services: attrs["services"].(types.Bool).ValueBoolPointer(), + Iac: attrs["iac"].(types.Bool).ValueBoolPointer(), + } + } + + var packageVersions []string + d = attrs["package_versions"].(types.Set).ElementsAs(ctx, &packageVersions, false) + if d.HasError() { + diags.Append(d...) + } + + criteria = &PolicyRuleCriteriaAPIModel{ + MinimumSeverity: attrs["min_severity"].(types.String).ValueString(), + CVSSRange: cvssRange, + FixVersionDependant: attrs["fix_version_dependant"].(types.Bool).ValueBool(), + ApplicableCVEsOnly: attrs["applicable_cves_only"].(types.Bool).ValueBool(), + MaliciousPackage: attrs["malicious_package"].(types.Bool).ValueBool(), + VulnerabilityIds: vulnerabilityIds, + Exposures: exposures, + PackageName: attrs["package_name"].(types.String).ValueString(), + PackageType: attrs["package_type"].(types.String).ValueString(), + PackageVersions: packageVersions, + } + } + + return criteria, diags +} + +func (r SecurityPolicyResource) toAPIModel(ctx context.Context, plan PolicyResourceModel, policy *PolicyAPIModel) diag.Diagnostics { + return plan.toAPIModel(ctx, policy, r.toCriteriaAPIModel, toActionsAPIModel) } -func (m SecurityPolicyV2ResourceModel) toAPIModel(ctx context.Context, apiModel *PolicyAPIModel) diag.Diagnostics { +func (r *SecurityPolicyResource) fromCriteriaAPIModel(ctx context.Context, criteraAPIModel *PolicyRuleCriteriaAPIModel) (types.Set, diag.Diagnostics) { diags := diag.Diagnostics{} - rules := lo.Map( - m.Rules.Elements(), - func(elem attr.Value, _ int) PolicyRuleAPIModel { - attrs := elem.(types.Object).Attributes() - - var criteria *PolicyRuleCriteriaAPIModel - criteriaElems := attrs["criteria"].(types.Set).Elements() - if len(criteriaElems) > 0 { - attrs := criteriaElems[0].(types.Object).Attributes() - - var vulnerabilityIds []string - d := attrs["vulnerability_ids"].(types.Set).ElementsAs(ctx, &vulnerabilityIds, false) - if d.HasError() { - diags.Append(d...) - } - - var cvssRange *PolicyCVSSRangeAPIModel - cvssRangeElems := attrs["cvss_range"].(types.List).Elements() - if len(cvssRangeElems) > 0 { - attrs := cvssRangeElems[0].(types.Object).Attributes() - - cvssRange = &PolicyCVSSRangeAPIModel{ - From: attrs["from"].(types.Float64).ValueFloat64Pointer(), - To: attrs["to"].(types.Float64).ValueFloat64Pointer(), - } - } - - var exposures *PolicyExposuresAPIModel - exposuresElem := attrs["exposures"].(types.List).Elements() - if len(exposuresElem) > 0 { - attrs := exposuresElem[0].(types.Object).Attributes() - - exposures = &PolicyExposuresAPIModel{ - MinSeverity: attrs["min_severity"].(types.String).ValueStringPointer(), - Secrets: attrs["secrets"].(types.Bool).ValueBoolPointer(), - Applications: attrs["applications"].(types.Bool).ValueBoolPointer(), - Services: attrs["services"].(types.Bool).ValueBoolPointer(), - Iac: attrs["iac"].(types.Bool).ValueBoolPointer(), - } - } - - var packageVersions []string - d = attrs["package_versions"].(types.Set).ElementsAs(ctx, &packageVersions, false) - if d.HasError() { - diags.Append(d...) - } - - criteria = &PolicyRuleCriteriaAPIModel{ - MinimumSeverity: attrs["min_severity"].(types.String).ValueString(), - CVSSRange: cvssRange, - FixVersionDependant: attrs["fix_version_dependant"].(types.Bool).ValueBool(), - ApplicableCVEsOnly: attrs["applicable_cves_only"].(types.Bool).ValueBool(), - MaliciousPackage: attrs["malicious_package"].(types.Bool).ValueBool(), - VulnerabilityIds: vulnerabilityIds, - Exposures: exposures, - PackageName: attrs["package_name"].(types.String).ValueString(), - PackageType: attrs["package_type"].(types.String).ValueString(), - PackageVersions: packageVersions, - } + criteriaSet := types.SetNull(securityCriteriaSetElementType) + if criteraAPIModel != nil { + minimumSeverity := types.StringNull() + if criteraAPIModel.MinimumSeverity != "" { + minimumSeverity = types.StringValue(criteraAPIModel.MinimumSeverity) + } + + cvssRangeList := types.ListNull(cvssRangeElementType) + if criteraAPIModel.CVSSRange != nil { + cvssRange, d := types.ObjectValue( + cvssRangeAttrType, + map[string]attr.Value{ + "from": types.Float64PointerValue(criteraAPIModel.CVSSRange.From), + "to": types.Float64PointerValue(criteraAPIModel.CVSSRange.To), + }, + ) + if d.HasError() { + diags.Append(d...) + } + + cr, d := types.ListValue( + cvssRangeElementType, + []attr.Value{cvssRange}, + ) + if d.HasError() { + diags.Append(d...) } - actions := PolicyRuleActionsAPIModel{} - actionsElems := attrs["actions"].(types.Set).Elements() - if len(actionsElems) > 0 { - attrs := actionsElems[0].(types.Object).Attributes() - - var webhooks []string - d := attrs["webhooks"].(types.Set).ElementsAs(ctx, &webhooks, false) - if d.HasError() { - diags.Append(d...) - } - - var mails []string - d = attrs["mails"].(types.Set).ElementsAs(ctx, &mails, false) - if d.HasError() { - diags.Append(d...) - } - - blockDownload := BlockDownloadSettingsAPIModel{} - blockDownloadElems := attrs["block_download"].(types.Set).Elements() - if len(blockDownloadElems) > 0 { - attrs := blockDownloadElems[0].(types.Object).Attributes() - - blockDownload.Unscanned = attrs["unscanned"].(types.Bool).ValueBool() - blockDownload.Active = attrs["active"].(types.Bool).ValueBool() - } - - actions.Webhooks = webhooks - actions.Mails = mails - actions.FailBuild = attrs["fail_build"].(types.Bool).ValueBool() - actions.BlockDownload = blockDownload - actions.BlockReleaseBundleDistribution = attrs["block_release_bundle_distribution"].(types.Bool).ValueBool() - actions.BlockReleaseBundlePromotion = attrs["block_release_bundle_promotion"].(types.Bool).ValueBool() - actions.NotifyWatchRecipients = attrs["notify_watch_recipients"].(types.Bool).ValueBool() - actions.NotifyDeployer = attrs["notify_deployer"].(types.Bool).ValueBool() - actions.CreateJiraTicketEnabled = attrs["create_ticket_enabled"].(types.Bool).ValueBool() - actions.FailureGracePeriodDays = attrs["build_failure_grace_period_in_days"].(types.Int64).ValueInt64() - // actions.CustomSeverity = attrs["custom_severity"].(types.String).ValueString() + cvssRangeList = cr + } + + vulnerabilityIDs, d := types.SetValueFrom(ctx, types.StringType, criteraAPIModel.VulnerabilityIds) + if d.HasError() { + diags.Append(d...) + } + + exposuresList := types.ListNull(exposuresElementType) + if criteraAPIModel.Exposures != nil { + exposures, d := types.ObjectValue( + exposuresAttrType, + map[string]attr.Value{ + "min_severity": types.StringPointerValue(criteraAPIModel.Exposures.MinSeverity), + "secrets": types.BoolPointerValue(criteraAPIModel.Exposures.Secrets), + "applications": types.BoolPointerValue(criteraAPIModel.Exposures.Applications), + "services": types.BoolPointerValue(criteraAPIModel.Exposures.Services), + "iac": types.BoolPointerValue(criteraAPIModel.Exposures.Iac), + }, + ) + if d.HasError() { + diags.Append(d...) } - return PolicyRuleAPIModel{ - Name: attrs["name"].(types.String).ValueString(), - Priority: attrs["priority"].(types.Int64).ValueInt64(), - Criteria: criteria, - Actions: actions, + es, d := types.ListValue( + exposuresElementType, + []attr.Value{exposures}, + ) + if d.HasError() { + diags.Append(d...) } - }, - ) - *apiModel = PolicyAPIModel{ - Name: m.Name.ValueString(), - Description: m.Description.ValueString(), - Type: m.Type.ValueString(), - Rules: &rules, + exposuresList = es + } + + packageName := types.StringNull() + if criteraAPIModel.PackageName != "" { + packageName = types.StringValue(criteraAPIModel.PackageName) + } + + packageType := types.StringNull() + if criteraAPIModel.PackageType != "" { + packageType = types.StringValue(criteraAPIModel.PackageType) + } + + packageVersions, d := types.SetValueFrom(ctx, types.StringType, criteraAPIModel.PackageVersions) + if d.HasError() { + diags.Append(d...) + } + + criteria, d := types.ObjectValue( + securityCriteriaAttrTypes, + map[string]attr.Value{ + "min_severity": minimumSeverity, + "fix_version_dependant": types.BoolValue(criteraAPIModel.FixVersionDependant), + "applicable_cves_only": types.BoolValue(criteraAPIModel.ApplicableCVEsOnly), + "malicious_package": types.BoolValue(criteraAPIModel.MaliciousPackage), + "cvss_range": cvssRangeList, + "vulnerability_ids": vulnerabilityIDs, + "exposures": exposuresList, + "package_name": packageName, + "package_type": packageType, + "package_versions": packageVersions, + }, + ) + if d.HasError() { + diags.Append(d...) + } + cs, d := types.SetValue( + securityCriteriaSetElementType, + []attr.Value{criteria}, + ) + if d.HasError() { + diags.Append(d...) + } + + criteriaSet = cs } - return diags + return criteriaSet, diags +} + +func (r SecurityPolicyResource) fromAPIModel(ctx context.Context, policy PolicyAPIModel, plan *PolicyResourceModel) diag.Diagnostics { + return plan.fromAPIModel(ctx, policy, r.fromCriteriaAPIModel, plan.fromActionsAPIModel) } var cvssRangeAttrType = map[string]attr.Type{ @@ -202,7 +247,7 @@ var exposuresElementType = types.ObjectType{ AttrTypes: exposuresAttrType, } -var criteriaAttrTypes = map[string]attr.Type{ +var securityCriteriaAttrTypes = map[string]attr.Type{ "min_severity": types.StringType, "fix_version_dependant": types.BoolType, "applicable_cves_only": types.BoolType, @@ -215,8 +260,8 @@ var criteriaAttrTypes = map[string]attr.Type{ "package_versions": types.SetType{ElemType: types.StringType}, } -var criteriaSetElementType = types.ObjectType{ - AttrTypes: criteriaAttrTypes, +var securityCriteriaSetElementType = types.ObjectType{ + AttrTypes: securityCriteriaAttrTypes, } var blockDownloadAttrTypes = map[string]attr.Type{ @@ -228,237 +273,15 @@ var blockDownloadElementType = types.ObjectType{ AttrTypes: blockDownloadAttrTypes, } -var actionsAttrTypes = map[string]attr.Type{ - "webhooks": types.SetType{ElemType: types.StringType}, - "mails": types.SetType{ElemType: types.StringType}, - "block_download": types.SetType{ElemType: blockDownloadElementType}, - "block_release_bundle_distribution": types.BoolType, - "block_release_bundle_promotion": types.BoolType, - "fail_build": types.BoolType, - "notify_deployer": types.BoolType, - "notify_watch_recipients": types.BoolType, - "create_ticket_enabled": types.BoolType, - "build_failure_grace_period_in_days": types.Int64Type, -} - -var actionsSetElementType = types.ObjectType{ - AttrTypes: actionsAttrTypes, -} - -var ruleAttrTypes = map[string]attr.Type{ +var securityRuleAttrTypes = map[string]attr.Type{ "name": types.StringType, "priority": types.Int64Type, - "criteria": types.SetType{ElemType: criteriaSetElementType}, + "criteria": types.SetType{ElemType: securityCriteriaSetElementType}, "actions": types.SetType{ElemType: actionsSetElementType}, } -var ruleSetElementType = types.ObjectType{ - AttrTypes: ruleAttrTypes, -} - -func (m *SecurityPolicyV2ResourceModel) fromAPIModel(ctx context.Context, apiModel PolicyAPIModel) diag.Diagnostics { - diags := diag.Diagnostics{} - - rules := lo.Map( - *apiModel.Rules, - func(rule PolicyRuleAPIModel, _ int) attr.Value { - criteriaSet := types.SetNull(criteriaSetElementType) - if rule.Criteria != nil { - minimumSeverity := types.StringNull() - if rule.Criteria.MinimumSeverity != "" { - minimumSeverity = types.StringValue(rule.Criteria.MinimumSeverity) - } - - cvssRangeList := types.ListNull(cvssRangeElementType) - if rule.Criteria.CVSSRange != nil { - cvssRange, d := types.ObjectValue( - cvssRangeAttrType, - map[string]attr.Value{ - "from": types.Float64PointerValue(rule.Criteria.CVSSRange.From), - "to": types.Float64PointerValue(rule.Criteria.CVSSRange.To), - }, - ) - if d.HasError() { - diags.Append(d...) - } - - cr, d := types.ListValue( - cvssRangeElementType, - []attr.Value{cvssRange}, - ) - if d.HasError() { - diags.Append(d...) - } - - cvssRangeList = cr - } - - vulnerabilityIDs, d := types.SetValueFrom(ctx, types.StringType, rule.Criteria.VulnerabilityIds) - if d.HasError() { - diags.Append(d...) - } - - exposuresList := types.ListNull(exposuresElementType) - if rule.Criteria.Exposures != nil { - exposures, d := types.ObjectValue( - exposuresAttrType, - map[string]attr.Value{ - "min_severity": types.StringPointerValue(rule.Criteria.Exposures.MinSeverity), - "secrets": types.BoolPointerValue(rule.Criteria.Exposures.Secrets), - "applications": types.BoolPointerValue(rule.Criteria.Exposures.Applications), - "services": types.BoolPointerValue(rule.Criteria.Exposures.Services), - "iac": types.BoolPointerValue(rule.Criteria.Exposures.Iac), - }, - ) - if d.HasError() { - diags.Append(d...) - } - - es, d := types.ListValue( - exposuresElementType, - []attr.Value{exposures}, - ) - if d.HasError() { - diags.Append(d...) - } - - exposuresList = es - } - - packageName := types.StringNull() - if rule.Criteria.PackageName != "" { - packageName = types.StringValue(rule.Criteria.PackageName) - } - - packageType := types.StringNull() - if rule.Criteria.PackageType != "" { - packageType = types.StringValue(rule.Criteria.PackageType) - } - - packageVersions, d := types.SetValueFrom(ctx, types.StringType, rule.Criteria.PackageVersions) - if d.HasError() { - diags.Append(d...) - } - - criteria, d := types.ObjectValue( - criteriaAttrTypes, - map[string]attr.Value{ - "min_severity": minimumSeverity, - "fix_version_dependant": types.BoolValue(rule.Criteria.FixVersionDependant), - "applicable_cves_only": types.BoolValue(rule.Criteria.ApplicableCVEsOnly), - "malicious_package": types.BoolValue(rule.Criteria.MaliciousPackage), - "cvss_range": cvssRangeList, - "vulnerability_ids": vulnerabilityIDs, - "exposures": exposuresList, - "package_name": packageName, - "package_type": packageType, - "package_versions": packageVersions, - }, - ) - if d.HasError() { - diags.Append(d...) - } - cs, d := types.SetValue( - criteriaSetElementType, - []attr.Value{criteria}, - ) - if d.HasError() { - diags.Append(d...) - } - - criteriaSet = cs - } - - webhooks, d := types.SetValueFrom(ctx, types.StringType, rule.Actions.Webhooks) - if d.HasError() { - diags.Append(d...) - } - - mails, d := types.SetValueFrom(ctx, types.StringType, rule.Actions.Mails) - if d.HasError() { - diags.Append(d...) - } - - blockDownload, d := types.ObjectValue( - blockDownloadAttrTypes, - map[string]attr.Value{ - "unscanned": types.BoolValue(rule.Actions.BlockDownload.Unscanned), - "active": types.BoolValue(rule.Actions.BlockDownload.Active), - }, - ) - if d.HasError() { - diags.Append(d...) - } - blockDownloadSet, d := types.SetValue( - blockDownloadElementType, - []attr.Value{blockDownload}, - ) - if d.HasError() { - diags.Append(d...) - } - - actions, d := types.ObjectValue( - actionsAttrTypes, - map[string]attr.Value{ - "webhooks": webhooks, - "mails": mails, - "block_download": blockDownloadSet, - "block_release_bundle_distribution": types.BoolValue(rule.Actions.BlockReleaseBundleDistribution), - "block_release_bundle_promotion": types.BoolValue(rule.Actions.BlockReleaseBundlePromotion), - "fail_build": types.BoolValue(rule.Actions.FailBuild), - "notify_deployer": types.BoolValue(rule.Actions.NotifyDeployer), - "notify_watch_recipients": types.BoolValue(rule.Actions.NotifyWatchRecipients), - "create_ticket_enabled": types.BoolValue(rule.Actions.CreateJiraTicketEnabled), - "build_failure_grace_period_in_days": types.Int64Value(rule.Actions.FailureGracePeriodDays), - }, - ) - if d.HasError() { - diags.Append(d...) - } - actionsSet, d := types.SetValue( - actionsSetElementType, - []attr.Value{actions}, - ) - if d.HasError() { - diags.Append(d...) - } - - r, d := types.ObjectValue( - ruleAttrTypes, - map[string]attr.Value{ - "name": types.StringValue(rule.Name), - "priority": types.Int64Value(rule.Priority), - "criteria": criteriaSet, - "actions": actionsSet, - }, - ) - if d.HasError() { - diags.Append(d...) - } - - return r - }, - ) - - rulesSet, d := types.SetValue( - ruleSetElementType, - rules, - ) - if d.HasError() { - diags.Append(d...) - } - - m.ID = types.StringValue(apiModel.Name) - m.Name = types.StringValue(apiModel.Name) - m.Description = types.StringValue(apiModel.Description) - m.Type = types.StringValue(apiModel.Type) - m.Author = types.StringValue(apiModel.Author) - m.Created = types.StringValue(apiModel.Created) - m.Modified = types.StringValue(apiModel.Modified) - - m.Rules = rulesSet - - return diags +var securityRuleSetElementType = types.ObjectType{ + AttrTypes: securityRuleAttrTypes, } var projectKeySchemaAttrs = func(isForceNew bool, additionalDescription string) map[string]schema.Attribute { @@ -523,88 +346,6 @@ var policySchemaAttrs = lo.Assign( }, ) -var commonActionsBlocks = map[string]schema.Block{ - "block_download": schema.SetNestedBlock{ - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - "unscanned": schema.BoolAttribute{ - Optional: true, - Computed: true, - Default: booldefault.StaticBool(false), - Description: "Whether or not to block download of artifacts that meet the artifact `filters` for the associated `xray_watch` resource but have not been scanned yet. Can not be set to `true` if attribute `active` is `false`. Default value is `false`.", - }, - "active": schema.BoolAttribute{ - Optional: true, - Computed: true, - Default: booldefault.StaticBool(false), - Description: "Whether or not to block download of artifacts that meet the artifact and severity `filters` for the associated `xray_watch` resource. Default value is `false`.", - }, - }, - }, - Validators: []validator.Set{ - setvalidator.IsRequired(), - setvalidator.SizeAtMost(1), - }, - Description: "Block download of artifacts that meet the Artifact Filter and Severity Filter specifications for this watch", - }, -} - -var commonActionsAttrs = map[string]schema.Attribute{ - "webhooks": schema.SetAttribute{ - ElementType: types.StringType, - Optional: true, - Description: "A list of Xray-configured webhook URLs to be invoked if a violation is triggered.", - }, - "mails": schema.SetAttribute{ - ElementType: types.StringType, - Optional: true, - Description: "A list of email addressed that will get emailed when a violation is triggered.", - }, - "block_release_bundle_distribution": schema.BoolAttribute{ - Optional: true, - Computed: true, - Default: booldefault.StaticBool(false), - Description: "Blocks Release Bundle distribution to Edge nodes if a violation is found. Default value is `false`.", - }, - "block_release_bundle_promotion": schema.BoolAttribute{ - Optional: true, - Computed: true, - Default: booldefault.StaticBool(false), - Description: "Blocks Release Bundle promotion if a violation is found. Default value is `false`.", - }, - "fail_build": schema.BoolAttribute{ - Optional: true, - Computed: true, - Default: booldefault.StaticBool(false), - Description: "Whether or not the related CI build should be marked as failed if a violation is triggered. This option is only available when the policy is applied to an `xray_watch` resource with a `type` of `builds`. Default value is `false`.", - }, - "notify_deployer": schema.BoolAttribute{ - Optional: true, - Computed: true, - Default: booldefault.StaticBool(false), - Description: "Sends an email message to component deployer with details about the generated Violations. Default value is `false`.", - }, - "notify_watch_recipients": schema.BoolAttribute{ - Optional: true, - Computed: true, - Default: booldefault.StaticBool(false), - Description: "Sends an email message to all configured recipients inside a specific watch with details about the generated Violations. Default value is `false`.", - }, - "create_ticket_enabled": schema.BoolAttribute{ - Optional: true, - Computed: true, - Default: booldefault.StaticBool(false), - Description: "Create Jira Ticket for this Policy Violation. Requires configured Jira integration. Default value is `false`.", - }, - "build_failure_grace_period_in_days": schema.Int64Attribute{ - Optional: true, - Validators: []validator.Int64{ - int64validator.AtLeast(0), - }, - Description: "Allow grace period for certain number of days. All violations will be ignored during this time. To be used only if `fail_build` is enabled.", - }, -} - var securityPolicyCriteriaBlocks = map[string]schema.Block{ "cvss_range": schema.ListNestedBlock{ NestedObject: schema.NestedBlockObject{ @@ -783,61 +524,7 @@ var securityPolicyCriteriaAttrs = map[string]schema.Attribute{ }, } -var policyBlocks = func(criteriaAttrs map[string]schema.Attribute, criteriaBlocks map[string]schema.Block, actionsAttrs map[string]schema.Attribute, actionsBlocks map[string]schema.Block) map[string]schema.Block { - return map[string]schema.Block{ - "rule": schema.SetNestedBlock{ - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - "name": schema.StringAttribute{ - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - Description: "Name of the rule", - }, - "priority": schema.Int64Attribute{ - Required: true, - Validators: []validator.Int64{ - int64validator.AtLeast(1), - }, - Description: "Integer describing the rule priority. Must be at least 1", - }, - }, - Blocks: map[string]schema.Block{ - "criteria": schema.SetNestedBlock{ - NestedObject: schema.NestedBlockObject{ - Attributes: criteriaAttrs, - Blocks: criteriaBlocks, - }, - Validators: []validator.Set{ - setvalidator.IsRequired(), - setvalidator.SizeBetween(1, 1), - }, - Description: "The set of security conditions to examine when an scanned artifact is scanned.", - }, - "actions": schema.SetNestedBlock{ - NestedObject: schema.NestedBlockObject{ - Attributes: actionsAttrs, - Blocks: actionsBlocks, - }, - Validators: []validator.Set{ - setvalidator.IsRequired(), - setvalidator.SizeBetween(1, 1), - }, - Description: "Specifies the actions to take once a security policy violation has been triggered.", - }, - }, - }, - Validators: []validator.Set{ - setvalidator.IsRequired(), - setvalidator.SizeAtLeast(1), - }, - Description: "A list of user-defined rules allowing you to trigger violations for specific vulnerability or license breaches by setting a license or security criteria, with a corresponding set of automatic actions according to your needs. Rules are processed according to the ascending order in which they are placed in the Rules list on the Policy. If a rule is met, the subsequent rules in the list will not be applied.", - }, - } -} - -func (r *SecurityPolicyV2Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { +func (r *SecurityPolicyResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Version: 1, Attributes: policySchemaAttrs, @@ -847,7 +534,7 @@ func (r *SecurityPolicyV2Resource) Schema(ctx context.Context, req resource.Sche } } -func (r *SecurityPolicyV2Resource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { +func (r *SecurityPolicyResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { // Prevent panic if the provider has not been configured. if req.ProviderData == nil { return @@ -855,8 +542,8 @@ func (r *SecurityPolicyV2Resource) Configure(ctx context.Context, req resource.C r.ProviderData = req.ProviderData.(util.ProviderMetadata) } -func (r SecurityPolicyV2Resource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { - var data SecurityPolicyV2ResourceModel +func (r SecurityPolicyResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + var data PolicyResourceModel resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) if resp.Diagnostics.HasError() { @@ -900,442 +587,23 @@ func (r SecurityPolicyV2Resource) ValidateConfig(ctx context.Context, req resour } } -func (r *SecurityPolicyV2Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - go util.SendUsageResourceCreate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) - - var plan SecurityPolicyV2ResourceModel - - // Read Terraform plan data into the model - resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) - if resp.Diagnostics.HasError() { - return - } - - request, err := getRestyRequest(r.ProviderData.Client, plan.ProjectKey.ValueString()) - if err != nil { - resp.Diagnostics.AddError( - "failed to get Resty client", - err.Error(), - ) - return - } - - var policy PolicyAPIModel - resp.Diagnostics.Append(plan.toAPIModel(ctx, &policy)...) - if resp.Diagnostics.HasError() { - return - } - - var policyError PolicyError - response, err := request. - SetBody(policy). - SetError(&policyError). - Post(PoliciesEndpoint) - - if err != nil { - utilfw.UnableToCreateResourceError(resp, err.Error()) - return - } - - if response.IsError() { - utilfw.UnableToCreateResourceError(resp, policyError.Error) - return - } - - response, err = request. - SetResult(&policy). - SetPathParam("name", plan.Name.ValueString()). - SetError(&policyError). - Get(PolicyEndpoint) - - if err != nil { - utilfw.UnableToCreateResourceError(resp, err.Error()) - return - } - - if response.IsError() { - utilfw.UnableToCreateResourceError(resp, policyError.Error) - return - } - - resp.Diagnostics.Append(plan.fromAPIModel(ctx, policy)...) - if resp.Diagnostics.HasError() { - return - } - - // Save data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +func (r *SecurityPolicyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + r.PolicyResource.Create(ctx, r.toAPIModel, r.fromAPIModel, req, resp) } -func (r *SecurityPolicyV2Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - go util.SendUsageResourceRead(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) - - var state SecurityPolicyV2ResourceModel - - // Read Terraform state data into the model - resp.Diagnostics.Append(req.State.Get(ctx, &state)...) - if resp.Diagnostics.HasError() { - return - } - - request, err := getRestyRequest(r.ProviderData.Client, state.ProjectKey.ValueString()) - if err != nil { - resp.Diagnostics.AddError( - "failed to get Resty client", - err.Error(), - ) - return - } - - var policy PolicyAPIModel - var policyError PolicyError - - response, err := request. - SetResult(&policy). - SetPathParam("name", state.Name.ValueString()). - SetError(&policyError). - Get(PolicyEndpoint) - - if err != nil { - utilfw.UnableToRefreshResourceError(resp, err.Error()) - return - } - - if response.StatusCode() == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - - if response.IsError() { - utilfw.UnableToRefreshResourceError(resp, policyError.Error) - return - } - - resp.Diagnostics.Append(state.fromAPIModel(ctx, policy)...) - if resp.Diagnostics.HasError() { - return - } - - // Save data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +func (r *SecurityPolicyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + r.PolicyResource.Read(ctx, r.fromAPIModel, req, resp) } -func (r *SecurityPolicyV2Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - go util.SendUsageResourceUpdate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) - - var plan SecurityPolicyV2ResourceModel - - // Read Terraform plan data into the model - resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) - if resp.Diagnostics.HasError() { - return - } - - request, err := getRestyRequest(r.ProviderData.Client, plan.ProjectKey.ValueString()) - if err != nil { - resp.Diagnostics.AddError( - "failed to get Resty client", - err.Error(), - ) - return - } - - var policy PolicyAPIModel - resp.Diagnostics.Append(plan.toAPIModel(ctx, &policy)...) - if resp.Diagnostics.HasError() { - return - } - - var policyError PolicyError - - response, err := request. - SetPathParam("name", plan.Name.ValueString()). - SetBody(policy). - SetError(&policyError). - Put(PolicyEndpoint) - - if err != nil { - utilfw.UnableToUpdateResourceError(resp, err.Error()) - return - } - - if response.IsError() { - utilfw.UnableToUpdateResourceError(resp, policyError.Error) - return - } - - response, err = request. - SetResult(&policy). - SetPathParam("name", plan.Name.ValueString()). - SetError(&policyError). - Get(PolicyEndpoint) - - if err != nil { - utilfw.UnableToUpdateResourceError(resp, err.Error()) - return - } - - if response.IsError() { - utilfw.UnableToUpdateResourceError(resp, policyError.Error) - return - } - - resp.Diagnostics.Append(plan.fromAPIModel(ctx, policy)...) - if resp.Diagnostics.HasError() { - return - } - - // Save data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +func (r *SecurityPolicyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + r.PolicyResource.Update(ctx, r.toAPIModel, r.fromAPIModel, req, resp) } -func (r *SecurityPolicyV2Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - go util.SendUsageResourceDelete(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) - - var state SecurityPolicyV2ResourceModel - - // Read Terraform prior state data into the model - resp.Diagnostics.Append(req.State.Get(ctx, &state)...) - - request, err := getRestyRequest(r.ProviderData.Client, state.ProjectKey.ValueString()) - if err != nil { - resp.Diagnostics.AddError( - "failed to get Resty client", - err.Error(), - ) - return - } - - var policyError PolicyError - response, err := request. - SetPathParam("name", state.Name.ValueString()). - SetError(&policyError). - Delete(PolicyEndpoint) - - if err != nil { - utilfw.UnableToDeleteResourceError(resp, err.Error()) - return - } - - if response.IsError() { - utilfw.UnableToDeleteResourceError(resp, policyError.Error) - return - } - - // If the logic reaches here, it implicitly succeeded and will remove - // the resource from state if there are no other errors. +func (r *SecurityPolicyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + r.PolicyResource.Delete(ctx, req, resp) } // ImportState imports the resource into the Terraform state. -func (r *SecurityPolicyV2Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - parts := strings.SplitN(req.ID, ":", 2) - - if len(parts) > 0 && parts[0] != "" { - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), parts[0])...) - } - - if len(parts) == 2 && parts[1] != "" { - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_key"), parts[1])...) - } +func (r *SecurityPolicyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + r.PolicyResource.ImportState(ctx, req, resp) } - -// -// func ResourceXraySecurityPolicyV2() *schema.Resource { -// var criteriaSchema = map[string]*schema.Schema{ -// "min_severity": { -// Type: schema.TypeString, -// Optional: true, -// Description: "The minimum security vulnerability severity that will be impacted by the policy. Valid values: `All Severities`, `Critical`, `High`, `Medium`, `Low`", -// ValidateDiagFunc: validator.StringInSlice(true, "All Severities", "Critical", "High", "Medium", "Low"), -// }, -// "fix_version_dependant": { -// Type: schema.TypeBool, -// Optional: true, -// Default: false, -// Description: "Default value is `false`. Issues that do not have a fixed version are not generated until a fixed version is available. Must be `false` with `malicious_package` enabled.", -// }, -// "applicable_cves_only": { -// Type: schema.TypeBool, -// Optional: true, -// Default: false, -// Description: "Default value is `false`. Mark to skip CVEs that are not applicable in the context of the artifact. The contextual analysis operation might be long and affect build time if the `fail_build` action is set.\n\n~>Only supported by JFrog Advanced Security", -// }, -// "malicious_package": { -// Type: schema.TypeBool, -// Optional: true, -// Default: false, -// Description: "Default value is `false`. Generating a violation on a malicious package.", -// }, -// "cvss_range": { -// Type: schema.TypeList, -// Optional: true, -// MaxItems: 1, -// Description: "The CVSS score range to apply to the rule. This is used for a fine-grained control, rather than using the predefined severities. The score range is based on CVSS v3 scoring, and CVSS v2 score is CVSS v3 score is not available.", -// Elem: &schema.Resource{ -// Schema: map[string]*schema.Schema{ -// "from": { -// Type: schema.TypeFloat, -// Required: true, -// Description: "The beginning of the range of CVS scores (from 1-10, float) to flag.", -// ValidateDiagFunc: validation.ToDiagFunc(validation.FloatBetween(0, 10)), -// }, -// "to": { -// Type: schema.TypeFloat, -// Required: true, -// Description: "The end of the range of CVS scores (from 1-10, float) to flag. ", -// ValidateDiagFunc: validation.ToDiagFunc(validation.FloatBetween(0, 10)), -// }, -// }, -// }, -// }, -// "vulnerability_ids": { -// Type: schema.TypeSet, -// Optional: true, -// MaxItems: 100, -// MinItems: 1, -// Description: "Creates policy rules for specific vulnerability IDs that you input. You can add multiple vulnerabilities IDs up to 100. CVEs and Xray IDs are supported. Example - CVE-2015-20107, XRAY-2344", -// Elem: &schema.Schema{ -// Type: schema.TypeString, -// ValidateDiagFunc: validation.ToDiagFunc( -// validation.StringMatch(regexp.MustCompile(`(CVE\W*\d{4}\W+\d{4,}|XRAY-\d{4,})`), "invalid Vulnerability, must be a valid CVE or Xray ID, example CVE-2021-12345, XRAY-1234"), -// ), -// }, -// }, -// "exposures": { -// Type: schema.TypeList, -// Optional: true, -// MaxItems: 1, -// Description: "Creates policy rules for specific exposures.\n\n~>Only supported by JFrog Advanced Security", -// Elem: &schema.Resource{ -// Schema: map[string]*schema.Schema{ -// "min_severity": { -// Type: schema.TypeString, -// Optional: true, -// Default: "All Severities", -// Description: "The minimum security vulnerability severity that will be impacted by the policy. Valid values: `All Severities`, `Critical`, `High`, `Medium`, `Low`", -// ValidateDiagFunc: validator.StringInSlice(true, "All Severities", "Critical", "High", "Medium", "Low"), -// }, -// "secrets": { -// Type: schema.TypeBool, -// Optional: true, -// Default: true, -// Description: "Secrets exposures.", -// }, -// "applications": { -// Type: schema.TypeBool, -// Optional: true, -// Default: true, -// Description: "Applications exposures.", -// }, -// "services": { -// Type: schema.TypeBool, -// Optional: true, -// Default: true, -// Description: "Services exposures.", -// }, -// "iac": { -// Type: schema.TypeBool, -// Optional: true, -// Default: true, -// Description: "Iac exposures.", -// }, -// }, -// }, -// }, -// "package_name": { -// Type: schema.TypeString, -// Optional: true, -// Description: "The package name to create a rule for", -// }, -// "package_type": { -// Type: schema.TypeString, -// Optional: true, -// Description: "The package type to create a rule for", -// ValidateDiagFunc: validator.StringInSlice(true, validPackageTypesSupportedXraySecPolicies...), -// }, -// "package_versions": { -// Type: schema.TypeSet, -// Optional: true, -// Description: "package versions to apply the rule on can be (,) for any version or an open range (1,4) or closed [1,4] or one version [1]", -// Elem: &schema.Schema{ -// Type: schema.TypeString, -// ValidateDiagFunc: validation.ToDiagFunc( -// validation.StringMatch(regexp.MustCompile(`((^(\(|\[)((\d+\.)?(\d+\.)?(\*|\d+)|(\s*))\,((\d+\.)?(\d+\.)?(\*|\d+)|(\s*))(\)|\])$|^\[(\d+\.)?(\d+\.)?(\*|\d+)\]$))`), "invalid Range, must be one of the follows: Any Version: (,) or Specific Version: [1.2], [3] or Range: (1,), [,1.2.3], (4.5.0,6.5.2]"), -// ), -// }, -// }, -// } -// -// return &schema.Resource{ -// SchemaVersion: 1, -// CreateContext: resourceXrayPolicyCreate, -// ReadContext: resourceXrayPolicyRead, -// UpdateContext: resourceXrayPolicyUpdate, -// DeleteContext: resourceXrayPolicyDelete, -// Description: "Creates an Xray policy using V2 of the underlying APIs. Please note: " + -// "It's only compatible with Bearer token auth method (Identity and Access => Access Tokens)", -// -// Importer: &schema.ResourceImporter{ -// StateContext: resourceImporterForProjectKey, -// }, -// CustomizeDiff: criteriaMaliciousPkgDiff, -// Schema: getPolicySchema(criteriaSchema, commonActionsSchema), -// } -// } -// -// var criteriaMaliciousPkgDiff = func(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { -// rules := diff.Get("rule").(*schema.Set).List() -// if len(rules) == 0 { -// return nil -// } -// criteria := rules[0].(map[string]interface{})["criteria"].(*schema.Set).List() -// if len(criteria) == 0 { -// return nil -// } -// -// criterion := criteria[0].(map[string]interface{}) -// // fixVersionDependant can't be set with malicious_package -// fixVersionDependant := criterion["fix_version_dependant"].(bool) -// // Only one of the following: -// minSeverity := criterion["min_severity"].(string) -// cvssRange := criterion["cvss_range"].([]interface{}) -// vulnerabilityIDs := criterion["vulnerability_ids"].(*schema.Set).List() -// maliciousPackage := criterion["malicious_package"].(bool) -// exposures := criterion["exposures"].([]interface{}) -// package_name := criterion["package_name"].(string) -// package_type := criterion["package_type"].(string) -// package_versions := criterion["package_versions"].(*schema.Set).List() -// isPackageSet := len(package_name) > 0 || len(package_type) > 0 || len(package_versions) > 0 //if one of them is not defined the API will return an error guiding which one is missing -// -// if len(exposures) > 0 && maliciousPackage || (len(exposures) > 0 && len(cvssRange) > 0) || -// (len(exposures) > 0 && len(minSeverity) > 0) || (len(exposures) > 0 && len(vulnerabilityIDs) > 0) { -// return fmt.Errorf("exsposures can't be set together with cvss_range, min_severity, malicious_package and vulnerability_ids") -// } -// // If `malicious_package` is enabled in the UI, `fix_version_dependant` is set to `false` in the UI call. -// // UI itself doesn't have this checkbox at all. We are adding this check to avoid unexpected behavior. -// if maliciousPackage && fixVersionDependant { -// return fmt.Errorf("fix_version_dependant must be set to false if malicious_package is true") -// } -// if (maliciousPackage && len(minSeverity) > 0) || (maliciousPackage && len(cvssRange) > 0) { -// return fmt.Errorf("malicious_package can't be set together with min_severity and/or cvss_range") -// } -// if len(minSeverity) > 0 && len(cvssRange) > 0 { -// return fmt.Errorf("min_severity can't be set together with cvss_range") -// } -// if (len(vulnerabilityIDs) > 0 && maliciousPackage) || (len(vulnerabilityIDs) > 0 && len(minSeverity) > 0) || -// (len(vulnerabilityIDs) > 0 && len(cvssRange) > 0) || (len(vulnerabilityIDs) > 0 && len(exposures) > 0) { -// return fmt.Errorf("vulnerability_ids can't be set together with with malicious_package, min_severity, cvss_range and exposures") -// } -// -// if (isPackageSet && len(vulnerabilityIDs) > 0) || (isPackageSet && maliciousPackage) || -// (isPackageSet && len(cvssRange) > 0) || (isPackageSet && len(minSeverity) > 0) || -// (isPackageSet && len(exposures) > 0) { -// return fmt.Errorf("package_name, package_type and package versions can't be set together with with vulnerability_ids, malicious_package, min_severity, cvss_range and exposures") -// } -// -// if isPackageSet && fixVersionDependant { -// return fmt.Errorf("fix_version_dependant must be set to false if package type policy is used") -// } -// -// return nil -// } diff --git a/pkg/xray/resource/resource_xray_security_policy_test.go b/pkg/xray/resource/resource_xray_security_policy_test.go index 35cea7e8..5a61d761 100644 --- a/pkg/xray/resource/resource_xray_security_policy_test.go +++ b/pkg/xray/resource/resource_xray_security_policy_test.go @@ -6,7 +6,6 @@ import ( "strconv" "testing" - "github.com/go-resty/resty/v2" "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/jfrog/terraform-provider-shared/testutil" @@ -43,6 +42,75 @@ var testDataSecurity = map[string]string{ "criteriaType": "cvss", } +func TestAccSecurityPolicy_UpgradeFromSDKv2(t *testing.T) { + _, fqrn, resourceName := testutil.MkNames("policy-", "xray_security_policy") + + testData := sdk.MergeMaps(testDataSecurity) + testData["resource_name"] = resourceName + testData["policy_name"] = fmt.Sprintf("terraform-security-policy-4-%d", testutil.RandomInt()) + testData["rule_name"] = fmt.Sprintf("test-security-rule-4-%d", testutil.RandomInt()) + + template := ` + resource "xray_security_policy" "{{ .resource_name }}" { + name = "{{ .policy_name }}" + description = "{{ .policy_description }}" + type = "security" + + rule { + name = "{{ .rule_name }}" + priority = 1 + criteria { + cvss_range { + from = {{ .cvss_from }} + to = {{ .cvss_to }} + } + applicable_cves_only = {{ .applicable_cves_only }} + } + actions { + block_release_bundle_distribution = {{ .block_release_bundle_distribution }} + block_release_bundle_promotion = {{ .block_release_bundle_promotion }} + fail_build = {{ .fail_build }} + notify_watch_recipients = {{ .notify_watch_recipients }} + notify_deployer = {{ .notify_deployer }} + create_ticket_enabled = {{ .create_ticket_enabled }} + build_failure_grace_period_in_days = {{ .grace_period_days }} + block_download { + unscanned = {{ .block_unscanned }} + active = {{ .block_active }} + } + } + } + }` + + config := util.ExecuteTemplate(fqrn, template, testData) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: acctest.VerifyDeleted(fqrn, "", acctest.CheckPolicy), + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "xray": { + Source: "jfrog/xray", + VersionConstraint: "2.11.0", + }, + }, + Config: config, + Check: resource.ComposeTestCheckFunc( + verifySecurityPolicy(fqrn, testData, criteriaTypeCvss), + ), + }, + { + ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + ResourceName: fqrn, + ImportState: true, + ImportStateId: testData["policy_name"], + ImportStateVerify: true, + }, + }, + }) +} + func TestAccSecurityPolicy_multipleRules(t *testing.T) { _, fqrn, resourceName := testutil.MkNames("policy-", "xray_security_policy") testData := sdk.MergeMaps(testDataSecurity) @@ -210,11 +278,22 @@ func TestAccSecurityPolicy_withProjectKey(t *testing.T) { testData["policy_name"] = fmt.Sprintf("terraform-security-policy-4-%d", testutil.RandomInt()) testData["rule_name"] = fmt.Sprintf("test-security-rule-4-%d", testutil.RandomInt()) - template := `resource "xray_security_policy" "{{ .resource_name }}" { + template := ` + resource "project" "{{ .project_key }}" { + key = "{{ .project_key }}" + display_name = "{{ .project_key }}" + admin_privileges { + manage_members = true + manage_resources = true + index_resources = true + } + } + + resource "xray_security_policy" "{{ .resource_name }}" { name = "{{ .policy_name }}" description = "{{ .policy_description }}" type = "security" - project_key = "{{ .project_key }}" + project_key = project.{{ .project_key }}.key rule { name = "{{ .rule_name }}" @@ -249,14 +328,13 @@ func TestAccSecurityPolicy_withProjectKey(t *testing.T) { updatedConfig := util.ExecuteTemplate(fqrn, template, updatedTestData) resource.Test(t, resource.TestCase{ - PreCheck: func() { - acctest.PreCheck(t) - acctest.CreateProject(t, projectKey) + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: acctest.VerifyDeleted(fqrn, "", acctest.CheckPolicy), + ExternalProviders: map[string]resource.ExternalProvider{ + "project": { + Source: "jfrog/project", + }, }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", func(id string, request *resty.Request) (*resty.Response, error) { - acctest.DeleteProject(t, projectKey) - return acctest.CheckPolicy(id, request) - }), ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, Steps: []resource.TestStep{ { From da7d6bc01ea19cf052e6749d6a0b0b975d909615 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Tue, 10 Sep 2024 13:17:57 -0700 Subject: [PATCH 04/11] Migrate Operational Risk policy to Plugin Framework --- pkg/xray/provider/framework.go | 1 + pkg/xray/provider/sdkv2.go | 1 - pkg/xray/resource/policies.go | 767 +----------------- .../resource_xray_ignore_rule_test.go | 4 - .../resource_xray_operational_risk_policy.go | 382 +++++++-- ...ource_xray_operational_risk_policy_test.go | 105 ++- .../resource/resource_xray_security_policy.go | 2 +- pkg/xray/resource/util.go | 13 - 8 files changed, 396 insertions(+), 879 deletions(-) diff --git a/pkg/xray/provider/framework.go b/pkg/xray/provider/framework.go index 383ef4e2..e37f6c05 100644 --- a/pkg/xray/provider/framework.go +++ b/pkg/xray/provider/framework.go @@ -181,6 +181,7 @@ func (p *XrayProvider) Resources(ctx context.Context) []func() resource.Resource xray_resource.NewCustomIssueResource, xray_resource.NewIgnoreRuleResource, xray_resource.NewLicensePolicyResource, + xray_resource.NewOperationalRiskPolicyResource, xray_resource.NewRepositoryConfigResource, xray_resource.NewSecurityPolicyResource, xray_resource.NewSettingsResource, diff --git a/pkg/xray/provider/sdkv2.go b/pkg/xray/provider/sdkv2.go index f9771a34..51a67f56 100644 --- a/pkg/xray/provider/sdkv2.go +++ b/pkg/xray/provider/sdkv2.go @@ -54,7 +54,6 @@ func SdkV2() *schema.Provider { ResourcesMap: sdk.AddTelemetry( productId, map[string]*schema.Resource{ - "xray_operational_risk_policy": xray.ResourceXrayOperationalRiskPolicy(), "xray_vulnerabilities_report": xray.ResourceXrayVulnerabilitiesReport(), "xray_licenses_report": xray.ResourceXrayLicensesReport(), "xray_violations_report": xray.ResourceXrayViolationsReport(), diff --git a/pkg/xray/resource/policies.go b/pkg/xray/resource/policies.go index ea1d32a6..b7e82400 100644 --- a/pkg/xray/resource/policies.go +++ b/pkg/xray/resource/policies.go @@ -16,12 +16,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - sdkv2_diag "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - sdkv2_schema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/jfrog/terraform-provider-shared/util" utilfw "github.com/jfrog/terraform-provider-shared/util/fw" - "github.com/jfrog/terraform-provider-shared/util/sdk" - shared_validator "github.com/jfrog/terraform-provider-shared/validator" "github.com/samber/lo" ) @@ -172,7 +168,7 @@ var actionsSetElementType = types.ObjectType{ AttrTypes: actionsAttrTypes, } -func (m *PolicyResourceModel) fromActionsAPIModel(ctx context.Context, actionsAPIModel PolicyRuleActionsAPIModel) (types.Set, diag.Diagnostics) { +var fromActionsAPIModel = func(ctx context.Context, actionsAPIModel PolicyRuleActionsAPIModel) (types.Set, diag.Diagnostics) { diags := diag.Diagnostics{} webhooks := types.SetNull(types.StringType) @@ -261,9 +257,9 @@ func (m *PolicyResourceModel) fromAPIModel( case "security": ruleAttrTypes = securityRuleAttrTypes ruleSetElementType = securityRuleSetElementType - // case "operational_risk": - // ruleAttrTypes = opRiskRuleAttrTypes - // ruleSetElementType = opRiskRuleSetElementType + case "operational_risk": + ruleAttrTypes = opRiskRuleAttrTypes + ruleSetElementType = opRiskRuleSetElementType } rules := lo.Map( @@ -405,171 +401,6 @@ var commonActionsAttrs = map[string]schema.Attribute{ }, } -var commonActionsSchema = map[string]*sdkv2_schema.Schema{ - "webhooks": { - Type: sdkv2_schema.TypeSet, - Optional: true, - Description: "A list of Xray-configured webhook URLs to be invoked if a violation is triggered.", - Elem: &sdkv2_schema.Schema{ - Type: sdkv2_schema.TypeString, - }, - }, - "mails": { - Type: sdkv2_schema.TypeSet, - Optional: true, - Description: "A list of email addressed that will get emailed when a violation is triggered.", - Elem: &sdkv2_schema.Schema{ - Type: sdkv2_schema.TypeString, - ValidateDiagFunc: shared_validator.IsEmail, - }, - }, - "block_download": { - Type: sdkv2_schema.TypeSet, - Required: true, - MaxItems: 1, - Description: "Block download of artifacts that meet the Artifact Filter and Severity Filter specifications for this watch", - Elem: &sdkv2_schema.Resource{ - Schema: map[string]*sdkv2_schema.Schema{ - "unscanned": { - Type: sdkv2_schema.TypeBool, - Optional: true, - Default: false, - Description: "Whether or not to block download of artifacts that meet the artifact `filters` for the associated `xray_watch` resource but have not been scanned yet. Can not be set to `true` if attribute `active` is `false`. Default value is `false`.", - }, - "active": { - Type: sdkv2_schema.TypeBool, - Optional: true, - Default: false, - Description: "Whether or not to block download of artifacts that meet the artifact and severity `filters` for the associated `xray_watch` resource. Default value is `false`.", - }, - }, - }, - }, - "block_release_bundle_distribution": { - Type: sdkv2_schema.TypeBool, - Optional: true, - Default: false, - Description: "Blocks Release Bundle distribution to Edge nodes if a violation is found. Default value is `false`.", - }, - "block_release_bundle_promotion": { - Type: sdkv2_schema.TypeBool, - Optional: true, - Default: false, - Description: "Blocks Release Bundle promotion if a violation is found. Default value is `false`.", - }, - "fail_build": { - Type: sdkv2_schema.TypeBool, - Optional: true, - Default: false, - Description: "Whether or not the related CI build should be marked as failed if a violation is triggered. This option is only available when the policy is applied to an `xray_watch` resource with a `type` of `builds`. Default value is `false`.", - }, - "notify_deployer": { - Type: sdkv2_schema.TypeBool, - Optional: true, - Default: false, - Description: "Sends an email message to component deployer with details about the generated Violations. Default value is `false`.", - }, - "notify_watch_recipients": { - Type: sdkv2_schema.TypeBool, - Optional: true, - Default: false, - Description: "Sends an email message to all configured recipients inside a specific watch with details about the generated Violations. Default value is `false`.", - }, - "create_ticket_enabled": { - Type: sdkv2_schema.TypeBool, - Optional: true, - Default: false, - Description: "Create Jira Ticket for this Policy Violation. Requires configured Jira integration. Default value is `false`.", - }, - "build_failure_grace_period_in_days": { - Type: sdkv2_schema.TypeInt, - Optional: true, - Description: "Allow grace period for certain number of days. All violations will be ignored during this time. To be used only if `fail_build` is enabled.", - ValidateDiagFunc: shared_validator.IntAtLeast(0), - }, -} - -var getPolicySchema = func(criteriaSchema map[string]*sdkv2_schema.Schema, actionsSchema map[string]*sdkv2_schema.Schema) map[string]*sdkv2_schema.Schema { - return sdk.MergeMaps( - getProjectKeySchema(false, ""), - map[string]*sdkv2_schema.Schema{ - "name": { - Type: sdkv2_schema.TypeString, - Required: true, - ForceNew: true, - Description: "Name of the policy (must be unique)", - ValidateDiagFunc: shared_validator.StringIsNotEmpty, - }, - "description": { - Type: sdkv2_schema.TypeString, - Optional: true, - Description: "More verbose description of the policy", - }, - "type": { - Type: sdkv2_schema.TypeString, - Required: true, - Description: "Type of the policy", - ValidateDiagFunc: shared_validator.StringInSlice(false, "security", "license", "operational_risk"), - }, - "author": { - Type: sdkv2_schema.TypeString, - Computed: true, - Description: "User, who created the policy", - }, - "created": { - Type: sdkv2_schema.TypeString, - Computed: true, - Description: "Creation timestamp", - }, - "modified": { - Type: sdkv2_schema.TypeString, - Computed: true, - Description: "Modification timestamp", - }, - "rule": { - Type: sdkv2_schema.TypeSet, - Required: true, - Description: "A list of user-defined rules allowing you to trigger violations for specific vulnerability or license breaches by setting a license or security criteria, with a corresponding set of automatic actions according to your needs. Rules are processed according to the ascending order in which they are placed in the Rules list on the Policy. If a rule is met, the subsequent rules in the list will not be applied.", - Elem: &sdkv2_schema.Resource{ - Schema: map[string]*sdkv2_schema.Schema{ - "name": { - Type: sdkv2_schema.TypeString, - Required: true, - Description: "Name of the rule", - ValidateDiagFunc: shared_validator.StringIsNotEmpty, - }, - "priority": { - Type: sdkv2_schema.TypeInt, - Required: true, - ValidateDiagFunc: shared_validator.IntAtLeast(1), - Description: "Integer describing the rule priority. Must be at least 1", - }, - "criteria": { - Type: sdkv2_schema.TypeSet, - Required: true, - MinItems: 1, - MaxItems: 1, - Description: "The set of security conditions to examine when an scanned artifact is scanned.", - Elem: &sdkv2_schema.Resource{ - Schema: criteriaSchema, - }, - }, - "actions": { - Type: sdkv2_schema.TypeSet, - MaxItems: 1, - Required: true, - Description: "Specifies the actions to take once a security policy violation has been triggered.", - Elem: &sdkv2_schema.Resource{ - Schema: actionsSchema, - }, - }, - }, - }, - }, - }, - ) -} - var policyBlocks = func(criteriaAttrs map[string]schema.Attribute, criteriaBlocks map[string]schema.Block, actionsAttrs map[string]schema.Attribute, actionsBlocks map[string]schema.Block) map[string]schema.Block { return map[string]schema.Block{ "rule": schema.SetNestedBlock{ @@ -640,11 +471,11 @@ type PolicyExposuresAPIModel struct { type OperationalRiskCriteriaAPIModel struct { UseAndCondition bool `json:"use_and_condition"` IsEOL bool `json:"is_eol"` - ReleaseDateGreaterThanMonths int64 `json:"release_date_greater_than_months,omitempty"` - NewerVersionsGreaterThan int64 `json:"newer_versions_greater_than,omitempty"` - ReleaseCadencePerYearLessThan int64 `json:"release_cadence_per_year_less_than,omitempty"` - CommitsLessThan int64 `json:"commits_less_than,omitempty"` - CommittersLessThan int64 `json:"committers_less_than,omitempty"` + ReleaseDateGreaterThanMonths *int64 `json:"release_date_greater_than_months,omitempty"` + NewerVersionsGreaterThan *int64 `json:"newer_versions_greater_than,omitempty"` + ReleaseCadencePerYearLessThan *int64 `json:"release_cadence_per_year_less_than,omitempty"` + CommitsLessThan *int64 `json:"commits_less_than,omitempty"` + CommittersLessThan *int64 `json:"committers_less_than,omitempty"` Risk string `json:"risk,omitempty"` } @@ -978,583 +809,3 @@ func (r *PolicyResource) ImportState(ctx context.Context, req resource.ImportSta resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_key"), parts[1])...) } } - -func unpackPolicy(d *sdkv2_schema.ResourceData) (*PolicyAPIModel, error) { - policy := new(PolicyAPIModel) - - policy.Name = d.Get("name").(string) - if v, ok := d.GetOk("type"); ok { - policy.Type = v.(string) - } - if v, ok := d.GetOk("project_key"); ok { - policy.ProjectKey = v.(string) - } - if v, ok := d.GetOk("description"); ok { - policy.Description = v.(string) - } - if v, ok := d.GetOk("author"); ok { - policy.Author = v.(string) - } - policyRules, err := unpackRules(d.Get("rule").(*sdkv2_schema.Set), policy.Type) - policy.Rules = &policyRules - - return policy, err -} - -func unpackRules(configured *sdkv2_schema.Set, policyType string) (policyRules []PolicyRuleAPIModel, err error) { - var rules []PolicyRuleAPIModel - - for _, raw := range configured.List() { - rule := new(PolicyRuleAPIModel) - data := raw.(map[string]interface{}) - rule.Name = data["name"].(string) - rule.Priority = data["priority"].(int64) - - rule.Criteria, err = unpackCriteria(data["criteria"].(*sdkv2_schema.Set), policyType) - if v, ok := data["actions"]; ok { - rule.Actions = unpackActions(v.(*sdkv2_schema.Set)) - } - rules = append(rules, *rule) - } - - return rules, err -} - -func unpackSecurityCriteria(tfCriteria map[string]interface{}) *PolicyRuleCriteriaAPIModel { - criteria := new(PolicyRuleCriteriaAPIModel) - - if v, ok := tfCriteria["fix_version_dependant"]; ok { - criteria.FixVersionDependant = v.(bool) - } - if v, ok := tfCriteria["applicable_cves_only"]; ok { - criteria.ApplicableCVEsOnly = v.(bool) - } - if v, ok := tfCriteria["malicious_package"]; ok { - criteria.MaliciousPackage = v.(bool) - } - if v, ok := tfCriteria["vulnerability_ids"]; ok { - criteria.VulnerabilityIds = sdk.CastToStringArr(v.(*sdkv2_schema.Set).List()) - } - if _, ok := tfCriteria["exposures"]; ok { - criteria.Exposures = unpackExposures(tfCriteria["exposures"].([]interface{})) - } - if v, ok := tfCriteria["package_name"]; ok { - criteria.PackageName = v.(string) - } - if v, ok := tfCriteria["package_type"]; ok { - criteria.PackageType = v.(string) - } - if v, ok := tfCriteria["package_versions"]; ok { - criteria.PackageVersions = sdk.CastToStringArr(v.(*sdkv2_schema.Set).List()) - } - // This is also picky about not allowing empty values to be set - cvss := unpackCVSSRange(tfCriteria["cvss_range"].([]interface{})) - if cvss == nil { - criteria.MinimumSeverity = tfCriteria["min_severity"].(string) - } else { - criteria.CVSSRange = cvss - } - - return criteria -} - -func unpackLicenseCriteria(tfCriteria map[string]interface{}) *PolicyRuleCriteriaAPIModel { - criteria := new(PolicyRuleCriteriaAPIModel) - if v, ok := tfCriteria["allow_unknown"]; ok { - criteria.AllowUnknown = sdk.BoolPtr(v.(bool)) - } - if v, ok := tfCriteria["banned_licenses"]; ok { - criteria.BannedLicenses = unpackLicenses(v.(*sdkv2_schema.Set)) - } - if v, ok := tfCriteria["allowed_licenses"]; ok { - criteria.AllowedLicenses = unpackLicenses(v.(*sdkv2_schema.Set)) - } - if v, ok := tfCriteria["multi_license_permissive"]; ok { - criteria.MultiLicensePermissive = sdk.BoolPtr(v.(bool)) - } - - return criteria -} - -func unpackOperationalRiskCustomCriteria(tfCriteria map[string]interface{}) *OperationalRiskCriteriaAPIModel { - criteria := OperationalRiskCriteriaAPIModel{} - if v, ok := tfCriteria["use_and_condition"]; ok { - criteria.UseAndCondition = v.(bool) - } - if v, ok := tfCriteria["is_eol"]; ok { - criteria.IsEOL = v.(bool) - } - if v, ok := tfCriteria["release_date_greater_than_months"]; ok { - criteria.ReleaseDateGreaterThanMonths = v.(int64) - } - if v, ok := tfCriteria["newer_versions_greater_than"]; ok { - criteria.NewerVersionsGreaterThan = v.(int64) - } - if v, ok := tfCriteria["release_cadence_per_year_less_than"]; ok { - criteria.ReleaseCadencePerYearLessThan = v.(int64) - } - if v, ok := tfCriteria["commits_less_than"]; ok { - criteria.CommitsLessThan = v.(int64) - } - if v, ok := tfCriteria["committers_less_than"]; ok { - criteria.CommittersLessThan = v.(int64) - } - if v, ok := tfCriteria["risk"]; ok { - criteria.Risk = v.(string) - } - - return &criteria -} - -func unpackOperationalRiskCriteria(tfCriteria map[string]interface{}) *PolicyRuleCriteriaAPIModel { - criteria := new(PolicyRuleCriteriaAPIModel) - if v, ok := tfCriteria["op_risk_custom"]; ok { - custom := v.([]interface{}) - if len(custom) > 0 { - criteria.OperationalRiskCustom = unpackOperationalRiskCustomCriteria(custom[0].(map[string]interface{})) - } - } - if v, ok := tfCriteria["op_risk_min_risk"]; ok { - criteria.OperationalRiskMinRisk = v.(string) - } - - return criteria -} - -func unpackCriteria(d *sdkv2_schema.Set, policyType string) (*PolicyRuleCriteriaAPIModel, error) { - tfCriteria := d.List() - if len(tfCriteria) == 0 { - return nil, nil - } - - m := tfCriteria[0].(map[string]interface{}) // We made this a list of one to make schema validation easier - var criteria *PolicyRuleCriteriaAPIModel - // criteria := new(PolicyRuleCriteria) - // The API doesn't allow both severity and license criteria to be _set_, even if they have empty values - // So we have to figure out which group is actually empty and not even set it - if policyType == "license" { - criteria = unpackLicenseCriteria(m) - } else if policyType == "security" { - criteria = unpackSecurityCriteria(m) - } else if policyType == "operational_risk" { - criteria = unpackOperationalRiskCriteria(m) - } - - return criteria, nil -} - -func Float64Ptr(v float64) *float64 { return &v } - -func StringPtr(v string) *string { return &v } - -func unpackCVSSRange(l []interface{}) *PolicyCVSSRangeAPIModel { - if len(l) == 0 { - return nil - } - - m := l[0].(map[string]interface{}) - cvssrange := &PolicyCVSSRangeAPIModel{ - From: Float64Ptr(m["from"].(float64)), - To: Float64Ptr(m["to"].(float64)), - } - return cvssrange -} - -func unpackExposures(l []interface{}) *PolicyExposuresAPIModel { - if len(l) == 0 { - return nil - } - - m := l[0].(map[string]interface{}) - exposures := &PolicyExposuresAPIModel{ - MinSeverity: StringPtr(m["min_severity"].(string)), - Secrets: sdk.BoolPtr(m["secrets"].(bool)), - Applications: sdk.BoolPtr(m["applications"].(bool)), - Services: sdk.BoolPtr(m["services"].(bool)), - Iac: sdk.BoolPtr(m["iac"].(bool)), - } - return exposures -} - -func unpackLicenses(d *sdkv2_schema.Set) []string { - var licenses []string - for _, license := range d.List() { - licenses = append(licenses, license.(string)) - } - return licenses -} - -func unpackActions(l *sdkv2_schema.Set) PolicyRuleActionsAPIModel { - actions := PolicyRuleActionsAPIModel{} - policyActions := l.List() - - if len(policyActions) > 0 { - m := policyActions[0].(map[string]interface{}) // We made this a list of one to make schema validation easier - if v, ok := m["webhooks"]; ok { - m := v.(*sdkv2_schema.Set).List() - var webhooks []string - for _, hook := range m { - webhooks = append(webhooks, hook.(string)) - } - actions.Webhooks = webhooks - } - if v, ok := m["mails"]; ok { - m := v.(*sdkv2_schema.Set).List() - var mails []string - for _, mail := range m { - mails = append(mails, mail.(string)) - } - actions.Mails = mails - } - if v, ok := m["fail_build"]; ok { - actions.FailBuild = v.(bool) - } - - if v, ok := m["block_download"]; ok { - if len(v.(*sdkv2_schema.Set).List()) > 0 { - vList := v.(*sdkv2_schema.Set).List() - vMap := vList[0].(map[string]interface{}) - - actions.BlockDownload = BlockDownloadSettingsAPIModel{ - Unscanned: vMap["unscanned"].(bool), - Active: vMap["active"].(bool), - } - } else { - actions.BlockDownload = BlockDownloadSettingsAPIModel{ - Unscanned: false, - Active: false, - } - // Setting this false/false block feels like it _should_ work, since putting a false/false block in the terraform resource works fine - // However, it doesn't, and we end up getting this diff when running acceptance tests when this is optional in the schema - // rule.0.actions.0.block_download.#: "1" => "0" - // rule.0.actions.0.block_download.0.active: "false" => "" - // rule.0.actions.0.block_download.0.unscanned: "false" => "" - } - } - - if v, ok := m["block_release_bundle_distribution"]; ok { - actions.BlockReleaseBundleDistribution = v.(bool) - } - if v, ok := m["block_release_bundle_promotion"]; ok { - actions.BlockReleaseBundlePromotion = v.(bool) - } - if v, ok := m["notify_watch_recipients"]; ok { - actions.NotifyWatchRecipients = v.(bool) - } - if v, ok := m["notify_deployer"]; ok { - actions.NotifyDeployer = v.(bool) - } - if v, ok := m["create_ticket_enabled"]; ok { - actions.CreateJiraTicketEnabled = v.(bool) - } - if v, ok := m["build_failure_grace_period_in_days"]; ok { - actions.FailureGracePeriodDays = v.(int64) - } - if v, ok := m["custom_severity"]; ok { - actions.CustomSeverity = v.(string) - } - - return actions - } - return actions -} - -func packRules(rules []PolicyRuleAPIModel, policyType string) []interface{} { - var rs []interface{} - - for _, rule := range rules { - var criteria []interface{} - var isLicense bool - - switch policyType { - case "license": - criteria = packLicenseCriteria(rule.Criteria) - isLicense = true - case "security": - criteria = packSecurityCriteria(rule.Criteria) - isLicense = false - case "operational_risk": - criteria = packOperationalRiskCriteria(rule.Criteria) - isLicense = false - } - - r := map[string]interface{}{ - "name": rule.Name, - "priority": rule.Priority, - "criteria": criteria, - "actions": packActions(rule.Actions, isLicense), - } - - rs = append(rs, r) - } - - return rs -} - -func packOperationalRiskCriteria(criteria *PolicyRuleCriteriaAPIModel) []interface{} { - m := map[string]interface{}{} - - if len(criteria.OperationalRiskMinRisk) > 0 { - m["op_risk_min_risk"] = criteria.OperationalRiskMinRisk - } - if criteria.OperationalRiskCustom != nil { - m["op_risk_custom"] = packOperationalRiskCustom(criteria.OperationalRiskCustom) - } - - return []interface{}{m} -} - -func packOperationalRiskCustom(custom *OperationalRiskCriteriaAPIModel) []interface{} { - m := map[string]interface{}{ - "use_and_condition": custom.UseAndCondition, - "is_eol": custom.IsEOL, - "release_date_greater_than_months": custom.ReleaseDateGreaterThanMonths, - "newer_versions_greater_than": custom.NewerVersionsGreaterThan, - "release_cadence_per_year_less_than": custom.ReleaseCadencePerYearLessThan, - "commits_less_than": custom.CommitsLessThan, - "committers_less_than": custom.CommittersLessThan, - "risk": custom.Risk, - } - - return []interface{}{m} -} - -func packLicenseCriteria(criteria *PolicyRuleCriteriaAPIModel) []interface{} { - - m := map[string]interface{}{} - - if criteria.BannedLicenses != nil { - m["banned_licenses"] = criteria.BannedLicenses - } - if criteria.AllowedLicenses != nil { - m["allowed_licenses"] = criteria.AllowedLicenses - } - m["allow_unknown"] = criteria.AllowUnknown - m["multi_license_permissive"] = criteria.MultiLicensePermissive - - return []interface{}{m} -} - -func packSecurityCriteria(criteria *PolicyRuleCriteriaAPIModel) []interface{} { - m := map[string]interface{}{} - // cvss_range and min_severity are conflicting, only one can be present in the JSON - m["cvss_range"] = packCVSSRange(criteria.CVSSRange) - m["vulnerability_ids"] = criteria.VulnerabilityIds - minSeverity := criteria.MinimumSeverity - // This is only needed for versions before 3.60.2 because a Xray API bug where it returns "Unknown" for "All severities" min severity setting - // See release note: https://www.jfrog.com/confluence/display/JFROG/Xray+Release+Notes#XrayReleaseNotes-Xray3.60.2 - // Issue: XRAY-9271 - if criteria.MinimumSeverity == "Unknown" { - minSeverity = "All severities" - } - m["min_severity"] = minSeverity - m["fix_version_dependant"] = criteria.FixVersionDependant - m["applicable_cves_only"] = criteria.ApplicableCVEsOnly - m["malicious_package"] = criteria.MaliciousPackage - m["exposures"] = packExposures(criteria.Exposures) - m["package_name"] = criteria.PackageName - m["package_type"] = criteria.PackageType - m["package_versions"] = criteria.PackageVersions - - return []interface{}{m} -} - -func packCVSSRange(cvss *PolicyCVSSRangeAPIModel) []interface{} { - if cvss == nil { - return []interface{}{} - } - m := map[string]interface{}{ - "from": *cvss.From, - "to": *cvss.To, - } - return []interface{}{m} -} - -func packExposures(exposures *PolicyExposuresAPIModel) []interface{} { - if exposures == nil { - return []interface{}{} - } - m := map[string]interface{}{ - "min_severity": *exposures.MinSeverity, - "secrets": *exposures.Secrets, - "applications": *exposures.Applications, - "services": *exposures.Services, - "iac": *exposures.Iac, - } - return []interface{}{m} -} - -func packActions(actions PolicyRuleActionsAPIModel, license bool) []interface{} { - m := map[string]interface{}{ - "block_download": packBlockDownload(actions.BlockDownload), - "webhooks": actions.Webhooks, - "mails": actions.Mails, - "fail_build": actions.FailBuild, - "block_release_bundle_distribution": actions.BlockReleaseBundleDistribution, - "block_release_bundle_promotion": actions.BlockReleaseBundlePromotion, - "notify_watch_recipients": actions.NotifyWatchRecipients, - "notify_deployer": actions.NotifyDeployer, - "create_ticket_enabled": actions.CreateJiraTicketEnabled, - "build_failure_grace_period_in_days": actions.FailureGracePeriodDays, - } - - if license { - m["custom_severity"] = actions.CustomSeverity - } - - return []interface{}{m} -} - -func packBlockDownload(bd BlockDownloadSettingsAPIModel) []interface{} { - m := map[string]interface{}{} - m["unscanned"] = bd.Unscanned - m["active"] = bd.Active - return []interface{}{m} -} - -func packPolicy(policy PolicyAPIModel, d *sdkv2_schema.ResourceData) sdkv2_diag.Diagnostics { - if err := d.Set("name", policy.Name); err != nil { - return sdkv2_diag.FromErr(err) - } - if err := d.Set("type", policy.Type); err != nil { - return sdkv2_diag.FromErr(err) - } - if len(policy.Description) > 0 { - if err := d.Set("description", policy.Description); err != nil { - return sdkv2_diag.FromErr(err) - } - } - if err := d.Set("author", policy.Author); err != nil { - return sdkv2_diag.FromErr(err) - } - if err := d.Set("created", policy.Created); err != nil { - return sdkv2_diag.FromErr(err) - } - if err := d.Set("modified", policy.Modified); err != nil { - return sdkv2_diag.FromErr(err) - } - if policy.Rules != nil { - if err := d.Set("rule", packRules(*policy.Rules, policy.Type)); err != nil { - return sdkv2_diag.FromErr(err) - } - } - - return nil -} - -func resourceXrayPolicyCreate(ctx context.Context, d *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { - policy, err := unpackPolicy(d) - // Warning or errors can be collected in a slice type - if err != nil { - return sdkv2_diag.FromErr(err) - } - - req, err := getRestyRequest(m.(util.ProviderMetadata).Client, policy.ProjectKey) - if err != nil { - return sdkv2_diag.FromErr(err) - } - - var policyError PolicyError - resp, err := req. - SetBody(policy). - SetError(&policyError). - Post("xray/api/v2/policies") - if err != nil { - return sdkv2_diag.FromErr(err) - } - if resp.IsError() { - return sdkv2_diag.Errorf("%s", policyError.Error) - } - - d.SetId(policy.Name) - return resourceXrayPolicyRead(ctx, d, m) -} - -func resourceXrayPolicyRead(ctx context.Context, d *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { - var policy PolicyAPIModel - - projectKey := d.Get("project_key").(string) - req, err := getRestyRequest(m.(util.ProviderMetadata).Client, projectKey) - if err != nil { - return sdkv2_diag.FromErr(err) - } - - var policyError PolicyError - resp, err := req. - SetResult(&policy). - SetPathParam("name", d.Id()). - SetError(&policyError). - Get("xray/api/v2/policies/{name}") - if err != nil { - return sdkv2_diag.FromErr(err) - } - if resp.StatusCode() == http.StatusNotFound { - d.SetId("") - return sdkv2_diag.Errorf("policy (%s) not found, removing from state", d.Id()) - } - if resp.IsError() { - return sdkv2_diag.Errorf("%s", policyError.Error) - } - - return packPolicy(policy, d) -} - -func resourceXrayPolicyUpdate(ctx context.Context, d *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { - policy, err := unpackPolicy(d) - if err != nil { - return sdkv2_diag.FromErr(err) - } - - req, err := getRestyRequest(m.(util.ProviderMetadata).Client, policy.ProjectKey) - if err != nil { - return sdkv2_diag.FromErr(err) - } - - var policyError PolicyError - resp, err := req. - SetBody(policy). - SetPathParams(map[string]string{ - "name": d.Id(), - }). - SetError(&policyError). - Put("xray/api/v2/policies/{name}") - if err != nil { - return sdkv2_diag.FromErr(err) - } - if resp.IsError() { - return sdkv2_diag.Errorf("%s", policyError.Error) - } - - d.SetId(policy.Name) - return resourceXrayPolicyRead(ctx, d, m) -} - -func resourceXrayPolicyDelete(ctx context.Context, d *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { - policy, err := unpackPolicy(d) - if err != nil { - return sdkv2_diag.FromErr(err) - } - - req, err := getRestyRequest(m.(util.ProviderMetadata).Client, policy.ProjectKey) - if err != nil { - return sdkv2_diag.FromErr(err) - } - - var policyError PolicyError - resp, err := req. - SetPathParams(map[string]string{ - "name": d.Id(), - }). - SetError(&policyError). - Delete("xray/api/v2/policies/{name}") - if err != nil { - return sdkv2_diag.FromErr(err) - } - if resp.IsError() { - return sdkv2_diag.Errorf("%s", policyError.Error) - } - - d.SetId("") - - return nil -} diff --git a/pkg/xray/resource/resource_xray_ignore_rule_test.go b/pkg/xray/resource/resource_xray_ignore_rule_test.go index 762eca0d..ff43b8f7 100644 --- a/pkg/xray/resource/resource_xray_ignore_rule_test.go +++ b/pkg/xray/resource/resource_xray_ignore_rule_test.go @@ -207,7 +207,6 @@ func TestAccIgnoreRule_scopes_policies(t *testing.T) { min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true @@ -275,7 +274,6 @@ func TestAccIgnoreRule_scopes_watches_policies(t *testing.T) { min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true @@ -361,7 +359,6 @@ func TestAccIgnoreRule_scopes_no_expiration_policies(t *testing.T) { min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true @@ -421,7 +418,6 @@ func TestAccIgnoreRule_scopes_no_expiration_watches(t *testing.T) { min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true diff --git a/pkg/xray/resource/resource_xray_operational_risk_policy.go b/pkg/xray/resource/resource_xray_operational_risk_policy.go index 711d3ca3..b1ac5bde 100644 --- a/pkg/xray/resource/resource_xray_operational_risk_policy.go +++ b/pkg/xray/resource/resource_xray_operational_risk_policy.go @@ -2,120 +2,328 @@ package xray import ( "context" - "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - "github.com/jfrog/terraform-provider-shared/validator" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/jfrog/terraform-provider-shared/util" ) -func ResourceXrayOperationalRiskPolicy() *schema.Resource { +var _ resource.Resource = &OperationalRiskPolicyResource{} - var criteriaDiff = func(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { - rules := diff.Get("rule").(*schema.Set).List() - if len(rules) == 0 { - return nil - } +func NewOperationalRiskPolicyResource() resource.Resource { + return &OperationalRiskPolicyResource{ + PolicyResource: PolicyResource{ + TypeName: "xray_operational_risk_policy", + }, + } +} - criteria := rules[0].(map[string]interface{})["criteria"].(*schema.Set).List() - if len(criteria) == 0 { - return nil - } +type OperationalRiskPolicyResource struct { + PolicyResource +} + +func (r OperationalRiskPolicyResource) toCriteriaAPIModel(ctx context.Context, criteriaElems []attr.Value) (*PolicyRuleCriteriaAPIModel, diag.Diagnostics) { + diags := diag.Diagnostics{} - criterion := criteria[0].(map[string]interface{}) + var criteria *PolicyRuleCriteriaAPIModel + if len(criteriaElems) > 0 { + attrs := criteriaElems[0].(types.Object).Attributes() - minRisk := criterion["op_risk_min_risk"].(string) - customCriteria := criterion["op_risk_custom"].([]interface{}) + var opRiskCustom *OperationalRiskCriteriaAPIModel + customElem := attrs["op_risk_custom"].(types.List).Elements() + if len(customElem) > 0 { + attrs := customElem[0].(types.Object).Attributes() - if len(minRisk) > 0 && len(customCriteria) > 0 { - return fmt.Errorf("attribute 'op_risk_min_risk' cannot be set together with 'op_risk_custom'") + opRiskCustom = &OperationalRiskCriteriaAPIModel{ + UseAndCondition: attrs["use_and_condition"].(types.Bool).ValueBool(), + IsEOL: attrs["is_eol"].(types.Bool).ValueBool(), + ReleaseDateGreaterThanMonths: attrs["release_date_greater_than_months"].(types.Int64).ValueInt64Pointer(), + NewerVersionsGreaterThan: attrs["newer_versions_greater_than"].(types.Int64).ValueInt64Pointer(), + ReleaseCadencePerYearLessThan: attrs["release_cadence_per_year_less_than"].(types.Int64).ValueInt64Pointer(), + CommitsLessThan: attrs["commits_less_than"].(types.Int64).ValueInt64Pointer(), + CommittersLessThan: attrs["committers_less_than"].(types.Int64).ValueInt64Pointer(), + Risk: attrs["risk"].(types.String).ValueString(), + } } - return nil + criteria = &PolicyRuleCriteriaAPIModel{ + OperationalRiskMinRisk: attrs["op_risk_min_risk"].(types.String).ValueString(), + OperationalRiskCustom: opRiskCustom, + } } - var criteriaSchema = map[string]*schema.Schema{ - "op_risk_min_risk": { - Type: schema.TypeString, - Optional: true, - Description: "The minimum operational risk that will be impacted by the policy: High, Medium, Low", - ValidateDiagFunc: validator.StringInSlice(true, "High", "Medium", "Low"), + return criteria, diags +} + +func (r OperationalRiskPolicyResource) toAPIModel(ctx context.Context, plan PolicyResourceModel, policy *PolicyAPIModel) diag.Diagnostics { + return plan.toAPIModel(ctx, policy, r.toCriteriaAPIModel, toActionsAPIModel) +} + +func (r *OperationalRiskPolicyResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = r.TypeName +} + +var opRiskPolicyCriteriaAttrs = map[string]schema.Attribute{ + "op_risk_min_risk": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.OneOfCaseInsensitive("High", "Medium", "Low"), + stringvalidator.ConflictsWith( + path.MatchRelative().AtParent().AtName("op_risk_custom"), + ), }, - "op_risk_custom": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Description: "Custom Condition", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "use_and_condition": { - Type: schema.TypeBool, - Required: true, - Description: "Use 'AND' between conditions (true) or 'OR' condition (false)", - }, - "is_eol": { - Type: schema.TypeBool, - Optional: true, - Default: false, - Description: "Is End-of-Life?", - }, - "release_date_greater_than_months": { - Type: schema.TypeInt, - Optional: true, - Description: "Release age greater than (in months): 6, 12, 18, 24, 30, or 36", - ValidateDiagFunc: validation.ToDiagFunc(validation.IntInSlice([]int{6, 12, 18, 24, 30, 36})), + Description: "The minimum operational risk that will be impacted by the policy: High, Medium, Low", + }, +} + +var opRiskPolicyCriteriaBlocks = map[string]schema.Block{ + "op_risk_custom": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "use_and_condition": schema.BoolAttribute{ + Required: true, + MarkdownDescription: "Use `AND` between conditions (true) or `OR` condition (false)", + }, + "is_eol": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + MarkdownDescription: "Is End-of-Life?", + }, + "release_date_greater_than_months": schema.Int64Attribute{ + Optional: true, + Validators: []validator.Int64{ + int64validator.OneOf(6, 12, 18, 24, 30, 36), }, - "newer_versions_greater_than": { - Type: schema.TypeInt, - Optional: true, - Description: "Number of releases since greater than: 1, 2, 3, 4, or 5", - ValidateDiagFunc: validation.ToDiagFunc(validation.IntInSlice([]int{1, 2, 3, 4, 5})), + Description: "Release age greater than (in months): 6, 12, 18, 24, 30, or 36", + }, + "newer_versions_greater_than": schema.Int64Attribute{ + Optional: true, + Validators: []validator.Int64{ + int64validator.OneOf(1, 2, 3, 4, 5), }, - "release_cadence_per_year_less_than": { - Type: schema.TypeInt, - Optional: true, - Description: "Release cadence less than per year: 1, 2, 3, 4, or 5", - ValidateDiagFunc: validation.ToDiagFunc(validation.IntInSlice([]int{1, 2, 3, 4, 5})), + Description: "Number of releases since greater than: 1, 2, 3, 4, or 5", + }, + "release_cadence_per_year_less_than": schema.Int64Attribute{ + Optional: true, + Validators: []validator.Int64{ + int64validator.OneOf(1, 2, 3, 4, 5), }, - "commits_less_than": { - Type: schema.TypeInt, - Optional: true, - Description: "Number of commits less than per year: 10, 25, 50, or 100", - ValidateDiagFunc: validation.ToDiagFunc(validation.IntInSlice([]int{10, 25, 50, 100})), + Description: "Release cadence less than per year: 1, 2, 3, 4, or 5", + }, + "commits_less_than": schema.Int64Attribute{ + Optional: true, + Validators: []validator.Int64{ + int64validator.OneOf(10, 25, 50, 100), }, - "committers_less_than": { - Type: schema.TypeInt, - Optional: true, - Description: "Number of committers less than per year: 1, 2, 3, 4, or 5", - ValidateDiagFunc: validation.ToDiagFunc(validation.IntInSlice([]int{1, 2, 3, 4, 5})), + Description: "Number of commits less than per year: 10, 25, 50, or 100", + }, + "committers_less_than": schema.Int64Attribute{ + Optional: true, + Validators: []validator.Int64{ + int64validator.OneOf(1, 2, 3, 4, 5), }, - "risk": { - Type: schema.TypeString, - Optional: true, - Default: "low", - Description: "Risk severity: low, medium, high", - ValidateDiagFunc: validator.StringInSlice(true, "high", "medium", "low"), + Description: "Number of committers less than per year: 1, 2, 3, 4, or 5", + }, + "risk": schema.StringAttribute{ + Optional: true, + Computed: true, + Default: stringdefault.StaticString("low"), + Validators: []validator.String{ + stringvalidator.OneOfCaseInsensitive("high", "medium", "low"), }, + Description: "Risk severity: low, medium, high", }, }, }, + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + listvalidator.ConflictsWith( + path.MatchRelative().AtParent().AtName("op_risk_min_risk"), + ), + }, + Description: "Custom Condition", + }, +} + +var opRiskCustomAttrType = map[string]attr.Type{ + "use_and_condition": types.BoolType, + "is_eol": types.BoolType, + "release_date_greater_than_months": types.Int64Type, + "newer_versions_greater_than": types.Int64Type, + "release_cadence_per_year_less_than": types.Int64Type, + "commits_less_than": types.Int64Type, + "committers_less_than": types.Int64Type, + "risk": types.StringType, +} + +var opRiskCustomElementType = types.ObjectType{ + AttrTypes: opRiskCustomAttrType, +} + +var opRiskCriteriaAttrTypes = map[string]attr.Type{ + "op_risk_min_risk": types.StringType, + "op_risk_custom": types.ListType{ElemType: opRiskCustomElementType}, +} + +var opRiskCriteriaSetElementType = types.ObjectType{ + AttrTypes: opRiskCriteriaAttrTypes, +} + +func (r *OperationalRiskPolicyResource) fromCriteriaAPIModel(ctx context.Context, criteraAPIModel *PolicyRuleCriteriaAPIModel) (types.Set, diag.Diagnostics) { + diags := diag.Diagnostics{} + + criteriaSet := types.SetNull(opRiskCriteriaSetElementType) + if criteraAPIModel != nil { + minRisk := types.StringNull() + if criteraAPIModel.OperationalRiskMinRisk != "" { + minRisk = types.StringValue(criteraAPIModel.OperationalRiskMinRisk) + } + + customList := types.ListNull(opRiskCustomElementType) + if criteraAPIModel.OperationalRiskCustom != nil { + risk := types.StringNull() + if criteraAPIModel.OperationalRiskCustom.Risk != "" { + risk = types.StringValue(criteraAPIModel.OperationalRiskCustom.Risk) + } + + releaseDateGreaterThanMonths := types.Int64Null() + if criteraAPIModel.OperationalRiskCustom.ReleaseDateGreaterThanMonths != nil { + releaseDateGreaterThanMonths = types.Int64PointerValue(criteraAPIModel.OperationalRiskCustom.ReleaseDateGreaterThanMonths) + } + + newerVersionsGreaterThan := types.Int64Null() + if criteraAPIModel.OperationalRiskCustom.NewerVersionsGreaterThan != nil { + newerVersionsGreaterThan = types.Int64PointerValue(criteraAPIModel.OperationalRiskCustom.NewerVersionsGreaterThan) + } + + releaseCadencePerYearLessThan := types.Int64Null() + if criteraAPIModel.OperationalRiskCustom.ReleaseCadencePerYearLessThan != nil { + releaseCadencePerYearLessThan = types.Int64PointerValue(criteraAPIModel.OperationalRiskCustom.ReleaseCadencePerYearLessThan) + } + + commitsLessThan := types.Int64Null() + if criteraAPIModel.OperationalRiskCustom.CommitsLessThan != nil { + commitsLessThan = types.Int64PointerValue(criteraAPIModel.OperationalRiskCustom.CommitsLessThan) + } + + committersLessThan := types.Int64Null() + if criteraAPIModel.OperationalRiskCustom.CommittersLessThan != nil { + committersLessThan = types.Int64PointerValue(criteraAPIModel.OperationalRiskCustom.CommittersLessThan) + } + + custom, d := types.ObjectValue( + opRiskCustomAttrType, + map[string]attr.Value{ + "use_and_condition": types.BoolValue(criteraAPIModel.OperationalRiskCustom.UseAndCondition), + "is_eol": types.BoolValue(criteraAPIModel.OperationalRiskCustom.IsEOL), + "release_date_greater_than_months": releaseDateGreaterThanMonths, + "newer_versions_greater_than": newerVersionsGreaterThan, + "release_cadence_per_year_less_than": releaseCadencePerYearLessThan, + "commits_less_than": commitsLessThan, + "committers_less_than": committersLessThan, + "risk": risk, + }, + ) + if d.HasError() { + diags.Append(d...) + } + + c, d := types.ListValue( + opRiskCustomElementType, + []attr.Value{custom}, + ) + if d.HasError() { + diags.Append(d...) + } + + customList = c + } + + criteria, d := types.ObjectValue( + opRiskCriteriaAttrTypes, + map[string]attr.Value{ + "op_risk_min_risk": minRisk, + "op_risk_custom": customList, + }, + ) + if d.HasError() { + diags.Append(d...) + } + cs, d := types.SetValue( + opRiskCriteriaSetElementType, + []attr.Value{criteria}, + ) + if d.HasError() { + diags.Append(d...) + } + + criteriaSet = cs } - return &schema.Resource{ - SchemaVersion: 1, - CreateContext: resourceXrayPolicyCreate, - ReadContext: resourceXrayPolicyRead, - UpdateContext: resourceXrayPolicyUpdate, - DeleteContext: resourceXrayPolicyDelete, + return criteriaSet, diags +} + +var opRiskRuleAttrTypes = map[string]attr.Type{ + "name": types.StringType, + "priority": types.Int64Type, + "criteria": types.SetType{ElemType: opRiskCriteriaSetElementType}, + "actions": types.SetType{ElemType: actionsSetElementType}, +} + +var opRiskRuleSetElementType = types.ObjectType{ + AttrTypes: opRiskRuleAttrTypes, +} + +func (r OperationalRiskPolicyResource) fromAPIModel(ctx context.Context, policy PolicyAPIModel, plan *PolicyResourceModel) diag.Diagnostics { + return plan.fromAPIModel(ctx, policy, r.fromCriteriaAPIModel, fromActionsAPIModel) +} + +func (r *OperationalRiskPolicyResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Version: 1, + Attributes: policySchemaAttrs, + Blocks: policyBlocks(opRiskPolicyCriteriaAttrs, opRiskPolicyCriteriaBlocks, commonActionsAttrs, commonActionsBlocks), Description: "Creates an Xray policy using V2 of the underlying APIs. Please note: " + "It's only compatible with Bearer token auth method (Identity and Access => Access Tokens)", + } +} - Importer: &schema.ResourceImporter{ - StateContext: resourceImporterForProjectKey, - }, +func (r *OperationalRiskPolicyResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + r.ProviderData = req.ProviderData.(util.ProviderMetadata) +} - CustomizeDiff: criteriaDiff, +func (r *OperationalRiskPolicyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + r.PolicyResource.Create(ctx, r.toAPIModel, r.fromAPIModel, req, resp) +} - Schema: getPolicySchema(criteriaSchema, commonActionsSchema), - } +func (r *OperationalRiskPolicyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + r.PolicyResource.Read(ctx, r.fromAPIModel, req, resp) +} + +func (r *OperationalRiskPolicyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + r.PolicyResource.Update(ctx, r.toAPIModel, r.fromAPIModel, req, resp) +} + +func (r *OperationalRiskPolicyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + r.PolicyResource.Delete(ctx, req, resp) +} + +// ImportState imports the resource into the Terraform state. +func (r *OperationalRiskPolicyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + r.PolicyResource.ImportState(ctx, req, resp) } diff --git a/pkg/xray/resource/resource_xray_operational_risk_policy_test.go b/pkg/xray/resource/resource_xray_operational_risk_policy_test.go index d53a4a0f..3cc09d39 100644 --- a/pkg/xray/resource/resource_xray_operational_risk_policy_test.go +++ b/pkg/xray/resource/resource_xray_operational_risk_policy_test.go @@ -5,7 +5,6 @@ import ( "regexp" "testing" - "github.com/go-resty/resty/v2" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/jfrog/terraform-provider-shared/testutil" "github.com/jfrog/terraform-provider-shared/util" @@ -30,15 +29,91 @@ var testDataOperationalRisk = map[string]string{ "block_active": "true", } +func TestAccOperationalRiskPolicy_UpgradeFromSDKv2(t *testing.T) { + _, fqrn, resourceName := testutil.MkNames("policy-", "xray_operational_risk_policy") + + template := ` + resource "xray_operational_risk_policy" "{{ .resource_name }}" { + name = "{{ .policy_name }}" + description = "{{ .policy_description }}" + type = "operational_risk" + + rule { + name = "{{ .rule_name }}" + priority = 1 + criteria { + op_risk_min_risk = "{{ .op_risk_min_risk }}" + } + actions { + block_release_bundle_distribution = {{ .block_release_bundle_distribution }} + block_release_bundle_promotion = {{ .block_release_bundle_promotion }} + fail_build = {{ .fail_build }} + notify_watch_recipients = {{ .notify_watch_recipients }} + notify_deployer = {{ .notify_deployer }} + create_ticket_enabled = {{ .create_ticket_enabled }} + build_failure_grace_period_in_days = {{ .grace_period_days }} + block_download { + unscanned = {{ .block_unscanned }} + active = {{ .block_active }} + } + } + } + }` + + testData := sdk.MergeMaps(testDataOperationalRisk) + testData["resource_name"] = resourceName + testData["policy_name"] = fmt.Sprintf("terraform-operational-risk-policy-%d", testutil.RandomInt()) + testData["op_risk_min_risk"] = "Medium" + + config := util.ExecuteTemplate(fqrn, template, testData) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: acctest.VerifyDeleted(fqrn, "", acctest.CheckPolicy), + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "xray": { + Source: "jfrog/xray", + VersionConstraint: "2.11.0", + }, + }, + Config: config, + Check: resource.ComposeTestCheckFunc( + verifyOpertionalRiskPolicy(fqrn, testData), + ), + }, + { + ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + ResourceName: fqrn, + ImportState: true, + ImportStateId: testData["policy_name"], + ImportStateVerify: true, + }, + }, + }) +} + func TestAccOperationalRiskPolicy_withProjectKey(t *testing.T) { _, fqrn, resourceName := testutil.MkNames("policy-", "xray_operational_risk_policy") projectKey := fmt.Sprintf("testproj%d", testutil.RandSelect(1, 2, 3, 4, 5)) - template := `resource "xray_operational_risk_policy" "{{ .resource_name }}" { + template := ` + resource "project" "{{ .project_key }}" { + key = "{{ .project_key }}" + display_name = "{{ .project_key }}" + admin_privileges { + manage_members = true + manage_resources = true + index_resources = true + } + } + + resource "xray_operational_risk_policy" "{{ .resource_name }}" { name = "{{ .policy_name }}" description = "{{ .policy_description }}" type = "operational_risk" - project_key = "{{ .project_key }}" + project_key = project.{{ .project_key }}.key rule { name = "{{ .rule_name }}" @@ -75,14 +150,13 @@ func TestAccOperationalRiskPolicy_withProjectKey(t *testing.T) { updatedConfig := util.ExecuteTemplate(fqrn, template, updatedTestData) resource.Test(t, resource.TestCase{ - PreCheck: func() { - acctest.PreCheck(t) - acctest.CreateProject(t, projectKey) + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: acctest.VerifyDeleted(fqrn, "", acctest.CheckPolicy), + ExternalProviders: map[string]resource.ExternalProvider{ + "project": { + Source: "jfrog/project", + }, }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", func(id string, request *resty.Request) (*resty.Response, error) { - acctest.DeleteProject(t, projectKey) - return acctest.CheckPolicy(id, request) - }), ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, Steps: []resource.TestStep{ { @@ -286,7 +360,8 @@ func TestAccOperationalRiskPolicy_customCriteria(t *testing.T) { func TestAccOperationalRiskPolicy_customCriteria_migration(t *testing.T) { _, fqrn, resourceName := testutil.MkNames("policy-", "xray_operational_risk_policy") - const opertionalRiskPolicyCustom = `resource "xray_operational_risk_policy" "{{ .resource_name }}" { + const opertionalRiskPolicyCustom = ` + resource "xray_operational_risk_policy" "{{ .resource_name }}" { name = "{{ .policy_name }}" description = "{{ .policy_description }}" type = "operational_risk" @@ -295,9 +370,9 @@ func TestAccOperationalRiskPolicy_customCriteria_migration(t *testing.T) { priority = 1 criteria { op_risk_custom { - use_and_condition = {{ .op_risk_custom_use_and_condition }} - is_eol = {{ .op_risk_custom_is_eol }} - risk = "{{ .op_risk_custom_risk }}" + use_and_condition = {{ .op_risk_custom_use_and_condition }} + is_eol = {{ .op_risk_custom_is_eol }} + risk = "{{ .op_risk_custom_risk }}" } } actions { @@ -437,7 +512,7 @@ func TestAccOperationalRiskPolicy_criteriaValidation(t *testing.T) { Steps: []resource.TestStep{ { Config: config, - ExpectError: regexp.MustCompile("attribute 'op_risk_min_risk' cannot be set together with 'op_risk_custom'"), + ExpectError: regexp.MustCompile("(?s).*Invalid Attribute Combination.*op_risk_custom.*cannot be specified when.*op_risk_custom.*is specified.*"), }, }, }) diff --git a/pkg/xray/resource/resource_xray_security_policy.go b/pkg/xray/resource/resource_xray_security_policy.go index e2f68581..df87d9a0 100644 --- a/pkg/xray/resource/resource_xray_security_policy.go +++ b/pkg/xray/resource/resource_xray_security_policy.go @@ -223,7 +223,7 @@ func (r *SecurityPolicyResource) fromCriteriaAPIModel(ctx context.Context, crite } func (r SecurityPolicyResource) fromAPIModel(ctx context.Context, policy PolicyAPIModel, plan *PolicyResourceModel) diag.Diagnostics { - return plan.fromAPIModel(ctx, policy, r.fromCriteriaAPIModel, plan.fromActionsAPIModel) + return plan.fromAPIModel(ctx, policy, r.fromCriteriaAPIModel, fromActionsAPIModel) } var cvssRangeAttrType = map[string]attr.Type{ diff --git a/pkg/xray/resource/util.go b/pkg/xray/resource/util.go index 71a0c914..211b7b29 100644 --- a/pkg/xray/resource/util.go +++ b/pkg/xray/resource/util.go @@ -1,9 +1,7 @@ package xray import ( - "context" "fmt" - "strings" "github.com/go-resty/resty/v2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -36,14 +34,3 @@ var getProjectKeySchema = func(isForceNew bool, additionalDescription string) ma }, } } - -var resourceImporterForProjectKey = func(_ context.Context, d *schema.ResourceData, _ any) ([]*schema.ResourceData, error) { - parts := strings.SplitN(d.Id(), ":", 2) - - if len(parts) == 2 && parts[0] != "" && parts[1] != "" { - d.SetId(parts[0]) - d.Set("project_key", parts[1]) - } - - return []*schema.ResourceData{d}, nil -} From 47eff43068e54a18c5dcc96d0af34ea06ee2cf6b Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Tue, 10 Sep 2024 13:18:18 -0700 Subject: [PATCH 05/11] Update Workers Count resource documentation --- docs/resources/workers_count.md | 69 ++++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 10 deletions(-) diff --git a/docs/resources/workers_count.md b/docs/resources/workers_count.md index 76428976..42dfc47b 100644 --- a/docs/resources/workers_count.md +++ b/docs/resources/workers_count.md @@ -48,26 +48,22 @@ resource "xray_workers_count" "workers-count" { ### Optional -- `alert` (Block Set) The number of workers managing alerts. (see [below for nested schema](#nestedblock--alert)) - `analysis` (Block Set) The number of workers involved in scanning analysis. (see [below for nested schema](#nestedblock--analysis)) - `impact_analysis` (Block Set) The number of workers involved in Impact Analysis to determine how a component with a reported issue impacts others in the system. (see [below for nested schema](#nestedblock--impact_analysis)) - `index` (Block Set) The number of workers managing indexing of artifacts. (see [below for nested schema](#nestedblock--index)) +- `migration_sbom` (Block Set) The number of workers managing SBOM migration. (see [below for nested schema](#nestedblock--migration_sbom)) - `notification` (Block Set) The number of workers managing notifications. (see [below for nested schema](#nestedblock--notification)) +- `panoramic` (Block Set) The number of workers managing panoramic. (see [below for nested schema](#nestedblock--panoramic)) - `persist` (Block Set) The number of workers managing persistent storage needed to build the artifact relationship graph. (see [below for nested schema](#nestedblock--persist)) +- `policy_enforcer` (Block Set) The number of workers managing policy enforcer. (see [below for nested schema](#nestedblock--policy_enforcer)) +- `sbom` (Block Set) The number of workers managing SBOM. (see [below for nested schema](#nestedblock--sbom)) +- `sbom_impact_analysis` (Block Set) The number of workers managing SBOM impact analysis. (see [below for nested schema](#nestedblock--sbom_impact_analysis)) +- `user_catalog` (Block Set) The number of workers managing user catalog. (see [below for nested schema](#nestedblock--user_catalog)) ### Read-Only - `id` (String) The ID of this resource. - -### Nested Schema for `alert` - -Required: - -- `existing_content` (Number) Number of workers for existing content -- `new_content` (Number) Number of workers for new content - - ### Nested Schema for `analysis` @@ -94,6 +90,15 @@ Required: - `new_content` (Number) Number of workers for new content + +### Nested Schema for `migration_sbom` + +Required: + +- `existing_content` (Number) Number of workers for existing content +- `new_content` (Number) Number of workers for new content + + ### Nested Schema for `notification` @@ -102,6 +107,14 @@ Required: - `new_content` (Number) Number of workers for new content + +### Nested Schema for `panoramic` + +Required: + +- `new_content` (Number) Number of workers for new content + + ### Nested Schema for `persist` @@ -110,6 +123,42 @@ Required: - `existing_content` (Number) Number of workers for existing content - `new_content` (Number) Number of workers for new content + + +### Nested Schema for `policy_enforcer` + +Required: + +- `existing_content` (Number) Number of workers for existing content +- `new_content` (Number) Number of workers for new content + + + +### Nested Schema for `sbom` + +Required: + +- `existing_content` (Number) Number of workers for existing content +- `new_content` (Number) Number of workers for new content + + + +### Nested Schema for `sbom_impact_analysis` + +Required: + +- `existing_content` (Number) Number of workers for existing content +- `new_content` (Number) Number of workers for new content + + + +### Nested Schema for `user_catalog` + +Required: + +- `existing_content` (Number) Number of workers for existing content +- `new_content` (Number) Number of workers for new content + ## Import Workers count resource can be imported using their names, e.g. From ba13bbae85a6628247bd0fa420c16b4016470983 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Tue, 10 Sep 2024 14:36:12 -0700 Subject: [PATCH 06/11] Fix tests --- pkg/acctest/test.go | 2 +- pkg/xray/resource/resource_xray_watch_test.go | 24 ------------------- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/pkg/acctest/test.go b/pkg/acctest/test.go index 97f7908e..d1dfc623 100644 --- a/pkg/acctest/test.go +++ b/pkg/acctest/test.go @@ -205,7 +205,7 @@ func CreateRepos(t *testing.T, repo, repoType, projectKey, packageType string) { } if repoType == "remote" { - repository.Url = "http://tempurl.org" + repository.Url = "https://google.com" } req := restyClient.R() diff --git a/pkg/xray/resource/resource_xray_watch_test.go b/pkg/xray/resource/resource_xray_watch_test.go index 468f53be..ca4acde8 100644 --- a/pkg/xray/resource/resource_xray_watch_test.go +++ b/pkg/xray/resource/resource_xray_watch_test.go @@ -188,7 +188,6 @@ func TestAccWatch_allReposWithProjectKey(t *testing.T) { min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true @@ -405,7 +404,6 @@ func TestAccWatch_singleRepositoryWithProjectKey(t *testing.T) { min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true @@ -893,7 +891,6 @@ func TestAccWatch_buildWithProjectKey(t *testing.T) { min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true @@ -981,7 +978,6 @@ func TestAccWatch_allBuildsWithProjectKey(t *testing.T) { min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true @@ -1351,7 +1347,6 @@ const allReposSinglePolicyWatchTemplate = `resource "xray_security_policy" "secu min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true @@ -1398,7 +1393,6 @@ const allReposPathAntFilterWatchTemplate = `resource "xray_security_policy" "sec min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true @@ -1445,7 +1439,6 @@ const allReposKvFilterWatchTemplate = `resource "xray_security_policy" "security min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true @@ -1495,7 +1488,6 @@ const allReposMultiplePoliciesWatchTemplate = `resource "xray_security_policy" " min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true @@ -1524,7 +1516,6 @@ resource "xray_license_policy" "license" { multi_license_permissive = true } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true @@ -1620,7 +1611,6 @@ const singleRepositoryWatchTemplate = `resource "xray_security_policy" "security min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true @@ -1669,7 +1659,6 @@ const singleRepositoryInvalidWatchTemplate = `resource "xray_security_policy" "s min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true @@ -1717,7 +1706,6 @@ const multipleRepositoriesWatchTemplate = `resource "xray_security_policy" "secu min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true @@ -1776,7 +1764,6 @@ const pathAntPatterns = `resource "xray_security_policy" "security" { min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true @@ -1843,7 +1830,6 @@ const kvFilters = `resource "xray_security_policy" "security" { min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true @@ -1902,7 +1888,6 @@ const multipleRepositoriesKvFilter = `resource "xray_security_policy" "security" min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true @@ -1968,7 +1953,6 @@ const buildWatchTemplate = `resource "xray_security_policy" "security" { min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true @@ -2012,7 +1996,6 @@ const multipleBuildsWatchTemplate = `resource "xray_security_policy" "security" min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true @@ -2061,7 +2044,6 @@ const allBuildsWatchTemplate = `resource "xray_security_policy" "security" { min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true @@ -2113,7 +2095,6 @@ const invalidBuildsWatchFilterTemplate = `resource "xray_security_policy" "secur min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true @@ -2161,7 +2142,6 @@ const allProjectsWatchTemplate = `resource "xray_security_policy" "security" { min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true @@ -2208,7 +2188,6 @@ const singleProjectWatchTemplate = `resource "xray_security_policy" "security" { min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true @@ -2255,7 +2234,6 @@ const invalidProjectWatchFilterTemplate = `resource "xray_security_policy" "secu min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true @@ -2303,7 +2281,6 @@ const allReleaseBundlesWatchTemplate = `resource "xray_security_policy" "securit min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true @@ -2350,7 +2327,6 @@ const singleReleaseBundleWatchTemplate = `resource "xray_security_policy" "secur min_severity = "High" } actions { - webhooks = [] mails = ["test@email.com"] block_download { unscanned = true From da5dcb0807ee7230e0419befb04ab09f60e187be Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Tue, 10 Sep 2024 14:38:15 -0700 Subject: [PATCH 07/11] Update CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57a87017..5cda9813 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.11.1 (September 11, 2024) + +IMPROVEMENTS: + +* resource/xray_license_policy, resource/xray_operational_risk_policy, resource/xray_security_policy: Migrate from SDKv2 to Plugin Framework. PR: [#239](https://github.com/jfrog/terraform-provider-xray/pull/239) + ## 2.11.0 (August 27, 2024). Tested on Artifactory 7.90.8 and Xray 3.102.5 with Terraform 1.9.5 and OpenTofu 1.8.1 IMPROVEMENTS: From 17204f8b6377ecc86c3fb189e41ce75fbae26b47 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Tue, 10 Sep 2024 15:12:33 -0700 Subject: [PATCH 08/11] Add --shielded-secure-boot to GKE cluster creation --- .github/workflows/acceptance-tests.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml index aaf1ab06..941007c7 100644 --- a/.github/workflows/acceptance-tests.yml +++ b/.github/workflows/acceptance-tests.yml @@ -69,11 +69,17 @@ jobs: export WHITELIST_CIDR=$(curl -s ifconfig.me)/32 echo "WHITELIST_CIDR=$WHITELIST_CIDR" >> "$GITHUB_ENV" echo "Creating GKE cluster ${GKE_CLUSTER} using default authentication" - gcloud container clusters create "$GKE_CLUSTER" --zone "$GKE_ZONE" \ - --node-locations "$GKE_ZONE" --num-nodes "${NUM_NODES:-5}" --enable-autoscaling \ + gcloud container clusters create "$GKE_CLUSTER" \ + --zone "$GKE_ZONE" \ + --shielded-secure-boot \ + --node-locations "$GKE_ZONE" \ + --num-nodes "${NUM_NODES:-5}" \ + --enable-autoscaling \ --machine-type "$MACHINE_TYPE" \ --disk-size 50Gi \ - --min-nodes 1 --max-nodes 5 --project "$GKE_PROJECT" + --min-nodes 1 \ + --max-nodes 5 \ + --project "$GKE_PROJECT" # --enable-master-authorized-networks \ # --master-authorized-networks "$WHITELIST_CIDR" # add your NAT CIDR to whitelist local or CI/CD NAT IP. Set WHITELIST_CIDR in CI/CD to add CIDR to the list automatically. From cc46cde05ea3f47321d0ac998efaf8cd5f6d89e7 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Tue, 10 Sep 2024 15:27:57 -0700 Subject: [PATCH 09/11] Add --shielded-integrity-monitoring to GKE cluster creation --- .github/workflows/acceptance-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml index 941007c7..019cd7db 100644 --- a/.github/workflows/acceptance-tests.yml +++ b/.github/workflows/acceptance-tests.yml @@ -72,6 +72,7 @@ jobs: gcloud container clusters create "$GKE_CLUSTER" \ --zone "$GKE_ZONE" \ --shielded-secure-boot \ + --shielded-integrity-monitoring \ --node-locations "$GKE_ZONE" \ --num-nodes "${NUM_NODES:-5}" \ --enable-autoscaling \ From 0c509895cee400ebfabd34431d20738a9967176e Mon Sep 17 00:00:00 2001 From: JFrog CI Date: Tue, 10 Sep 2024 23:00:54 +0000 Subject: [PATCH 10/11] JFrog Pipelines - Add Artifactory version to CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cda9813..d50cb6ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 2.11.1 (September 11, 2024) +## 2.11.1 (September 11, 2024). Tested on Artifactory 7.90.9 and Xray 3.104.8 with Terraform 1.9.5 and OpenTofu 1.8.2 IMPROVEMENTS: From 0d2fa1d9d448982c811b75b54a8ed6239545363f Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Thu, 12 Sep 2024 13:00:39 -0700 Subject: [PATCH 11/11] Update CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d50cb6ff..53f56439 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 2.11.1 (September 11, 2024). Tested on Artifactory 7.90.9 and Xray 3.104.8 with Terraform 1.9.5 and OpenTofu 1.8.2 +## 2.11.1 (September 13, 2024). Tested on Artifactory 7.90.9 and Xray 3.104.8 with Terraform 1.9.5 and OpenTofu 1.8.2 IMPROVEMENTS: