diff --git a/datadog/fwprovider/framework_provider.go b/datadog/fwprovider/framework_provider.go
index 7d09629026..2ca302fbde 100644
--- a/datadog/fwprovider/framework_provider.go
+++ b/datadog/fwprovider/framework_provider.go
@@ -88,6 +88,7 @@ var Resources = []func() resource.Resource{
NewOnCallScheduleResource,
NewOnCallTeamRoutingRulesResource,
NewSecurityMonitoringRuleJSONResource,
+ NewComplianceCustomFrameworkResource,
}
var Datasources = []func() datasource.DataSource{
diff --git a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go
new file mode 100644
index 0000000000..282128dc0b
--- /dev/null
+++ b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go
@@ -0,0 +1,390 @@
+package fwprovider
+
+import (
+ "context"
+
+ "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2"
+ "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/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "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/hashicorp/terraform-plugin-framework-validators/listvalidator"
+ "github.com/terraform-providers/terraform-provider-datadog/datadog/internal/utils"
+ "github.com/terraform-providers/terraform-provider-datadog/datadog/internal/validators"
+)
+
+var _ resource.Resource = &complianceCustomFrameworkResource{}
+
+type complianceCustomFrameworkResource struct {
+ Api *datadogV2.SecurityMonitoringApi
+ Auth context.Context
+}
+
+// to handle a larger input, requirements and controls had to be lists even though order doesn't matter
+// but rules can be sets since requirements and controls are lists (the performance issue happened when all were sets)
+type complianceCustomFrameworkModel struct {
+ ID types.String `tfsdk:"id"`
+ Version types.String `tfsdk:"version"`
+ Handle types.String `tfsdk:"handle"`
+ Name types.String `tfsdk:"name"`
+ IconURL types.String `tfsdk:"icon_url"`
+ Requirements []complianceCustomFrameworkRequirementsModel `tfsdk:"requirements"`
+}
+
+type complianceCustomFrameworkRequirementsModel struct {
+ Name types.String `tfsdk:"name"`
+ Controls []complianceCustomFrameworkControlsModel `tfsdk:"controls"`
+}
+
+type complianceCustomFrameworkControlsModel struct {
+ Name types.String `tfsdk:"name"`
+ RulesID types.Set `tfsdk:"rules_id"`
+}
+
+func NewComplianceCustomFrameworkResource() resource.Resource {
+ return &complianceCustomFrameworkResource{}
+}
+
+func (r *complianceCustomFrameworkResource) Metadata(_ context.Context, _ resource.MetadataRequest, response *resource.MetadataResponse) {
+ response.TypeName = "compliance_custom_framework"
+}
+
+func (r *complianceCustomFrameworkResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) {
+ response.Schema = schema.Schema{
+ Description: "Provides a Datadog Compliance Custom Framework resource, which is used to create and manage compliance custom frameworks.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Description: "The ID of the compliance custom framework resource.",
+ Computed: true,
+ },
+ "version": schema.StringAttribute{
+ Description: "The framework version.",
+ Validators: []validator.String{
+ stringvalidator.LengthAtLeast(1),
+ },
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "handle": schema.StringAttribute{
+ Description: "The framework handle.",
+ Validators: []validator.String{
+ stringvalidator.LengthAtLeast(1),
+ },
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "name": schema.StringAttribute{
+ Description: "The framework name.",
+ Validators: []validator.String{
+ stringvalidator.LengthAtLeast(1),
+ },
+ Required: true,
+ },
+ "icon_url": schema.StringAttribute{
+ Description: "The URL of the icon representing the framework",
+ Optional: true,
+ },
+ },
+ Blocks: map[string]schema.Block{
+ "requirements": schema.ListNestedBlock{
+ Description: "The requirements of the framework.",
+ Validators: []validator.List{
+ validators.DuplicateRequirementControlValidator(),
+ listvalidator.IsRequired(),
+ },
+ NestedObject: schema.NestedBlockObject{
+ Attributes: map[string]schema.Attribute{
+ "name": schema.StringAttribute{
+ Description: "The name of the requirement.",
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.LengthAtLeast(1),
+ },
+ },
+ },
+ Blocks: map[string]schema.Block{
+ "controls": schema.ListNestedBlock{
+ Description: "The controls of the requirement.",
+ Validators: []validator.List{
+ listvalidator.IsRequired(),
+ },
+ NestedObject: schema.NestedBlockObject{
+ Attributes: map[string]schema.Attribute{
+ "name": schema.StringAttribute{
+ Description: "The name of the control.",
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.LengthAtLeast(1),
+ },
+ },
+ "rules_id": schema.SetAttribute{
+ Description: "The set of rules IDs for the control.",
+ ElementType: types.StringType,
+ Required: true,
+ Validators: []validator.Set{
+ setvalidator.SizeAtLeast(1),
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func (r *complianceCustomFrameworkResource) Configure(_ context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) {
+ providerData, _ := request.ProviderData.(*FrameworkProvider)
+ r.Api = providerData.DatadogApiInstances.GetSecurityMonitoringApiV2()
+ r.Auth = providerData.Auth
+}
+
+func (r *complianceCustomFrameworkResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) {
+ var state complianceCustomFrameworkModel
+
+ diags := request.Config.Get(ctx, &state)
+ response.Diagnostics.Append(diags...)
+ if response.Diagnostics.HasError() {
+ return
+ }
+
+ _, httpResp, err := r.Api.CreateCustomFramework(r.Auth, *buildCreateFrameworkRequest(state))
+ if err != nil {
+ if httpResp != nil && httpResp.StatusCode == 409 { // if framework already exists, try to update it with the new state
+ _, httpResp, getErr := r.Api.GetCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString())
+ // if the framework with the same handle and version does not exist, throw an error because
+ // only the handle matches which has to be unique
+ if httpResp != nil && httpResp.StatusCode == 400 {
+ response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "Framework with same handle already exists. Currently there is no support for two frameworks with the same handle."))
+ return
+ }
+ if getErr != nil {
+ response.Diagnostics.Append(utils.FrameworkErrorDiag(getErr, "error getting existing compliance custom framework"))
+ return
+ }
+ // if the framework with the same handle and version exists, update it
+ _, _, updateErr := r.Api.UpdateCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString(), *buildUpdateFrameworkRequest(state))
+ if updateErr != nil {
+ response.Diagnostics.Append(utils.FrameworkErrorDiag(updateErr, "error updating existing compliance custom framework"))
+ return
+ }
+ } else {
+ response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error creating compliance custom framework"))
+ return
+ }
+ }
+ state.ID = types.StringValue(state.Handle.ValueString() + string('-') + state.Version.ValueString())
+ diags = response.State.Set(ctx, &state)
+ response.Diagnostics.Append(diags...)
+}
+
+func (r *complianceCustomFrameworkResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) {
+ var state complianceCustomFrameworkModel
+ diags := request.State.Get(ctx, &state)
+ response.Diagnostics.Append(diags...)
+ if response.Diagnostics.HasError() {
+ return
+ }
+ _, _, err := r.Api.DeleteCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString())
+ if err != nil {
+ response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error deleting framework"))
+ return
+ }
+}
+
+func (r *complianceCustomFrameworkResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) {
+ var state complianceCustomFrameworkModel
+ diags := request.State.Get(ctx, &state)
+ response.Diagnostics.Append(diags...)
+ if response.Diagnostics.HasError() {
+ return
+ }
+
+ data, httpResp, err := r.Api.GetCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString())
+ // If the framework does not exist, remove it from terraform state
+ // This is to avoid the provider to return an error when the framework is deleted in the UI prior
+ if httpResp != nil && httpResp.StatusCode == 400 {
+ // 400 could only mean the framework does not exist
+ // because terraform would have already validated the framework in the create function
+ response.State.RemoveResource(ctx)
+ return
+ }
+ if err != nil {
+ response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error reading compliance custom framework"))
+ return
+ }
+ databaseState := readStateFromDatabase(data, state.Handle.ValueString(), state.Version.ValueString(), &state)
+ diags = response.State.Set(ctx, &databaseState)
+ response.Diagnostics.Append(diags...)
+}
+
+func (r *complianceCustomFrameworkResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) {
+ var state complianceCustomFrameworkModel
+ diags := request.Config.Get(ctx, &state)
+ response.Diagnostics.Append(diags...)
+ if response.Diagnostics.HasError() {
+ return
+ }
+
+ state.ID = types.StringValue(state.Handle.ValueString() + "-" + state.Version.ValueString())
+
+ _, _, err := r.Api.UpdateCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString(), *buildUpdateFrameworkRequest(state))
+ if err != nil {
+ response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error updating compliance custom framework"))
+ return
+ }
+ diags = response.State.Set(ctx, &state)
+ response.Diagnostics.Append(diags...)
+}
+
+func convertControlToModel(control datadogV2.CustomFrameworkControl) complianceCustomFrameworkControlsModel {
+ rulesID := make([]attr.Value, len(control.GetRulesId()))
+ for k, v := range control.GetRulesId() {
+ rulesID[k] = types.StringValue(v)
+ }
+ return complianceCustomFrameworkControlsModel{
+ Name: types.StringValue(control.GetName()),
+ RulesID: types.SetValueMust(types.StringType, rulesID),
+ }
+}
+
+func convertRequirementToModel(req datadogV2.CustomFrameworkRequirement) complianceCustomFrameworkRequirementsModel {
+ controls := make([]complianceCustomFrameworkControlsModel, len(req.GetControls()))
+ for j, control := range req.GetControls() {
+ controls[j] = convertControlToModel(control)
+ }
+ return complianceCustomFrameworkRequirementsModel{
+ Name: types.StringValue(req.GetName()),
+ Controls: controls,
+ }
+}
+
+func readStateFromDatabase(data datadogV2.GetCustomFrameworkResponse, handle string, version string, currentState *complianceCustomFrameworkModel) complianceCustomFrameworkModel {
+ var state complianceCustomFrameworkModel
+ state.ID = types.StringValue(handle + "-" + version)
+ state.Handle = types.StringValue(handle)
+ state.Version = types.StringValue(version)
+ state.Name = types.StringValue(data.GetData().Attributes.Name)
+ if data.GetData().Attributes.IconUrl != nil {
+ state.IconURL = types.StringValue(*data.GetData().Attributes.IconUrl)
+ }
+
+ apiReqMap := make(map[string]datadogV2.CustomFrameworkRequirement)
+ apiControlMap := make(map[string]map[string]datadogV2.CustomFrameworkControl)
+
+ for _, req := range data.GetData().Attributes.Requirements {
+ apiReqMap[req.GetName()] = req
+ apiControlMap[req.GetName()] = make(map[string]datadogV2.CustomFrameworkControl)
+ for _, control := range req.GetControls() {
+ apiControlMap[req.GetName()][control.GetName()] = control
+ }
+ }
+
+ // since the requirements and controls from the API response might be in a different order than the state
+ // we need to sort them to match the state so terraform can detect the changes
+ // without taking order into account
+ sortedRequirements := make([]complianceCustomFrameworkRequirementsModel, 0, len(data.GetData().Attributes.Requirements))
+
+ if currentState != nil {
+ for _, currentReq := range currentState.Requirements {
+ currentReqName := currentReq.Name.ValueString()
+ if apiReq, exists := apiReqMap[currentReqName]; exists {
+ sortedControls := make([]complianceCustomFrameworkControlsModel, 0, len(apiReq.GetControls()))
+
+ for _, currentControl := range currentReq.Controls {
+ currentControlName := currentControl.Name.ValueString()
+ if apiControl, exists := apiControlMap[currentReqName][currentControlName]; exists {
+ sortedControls = append(sortedControls, convertControlToModel(apiControl))
+ delete(apiControlMap[currentReqName], currentControlName)
+ }
+ }
+
+ for _, apiControl := range apiControlMap[currentReqName] {
+ sortedControls = append(sortedControls, convertControlToModel(apiControl))
+ }
+
+ sortedReq := complianceCustomFrameworkRequirementsModel{
+ Name: types.StringValue(apiReq.GetName()),
+ Controls: sortedControls,
+ }
+ sortedRequirements = append(sortedRequirements, sortedReq)
+ delete(apiReqMap, currentReqName)
+ }
+ }
+ }
+
+ for _, apiReq := range apiReqMap {
+ sortedRequirements = append(sortedRequirements, convertRequirementToModel(apiReq))
+ }
+
+ state.Requirements = sortedRequirements
+ return state
+}
+
+func convertStateRequirementsToFrameworkRequirements(requirements []complianceCustomFrameworkRequirementsModel) []datadogV2.CustomFrameworkRequirement {
+ frameworkRequirements := make([]datadogV2.CustomFrameworkRequirement, len(requirements))
+ for i, requirement := range requirements {
+ controls := make([]datadogV2.CustomFrameworkControl, len(requirement.Controls))
+ for j, control := range requirement.Controls {
+ rulesID := make([]string, 0)
+ for _, v := range control.RulesID.Elements() {
+ rulesID = append(rulesID, v.(types.String).ValueString())
+ }
+ controls[j] = *datadogV2.NewCustomFrameworkControl(control.Name.ValueString(), rulesID)
+ }
+ frameworkRequirements[i] = *datadogV2.NewCustomFrameworkRequirement(controls, requirement.Name.ValueString())
+ }
+ return frameworkRequirements
+}
+
+func buildCreateFrameworkRequest(state complianceCustomFrameworkModel) *datadogV2.CreateCustomFrameworkRequest {
+ var iconURL *string
+ if !state.IconURL.IsNull() && !state.IconURL.IsUnknown() {
+ iconURLStr := state.IconURL.ValueString()
+ iconURL = &iconURLStr
+ }
+ createFrameworkRequest := datadogV2.NewCreateCustomFrameworkRequestWithDefaults()
+ createFrameworkRequest.SetData(datadogV2.CustomFrameworkData{
+ Type: "custom_framework",
+ Attributes: datadogV2.CustomFrameworkDataAttributes{
+ Handle: state.Handle.ValueString(),
+ Name: state.Name.ValueString(),
+ IconUrl: iconURL,
+ Version: state.Version.ValueString(),
+ Requirements: convertStateRequirementsToFrameworkRequirements(state.Requirements),
+ },
+ })
+ return createFrameworkRequest
+}
+
+func buildUpdateFrameworkRequest(state complianceCustomFrameworkModel) *datadogV2.UpdateCustomFrameworkRequest {
+ var iconURL *string
+ if !state.IconURL.IsNull() && !state.IconURL.IsUnknown() {
+ iconURLStr := state.IconURL.ValueString()
+ iconURL = &iconURLStr
+ }
+ updateFrameworkRequest := datadogV2.NewUpdateCustomFrameworkRequestWithDefaults()
+ updateFrameworkRequest.SetData(datadogV2.CustomFrameworkData{
+ Type: "custom_framework",
+ Attributes: datadogV2.CustomFrameworkDataAttributes{
+ Handle: state.Handle.ValueString(),
+ Name: state.Name.ValueString(),
+ Version: state.Version.ValueString(),
+ IconUrl: iconURL,
+ Requirements: convertStateRequirementsToFrameworkRequirements(state.Requirements),
+ },
+ })
+ return updateFrameworkRequest
+}
diff --git a/datadog/internal/validators/duplicate_requirement_control_validator.go b/datadog/internal/validators/duplicate_requirement_control_validator.go
new file mode 100644
index 0000000000..89a5183f3f
--- /dev/null
+++ b/datadog/internal/validators/duplicate_requirement_control_validator.go
@@ -0,0 +1,57 @@
+package validators
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+type duplicateRequirementControlValidator struct{}
+
+func (v duplicateRequirementControlValidator) Description(context.Context) string {
+ return "checks for duplicate requirement and control names"
+}
+
+func (v duplicateRequirementControlValidator) MarkdownDescription(ctx context.Context) string {
+ return v.Description(ctx)
+}
+
+func (v duplicateRequirementControlValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) {
+ if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
+ return
+ }
+
+ seen := make(map[string]bool)
+ for _, requirement := range req.ConfigValue.Elements() {
+ reqObj := requirement.(types.Object)
+ name := reqObj.Attributes()["name"].(types.String).ValueString()
+ if seen[name] {
+ resp.Diagnostics.AddError(
+ "Each Requirement must have a unique name",
+ fmt.Sprintf("Requirement name '%s' is used more than once.", name),
+ )
+ return
+ }
+ seen[name] = true
+ controls := reqObj.Attributes()["controls"].(types.List)
+ controlNames := make(map[string]bool)
+ for _, control := range controls.Elements() {
+ ctrlObj := control.(types.Object)
+ ctrlName := ctrlObj.Attributes()["name"].(types.String).ValueString()
+ if controlNames[ctrlName] {
+ resp.Diagnostics.AddError(
+ "Each Control must have a unique name under the same requirement",
+ fmt.Sprintf("Control name '%s' is used more than once under requirement '%s'", ctrlName, name),
+ )
+ return
+ }
+ controlNames[ctrlName] = true
+ }
+ }
+}
+
+func DuplicateRequirementControlValidator() validator.List {
+ return &duplicateRequirementControlValidator{}
+}
diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateAndUpdateMultipleRequirements.freeze b/datadog/tests/cassettes/TestCustomFramework_CreateAndUpdateMultipleRequirements.freeze
new file mode 100644
index 0000000000..74d749e146
--- /dev/null
+++ b/datadog/tests/cassettes/TestCustomFramework_CreateAndUpdateMultipleRequirements.freeze
@@ -0,0 +1 @@
+2025-05-22T10:29:37.579487-05:00
\ No newline at end of file
diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateAndUpdateMultipleRequirements.yaml b/datadog/tests/cassettes/TestCustomFramework_CreateAndUpdateMultipleRequirements.yaml
new file mode 100644
index 0000000000..94e04d8d03
--- /dev/null
+++ b/datadog/tests/cassettes/TestCustomFramework_CreateAndUpdateMultipleRequirements.yaml
@@ -0,0 +1,240 @@
+---
+version: 2
+interactions:
+ - id: 0
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 385
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: |
+ {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"},{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"}],"version":"1.0"},"type":"custom_framework"}}
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ Content-Type:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks
+ method: POST
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 123
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 1.052087125s
+ - id: 1
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 412
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 911.326958ms
+ - id: 2
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 412
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 250.824375ms
+ - id: 3
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 483
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: |
+ {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"security-control","rules_id":["def-000-cea"]}],"name":"security-requirement"},{"controls":[{"name":"control","rules_id":["def-000-be9"]}],"name":"requirement"},{"controls":[{"name":"control-2","rules_id":["def-000-be9"]},{"name":"control-3","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement-2"}],"version":"1.0"},"type":"custom_framework"}}
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ Content-Type:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: PUT
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 123
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 874.516417ms
+ - id: 4
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 510
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"name":"requirement","controls":[{"name":"control","rules_id":["def-000-be9"]}]},{"name":"requirement-2","controls":[{"name":"control-3","rules_id":["def-000-be9","def-000-cea"]},{"name":"control-2","rules_id":["def-000-be9"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-cea"]}]}],"version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 609.843333ms
+ - id: 5
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: DELETE
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 180
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"test url","name":"new-name","version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 492.356333ms
+ - id: 6
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 51
+ uncompressed: false
+ body: '{"errors":[{"status":"400","title":"Bad Request"}]}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 400 Bad Request
+ code: 400
+ duration: 81.528292ms
diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateBasic.freeze b/datadog/tests/cassettes/TestCustomFramework_CreateBasic.freeze
new file mode 100644
index 0000000000..44a6b5c440
--- /dev/null
+++ b/datadog/tests/cassettes/TestCustomFramework_CreateBasic.freeze
@@ -0,0 +1 @@
+2025-05-22T10:28:47.103907-05:00
\ No newline at end of file
diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateBasic.yaml b/datadog/tests/cassettes/TestCustomFramework_CreateBasic.yaml
new file mode 100644
index 0000000000..4e46283fce
--- /dev/null
+++ b/datadog/tests/cassettes/TestCustomFramework_CreateBasic.yaml
@@ -0,0 +1,342 @@
+---
+version: 2
+interactions:
+ - id: 0
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 252
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: |
+ {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}}
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ Content-Type:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks
+ method: POST
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 123
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 496.214583ms
+ - id: 1
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 279
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 138.789792ms
+ - id: 2
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 279
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 216.784209ms
+ - id: 3
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 385
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: |
+ {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"},{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"}],"version":"1.0"},"type":"custom_framework"}}
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ Content-Type:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: PUT
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 123
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 579.1545ms
+ - id: 4
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 412
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 387.4965ms
+ - id: 5
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 412
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 325.68ms
+ - id: 6
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 230
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: |
+ {"data":{"attributes":{"handle":"terraform-handle","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}}
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ Content-Type:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: PUT
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 123
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 595.280625ms
+ - id: 7
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 257
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 191.487459ms
+ - id: 8
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: DELETE
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 187
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"","name":"new-framework-terraform","version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 416.820542ms
+ - id: 9
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 51
+ uncompressed: false
+ body: '{"errors":[{"status":"400","title":"Bad Request"}]}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 400 Bad Request
+ code: 400
+ duration: 83.548458ms
diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.freeze b/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.freeze
new file mode 100644
index 0000000000..bcd1668ea5
--- /dev/null
+++ b/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.freeze
@@ -0,0 +1 @@
+2025-05-22T10:29:15.759609-05:00
\ No newline at end of file
diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.yaml b/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.yaml
new file mode 100644
index 0000000000..8ac6513926
--- /dev/null
+++ b/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.yaml
@@ -0,0 +1,138 @@
+---
+version: 2
+interactions:
+ - id: 0
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 230
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: |
+ {"data":{"attributes":{"handle":"terraform-handle","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}}
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ Content-Type:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks
+ method: POST
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 123
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 506.19625ms
+ - id: 1
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 257
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 276.140542ms
+ - id: 2
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: DELETE
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 187
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"","name":"new-framework-terraform","version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 750.406292ms
+ - id: 3
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 51
+ uncompressed: false
+ body: '{"errors":[{"status":"400","title":"Bad Request"}]}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 400 Bad Request
+ code: 400
+ duration: 70.235625ms
diff --git a/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.freeze b/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.freeze
new file mode 100644
index 0000000000..548c782406
--- /dev/null
+++ b/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.freeze
@@ -0,0 +1 @@
+2025-05-22T11:41:02.727814-05:00
\ No newline at end of file
diff --git a/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.yaml b/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.yaml
new file mode 100644
index 0000000000..f929e5081a
--- /dev/null
+++ b/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.yaml
@@ -0,0 +1,273 @@
+---
+version: 2
+interactions:
+ - id: 0
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 252
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: |
+ {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}}
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ Content-Type:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks
+ method: POST
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 123
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 519.797292ms
+ - id: 1
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 279
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 379.494375ms
+ - id: 2
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: DELETE
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 195
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 338.217417ms
+ - id: 3
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 51
+ uncompressed: false
+ body: '{"errors":[{"status":"400","title":"Bad Request"}]}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 400 Bad Request
+ code: 400
+ duration: 101.837333ms
+ - id: 4
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 252
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: |
+ {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}}
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ Content-Type:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks
+ method: POST
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 123
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 370.99ms
+ - id: 5
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 279
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 189.421584ms
+ - id: 6
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: DELETE
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 195
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 656.383917ms
+ - id: 7
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 51
+ uncompressed: false
+ body: '{"errors":[{"status":"400","title":"Bad Request"}]}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 400 Bad Request
+ code: 400
+ duration: 99.99025ms
diff --git a/datadog/tests/cassettes/TestCustomFramework_DeleteNonExistent.freeze b/datadog/tests/cassettes/TestCustomFramework_DeleteNonExistent.freeze
new file mode 100644
index 0000000000..d8713a1fad
--- /dev/null
+++ b/datadog/tests/cassettes/TestCustomFramework_DeleteNonExistent.freeze
@@ -0,0 +1 @@
+2025-04-30T17:54:47.983034-04:00
\ No newline at end of file
diff --git a/datadog/tests/cassettes/TestCustomFramework_DeleteNonExistent.yaml b/datadog/tests/cassettes/TestCustomFramework_DeleteNonExistent.yaml
new file mode 100644
index 0000000000..8d079d2974
--- /dev/null
+++ b/datadog/tests/cassettes/TestCustomFramework_DeleteNonExistent.yaml
@@ -0,0 +1,138 @@
+---
+version: 2
+interactions:
+ - id: 0
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 289
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: |
+ {"data":{"attributes":{"description":"test description","handle":"fake-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"fake-version"},"type":"custom_framework"}}
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ Content-Type:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks
+ method: POST
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 131
+ uncompressed: false
+ body: '{"data":{"id":"fake-handle-fake-version","type":"custom_framework","attributes":{"handle":"fake-handle","version":"fake-version"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 376.17375ms
+ - id: 1
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/fake-handle/fake-version
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 320
+ uncompressed: false
+ body: '{"data":{"id":"fake-handle-fake-version","type":"custom_framework","attributes":{"description":"test description","handle":"fake-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"fake-version"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 217.58925ms
+ - id: 2
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/fake-handle/fake-version
+ method: DELETE
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 219
+ uncompressed: false
+ body: '{"data":{"id":"fake-handle-fake-version","type":"custom_framework","attributes":{"description":"test description","handle":"fake-handle","icon_url":"test url","name":"new-framework-terraform","version":"fake-version"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 417.445333ms
+ - id: 3
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/fake-handle/fake-version
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 51
+ uncompressed: false
+ body: '{"errors":[{"status":"400","title":"Bad Request"}]}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 400 Bad Request
+ code: 400
+ duration: 41.793375ms
diff --git a/datadog/tests/cassettes/TestCustomFramework_DuplicateHandle.freeze b/datadog/tests/cassettes/TestCustomFramework_DuplicateHandle.freeze
new file mode 100644
index 0000000000..4ce29ffaff
--- /dev/null
+++ b/datadog/tests/cassettes/TestCustomFramework_DuplicateHandle.freeze
@@ -0,0 +1 @@
+2025-05-26T23:32:54.142418-05:00
\ No newline at end of file
diff --git a/datadog/tests/cassettes/TestCustomFramework_DuplicateHandle.yaml b/datadog/tests/cassettes/TestCustomFramework_DuplicateHandle.yaml
new file mode 100644
index 0000000000..1f8a18a363
--- /dev/null
+++ b/datadog/tests/cassettes/TestCustomFramework_DuplicateHandle.yaml
@@ -0,0 +1,207 @@
+---
+version: 2
+interactions:
+ - id: 0
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 252
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: |
+ {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}}
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ Content-Type:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks
+ method: POST
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 123
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 368.886833ms
+ - id: 1
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 279
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 171.795125ms
+ - id: 2
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 279
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 169.562916ms
+ - id: 3
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 266
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: |
+ {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"different-version"},"type":"custom_framework"}}
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ Content-Type:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks
+ method: POST
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 127
+ uncompressed: false
+ body: '{"errors":[{"status":"409","title":"Status Conflict","detail":"already_exists(Framework ''terraform-handle'' already existed)"}]}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 409 Conflict
+ code: 409
+ duration: 69.033792ms
+ - id: 4
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: DELETE
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 195
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 372.856ms
+ - id: 5
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/different-version
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 51
+ uncompressed: false
+ body: '{"errors":[{"status":"400","title":"Bad Request"}]}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 400 Bad Request
+ code: 400
+ duration: 306.9705ms
diff --git a/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.freeze b/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.freeze
new file mode 100644
index 0000000000..dc47d9e490
--- /dev/null
+++ b/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.freeze
@@ -0,0 +1 @@
+2025-05-22T11:48:01.178393-05:00
\ No newline at end of file
diff --git a/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.yaml b/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.yaml
new file mode 100644
index 0000000000..9707f7b36c
--- /dev/null
+++ b/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.yaml
@@ -0,0 +1,138 @@
+---
+version: 2
+interactions:
+ - id: 0
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 243
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: |
+ {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"framework-name","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}}
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ Content-Type:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks
+ method: POST
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 123
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 646.051833ms
+ - id: 1
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 270
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test url","name":"framework-name","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 236.522542ms
+ - id: 2
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: DELETE
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 186
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"test url","name":"framework-name","version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 479.3125ms
+ - id: 3
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 51
+ uncompressed: false
+ body: '{"errors":[{"status":"400","title":"Bad Request"}]}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 400 Bad Request
+ code: 400
+ duration: 97.182917ms
diff --git a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze
new file mode 100644
index 0000000000..c80ac1347c
--- /dev/null
+++ b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze
@@ -0,0 +1 @@
+2025-05-22T12:37:52.652765-05:00
\ No newline at end of file
diff --git a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml
new file mode 100644
index 0000000000..92fb6030eb
--- /dev/null
+++ b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml
@@ -0,0 +1,39 @@
+---
+version: 2
+interactions:
+ - id: 0
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 256
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: |
+ {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["invalid-rule-id"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}}
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ Content-Type:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks
+ method: POST
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 116
+ uncompressed: false
+ body: '{"errors":[{"status":"400","title":"Bad Request","detail":"invalid_argument(Cannot find rule id invalid-rule-id)"}]}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 400 Bad Request
+ code: 400
+ duration: 364.630958ms
diff --git a/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.freeze b/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.freeze
new file mode 100644
index 0000000000..fed2f6f156
--- /dev/null
+++ b/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.freeze
@@ -0,0 +1 @@
+2025-05-22T11:40:36.279056-05:00
\ No newline at end of file
diff --git a/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.yaml b/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.yaml
new file mode 100644
index 0000000000..80c310b837
--- /dev/null
+++ b/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.yaml
@@ -0,0 +1,273 @@
+---
+version: 2
+interactions:
+ - id: 0
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 252
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: |
+ {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}}
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ Content-Type:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks
+ method: POST
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 123
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 656.768083ms
+ - id: 1
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 279
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 180.020208ms
+ - id: 2
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: DELETE
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 195
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 536.796417ms
+ - id: 3
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 51
+ uncompressed: false
+ body: '{"errors":[{"status":"400","title":"Bad Request"}]}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 400 Bad Request
+ code: 400
+ duration: 80.27375ms
+ - id: 4
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 252
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: |
+ {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}}
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ Content-Type:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks
+ method: POST
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 123
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 517.871542ms
+ - id: 5
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 279
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 153.309084ms
+ - id: 6
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: DELETE
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 195
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 346.506375ms
+ - id: 7
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 51
+ uncompressed: false
+ body: '{"errors":[{"status":"400","title":"Bad Request"}]}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 400 Bad Request
+ code: 400
+ duration: 93.734042ms
diff --git a/datadog/tests/cassettes/TestCustomFramework_RecreateOnImmutableFields.freeze b/datadog/tests/cassettes/TestCustomFramework_RecreateOnImmutableFields.freeze
new file mode 100644
index 0000000000..fa34135a73
--- /dev/null
+++ b/datadog/tests/cassettes/TestCustomFramework_RecreateOnImmutableFields.freeze
@@ -0,0 +1 @@
+2025-05-22T11:43:27.403873-05:00
\ No newline at end of file
diff --git a/datadog/tests/cassettes/TestCustomFramework_RecreateOnImmutableFields.yaml b/datadog/tests/cassettes/TestCustomFramework_RecreateOnImmutableFields.yaml
new file mode 100644
index 0000000000..255c5c20ad
--- /dev/null
+++ b/datadog/tests/cassettes/TestCustomFramework_RecreateOnImmutableFields.yaml
@@ -0,0 +1,306 @@
+---
+version: 2
+interactions:
+ - id: 0
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 252
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: |
+ {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}}
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ Content-Type:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks
+ method: POST
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 123
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 417.890125ms
+ - id: 1
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 279
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 356.255ms
+ - id: 2
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 279
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 177.291709ms
+ - id: 3
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: DELETE
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 195
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 439.09975ms
+ - id: 4
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 256
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: |
+ {"data":{"attributes":{"handle":"terraform-handle-new","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"2.0"},"type":"custom_framework"}}
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ Content-Type:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks
+ method: POST
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 131
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-new-2.0","type":"custom_framework","attributes":{"handle":"terraform-handle-new","version":"2.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 430.19675ms
+ - id: 5
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 51
+ uncompressed: false
+ body: '{"errors":[{"status":"400","title":"Bad Request"}]}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 400 Bad Request
+ code: 400
+ duration: 100.05225ms
+ - id: 6
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle-new/2.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 287
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-new-2.0","type":"custom_framework","attributes":{"handle":"terraform-handle-new","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"2.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 168.000083ms
+ - id: 7
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle-new/2.0
+ method: DELETE
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 203
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-new-2.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle-new","icon_url":"test-url","name":"new-framework-terraform","version":"2.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 336.826417ms
+ - id: 8
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle-new/2.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 51
+ uncompressed: false
+ body: '{"errors":[{"status":"400","title":"Bad Request"}]}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 400 Bad Request
+ code: 400
+ duration: 84.338167ms
diff --git a/datadog/tests/cassettes/TestCustomFramework_SameFrameworkID.freeze b/datadog/tests/cassettes/TestCustomFramework_SameFrameworkID.freeze
new file mode 100644
index 0000000000..717d43f5ba
--- /dev/null
+++ b/datadog/tests/cassettes/TestCustomFramework_SameFrameworkID.freeze
@@ -0,0 +1 @@
+2025-05-22T10:30:04.353302-05:00
\ No newline at end of file
diff --git a/datadog/tests/cassettes/TestCustomFramework_SameFrameworkID.yaml b/datadog/tests/cassettes/TestCustomFramework_SameFrameworkID.yaml
new file mode 100644
index 0000000000..73889a4c7c
--- /dev/null
+++ b/datadog/tests/cassettes/TestCustomFramework_SameFrameworkID.yaml
@@ -0,0 +1,408 @@
+---
+version: 2
+interactions:
+ - id: 0
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 259
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: |
+ {"data":{"attributes":{"handle":"new-terraform","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"framework-1.0"},"type":"custom_framework"}}
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ Content-Type:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks
+ method: POST
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 137
+ uncompressed: false
+ body: '{"data":{"id":"new-terraform-framework-1.0","type":"custom_framework","attributes":{"handle":"new-terraform","version":"framework-1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 620.897666ms
+ - id: 1
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/new-terraform/framework-1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 293
+ uncompressed: false
+ body: '{"data":{"id":"new-terraform-framework-1.0","type":"custom_framework","attributes":{"handle":"new-terraform","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"framework-1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 173.264625ms
+ - id: 2
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/new-terraform/framework-1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 293
+ uncompressed: false
+ body: '{"data":{"id":"new-terraform-framework-1.0","type":"custom_framework","attributes":{"handle":"new-terraform","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"framework-1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 242.677875ms
+ - id: 3
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/new-terraform/framework-1.0
+ method: DELETE
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 209
+ uncompressed: false
+ body: '{"data":{"id":"new-terraform-framework-1.0","type":"custom_framework","attributes":{"description":"","handle":"new-terraform","icon_url":"test-url","name":"new-framework-terraform","version":"framework-1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 343.581417ms
+ - id: 4
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 259
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: |
+ {"data":{"attributes":{"handle":"new","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"terraform-framework-1.0"},"type":"custom_framework"}}
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ Content-Type:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks
+ method: POST
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 137
+ uncompressed: false
+ body: '{"data":{"id":"new-terraform-framework-1.0","type":"custom_framework","attributes":{"handle":"new","version":"terraform-framework-1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 525.991166ms
+ - id: 5
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/new/terraform-framework-1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 293
+ uncompressed: false
+ body: '{"data":{"id":"new-terraform-framework-1.0","type":"custom_framework","attributes":{"handle":"new","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"terraform-framework-1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 214.634792ms
+ - id: 6
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/new/terraform-framework-1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 293
+ uncompressed: false
+ body: '{"data":{"id":"new-terraform-framework-1.0","type":"custom_framework","attributes":{"handle":"new","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"terraform-framework-1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 200.667916ms
+ - id: 7
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/new/terraform-framework-1.0
+ method: DELETE
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 209
+ uncompressed: false
+ body: '{"data":{"id":"new-terraform-framework-1.0","type":"custom_framework","attributes":{"description":"","handle":"new","icon_url":"test-url","name":"new-framework-terraform","version":"terraform-framework-1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 331.03ms
+ - id: 8
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 259
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: |
+ {"data":{"attributes":{"handle":"new-terraform","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"framework-1.0"},"type":"custom_framework"}}
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ Content-Type:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks
+ method: POST
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 137
+ uncompressed: false
+ body: '{"data":{"id":"new-terraform-framework-1.0","type":"custom_framework","attributes":{"handle":"new-terraform","version":"framework-1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 347.11125ms
+ - id: 9
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/new-terraform/framework-1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 293
+ uncompressed: false
+ body: '{"data":{"id":"new-terraform-framework-1.0","type":"custom_framework","attributes":{"handle":"new-terraform","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"framework-1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 231.189833ms
+ - id: 10
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/new-terraform/framework-1.0
+ method: DELETE
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 209
+ uncompressed: false
+ body: '{"data":{"id":"new-terraform-framework-1.0","type":"custom_framework","attributes":{"description":"","handle":"new-terraform","icon_url":"test-url","name":"new-framework-terraform","version":"framework-1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 362.871458ms
+ - id: 11
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/new-terraform/framework-1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 51
+ uncompressed: false
+ body: '{"errors":[{"status":"400","title":"Bad Request"}]}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 400 Bad Request
+ code: 400
+ duration: 69.891458ms
diff --git a/datadog/tests/cassettes/TestCustomFramework_TestImportingFramework.freeze b/datadog/tests/cassettes/TestCustomFramework_TestImportingFramework.freeze
new file mode 100644
index 0000000000..505bd65163
--- /dev/null
+++ b/datadog/tests/cassettes/TestCustomFramework_TestImportingFramework.freeze
@@ -0,0 +1 @@
+2025-05-01T15:59:42.287109-04:00
\ No newline at end of file
diff --git a/datadog/tests/cassettes/TestCustomFramework_TestImportingFramework.yaml b/datadog/tests/cassettes/TestCustomFramework_TestImportingFramework.yaml
new file mode 100644
index 0000000000..53a7d252e3
--- /dev/null
+++ b/datadog/tests/cassettes/TestCustomFramework_TestImportingFramework.yaml
@@ -0,0 +1,69 @@
+---
+version: 2
+interactions:
+ - id: 0
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/new-terraform-framework-test/2
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 313
+ uncompressed: false
+ body: '{"data":{"id":"new-terraform-framework-test-2","type":"custom_framework","attributes":{"description":"","handle":"new-terraform-framework-test","icon_url":"","name":"new-terraform-framework-test","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-u48"]}]}],"version":"2"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 245.226916ms
+ - id: 1
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/new-terraform-framework-test/2
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 313
+ uncompressed: false
+ body: '{"data":{"id":"new-terraform-framework-test-2","type":"custom_framework","attributes":{"description":"","handle":"new-terraform-framework-test","icon_url":"","name":"new-terraform-framework-test","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-u48"]}]}],"version":"2"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 159.962333ms
diff --git a/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.freeze b/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.freeze
new file mode 100644
index 0000000000..1e94fe1bad
--- /dev/null
+++ b/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.freeze
@@ -0,0 +1 @@
+2025-05-27T08:37:45.083914-05:00
\ No newline at end of file
diff --git a/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.yaml b/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.yaml
new file mode 100644
index 0000000000..3411325ac1
--- /dev/null
+++ b/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.yaml
@@ -0,0 +1,243 @@
+---
+version: 2
+interactions:
+ - id: 0
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 255
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: |
+ {"data":{"attributes":{"handle":"terraform-handle","icon_url":"","name":"existing-framework","requirements":[{"controls":[{"name":"existing-control","rules_id":["def-000-be9"]}],"name":"existing-requirement"}],"version":"1.0"},"type":"custom_framework"}}
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ Content-Type:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks
+ method: POST
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 123
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 262.048625ms
+ - id: 1
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 252
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: |
+ {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}}
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ Content-Type:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks
+ method: POST
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 127
+ uncompressed: false
+ body: '{"errors":[{"status":"409","title":"Status Conflict","detail":"already_exists(Framework ''terraform-handle'' already existed)"}]}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 409 Conflict
+ code: 409
+ duration: 63.423875ms
+ - id: 2
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 268
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","name":"existing-framework","requirements":[{"name":"existing-requirement","controls":[{"name":"existing-control","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 165.03725ms
+ - id: 3
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 252
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: |
+ {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}}
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ Content-Type:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: PUT
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 123
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 394.019625ms
+ - id: 4
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 279
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 171.630875ms
+ - id: 5
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: DELETE
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 195
+ uncompressed: false
+ body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","version":"1.0"}}}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 200 OK
+ code: 200
+ duration: 377.870958ms
+ - id: 6
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ transfer_encoding: []
+ trailer: {}
+ host: api.datadoghq.com
+ remote_addr: ""
+ request_uri: ""
+ body: ""
+ form: {}
+ headers:
+ Accept:
+ - application/json
+ url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0
+ method: GET
+ response:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ transfer_encoding: []
+ trailer: {}
+ content_length: 51
+ uncompressed: false
+ body: '{"errors":[{"status":"400","title":"Bad Request"}]}'
+ headers:
+ Content-Type:
+ - application/vnd.api+json
+ status: 400 Bad Request
+ code: 400
+ duration: 61.166708ms
diff --git a/datadog/tests/provider_test.go b/datadog/tests/provider_test.go
index 51b5af5bb3..fd45e2903f 100644
--- a/datadog/tests/provider_test.go
+++ b/datadog/tests/provider_test.go
@@ -270,6 +270,7 @@ var testFiles2EndpointTags = map[string]string{
"tests/resource_datadog_webhook_custom_variable_test": "webhook_custom_variable",
"tests/resource_datadog_webhook_test": "webhook",
"tests/resource_datadog_workflow_automation_test": "workflow_automation",
+ "tests/resource_datadog_compliance_custom_framework_test": "compliance_custom_framework",
}
// getEndpointTagValue traverses callstack frames to find the test function that invoked this call;
diff --git a/datadog/tests/resource_datadog_compliance_custom_framework_test.go b/datadog/tests/resource_datadog_compliance_custom_framework_test.go
new file mode 100644
index 0000000000..4533fe040b
--- /dev/null
+++ b/datadog/tests/resource_datadog_compliance_custom_framework_test.go
@@ -0,0 +1,937 @@
+package test
+
+import (
+ "context"
+ "fmt"
+ "regexp"
+ "testing"
+
+ "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/hashicorp/terraform-plugin-testing/terraform"
+
+ "github.com/terraform-providers/terraform-provider-datadog/datadog/fwprovider"
+)
+
+type complianceCustomFrameworkModel struct {
+ Handle types.String
+ Version types.String
+ Name types.String
+ IconURL types.String
+ Requirements []complianceCustomFrameworkRequirementsModel
+}
+
+type complianceCustomFrameworkRequirementsModel struct {
+ Name types.String
+ Controls []complianceCustomFrameworkControlsModel
+}
+
+type complianceCustomFrameworkControlsModel struct {
+ Name types.String
+ RulesID []string
+}
+
+func buildCreateFrameworkRequest(model complianceCustomFrameworkModel) *datadogV2.CreateCustomFrameworkRequest {
+ req := datadogV2.NewCreateCustomFrameworkRequestWithDefaults()
+ iconURL := model.IconURL.ValueString()
+ req.SetData(datadogV2.CustomFrameworkData{
+ Type: "custom_framework",
+ Attributes: datadogV2.CustomFrameworkDataAttributes{
+ Handle: model.Handle.ValueString(),
+ Name: model.Name.ValueString(),
+ IconUrl: &iconURL,
+ Version: model.Version.ValueString(),
+ Requirements: func() []datadogV2.CustomFrameworkRequirement {
+ requirements := make([]datadogV2.CustomFrameworkRequirement, len(model.Requirements))
+ for i, req := range model.Requirements {
+ controls := make([]datadogV2.CustomFrameworkControl, len(req.Controls))
+ for j, ctrl := range req.Controls {
+ controls[j] = *datadogV2.NewCustomFrameworkControl(ctrl.Name.ValueString(), ctrl.RulesID)
+ }
+ requirements[i] = *datadogV2.NewCustomFrameworkRequirement(controls, req.Name.ValueString())
+ }
+ return requirements
+ }(),
+ },
+ })
+ return req
+}
+
+func TestCustomFramework_CreateBasic(t *testing.T) {
+ handle := "terraform-handle"
+ version := "1.0"
+
+ ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t)
+ path := "datadog_compliance_custom_framework.sample_rules"
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV5ProviderFactories: accProviders,
+ CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle),
+ Steps: []resource.TestStep{
+ {
+ Config: testAccCheckDatadogCreateFramework(version, handle),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(path, "handle", handle),
+ resource.TestCheckResourceAttr(path, "version", version),
+ resource.TestCheckResourceAttr(path, "name", "new-framework-terraform"),
+ resource.TestCheckResourceAttr(path, "icon_url", "test-url"),
+ resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement1"),
+ resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"),
+ resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9")),
+ },
+ {
+ Config: testAccCheckDatadogCreateFrameworkWithMultipleRequirements(version, handle),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(path, "handle", handle),
+ resource.TestCheckResourceAttr(path, "version", version),
+ resource.TestCheckResourceAttr(path, "requirements.0.name", "security-requirement"),
+ resource.TestCheckResourceAttr(path, "requirements.1.name", "compliance-requirement"),
+ resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "security-control"),
+ resource.TestCheckResourceAttr(path, "requirements.1.controls.0.name", "compliance-control"),
+ resource.TestCheckTypeSetElemAttr(path, "requirements.0.controls.0.rules_id.*", "def-000-be9"),
+ resource.TestCheckTypeSetElemAttr(path, "requirements.0.controls.0.rules_id.*", "def-000-cea"),
+ resource.TestCheckTypeSetElemAttr(path, "requirements.1.controls.0.rules_id.*", "def-000-be9"),
+ resource.TestCheckTypeSetElemAttr(path, "requirements.1.controls.0.rules_id.*", "def-000-cea"),
+ ),
+ },
+ {
+ Config: testAccCheckDatadogCreateFrameworkWithoutOptionalFields(version, handle),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(path, "handle", handle),
+ resource.TestCheckResourceAttr(path, "version", version),
+ resource.TestCheckResourceAttr(path, "name", "new-framework-terraform"),
+ resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement1"),
+ resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"),
+ resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"),
+ ),
+ },
+ },
+ })
+}
+
+func TestCustomFramework_CreateWithoutIconURL(t *testing.T) {
+ handle := "terraform-handle"
+ version := "1.0"
+
+ ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t)
+ path := "datadog_compliance_custom_framework.sample_rules"
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV5ProviderFactories: accProviders,
+ CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle),
+ Steps: []resource.TestStep{
+ {
+ Config: testAccCheckDatadogCreateFrameworkWithoutOptionalFields(version, handle),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(path, "handle", handle),
+ resource.TestCheckResourceAttr(path, "version", version),
+ resource.TestCheckResourceAttr(path, "name", "new-framework-terraform"),
+ resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement1"),
+ resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"),
+ resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"),
+ ),
+ },
+ },
+ })
+}
+
+func TestCustomFramework_CreateAndUpdateMultipleRequirements(t *testing.T) {
+ handle := "terraform-handle"
+ version := "1.0"
+
+ ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t)
+ path := "datadog_compliance_custom_framework.sample_rules"
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV5ProviderFactories: accProviders,
+ CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle),
+ Steps: []resource.TestStep{
+ {
+ Config: testAccCheckDatadogCreateFrameworkWithMultipleRequirements(version, handle),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(path, "handle", handle),
+ resource.TestCheckResourceAttr(path, "version", version),
+ resource.TestCheckResourceAttr(path, "requirements.0.name", "security-requirement"),
+ resource.TestCheckResourceAttr(path, "requirements.1.name", "compliance-requirement"),
+ resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "security-control"),
+ resource.TestCheckResourceAttr(path, "requirements.1.controls.0.name", "compliance-control"),
+ resource.TestCheckTypeSetElemAttr(path, "requirements.0.controls.0.rules_id.*", "def-000-be9"),
+ resource.TestCheckTypeSetElemAttr(path, "requirements.0.controls.0.rules_id.*", "def-000-cea"),
+ resource.TestCheckTypeSetElemAttr(path, "requirements.1.controls.0.rules_id.*", "def-000-be9"),
+ resource.TestCheckTypeSetElemAttr(path, "requirements.1.controls.0.rules_id.*", "def-000-cea"),
+ ),
+ },
+ {
+ Config: testAccCheckDatadogUpdateFrameworkWithMultipleRequirements(version, handle),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(path, "handle", handle),
+ resource.TestCheckResourceAttr(path, "version", version),
+ resource.TestCheckResourceAttr(path, "requirements.0.name", "security-requirement"),
+ resource.TestCheckResourceAttr(path, "requirements.1.name", "requirement"),
+ resource.TestCheckResourceAttr(path, "requirements.2.name", "requirement-2"),
+ resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "security-control"),
+ resource.TestCheckResourceAttr(path, "requirements.1.controls.0.name", "control"),
+ resource.TestCheckResourceAttr(path, "requirements.2.controls.0.name", "control-2"),
+ resource.TestCheckResourceAttr(path, "requirements.2.controls.1.name", "control-3"),
+ resource.TestCheckTypeSetElemAttr(path, "requirements.0.controls.0.rules_id.*", "def-000-cea"),
+ resource.TestCheckTypeSetElemAttr(path, "requirements.1.controls.0.rules_id.*", "def-000-be9"),
+ resource.TestCheckTypeSetElemAttr(path, "requirements.2.controls.0.rules_id.*", "def-000-be9"),
+ resource.TestCheckTypeSetElemAttr(path, "requirements.2.controls.1.rules_id.*", "def-000-cea"),
+ resource.TestCheckTypeSetElemAttr(path, "requirements.2.controls.1.rules_id.*", "def-000-be9"),
+ ),
+ },
+ },
+ })
+}
+
+// these handle and version combinations would result in the same state ID however
+// terraform would still be able to tell the framework states apart since they have unique resource names (which is required by terraform)
+func TestCustomFramework_SameFrameworkID(t *testing.T) {
+ handle := "new-terraform"
+ version := "framework-1.0"
+
+ handle2 := "new"
+ version2 := "terraform-framework-1.0"
+
+ ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t)
+ path := "datadog_compliance_custom_framework.sample_rules"
+ path2 := "datadog_compliance_custom_framework.sample_framework"
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV5ProviderFactories: accProviders,
+ CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle),
+ Steps: []resource.TestStep{
+ {
+ Config: testAccCheckDatadogCreateFramework(version, handle),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(path, "handle", handle),
+ resource.TestCheckResourceAttr(path, "version", version),
+ ),
+ },
+ {
+ Config: testAccCheckDatadogCreateSecondFramework(version2, handle2),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(path2, "handle", handle2),
+ resource.TestCheckResourceAttr(path2, "version", version2),
+ ),
+ },
+ {
+ Config: testAccCheckDatadogCreateFramework(version, handle),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(path, "handle", handle),
+ resource.TestCheckResourceAttr(path, "version", version),
+ ),
+ ExpectNonEmptyPlan: false,
+ },
+ },
+ })
+}
+
+func TestCustomFramework_InvalidCreate(t *testing.T) {
+ handle := "terraform-handle"
+ version := "1.0"
+
+ ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t)
+ path := "datadog_compliance_custom_framework.sample_rules"
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV5ProviderFactories: accProviders,
+ CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle),
+ Steps: []resource.TestStep{
+ {
+ Config: testAccCheckDatadogCreateInvalidFrameworkName(version, handle),
+ ExpectError: regexp.MustCompile("Invalid Attribute Value Length"),
+ },
+ {
+ Config: testAccCheckDatadogCreateFrameworkRuleIdsInvalid(version, handle),
+ ExpectError: regexp.MustCompile("400 Bad Request"),
+ },
+ {
+ Config: testAccCheckDatadogCreateFrameworkNoRequirements(version, handle),
+ ExpectError: regexp.MustCompile("Invalid Block"),
+ },
+ {
+ Config: testAccCheckDatadogCreateFrameworkWithNoControls(version, handle),
+ ExpectError: regexp.MustCompile("Invalid Block"),
+ },
+ {
+ Config: testAccCheckDatadogCreateEmptyHandle(version),
+ ExpectError: regexp.MustCompile("Invalid Attribute Value Length"),
+ },
+ {
+ Config: testAccCheckDatadogCreateEmptyVersion(handle),
+ ExpectError: regexp.MustCompile("Invalid Attribute Value Length"),
+ },
+ {
+ Config: testAccCheckDatadogDuplicateRequirementName(version, handle),
+ ExpectError: regexp.MustCompile(".*Each Requirement must have a unique name.*"),
+ },
+ {
+ Config: testAccCheckDatadogDuplicateControlName(version, handle),
+ ExpectError: regexp.MustCompile(".*Each Control must have a unique name under the same requirement.*"),
+ },
+ {
+ Config: testAccCheckDatadogDuplicateRequirement(version, handle),
+ ExpectError: regexp.MustCompile(".*Each Requirement must have a unique name.*"),
+ },
+ {
+ Config: testAccCheckDatadogDuplicateControl(version, handle),
+ ExpectError: regexp.MustCompile(".*Each Control must have a unique name under the same requirement.*"),
+ },
+ {
+ Config: testAccCheckDatadogEmptyRequirementName(version, handle),
+ ExpectError: regexp.MustCompile("Invalid Attribute Value Length"),
+ },
+ {
+ Config: testAccCheckDatadogEmptyControlName(version, handle),
+ ExpectError: regexp.MustCompile("Invalid Attribute Value Length"),
+ },
+ {
+ Config: testAccCheckDatadogCreateFrameworkWithNoRulesId(version, handle),
+ ExpectError: regexp.MustCompile("Invalid Attribute Value"),
+ },
+ },
+ })
+}
+
+func TestCustomFramework_RecreateAfterAPIDelete(t *testing.T) {
+ handle := "terraform-handle"
+ version := "1.0"
+
+ ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t)
+ path := "datadog_compliance_custom_framework.sample_rules"
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV5ProviderFactories: accProviders,
+ CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle),
+ Steps: []resource.TestStep{
+ {
+ Config: testAccCheckDatadogCreateFramework(version, handle),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(path, "handle", handle),
+ resource.TestCheckResourceAttr(path, "version", version),
+ resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement1"),
+ resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"),
+ resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"),
+ ),
+ },
+ {
+ PreConfig: func() {
+ // Delete the framework directly via API
+ _, _, err := providers.frameworkProvider.DatadogApiInstances.GetSecurityMonitoringApiV2().DeleteCustomFramework(providers.frameworkProvider.Auth, handle, version)
+ if err != nil {
+ t.Fatalf("Failed to delete framework: %v", err)
+ }
+ },
+ Config: testAccCheckDatadogCreateFramework(version, handle),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(path, "handle", handle),
+ resource.TestCheckResourceAttr(path, "version", version),
+ resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement1"),
+ resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"),
+ resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"),
+ ),
+ },
+ },
+ })
+}
+
+func TestCustomFramework_DeleteAfterAPIDelete(t *testing.T) {
+ handle := "terraform-handle"
+ version := "1.0"
+
+ ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t)
+ path := "datadog_compliance_custom_framework.sample_rules"
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV5ProviderFactories: accProviders,
+ CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle),
+ Steps: []resource.TestStep{
+ {
+ Config: testAccCheckDatadogCreateFramework(version, handle),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(path, "handle", handle),
+ resource.TestCheckResourceAttr(path, "version", version),
+ resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement1"),
+ resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"),
+ resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"),
+ ),
+ },
+ {
+ PreConfig: func() {
+ // Delete the framework directly via API
+ _, _, err := providers.frameworkProvider.DatadogApiInstances.GetSecurityMonitoringApiV2().DeleteCustomFramework(providers.frameworkProvider.Auth, handle, version)
+ if err != nil {
+ t.Fatalf("Failed to delete framework: %v", err)
+ }
+ },
+ Config: testAccCheckDatadogCreateFramework(version, handle),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(path, "handle", handle),
+ resource.TestCheckResourceAttr(path, "version", version),
+ resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement1"),
+ resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"),
+ resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"),
+ ),
+ },
+ },
+ })
+}
+
+func TestCustomFramework_UpdateIfFrameworkExists(t *testing.T) {
+ handle := "terraform-handle"
+ version := "1.0"
+
+ ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t)
+ path := "datadog_compliance_custom_framework.sample_rules"
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV5ProviderFactories: accProviders,
+ CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle),
+ Steps: []resource.TestStep{
+ {
+ PreConfig: func() {
+ // Create the framework directly via API
+ _, _, err := providers.frameworkProvider.DatadogApiInstances.GetSecurityMonitoringApiV2().CreateCustomFramework(providers.frameworkProvider.Auth, *buildCreateFrameworkRequest(complianceCustomFrameworkModel{
+ Handle: types.StringValue(handle),
+ Version: types.StringValue(version),
+ Name: types.StringValue("existing-framework"),
+ Requirements: []complianceCustomFrameworkRequirementsModel{
+ {
+ Name: types.StringValue("existing-requirement"),
+ Controls: []complianceCustomFrameworkControlsModel{
+ {
+ Name: types.StringValue("existing-control"),
+ RulesID: []string{"def-000-be9"},
+ },
+ },
+ },
+ },
+ }))
+ if err != nil {
+ t.Fatalf("Failed to create framework: %v", err)
+ }
+ },
+ Config: testAccCheckDatadogCreateFramework(version, handle),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(path, "handle", handle),
+ resource.TestCheckResourceAttr(path, "version", version),
+ resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement1"),
+ resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"),
+ resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"),
+ ),
+ },
+ },
+ })
+}
+
+func TestCustomFramework_RecreateOnImmutableFields(t *testing.T) {
+ handle := "terraform-handle"
+ version := "1.0"
+ newHandle := "terraform-handle-new"
+ newVersion := "2.0"
+
+ ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t)
+ path := "datadog_compliance_custom_framework.sample_rules"
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV5ProviderFactories: accProviders,
+ CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle),
+ Steps: []resource.TestStep{
+ {
+ Config: testAccCheckDatadogCreateFramework(version, handle),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(path, "handle", handle),
+ resource.TestCheckResourceAttr(path, "version", version),
+ ),
+ },
+ {
+ Config: testAccCheckDatadogCreateFramework(newVersion, newHandle),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(path, "handle", newHandle),
+ resource.TestCheckResourceAttr(path, "version", newVersion),
+ // Verify old resource is deleted
+ func(s *terraform.State) error {
+ _, httpResp, err := providers.frameworkProvider.DatadogApiInstances.GetSecurityMonitoringApiV2().GetCustomFramework(providers.frameworkProvider.Auth, handle, version)
+ if err == nil || (httpResp != nil && httpResp.StatusCode != 400) {
+ return fmt.Errorf("old framework with handle %s and version %s still exists", handle, version)
+ }
+ return nil
+ },
+ ),
+ },
+ },
+ })
+}
+
+// There is no way to validate the duplicate rule IDs in the config before they are removed from the state in Terraform
+// because the state model converts the rules_id into sets, the duplicate rule IDs are removed
+// This test validates that the duplicate rule IDs are removed from the state and only one rule ID is present
+func TestCustomFramework_DuplicateRuleIds(t *testing.T) {
+ handle := "terraform-handle"
+ version := "1.0"
+
+ ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t)
+ path := "datadog_compliance_custom_framework.sample_rules"
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV5ProviderFactories: accProviders,
+ CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle),
+ Steps: []resource.TestStep{
+ {
+ Config: testAccCheckDatadogDuplicateRulesId(version, handle),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(path, "handle", handle),
+ resource.TestCheckResourceAttr(path, "version", version),
+ resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"),
+ resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"),
+ resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.#", "1"),
+ ),
+ },
+ },
+ })
+}
+
+func TestCustomFramework_DuplicateHandle(t *testing.T) {
+ handle := "terraform-handle"
+ version := "1.0"
+
+ ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t)
+ path := "datadog_compliance_custom_framework.sample_rules"
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV5ProviderFactories: accProviders,
+ CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle),
+ Steps: []resource.TestStep{
+ {
+ Config: testAccCheckDatadogCreateFramework(version, handle),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(path, "handle", handle),
+ resource.TestCheckResourceAttr(path, "version", version),
+ ),
+ },
+ {
+ Config: testAccCheckDatadogCreateSecondFramework("different-version", handle),
+ ExpectError: regexp.MustCompile("Framework with same handle already exists"),
+ },
+ },
+ })
+}
+
+func testAccCheckDatadogCreateFrameworkWithMultipleRequirements(version string, handle string) string {
+ return fmt.Sprintf(`
+ resource "datadog_compliance_custom_framework" "sample_rules" {
+ version = "%s"
+ handle = "%s"
+ name = "new-name"
+ icon_url = "test url"
+ requirements {
+ name = "security-requirement"
+ controls {
+ name = "security-control"
+ rules_id = ["def-000-cea", "def-000-be9"]
+ }
+ }
+ requirements {
+ name = "compliance-requirement"
+ controls {
+ name = "compliance-control"
+ rules_id = ["def-000-be9", "def-000-cea"]
+ }
+ }
+ }
+ `, version, handle)
+}
+
+func testAccCheckDatadogUpdateFrameworkWithMultipleRequirements(version, handle string) string {
+ return fmt.Sprintf(`
+ resource "datadog_compliance_custom_framework" "sample_rules" {
+ version = "%s"
+ handle = "%s"
+ name = "new-name"
+ icon_url = "test url"
+ requirements {
+ name = "security-requirement"
+ controls {
+ name = "security-control"
+ rules_id = ["def-000-cea"]
+ }
+ }
+ requirements {
+ name = "requirement"
+ controls {
+ name = "control"
+ rules_id = ["def-000-be9"]
+ }
+ }
+ requirements {
+ name = "requirement-2"
+ controls {
+ name = "control-2"
+ rules_id = ["def-000-be9"]
+ }
+ controls {
+ name = "control-3"
+ rules_id = ["def-000-cea", "def-000-be9"]
+ }
+ }
+ }
+ `, version, handle)
+}
+
+func testAccCheckDatadogCreateFramework(version string, handle string) string {
+ return fmt.Sprintf(`
+ resource "datadog_compliance_custom_framework" "sample_rules" {
+ version = "%s"
+ handle = "%s"
+ name = "new-framework-terraform"
+ icon_url = "test-url"
+ requirements {
+ name = "requirement1"
+ controls {
+ name = "control1"
+ rules_id = ["def-000-be9"]
+ }
+ }
+ }
+ `, version, handle)
+}
+
+func testAccCheckDatadogCreateSecondFramework(version string, handle string) string {
+ return fmt.Sprintf(`
+ resource "datadog_compliance_custom_framework" "sample_framework" {
+ version = "%s"
+ handle = "%s"
+ name = "new-framework-terraform"
+ icon_url = "test-url"
+ requirements {
+ name = "requirement1"
+ controls {
+ name = "control1"
+ rules_id = ["def-000-be9"]
+ }
+ }
+ }
+ `, version, handle)
+}
+
+func testAccCheckDatadogCreateFrameworkWithoutOptionalFields(version string, handle string) string {
+ return fmt.Sprintf(`
+ resource "datadog_compliance_custom_framework" "sample_rules" {
+ version = "%s"
+ handle = "%s"
+ name = "new-framework-terraform"
+ requirements {
+ name = "requirement1"
+ controls {
+ name = "control1"
+ rules_id = ["def-000-be9"]
+ }
+ }
+ }
+ `, version, handle)
+}
+
+func testAccCheckDatadogCreateFrameworkRuleIdsInvalid(version string, handle string) string {
+ return fmt.Sprintf(`
+ resource "datadog_compliance_custom_framework" "sample_rules" {
+ version = "%s"
+ handle = "%s"
+ name = "new-framework-terraform"
+ icon_url = "test url"
+ requirements {
+ name = "requirement1"
+ controls {
+ name = "control1"
+ rules_id = ["invalid-rule-id"]
+ }
+ }
+ }
+ `, version, handle)
+}
+
+func testAccCheckDatadogCreateFrameworkWithNoControls(version string, handle string) string {
+ return fmt.Sprintf(`
+ resource "datadog_compliance_custom_framework" "sample_rules" {
+ version = "%s"
+ handle = "%s"
+ name = "new-framework-terraform"
+ icon_url = "test url"
+ requirements {
+ name = "requirement1"
+ }
+ }
+ `, version, handle)
+}
+
+func testAccCheckDatadogCreateFrameworkWithNoRulesId(version string, handle string) string {
+ return fmt.Sprintf(`
+ resource "datadog_compliance_custom_framework" "sample_rules" {
+ version = "%s"
+ handle = "%s"
+ name = "new-framework-terraform"
+ icon_url = "test url"
+ requirements {
+ name = "requirement1"
+ controls {
+ name = "control1"
+ rules_id = []
+ }
+ }
+ }
+ `, version, handle)
+}
+
+func testAccCheckDatadogCreateFrameworkNoRequirements(version string, handle string) string {
+ return fmt.Sprintf(`
+ resource "datadog_compliance_custom_framework" "sample_rules" {
+ version = "%s"
+ handle = "%s"
+ name = "new-framework-terraform"
+ icon_url = "test url"
+ }
+ `, version, handle)
+}
+
+func testAccCheckDatadogCreateInvalidFrameworkName(version string, handle string) string {
+ return fmt.Sprintf(`
+ resource "datadog_compliance_custom_framework" "sample_rules" {
+ version = "%s"
+ handle = "%s"
+ name = ""
+ icon_url = "test url"
+ requirements {
+ name = "requirement1"
+ controls {
+ name = "control1"
+ rules_id = ["def-000-be9"]
+ }
+ }
+ }
+ `, version, handle)
+}
+
+func testAccCheckDatadogEmptyRequirementName(version string, handle string) string {
+ return fmt.Sprintf(`
+ resource "datadog_compliance_custom_framework" "sample_rules" {
+ version = "%s"
+ handle = "%s"
+ name = "name"
+ icon_url = "test url"
+ requirements {
+ name = ""
+ controls {
+ name = "control1"
+ rules_id = ["def-000-be9"]
+ }
+ }
+ }
+ `, version, handle)
+}
+
+func testAccCheckDatadogEmptyControlName(version string, handle string) string {
+ return fmt.Sprintf(`
+ resource "datadog_compliance_custom_framework" "sample_rules" {
+ version = "%s"
+ handle = "%s"
+ name = "name"
+ icon_url = "test url"
+ requirements {
+ name = "requirement1"
+ controls {
+ name = ""
+ rules_id = ["def-000-be9"]
+ }
+ }
+ }
+ `, version, handle)
+}
+
+func testAccCheckDatadogCreateEmptyHandle(version string) string {
+ return fmt.Sprintf(`
+ resource "datadog_compliance_custom_framework" "sample_rules" {
+ version = "%s"
+ handle = ""
+ name = "framework-name"
+ icon_url = "test url"
+ requirements {
+ name = "requirement1"
+ controls {
+ name = "control1"
+ rules_id = ["def-000-be9"]
+ }
+ }
+ }
+ `, version)
+}
+
+func testAccCheckDatadogCreateEmptyVersion(handle string) string {
+ return fmt.Sprintf(`
+ resource "datadog_compliance_custom_framework" "sample_rules" {
+ version = ""
+ handle = "%s"
+ name = "framework-name"
+ icon_url = "test url"
+ requirements {
+ name = "requirement1"
+ controls {
+ name = "control1"
+ rules_id = ["def-000-be9"]
+ }
+ }
+ }
+ `, handle)
+}
+
+func testAccCheckDatadogDuplicateRequirementName(version string, handle string) string {
+ return fmt.Sprintf(`
+ resource "datadog_compliance_custom_framework" "sample_rules" {
+ version = "%s"
+ handle = "%s"
+ name = "framework-name"
+ icon_url = "test url"
+ requirements {
+ name = "requirement1"
+ controls {
+ name = "control1"
+ rules_id = ["def-000-be9"]
+ }
+ }
+ requirements {
+ name = "requirement1"
+ controls {
+ name = "control2"
+ rules_id = ["def-000-cea"]
+ }
+ }
+ }
+ `, version, handle)
+}
+
+func testAccCheckDatadogDuplicateRequirement(version string, handle string) string {
+ return fmt.Sprintf(`
+ resource "datadog_compliance_custom_framework" "sample_rules" {
+ version = "%s"
+ handle = "%s"
+ name = "framework-name"
+ icon_url = "test url"
+ requirements {
+ name = "requirement1"
+ controls {
+ name = "control1"
+ rules_id = ["def-000-be9"]
+ }
+ }
+ requirements {
+ name = "requirement1"
+ controls {
+ name = "control1"
+ rules_id = ["def-000-be9"]
+ }
+ }
+ }
+ `, version, handle)
+}
+
+func testAccCheckDatadogDuplicateControlName(version string, handle string) string {
+ return fmt.Sprintf(`
+ resource "datadog_compliance_custom_framework" "sample_rules" {
+ version = "%s"
+ handle = "%s"
+ name = "framework-name"
+ icon_url = "test url"
+ requirements {
+ name = "requirement1"
+ controls {
+ name = "control1"
+ rules_id = ["def-000-be9"]
+ }
+ }
+ requirements {
+ name = "requirement2"
+ controls {
+ name = "control1"
+ rules_id = ["def-000-be9"]
+ }
+ controls {
+ name = "control1"
+ rules_id = ["def-000-cea"]
+ }
+ }
+ }
+ `, version, handle)
+}
+
+func testAccCheckDatadogDuplicateControl(version string, handle string) string {
+ return fmt.Sprintf(`
+ resource "datadog_compliance_custom_framework" "sample_rules" {
+ version = "%s"
+ handle = "%s"
+ name = "framework-name"
+ icon_url = "test url"
+ requirements {
+ name = "requirement1"
+ controls {
+ name = "control1"
+ rules_id = ["def-000-be9"]
+ }
+ }
+ requirements {
+ name = "requirement2"
+ controls {
+ name = "control1"
+ rules_id = ["def-000-be9"]
+ }
+ controls {
+ name = "control1"
+ rules_id = ["def-000-be9"]
+ }
+ }
+ }
+ `, version, handle)
+}
+
+func testAccCheckDatadogDuplicateRulesId(version string, handle string) string {
+ return fmt.Sprintf(`
+ resource "datadog_compliance_custom_framework" "sample_rules" {
+ version = "%s"
+ handle = "%s"
+ name = "framework-name"
+ icon_url = "test url"
+ requirements {
+ name = "requirement1"
+ controls {
+ name = "control1"
+ rules_id = ["def-000-be9", "def-000-be9"]
+ }
+ }
+ }
+ `, version, handle)
+}
+
+func testAccCheckDatadogFrameworkDestroy(ctx context.Context, accProvider *fwprovider.FrameworkProvider, resourceName string, version string, handle string) func(*terraform.State) error {
+ return func(s *terraform.State) error {
+ apiInstances := accProvider.DatadogApiInstances
+ resource, ok := s.RootModule().Resources[resourceName]
+ if !ok {
+ return nil
+ }
+ if resource.Primary == nil {
+ return nil
+ }
+ handle := resource.Primary.Attributes["handle"]
+ version := resource.Primary.Attributes["version"]
+ _, httpRes, err := apiInstances.GetSecurityMonitoringApiV2().GetCustomFramework(ctx, handle, version)
+ if err != nil {
+ if httpRes != nil && httpRes.StatusCode == 400 {
+ return nil
+ }
+ return err
+ }
+
+ return fmt.Errorf("framework destroy check failed")
+ }
+}
diff --git a/docs/resources/compliance_custom_framework.md b/docs/resources/compliance_custom_framework.md
new file mode 100644
index 0000000000..4fa8d0d37a
--- /dev/null
+++ b/docs/resources/compliance_custom_framework.md
@@ -0,0 +1,80 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "datadog_compliance_custom_framework Resource - terraform-provider-datadog"
+subcategory: ""
+description: |-
+ Provides a Datadog Compliance Custom Framework resource, which is used to create and manage compliance custom frameworks.
+---
+
+# datadog_compliance_custom_framework (Resource)
+
+Provides a Datadog Compliance Custom Framework resource, which is used to create and manage compliance custom frameworks.
+
+## Example Usage
+
+```terraform
+resource "datadog_compliance_custom_framework" "framework" {
+ name = "my-custom-framework-terraform-2"
+ version = "2.0.0"
+ handle = "my-custom-framework-terraform-2"
+
+ requirements {
+ name = "requirement2"
+ controls {
+ name = "control2"
+ rules_id = ["def-000-h9o", "def-000-b6i", "def-000-yed", "def-000-h5a", "def-000-aw5"]
+ }
+ controls {
+ name = "control1"
+ rules_id = ["def-000-j9v", "def-000-465", "def-000-vq1", "def-000-4hf", "def-000-s2d", "def-000-vnl"]
+ }
+ }
+
+ requirements {
+ name = "requirement1"
+ controls {
+ name = "control2"
+ rules_id = ["def-000-wuf", "def-000-7og"]
+ }
+ controls {
+ name = "control5"
+ rules_id = ["def-000-mdt", "def-000-zrx", "def-000-z6k"]
+ }
+ }
+}
+```
+
+
+## Schema
+
+### Required
+
+- `handle` (String) The framework handle. String length must be at least 1. This field is immutable.
+- `name` (String) The framework name. String length must be at least 1.
+- `version` (String) The framework version. String length must be at least 1. This field is immutable.
+- `requirements` (Block List) The requirements of the framework. Length must be at least 1. (see [below for nested schema](#nestedblock--requirements))
+
+
+### Optional
+
+- `icon_url` (String) The URL of the icon representing the framework
+
+### Read-Only
+
+- `id` (String) The ID of the compliance custom framework resource.
+
+
+### Nested Schema for `requirements`
+
+Required:
+
+- `name` (String) The name of the requirement. String length must be at least 1.
+- `controls` (Block List) The controls of the requirement. Length must be at least 1. (see [below for nested schema](#nestedblock--requirements--controls))
+
+
+### Nested Schema for `requirements.controls`
+
+Required:
+
+- `name` (String) The name of the control. String length must be at least 1.
+- `rules_id` (Set of String) The set of rules IDs for the control. Length must be at least 1.
diff --git a/examples/resources/datadog_compliance_custom_framework/resource.tf b/examples/resources/datadog_compliance_custom_framework/resource.tf
new file mode 100644
index 0000000000..12def186b5
--- /dev/null
+++ b/examples/resources/datadog_compliance_custom_framework/resource.tf
@@ -0,0 +1,29 @@
+resource "datadog_compliance_custom_framework" "framework" {
+ name = "my-custom-framework-terraform-2"
+ version = "2.0.0"
+ handle = "my-custom-framework-terraform-2"
+
+ requirements {
+ name = "requirement2"
+ controls {
+ name = "control2"
+ rules_id = ["def-000-h9o", "def-000-b6i", "def-000-yed", "def-000-h5a", "def-000-aw5"]
+ }
+ controls {
+ name = "control1"
+ rules_id = ["def-000-j9v", "def-000-465", "def-000-vq1", "def-000-4hf", "def-000-s2d", "def-000-vnl"]
+ }
+ }
+
+ requirements {
+ name = "requirement1"
+ controls {
+ name = "control2"
+ rules_id = ["def-000-wuf", "def-000-7og"]
+ }
+ controls {
+ name = "control5"
+ rules_id = ["def-000-mdt", "def-000-zrx", "def-000-z6k"]
+ }
+ }
+}
\ No newline at end of file
diff --git a/scripts/generate-docs.sh b/scripts/generate-docs.sh
index 7eb1cd4bc5..522010be16 100755
--- a/scripts/generate-docs.sh
+++ b/scripts/generate-docs.sh
@@ -4,6 +4,7 @@
# Add here the files to be excluded from the doc generation
exclude_files=(
"docs/resources/integration_aws_account.md"
+ "docs/resources/compliance_custom_framework.md"
)
# Check if manual changes were made to any excluded files and exit