diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml index aaf1ab06..019cd7db 100644 --- a/.github/workflows/acceptance-tests.yml +++ b/.github/workflows/acceptance-tests.yml @@ -69,11 +69,18 @@ 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 \ + --shielded-integrity-monitoring \ + --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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 57a87017..53f56439 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 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: + +* 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: 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. 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/provider/framework.go b/pkg/xray/provider/framework.go index c5debb64..e37f6c05 100644 --- a/pkg/xray/provider/framework.go +++ b/pkg/xray/provider/framework.go @@ -175,12 +175,15 @@ 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.NewOperationalRiskPolicyResource, xray_resource.NewRepositoryConfigResource, + 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 b8a34bc1..51a67f56 100644 --- a/pkg/xray/provider/sdkv2.go +++ b/pkg/xray/provider/sdkv2.go @@ -54,9 +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(), "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 71e7d618..b7e82400 100644 --- a/pkg/xray/resource/policies.go +++ b/pkg/xray/resource/policies.go @@ -3,16 +3,32 @@ 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" "github.com/jfrog/terraform-provider-shared/util" - "github.com/jfrog/terraform-provider-shared/util/sdk" - "github.com/jfrog/terraform-provider-shared/validator" + utilfw "github.com/jfrog/terraform-provider-shared/util/fw" + "github.com/samber/lo" +) + +const ( + PoliciesEndpoint = "xray/api/v2/policies" + PolicyEndpoint = "xray/api/v2/policies/{name}" ) var validPackageTypesSupportedXraySecPolicies = []string{ "alpine", + "bower", "cargo", "composer", "conan", @@ -30,179 +46,421 @@ var validPackageTypesSupportedXraySecPolicies = []string{ "pypi", "rpm", "rubygems", + "terraformbe", } -var commonActionsSchema = map[string]*schema.Schema{ - "webhooks": { - Type: 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, +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, + } }, - }, - "mails": { - Type: 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, + ) + + *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, +} + +var fromActionsAPIModel = func(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), }, - }, - "block_download": { - Type: 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{ - "unscanned": { - Type: schema.TypeBool, + ) + 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, - Default: false, + 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": { - Type: schema.TypeBool, + "active": schema.BoolAttribute{ Optional: true, - Default: false, + 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", }, - "block_release_bundle_distribution": { - Type: schema.TypeBool, +} + +var commonActionsAttrs = map[string]schema.Attribute{ + "webhooks": schema.SetAttribute{ + ElementType: types.StringType, Optional: true, - Default: false, + 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": { - Type: schema.TypeBool, + "block_release_bundle_promotion": schema.BoolAttribute{ Optional: true, - Default: false, + Computed: true, + Default: booldefault.StaticBool(false), Description: "Blocks Release Bundle promotion if a violation is found. Default value is `false`.", }, - "fail_build": { - Type: schema.TypeBool, + "fail_build": schema.BoolAttribute{ Optional: true, - Default: false, + 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": { - Type: schema.TypeBool, + "notify_deployer": schema.BoolAttribute{ Optional: true, - Default: false, + 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": { - Type: schema.TypeBool, + "notify_watch_recipients": schema.BoolAttribute{ Optional: true, - Default: false, + 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": { - Type: schema.TypeBool, + "create_ticket_enabled": schema.BoolAttribute{ Optional: true, - Default: false, + 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": { - Type: 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), + "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 getPolicySchema = func(criteriaSchema map[string]*schema.Schema, actionsSchema map[string]*schema.Schema) map[string]*schema.Schema { - return sdk.MergeMaps( - getProjectKeySchema(false, ""), - map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "Name of the policy (must be unique)", - ValidateDiagFunc: validator.StringIsNotEmpty, - }, - "description": { - Type: schema.TypeString, - Optional: true, - Description: "More verbose description of the policy", - }, - "type": { - Type: schema.TypeString, - Required: true, - Description: "Type of the policy", - ValidateDiagFunc: validator.StringInSlice(false, "security", "license", "operational_risk"), - }, - "author": { - Type: schema.TypeString, - Computed: true, - Description: "User, who created the policy", - }, - "created": { - Type: schema.TypeString, - Computed: true, - Description: "Creation timestamp", - }, - "modified": { - Type: schema.TypeString, - Computed: true, - Description: "Modification timestamp", - }, - "rule": { - Type: 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{ - "name": { - Type: schema.TypeString, - Required: true, - Description: "Name of the rule", - ValidateDiagFunc: validator.StringIsNotEmpty, +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), }, - "priority": { - Type: schema.TypeInt, - Required: true, - ValidateDiagFunc: validator.IntAtLeast(1), - Description: "Integer describing the rule priority. Must be at least 1", + Description: "Name of the rule", + }, + "priority": schema.Int64Attribute{ + Required: true, + Validators: []validator.Int64{ + int64validator.AtLeast(1), }, - "criteria": { - Type: 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{ - Schema: criteriaSchema, - }, + 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, }, - "actions": { - Type: schema.TypeSet, - MaxItems: 1, - Required: true, - Description: "Specifies the actions to take once a security policy violation has been triggered.", - Elem: &schema.Resource{ - Schema: actionsSchema, - }, + 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 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 +468,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,628 +508,304 @@ 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) - - 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").(*schema.Set), policy.Type) - policy.Rules = &policyRules - - return policy, err -} - -func unpackRules(configured *schema.Set, policyType string) (policyRules []PolicyRule, err error) { - var rules []PolicyRule +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) - for _, raw := range configured.List() { - rule := new(PolicyRule) - data := raw.(map[string]interface{}) - rule.Name = data["name"].(string) - rule.Priority = data["priority"].(int) + var plan PolicyResourceModel - rule.Criteria, err = unpackCriteria(data["criteria"].(*schema.Set), policyType) - if v, ok := data["actions"]; ok { - rule.Actions = unpackActions(v.(*schema.Set)) - } - rules = append(rules, *rule) + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return } - return rules, err -} - -func unpackSecurityCriteria(tfCriteria map[string]interface{}) *PolicyRuleCriteria { - criteria := new(PolicyRuleCriteria) - - 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.(*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.(*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{}) *PolicyRuleCriteria { - criteria := new(PolicyRuleCriteria) - if v, ok := tfCriteria["allow_unknown"]; ok { - criteria.AllowUnknown = sdk.BoolPtr(v.(bool)) - } - if v, ok := tfCriteria["banned_licenses"]; ok { - criteria.BannedLicenses = unpackLicenses(v.(*schema.Set)) - } - if v, ok := tfCriteria["allowed_licenses"]; ok { - criteria.AllowedLicenses = unpackLicenses(v.(*schema.Set)) - } - if v, ok := tfCriteria["multi_license_permissive"]; ok { - criteria.MultiLicensePermissive = sdk.BoolPtr(v.(bool)) + request, err := getRestyRequest(r.ProviderData.Client, plan.ProjectKey.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "failed to get Resty client", + err.Error(), + ) + return } - return criteria -} - -func unpackOperationalRiskCustomCriteria(tfCriteria map[string]interface{}) *OperationalRiskCriteria { - criteria := OperationalRiskCriteria{} - if v, ok := tfCriteria["use_and_condition"]; ok { - criteria.UseAndCondition = v.(bool) - } - if v, ok := tfCriteria["is_eol"]; ok { - criteria.IsEOL = v.(bool) + var policy PolicyAPIModel + resp.Diagnostics.Append(toAPIModel(ctx, plan, &policy)...) + if resp.Diagnostics.HasError() { + return } - if v, ok := tfCriteria["release_date_greater_than_months"]; ok { - criteria.ReleaseDateGreaterThanMonths = v.(int) - } - if v, ok := tfCriteria["newer_versions_greater_than"]; ok { - criteria.NewerVersionsGreaterThan = v.(int) - } - if v, ok := tfCriteria["release_cadence_per_year_less_than"]; ok { - criteria.ReleaseCadencePerYearLessThan = v.(int) - } - if v, ok := tfCriteria["commits_less_than"]; ok { - criteria.CommitsLessThan = v.(int) - } - if v, ok := tfCriteria["committers_less_than"]; ok { - criteria.CommittersLessThan = v.(int) - } - if v, ok := tfCriteria["risk"]; ok { - criteria.Risk = v.(string) - } - - return &criteria -} -func unpackOperationalRiskCriteria(tfCriteria map[string]interface{}) *PolicyRuleCriteria { - criteria := new(PolicyRuleCriteria) - 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 -} + var policyError PolicyError + response, err := request. + SetBody(policy). + SetError(&policyError). + Post(PoliciesEndpoint) -func unpackCriteria(d *schema.Set, policyType string) (*PolicyRuleCriteria, error) { - tfCriteria := d.List() - if len(tfCriteria) == 0 { - return nil, nil + if err != nil { + utilfw.UnableToCreateResourceError(resp, err.Error()) + return } - m := tfCriteria[0].(map[string]interface{}) // We made this a list of one to make schema validation easier - var criteria *PolicyRuleCriteria - // 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) + if response.IsError() { + utilfw.UnableToCreateResourceError(resp, policyError.Error) + return } - return criteria, nil -} - -func Float64Ptr(v float64) *float64 { return &v } - -func StringPtr(v string) *string { return &v } - -func unpackCVSSRange(l []interface{}) *PolicyCVSSRange { - if len(l) == 0 { - return nil - } + response, err = request. + SetResult(&policy). + SetPathParam("name", plan.Name.ValueString()). + SetError(&policyError). + Get(PolicyEndpoint) - m := l[0].(map[string]interface{}) - cvssrange := &PolicyCVSSRange{ - From: Float64Ptr(m["from"].(float64)), - To: Float64Ptr(m["to"].(float64)), + if err != nil { + utilfw.UnableToCreateResourceError(resp, err.Error()) + return } - return cvssrange -} -func unpackExposures(l []interface{}) *PolicyExposures { - if len(l) == 0 { - return nil + if response.IsError() { + utilfw.UnableToCreateResourceError(resp, policyError.Error) + return } - m := l[0].(map[string]interface{}) - exposures := &PolicyExposures{ - 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)), + resp.Diagnostics.Append(fromAPIModel(ctx, policy, &plan)...) + if resp.Diagnostics.HasError() { + return } - return exposures -} -func unpackLicenses(d *schema.Set) []string { - var licenses []string - for _, license := range d.List() { - licenses = append(licenses, license.(string)) - } - return licenses + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) } -func unpackActions(l *schema.Set) PolicyRuleActions { - actions := PolicyRuleActions{} - 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() - var webhooks []string - for _, hook := range m { - webhooks = append(webhooks, hook.(string)) - } - actions.Webhooks = webhooks - } - if v, ok := m["mails"]; ok { - m := v.(*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) - } +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) - if v, ok := m["block_download"]; ok { - if len(v.(*schema.Set).List()) > 0 { - vList := v.(*schema.Set).List() - vMap := vList[0].(map[string]interface{}) - - actions.BlockDownload = BlockDownloadSettings{ - Unscanned: vMap["unscanned"].(bool), - Active: vMap["active"].(bool), - } - } else { - actions.BlockDownload = BlockDownloadSettings{ - 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.(int) - } - if v, ok := m["custom_severity"]; ok { - actions.CustomSeverity = v.(string) - } + var state PolicyResourceModel - return actions + // Read Terraform state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return } - return actions -} - -func packRules(rules []PolicyRule, 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) + request, err := getRestyRequest(r.ProviderData.Client, state.ProjectKey.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "failed to get Resty client", + err.Error(), + ) + return } - return rs -} + var policy PolicyAPIModel + var policyError PolicyError -func packOperationalRiskCriteria(criteria *PolicyRuleCriteria) []interface{} { - m := map[string]interface{}{} + response, err := request. + SetResult(&policy). + SetPathParam("name", state.Name.ValueString()). + SetError(&policyError). + Get(PolicyEndpoint) - if len(criteria.OperationalRiskMinRisk) > 0 { - m["op_risk_min_risk"] = criteria.OperationalRiskMinRisk - } - if criteria.OperationalRiskCustom != nil { - m["op_risk_custom"] = packOperationalRiskCustom(criteria.OperationalRiskCustom) + if err != nil { + utilfw.UnableToRefreshResourceError(resp, err.Error()) + return } - return []interface{}{m} -} - -func packOperationalRiskCustom(custom *OperationalRiskCriteria) []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, + if response.StatusCode() == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return } - return []interface{}{m} -} - -func packLicenseCriteria(criteria *PolicyRuleCriteria) []interface{} { - - m := map[string]interface{}{} - - if criteria.BannedLicenses != nil { - m["banned_licenses"] = criteria.BannedLicenses + if response.IsError() { + utilfw.UnableToRefreshResourceError(resp, policyError.Error) + return } - 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 *PolicyRuleCriteria) []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 *PolicyCVSSRange) []interface{} { - if cvss == nil { - return []interface{}{} - } - m := map[string]interface{}{ - "from": *cvss.From, - "to": *cvss.To, + resp.Diagnostics.Append(fromAPIModel(ctx, policy, &state)...) + if resp.Diagnostics.HasError() { + return } - return []interface{}{m} -} -func packExposures(exposures *PolicyExposures) []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} + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } -func packActions(actions PolicyRuleActions, 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 (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) -func packBlockDownload(bd BlockDownloadSettings) []interface{} { - m := map[string]interface{}{} - m["unscanned"] = bd.Unscanned - m["active"] = bd.Active - return []interface{}{m} -} + var plan PolicyResourceModel -func packPolicy(policy Policy, d *schema.ResourceData) diag.Diagnostics { - if err := d.Set("name", policy.Name); err != nil { - return diag.FromErr(err) - } - if err := d.Set("type", policy.Type); err != nil { - return diag.FromErr(err) - } - if len(policy.Description) > 0 { - if err := d.Set("description", policy.Description); err != nil { - return diag.FromErr(err) - } - } - if err := d.Set("author", policy.Author); err != nil { - return diag.FromErr(err) - } - if err := d.Set("created", policy.Created); err != nil { - return diag.FromErr(err) - } - if err := d.Set("modified", policy.Modified); err != nil { - return diag.FromErr(err) - } - if policy.Rules != nil { - if err := d.Set("rule", packRules(*policy.Rules, policy.Type)); err != nil { - return diag.FromErr(err) - } + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return } - return nil -} - -func resourceXrayPolicyCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - policy, err := unpackPolicy(d) - // Warning or errors can be collected in a slice type + request, err := getRestyRequest(r.ProviderData.Client, plan.ProjectKey.ValueString()) if err != nil { - return diag.FromErr(err) + resp.Diagnostics.AddError( + "failed to get Resty client", + err.Error(), + ) + return } - req, err := getRestyRequest(m.(util.ProviderMetadata).Client, policy.ProjectKey) - if err != nil { - return diag.FromErr(err) + var policy PolicyAPIModel + resp.Diagnostics.Append(toAPIModel(ctx, plan, &policy)...) + if resp.Diagnostics.HasError() { + return } var policyError PolicyError - resp, err := req. + + response, err := request. + SetPathParam("name", plan.Name.ValueString()). SetBody(policy). SetError(&policyError). - Post("xray/api/v2/policies") + Put(PolicyEndpoint) + if err != nil { - return diag.FromErr(err) - } - if resp.IsError() { - return diag.Errorf("%s", policyError.Error) + utilfw.UnableToUpdateResourceError(resp, err.Error()) + return } - d.SetId(policy.Name) - return resourceXrayPolicyRead(ctx, d, m) -} - -func resourceXrayPolicyRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - var policy Policy - - projectKey := d.Get("project_key").(string) - req, err := getRestyRequest(m.(util.ProviderMetadata).Client, projectKey) - if err != nil { - return diag.FromErr(err) + if response.IsError() { + utilfw.UnableToUpdateResourceError(resp, policyError.Error) + return } - var policyError PolicyError - resp, err := req. + response, err = request. SetResult(&policy). - SetPathParam("name", d.Id()). + SetPathParam("name", plan.Name.ValueString()). SetError(&policyError). - Get("xray/api/v2/policies/{name}") + Get(PolicyEndpoint) + if err != nil { - return diag.FromErr(err) + utilfw.UnableToUpdateResourceError(resp, err.Error()) + return } - if resp.StatusCode() == http.StatusNotFound { - d.SetId("") - return diag.Errorf("policy (%s) not found, removing from state", d.Id()) + + if response.IsError() { + utilfw.UnableToUpdateResourceError(resp, policyError.Error) + return } - if resp.IsError() { - return diag.Errorf("%s", policyError.Error) + + resp.Diagnostics.Append(fromAPIModel(ctx, policy, &plan)...) + if resp.Diagnostics.HasError() { + return } - return packPolicy(policy, d) + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) } -func resourceXrayPolicyUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - policy, err := unpackPolicy(d) - if err != nil { - return diag.FromErr(err) - } +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 - req, err := getRestyRequest(m.(util.ProviderMetadata).Client, policy.ProjectKey) + // 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 { - return diag.FromErr(err) + resp.Diagnostics.AddError( + "failed to get Resty client", + err.Error(), + ) + return } var policyError PolicyError - resp, err := req. - SetBody(policy). - SetPathParams(map[string]string{ - "name": d.Id(), - }). + response, err := request. + SetPathParam("name", state.Name.ValueString()). SetError(&policyError). - Put("xray/api/v2/policies/{name}") + Delete(PolicyEndpoint) + if err != nil { - return diag.FromErr(err) + utilfw.UnableToDeleteResourceError(resp, err.Error()) + return } - if resp.IsError() { - return diag.Errorf("%s", policyError.Error) + + if response.IsError() { + utilfw.UnableToDeleteResourceError(resp, policyError.Error) + return } - d.SetId(policy.Name) - return resourceXrayPolicyRead(ctx, d, m) + // If the logic reaches here, it implicitly succeeded and will remove + // the resource from state if there are no other errors. } -func resourceXrayPolicyDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - policy, err := unpackPolicy(d) - if err != nil { - return diag.FromErr(err) - } +// 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) - req, err := getRestyRequest(m.(util.ProviderMetadata).Client, policy.ProjectKey) - if err != nil { - return diag.FromErr(err) + if len(parts) > 0 && parts[0] != "" { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), parts[0])...) } - 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 diag.FromErr(err) + if len(parts) == 2 && parts[1] != "" { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_key"), parts[1])...) } - if resp.IsError() { - return diag.Errorf("%s", policyError.Error) - } - - d.SetId("") - - return nil } 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_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_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_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 397d046b..df87d9a0 100644 --- a/pkg/xray/resource/resource_xray_security_policy.go +++ b/pkg/xray/resource/resource_xray_security_policy.go @@ -5,207 +5,605 @@ import ( "fmt" "regexp" - "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/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" + 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 = &SecurityPolicyResource{} + +func NewSecurityPolicyResource() resource.Resource { + return &SecurityPolicyResource{ + PolicyResource: PolicyResource{ + TypeName: "xray_security_policy", }, - "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.", + } +} + +type SecurityPolicyResource struct { + PolicyResource +} + +func (r *SecurityPolicyResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = r.TypeName +} + +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 (r *SecurityPolicyResource) fromCriteriaAPIModel(ctx context.Context, criteraAPIModel *PolicyRuleCriteriaAPIModel) (types.Set, diag.Diagnostics) { + diags := diag.Diagnostics{} + + 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...) + } + + 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...) + } + + es, d := types.ListValue( + exposuresElementType, + []attr.Value{exposures}, + ) + if d.HasError() { + diags.Append(d...) + } + + 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 criteriaSet, diags +} + +func (r SecurityPolicyResource) fromAPIModel(ctx context.Context, policy PolicyAPIModel, plan *PolicyResourceModel) diag.Diagnostics { + return plan.fromAPIModel(ctx, policy, r.fromCriteriaAPIModel, fromActionsAPIModel) +} + +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 securityCriteriaAttrTypes = 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 securityCriteriaSetElementType = types.ObjectType{ + AttrTypes: securityCriteriaAttrTypes, +} + +var blockDownloadAttrTypes = map[string]attr.Type{ + "unscanned": types.BoolType, + "active": types.BoolType, +} + +var blockDownloadElementType = types.ObjectType{ + AttrTypes: blockDownloadAttrTypes, +} + +var securityRuleAttrTypes = map[string]attr.Type{ + "name": types.StringType, + "priority": types.Int64Type, + "criteria": types.SetType{ElemType: securityCriteriaSetElementType}, + "actions": types.SetType{ElemType: actionsSetElementType}, +} + +var securityRuleSetElementType = types.ObjectType{ + AttrTypes: securityRuleAttrTypes, +} + +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, }, - "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", + } +} + +var policySchemaAttrs = lo.Assign( + projectKeySchemaAttrs(false, ""), + map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, }, - "malicious_package": { - Type: schema.TypeBool, - Optional: true, - Default: false, - Description: "Default value is `false`. Generating a violation on a malicious package.", + "name": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Description: "Name of the policy (must be unique)", }, - "cvss_range": { - Type: schema.TypeList, + "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 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"), - }, - "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.", + 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"), }, + 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.", }, }, }, - "package_name": { - Type: schema.TypeString, - Optional: true, - Description: "The package name to create a rule for", + 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"), + ), }, - "package_type": { - Type: schema.TypeString, - Optional: true, - Description: "The package type to create a rule for", - ValidateDiagFunc: validator.StringInSlice(true, validPackageTypesSupportedXraySecPolicies...), + 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"), + ), }, - "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]"), - ), - }, + 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]", + }, +} - return &schema.Resource{ - SchemaVersion: 1, - CreateContext: resourceXrayPolicyCreate, - ReadContext: resourceXrayPolicyRead, - UpdateContext: resourceXrayPolicyUpdate, - DeleteContext: resourceXrayPolicyDelete, +func (r *SecurityPolicyResource) 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), } } -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 +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 } + r.ProviderData = req.ProviderData.(util.ProviderMetadata) +} - 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") +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() { + 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 rule is not configured, return without warning. + if data.Rules.IsNull() || data.Rules.IsUnknown() { + return } - if isPackageSet && fixVersionDependant { - return fmt.Errorf("fix_version_dependant must be set to false if package type policy is used") + 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 *SecurityPolicyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + r.PolicyResource.Create(ctx, r.toAPIModel, r.fromAPIModel, req, resp) +} + +func (r *SecurityPolicyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + r.PolicyResource.Read(ctx, r.fromAPIModel, req, resp) +} + +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 *SecurityPolicyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + r.PolicyResource.Delete(ctx, req, resp) +} - return nil +// ImportState imports the resource into the Terraform state. +func (r *SecurityPolicyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + r.PolicyResource.ImportState(ctx, req, resp) } diff --git a/pkg/xray/resource/resource_xray_security_policy_test.go b/pkg/xray/resource/resource_xray_security_policy_test.go index d8ea00de..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{ { @@ -299,9 +377,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 +519,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 +542,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 +565,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 +621,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 +674,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 +732,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 +749,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 +772,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 +808,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 +837,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 +871,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 +903,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 +931,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"), }, }, }) 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_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 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 { 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 -}