diff --git a/docs/data-sources/cloud_compliance_framework_controls.md b/docs/data-sources/cloud_compliance_framework_controls.md
new file mode 100644
index 0000000..bfda9e7
--- /dev/null
+++ b/docs/data-sources/cloud_compliance_framework_controls.md
@@ -0,0 +1,86 @@
+---
+page_title: "crowdstrike_cloud_compliance_framework_controls Data Source - crowdstrike"
+subcategory: "Cloud Compliance"
+description: |-
+ This data source retrieves all or a subset of controls within compliance benchmarks. All non-FQL fields can accept wildcards * and query Falcon using logical AND. If FQL is defined, all other fields will be ignored. For advanced queries to further narrow your search, please use a Falcon Query Language (FQL) filter. For additional information on FQL filtering and usage, refer to the official CrowdStrike documentation: Falcon Query Language (FQL) https://falcon.crowdstrike.com/documentation/page/d3c84a1b/falcon-query-language-fql
+ API Scopes
+ The following API scopes are required:
+ Cloud Security Policies | Read
+---
+
+# crowdstrike_cloud_compliance_framework_controls (Data Source)
+
+This data source retrieves all or a subset of controls within compliance benchmarks. All non-FQL fields can accept wildcards `*` and query Falcon using logical AND. If FQL is defined, all other fields will be ignored. For advanced queries to further narrow your search, please use a Falcon Query Language (FQL) filter. For additional information on FQL filtering and usage, refer to the official CrowdStrike documentation: [Falcon Query Language (FQL)](https://falcon.crowdstrike.com/documentation/page/d3c84a1b/falcon-query-language-fql)
+
+## API Scopes
+
+The following API scopes are required:
+
+- Cloud Security Policies | Read
+
+
+## Example Usage
+
+```terraform
+terraform {
+ required_providers {
+ crowdstrike = {
+ source = "registry.terraform.io/crowdstrike/crowdstrike"
+ }
+ }
+}
+
+provider "crowdstrike" {
+ cloud = "us-2"
+}
+
+# retrieve all controls under a named benchmark
+data "crowdstrike_cloud_compliance_framework_controls" "all" {
+ benchmark = "CIS 1.0.0 AWS Web Architecture"
+}
+
+# retrieve a single control within a benchmark by name
+data "crowdstrike_cloud_compliance_framework_controls" "by_name" {
+ name = "Ensure subnets for the Web tier are created"
+ benchmark = "CIS 1.0.0 AWS Web Architecture"
+}
+
+# retrieve a single control within a benchmark by requirement
+data "crowdstrike_cloud_compliance_framework_controls" "by_requirement" {
+ requirement = "2.1"
+ benchmark = "CIS 1.0.0 AWS Web Architecture"
+}
+
+# query by FQL filter
+data "crowdstrike_cloud_compliance_framework_controls" "fql" {
+ fql = "compliance_control_name:'Ensure subnets for the Web tier are created'"
+}
+```
+
+
+## Schema
+
+### Optional
+
+- `benchmark` (String) Name of the compliance benchmark in the framework. Examples: `AWS Foundational Security Best Practices v1.*`, `CIS 1.2.0 GCP`, `CIS 1.8.0 GKE`
+- `control_name` (String) Name of the control. Examples: `Ensure security contact phone is set`, `Ensure that Azure Defender*`
+- `fql` (String) Falcon Query Language (FQL) filter for advanced control searches. FQL filter, allowed props: `compliance_control_name`, `compliance_control_authority`, `compliance_control_type`, `compliance_control_section`, `compliance_control_requirement`, `compliance_control_benchmark_name`, `compliance_control_benchmark_version`
+- `requirement` (String) Requirement of the control(s) within the framework. Examples: `2.*`, `1.1`
+- `section` (String) Section of the benchmark where the control(s) reside. Examples: `Data Protection`, `Data*`
+
+### Read-Only
+
+- `controls` (Attributes Set) Security framework and compliance rule information. (see [below for nested schema](#nestedatt--controls))
+
+
+### Nested Schema for `controls`
+
+Read-Only:
+
+- `authority` (String) The compliance authority for the framework
+- `benchmark` (String) The compliance benchmark within the framework.
+- `code` (String) The unique compliance framework rule code.
+- `id` (String) The id of the compliance control.
+- `name` (String) The name of the control.
+- `requirement` (String) The compliance framework requirement.
+- `section` (String) The section within the compliance benchmark.
diff --git a/docs/data-sources/cloud_posture_rules.md b/docs/data-sources/cloud_posture_rules.md
new file mode 100644
index 0000000..50640dd
--- /dev/null
+++ b/docs/data-sources/cloud_posture_rules.md
@@ -0,0 +1,106 @@
+---
+page_title: "crowdstrike_cloud_posture_rules Data Source - crowdstrike"
+subcategory: "Cloud Posture"
+description: |-
+ This data source retrieves detailed information about a specific cloud posture rule, including its unique identifier (ID) and associated attributes.All non-FQL fields can accept wildcards * and query Falcon using logical AND. If FQL is defined, all other fields will be ignored. For advanced queries to further narrow your search, please use a Falcon Query Language (FQL) filter. For additional information on FQL filtering and usage, refer to the official CrowdStrike documentation: Falcon Query Language (FQL) https://falcon.crowdstrike.com/documentation/page/d3c84a1b/falcon-query-language-fql
+ API Scopes
+ The following API scopes are required:
+ Cloud Security Policies | Read & Write
+---
+
+# crowdstrike_cloud_posture_rules (Data Source)
+
+This data source retrieves detailed information about a specific cloud posture rule, including its unique identifier (ID) and associated attributes.All non-FQL fields can accept wildcards `*` and query Falcon using logical AND. If FQL is defined, all other fields will be ignored. For advanced queries to further narrow your search, please use a Falcon Query Language (FQL) filter. For additional information on FQL filtering and usage, refer to the official CrowdStrike documentation: [Falcon Query Language (FQL)](https://falcon.crowdstrike.com/documentation/page/d3c84a1b/falcon-query-language-fql)
+
+## API Scopes
+
+The following API scopes are required:
+
+- Cloud Security Policies | Read & Write
+
+
+## Example Usage
+
+```terraform
+terraform {
+ required_providers {
+ crowdstrike = {
+ source = "registry.terraform.io/crowdstrike/crowdstrike"
+ }
+ }
+}
+
+provider "crowdstrike" {
+ cloud = "us-2"
+}
+
+# return a single rule within a cloud provider
+data "crowdstrike_cloud_posture_rules" "specific" {
+ cloud_provider = "AWS"
+ rule_name = "NLB/ALB configured publicly with TLS/SSL disabled"
+}
+
+# query by FQL filter
+data "crowdstrike_cloud_posture_rules" "original" {
+ fql = "rule_name:'NLB/ALB configured publicly with TLS/SSL disabled'"
+}
+
+# return all rules for a specific resource type within a benchmark
+data "crowdstrike_cloud_posture_rules" "original" {
+ resource_type = "AWS::ElasticLoadBalancingV2::*"
+ benchmark = "CIS 1.0.0 AWS Web Architecture"
+}
+
+# return all rules for a specific resource type within an entire framework
+data "crowdstrike_cloud_posture_rules" "original" {
+ resource_type = "AWS::ElasticLoadBalancingV2::*"
+ framework = "CIS"
+}
+```
+
+
+## Schema
+
+### Optional
+
+- `benchmark` (String) Name of the benchmark that this rule is attached to. Note that rules can be associated with multiple benchmarks. Example: `CIS 1.0.0 AWS*`
+- `cloud_provider` (String) Cloud provider for where the rule resides.
+- `fql` (String) Falcon Query Language (FQL) filter for advanced control searches. FQL filter, allowed props: `rule_origin`, `rule_parent_uuid`, `rule_name`, `rule_description`, `rule_domain`, `rule_status`, `rule_severity`, `rule_short_code`, `rule_service`, `rule_resource_type`, `rule_provider`, `rule_subdomain`, `rule_auto_remediable`, `rule_control_requirement`, `rule_control_section`, `rule_compliance_benchmark`, `rule_compliance_framework`, `rule_mitre_tactic`, `rule_mitre_technique`, `rule_created_at`, `rule_updated_at`, `rule_updated_by`
+- `framework` (String) Name of the framework that this rule is attached to. Note that rules can be associated with multiple benchmarks. Examples: CIS, NIST
+- `resource_type` (String) Name of the resource type to search for. Examples: `AWS::IAM::CredentialReport`, `Microsoft.Compute/virtualMachines`, `container.googleapis.com/Cluster`.
+- `rule_name` (String) Name of the rule to search for. If no name is defined all rules in a cloud provider will be returned.
+- `service` (String) Name of the service within the cloud provider that rule is for. Examples: IAM, S3, Microsoft.Compute
+
+### Read-Only
+
+- `rules` (Attributes Set) List of cloud posture rules (see [below for nested schema](#nestedatt--rules))
+
+
+### Nested Schema for `rules`
+
+Read-Only:
+
+- `alert_info` (List of String) A list of the alert logic and detection criteria for rule violations.
+- `attack_types` (Set of String) Specific attack types associated with the rule.
+- `auto_remediable` (Boolean) Autoremediation enabled for the policy rule
+- `cloud_platform` (String) Cloud platform for the policy rule.
+- `cloud_provider` (String) Cloud provider for the policy rule.
+- `controls` (Attributes Set) Security framework and compliance rule information. (see [below for nested schema](#nestedatt--rules--controls))
+- `description` (String) Description of the policy rule.
+- `domain` (String) Domain for the policy rule.
+- `id` (String) Unique identifier of the policy rule.
+- `logic` (String) Rego logic for the policy rule.
+- `name` (String) Name of the policy rule.
+- `parent_rule_id` (String) Id of the parent rule to inherit properties from.
+- `remediation_info` (List of String) Information about how to remediate issues detected by this rule.
+- `resource_type` (String) The full resource type. Format examples: `AWS::IAM::CredentialReport`, `Microsoft.Compute/virtualMachines`, `container.googleapis.com/Cluster`
+- `severity` (String) Severity of the rule. Valid values are `critical`, `high`, `medium`, `informational`.
+- `subdomain` (String) Subdomain for the policy rule. Valid values are 'IOM' (Indicators of Misconfiguration) or 'IAC' (Infrastructure as Code). IOM is only supported at this time.
+
+
+### Nested Schema for `rules.controls`
+
+Required:
+
+- `authority` (String) The compliance framework
+- `code` (String) The compliance framework rule code
diff --git a/docs/resources/cloud_posture_custom_rule.md b/docs/resources/cloud_posture_custom_rule.md
new file mode 100644
index 0000000..a505dc7
--- /dev/null
+++ b/docs/resources/cloud_posture_custom_rule.md
@@ -0,0 +1,147 @@
+---
+page_title: "crowdstrike_cloud_posture_custom_rule Resource - crowdstrike"
+subcategory: "Cloud Posture"
+description: |-
+ This resource manages custom cloud posture rules. These rules can be created either by inheriting properties from a parent rule with minimal customization, or by fully customizing all attributes for maximum flexibility. To create a rule based on a parent rule, utilize the crowdstrike_cloud_posture_rules data source to gather parent rule information to use in the new custom rule. The crowdstrike_cloud_compliance_framework_controls data source can be used to query Falcon for compliance benchmark controls to associate with custom rules created with this resource.
+ API Scopes
+ The following API scopes are required:
+ Cloud Security Policies | Read & Write
+---
+
+# crowdstrike_cloud_posture_custom_rule (Resource)
+
+This resource manages custom cloud posture rules. These rules can be created either by inheriting properties from a parent rule with minimal customization, or by fully customizing all attributes for maximum flexibility. To create a rule based on a parent rule, utilize the `crowdstrike_cloud_posture_rules` data source to gather parent rule information to use in the new custom rule. The `crowdstrike_cloud_compliance_framework_controls` data source can be used to query Falcon for compliance benchmark controls to associate with custom rules created with this resource.
+
+## API Scopes
+
+The following API scopes are required:
+
+- Cloud Security Policies | Read & Write
+
+
+## Example Usage
+
+```terraform
+terraform {
+ required_providers {
+ crowdstrike = {
+ source = "registry.terraform.io/crowdstrike/crowdstrike"
+ }
+ }
+}
+
+provider "crowdstrike" {
+ cloud = "us-2"
+}
+
+# Custom rule derived from a parent rule with specific modifications
+resource "crowdstrike_cloud_posture_custom_rule" "copy_rule" {
+ resource_type = "AWS::EC2::Instance"
+ name = "Test Terraform"
+ description = "Test Terraform"
+ cloud_provider = "AWS"
+ severity = "informational"
+ remediation_info = [
+ "Remediation step 1",
+ "Remediation step 2",
+ "Remediation step 3",
+ ]
+ alert_info = [
+ "First item in alert info",
+ "Second item in alert info"
+ ]
+ controls = [
+ {
+ authority = "CIS",
+ code = "89"
+ },
+ {
+ authority = "CIS",
+ code = "791"
+ }
+ ]
+ parent_rule_id = "190c2d3d-8b0e-4838-bf11-4c6e044b9cb1"
+}
+
+resource "crowdstrike_cloud_posture_custom_rule" "custom_rule" {
+ resource_type = "AWS::EC2::Instance"
+ name = "Test Terraform"
+ description = "Test Terraform"
+ cloud_provider = "AWS"
+ attack_types = [
+ "Attack Type 1",
+ "Attack Type 2"
+ ]
+ remediation_info = [
+ "Remediation step 1",
+ "Remediation step 2",
+ "Remediation step 3",
+ ]
+ severity = "medium"
+ logic = <
+## Schema
+
+### Required
+
+- `cloud_provider` (String) Cloud provider for the policy rule.
+- `description` (String) Description of the policy rule.
+- `name` (String) Name of the policy rule.
+- `resource_type` (String) The full resource type. Examples: `AWS::IAM::CredentialReport`, `Microsoft.Compute/virtualMachines`, `container.googleapis.com/Cluster`
+
+### Optional
+
+- `alert_info` (List of String) A list of the alert logic and detection criteria for rule violations. When `alert_info` is not defined and `parent_rule_id` is defined, this field will inherit the parent rule's `alert_info`. Do not include numbering within this list. The Falcon console will automatically add numbering.
+- `attack_types` (Set of String) Specific attack types associated with the rule. Note: If `parent_rule_id` is defined, attack types will be inherited from the parent rule and cannot be specified using this field.
+- `controls` (Attributes Set) Security framework and compliance rule information. Utilize the `crowdstrike_cloud_compliance_framework_controls` data source to obtain this information. When `controls` is not defined and `parent_rule_id` is defined, this field will inherit the parent rule's `controls`. (see [below for nested schema](#nestedatt--controls))
+- `logic` (String) Rego logic for the rule. If this is not defined, then parent_rule_id must be defined. When `parent_rule_id` is defined, `logic` from the parent rule is not visible, but it is used for triggering this rule.
+- `parent_rule_id` (String) Id of the parent rule to inherit properties from. The `crowdstrike_cloud_posture_rules` data source can be used to query Falcon for parent rule information to use in this field. Required if `logic` is not specified.
+- `remediation_info` (List of String) Information about how to remediate issues detected by this rule. Do not include numbering within this list. The Falcon console will automatically add numbering.
+- `severity` (String) Severity of the rule. Valid values are `critical`, `high`, `medium`, `informational`.
+
+### Read-Only
+
+- `cloud_platform` (String) Cloud platform for the policy rule.
+- `domain` (String) CrowdStrike domain for the custom rule. Default is CSPM
+- `id` (String) Unique identifier of the policy rule.
+- `subdomain` (String) Subdomain for the policy rule. Valid values are 'IOM' (Indicators of Misconfiguration) or 'IAC' (Infrastructure as Code). IOM is only supported at this time.
+
+
+### Nested Schema for `controls`
+
+Required:
+
+- `authority` (String) The compliance framework
+- `code` (String) The compliance framework rule code
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+# Cloud Posture Custom Rule resources can be imported using their UUID, e.g.
+terraform import crowdstrike_cloud_posture_custom_rule.example 123e4567-e89b-12d3-a456-426614174000
+```
diff --git a/examples/data-sources/crowdstrike_cloud_compliance_framework_controls/data-source.tf b/examples/data-sources/crowdstrike_cloud_compliance_framework_controls/data-source.tf
new file mode 100644
index 0000000..18820c2
--- /dev/null
+++ b/examples/data-sources/crowdstrike_cloud_compliance_framework_controls/data-source.tf
@@ -0,0 +1,33 @@
+terraform {
+ required_providers {
+ crowdstrike = {
+ source = "registry.terraform.io/crowdstrike/crowdstrike"
+ }
+ }
+}
+
+provider "crowdstrike" {
+ cloud = "us-2"
+}
+
+# retrieve all controls under a named benchmark
+data "crowdstrike_cloud_compliance_framework_controls" "all" {
+ benchmark = "CIS 1.0.0 AWS Web Architecture"
+}
+
+# retrieve a single control within a benchmark by name
+data "crowdstrike_cloud_compliance_framework_controls" "by_name" {
+ name = "Ensure subnets for the Web tier are created"
+ benchmark = "CIS 1.0.0 AWS Web Architecture"
+}
+
+# retrieve a single control within a benchmark by requirement
+data "crowdstrike_cloud_compliance_framework_controls" "by_requirement" {
+ requirement = "2.1"
+ benchmark = "CIS 1.0.0 AWS Web Architecture"
+}
+
+# query by FQL filter
+data "crowdstrike_cloud_compliance_framework_controls" "fql" {
+ fql = "compliance_control_name:'Ensure subnets for the Web tier are created'"
+}
diff --git a/examples/data-sources/crowdstrike_cloud_posture_rules/data-source.tf b/examples/data-sources/crowdstrike_cloud_posture_rules/data-source.tf
new file mode 100644
index 0000000..3507a1f
--- /dev/null
+++ b/examples/data-sources/crowdstrike_cloud_posture_rules/data-source.tf
@@ -0,0 +1,34 @@
+terraform {
+ required_providers {
+ crowdstrike = {
+ source = "registry.terraform.io/crowdstrike/crowdstrike"
+ }
+ }
+}
+
+provider "crowdstrike" {
+ cloud = "us-2"
+}
+
+# return a single rule within a cloud provider
+data "crowdstrike_cloud_posture_rules" "specific" {
+ cloud_provider = "AWS"
+ rule_name = "NLB/ALB configured publicly with TLS/SSL disabled"
+}
+
+# query by FQL filter
+data "crowdstrike_cloud_posture_rules" "original" {
+ fql = "rule_name:'NLB/ALB configured publicly with TLS/SSL disabled'"
+}
+
+# return all rules for a specific resource type within a benchmark
+data "crowdstrike_cloud_posture_rules" "original" {
+ resource_type = "AWS::ElasticLoadBalancingV2::*"
+ benchmark = "CIS 1.0.0 AWS Web Architecture"
+}
+
+# return all rules for a specific resource type within an entire framework
+data "crowdstrike_cloud_posture_rules" "original" {
+ resource_type = "AWS::ElasticLoadBalancingV2::*"
+ framework = "CIS"
+}
diff --git a/examples/resources/crowdstrike_cloud_posture_custom_rule/import.sh b/examples/resources/crowdstrike_cloud_posture_custom_rule/import.sh
new file mode 100755
index 0000000..88d94ed
--- /dev/null
+++ b/examples/resources/crowdstrike_cloud_posture_custom_rule/import.sh
@@ -0,0 +1,2 @@
+# Cloud Posture Custom Rule resources can be imported using their UUID, e.g.
+terraform import crowdstrike_cloud_posture_custom_rule.example 123e4567-e89b-12d3-a456-426614174000
diff --git a/examples/resources/crowdstrike_cloud_posture_custom_rule/resource.tf b/examples/resources/crowdstrike_cloud_posture_custom_rule/resource.tf
new file mode 100644
index 0000000..adf2285
--- /dev/null
+++ b/examples/resources/crowdstrike_cloud_posture_custom_rule/resource.tf
@@ -0,0 +1,78 @@
+terraform {
+ required_providers {
+ crowdstrike = {
+ source = "registry.terraform.io/crowdstrike/crowdstrike"
+ }
+ }
+}
+
+provider "crowdstrike" {
+ cloud = "us-2"
+}
+
+# Custom rule derived from a parent rule with specific modifications
+resource "crowdstrike_cloud_posture_custom_rule" "copy_rule" {
+ resource_type = "AWS::EC2::Instance"
+ name = "Test Terraform"
+ description = "Test Terraform"
+ cloud_provider = "AWS"
+ severity = "informational"
+ remediation_info = [
+ "Remediation step 1",
+ "Remediation step 2",
+ "Remediation step 3",
+ ]
+ alert_info = [
+ "First item in alert info",
+ "Second item in alert info"
+ ]
+ controls = [
+ {
+ authority = "CIS",
+ code = "89"
+ },
+ {
+ authority = "CIS",
+ code = "791"
+ }
+ ]
+ parent_rule_id = "190c2d3d-8b0e-4838-bf11-4c6e044b9cb1"
+}
+
+resource "crowdstrike_cloud_posture_custom_rule" "custom_rule" {
+ resource_type = "AWS::EC2::Instance"
+ name = "Test Terraform"
+ description = "Test Terraform"
+ cloud_provider = "AWS"
+ attack_types = [
+ "Attack Type 1",
+ "Attack Type 2"
+ ]
+ remediation_info = [
+ "Remediation step 1",
+ "Remediation step 2",
+ "Remediation step 3",
+ ]
+ severity = "medium"
+ logic = < 0 {
+ filter = strings.Join(filters, "+")
+ }
+
+ if filter != "" {
+ params.Filter = &filter
+ }
+ } else {
+ params.Filter = &fql
+ }
+
+ for {
+ resp, err := r.client.CloudPolicies.QueryComplianceControls(¶ms)
+ if err != nil {
+ if badRequest, ok := err.(*cloud_policies.QueryComplianceControlsBadRequest); ok {
+ diags.AddError(
+ "Error Retrieving Control IDs",
+ fmt.Sprintf("Failed to retrieve controls (400): %+v", *badRequest.Payload.Errors[0].Message),
+ )
+ return controls, diags
+ }
+
+ if internalServerError, ok := err.(*cloud_policies.QueryComplianceControlsInternalServerError); ok {
+ diags.AddError(
+ "Error Retrieving Control IDs",
+ fmt.Sprintf("Failed to retrieve controls (500): %+v", *internalServerError.Payload.Errors[0].Message),
+ )
+ return controls, diags
+ }
+
+ diags.AddError(
+ "Error Retrieving Control IDs",
+ fmt.Sprintf("Failed to retrieve controls: %+v", err),
+ )
+
+ return controls, diags
+ }
+
+ if resp == nil || resp.Payload == nil || len(resp.Payload.Resources) == 0 {
+ return controls, diags
+ }
+
+ payload := resp.GetPayload()
+
+ if err = falcon.AssertNoError(payload.Errors); err != nil {
+ diags.AddError(
+ "Error Retrieving Control IDs",
+ fmt.Sprintf("Failed to retrieve control IDs: %s", err.Error()),
+ )
+ return controls, diags
+ }
+
+ if len(payload.Resources) < 1 {
+ return controls, diags
+ }
+
+ controlsInfo, diags := r.describeControls(ctx, payload.Resources)
+ if diags.HasError() {
+ return controls, diags
+ }
+
+ for _, control := range controlsInfo {
+ var benchmark types.String
+ if control.SecurityFramework != nil {
+ benchmark = types.StringPointerValue(control.SecurityFramework[0].Name)
+ }
+ controls = append(controls, cloudComplianceFrameworkControlModel{
+ Authority: types.StringPointerValue(control.Authority),
+ Code: types.StringPointerValue(control.Code),
+ Requirement: types.StringValue(control.Requirement),
+ Benchmark: benchmark,
+ Name: types.StringPointerValue(control.Name),
+ Section: types.StringValue(control.SectionName),
+ Id: types.StringPointerValue(control.UUID),
+ })
+ }
+
+ if payload.Meta != nil && payload.Meta.Pagination != nil {
+ pagination := payload.Meta.Pagination
+ if pagination.Offset != nil && pagination.Total != nil && *pagination.Offset >= int32(*pagination.Total) {
+ tflog.Info(ctx, "Pagination complete", map[string]any{"meta": payload.Meta})
+ break
+ }
+ }
+
+ offset += limit
+ }
+
+ return controls, diags
+}
+
+func (r *cloudComplianceFrameworkControlDataSource) describeControls(ctx context.Context, ids []string) ([]*models.ApimodelsControl, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var controls []*models.ApimodelsControl
+ params := cloud_policies.GetComplianceControlsParams{
+ Context: ctx,
+ Ids: ids,
+ }
+
+ resp, err := r.client.CloudPolicies.GetComplianceControls(¶ms)
+ if err != nil {
+ if badRequest, ok := err.(*cloud_policies.GetComplianceControlsBadRequest); ok {
+ diags.AddError(
+ "Error Retrieving Compliance Control Information",
+ fmt.Sprintf("Failed to retrieve compliance control information (400): %+v", *badRequest.Payload.Errors[0].Message),
+ )
+ return nil, diags
+ }
+
+ if notFound, ok := err.(*cloud_policies.GetComplianceControlsNotFound); ok {
+ diags.AddError(
+ "Error Retrieving Compliance Control Information",
+ fmt.Sprintf("Failed to retrieve compliance control information (404): %+v", *notFound.Payload.Errors[0].Message),
+ )
+ return nil, diags
+ }
+
+ if internalServerError, ok := err.(*cloud_policies.GetComplianceControlsInternalServerError); ok {
+ diags.AddError(
+ "Error Retrieving Compliance Control Information",
+ fmt.Sprintf("Failed to retrieve compliance control information (500): %+v", *internalServerError.Payload.Errors[0].Message),
+ )
+ return nil, diags
+ }
+
+ diags.AddError(
+ "Error Retrieving Compliance Control Information",
+ fmt.Sprintf("Failed to retrieve compliance control information: %+v", err),
+ )
+
+ return nil, diags
+ }
+
+ if resp == nil || resp.Payload == nil || len(resp.Payload.Resources) == 0 {
+ diags.AddError(
+ "Error Retrieving Compliance Control Information",
+ "Failed to retrieve compliance control information: The API returned an empty payload.",
+ )
+ return nil, diags
+ }
+
+ payload := resp.GetPayload()
+
+ if err = falcon.AssertNoError(payload.Errors); err != nil {
+ diags.AddError(
+ "Error Retrieving Compliance Control Information",
+ fmt.Sprintf("Failed to retrieve compliance controls: %s", err.Error()),
+ )
+ return nil, diags
+ }
+
+ controls = payload.Resources
+
+ return controls, diags
+}
diff --git a/internal/cloud_compliance/framework_control_data_source_test.go b/internal/cloud_compliance/framework_control_data_source_test.go
new file mode 100644
index 0000000..18fa532
--- /dev/null
+++ b/internal/cloud_compliance/framework_control_data_source_test.go
@@ -0,0 +1,96 @@
+package cloudcompliance_test
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/crowdstrike/terraform-provider-crowdstrike/internal/acctest"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+)
+
+func TestCloudComplianceFrameworkControlDataSource(t *testing.T) {
+ controlName := `Ensure CloudFront to Origin connection is configured using TLS1.1+ as the SSL\\TLS protocol`
+ controlNameResponse := "Ensure CloudFront to Origin connection is configured using TLS1.1+ as the SSL\\TLS protocol"
+ benchmark := "CIS 1.0.0 AWS Web Architecture"
+ requirement := "1.17"
+ resourcePrefix := "data.crowdstrike_cloud_compliance_framework_controls."
+
+ resource.ParallelTest(t, resource.TestCase{
+ ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories,
+ PreCheck: func() { acctest.PreCheck(t) },
+ Steps: []resource.TestStep{
+ {
+ Config: testByFqlConfig(
+ fmt.Sprintf(
+ "compliance_control_name:'%s'+"+
+ "compliance_control_requirement:'%s'+"+
+ "compliance_control_benchmark_name:'%s'",
+ controlName,
+ requirement,
+ benchmark,
+ ),
+ "by_fql",
+ ),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr(resourcePrefix+"by_fql", "controls.0.benchmark", benchmark),
+ resource.TestCheckResourceAttr(resourcePrefix+"by_fql", "controls.0.name", controlNameResponse),
+ resource.TestCheckResourceAttr(resourcePrefix+"by_fql", "controls.0.requirement", requirement),
+ resource.TestCheckResourceAttrSet(resourcePrefix+"by_fql", "controls.0.section"),
+ resource.TestCheckResourceAttrSet(resourcePrefix+"by_fql", "controls.0.id"),
+ resource.TestCheckResourceAttrSet(resourcePrefix+"by_fql", "controls.0.authority"),
+ resource.TestCheckResourceAttrSet(resourcePrefix+"by_fql", "controls.0.code"),
+ ),
+ },
+ {
+ Config: testByNameConfig(controlName, benchmark, "by_name"),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr(resourcePrefix+"by_name", "controls.0.benchmark", benchmark),
+ resource.TestCheckResourceAttr(resourcePrefix+"by_name", "controls.0.name", controlNameResponse),
+ resource.TestCheckResourceAttr(resourcePrefix+"by_name", "controls.0.requirement", requirement),
+ resource.TestCheckResourceAttrSet(resourcePrefix+"by_name", "controls.0.section"),
+ resource.TestCheckResourceAttrSet(resourcePrefix+"by_name", "controls.0.id"),
+ resource.TestCheckResourceAttrSet(resourcePrefix+"by_name", "controls.0.authority"),
+ resource.TestCheckResourceAttrSet(resourcePrefix+"by_name", "controls.0.code"),
+ ),
+ },
+ {
+ Config: testByRequirementConfig(requirement, benchmark, "by_requirement"),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr(resourcePrefix+"by_requirement", "controls.0.benchmark", benchmark),
+ resource.TestCheckResourceAttr(resourcePrefix+"by_requirement", "controls.0.name", controlNameResponse),
+ resource.TestCheckResourceAttr(resourcePrefix+"by_requirement", "controls.0.requirement", requirement),
+ resource.TestCheckResourceAttrSet(resourcePrefix+"by_requirement", "controls.0.section"),
+ resource.TestCheckResourceAttrSet(resourcePrefix+"by_requirement", "controls.0.id"),
+ resource.TestCheckResourceAttrSet(resourcePrefix+"by_requirement", "controls.0.authority"),
+ resource.TestCheckResourceAttrSet(resourcePrefix+"by_requirement", "controls.0.code"),
+ ),
+ },
+ },
+ })
+}
+
+func testByFqlConfig(fql string, resourceName string) string {
+ return fmt.Sprintf(`
+data "crowdstrike_cloud_compliance_framework_controls" "%s" {
+ fql = "%s"
+}
+`, resourceName, fql)
+}
+
+func testByNameConfig(name string, benchmark string, resourceName string) string {
+ return fmt.Sprintf(`
+data "crowdstrike_cloud_compliance_framework_controls" "%s" {
+ control_name = "%s"
+ benchmark = "%s"
+}
+`, resourceName, name, benchmark)
+}
+
+func testByRequirementConfig(requirement string, benchmark string, resourceName string) string {
+ return fmt.Sprintf(`
+data "crowdstrike_cloud_compliance_framework_controls" "%s" {
+ requirement = "%s"
+ benchmark = "%s"
+}
+`, resourceName, requirement, benchmark)
+}
diff --git a/internal/cloud_compliance/scopes.go b/internal/cloud_compliance/scopes.go
new file mode 100644
index 0000000..c589276
--- /dev/null
+++ b/internal/cloud_compliance/scopes.go
@@ -0,0 +1,11 @@
+package cloudcompliance
+
+import "github.com/crowdstrike/terraform-provider-crowdstrike/internal/scopes"
+
+var cloudComplianceFrameworkScopes = []scopes.Scope{
+ {
+ Name: "Cloud Security Policies",
+ Read: true,
+ Write: false,
+ },
+}
diff --git a/internal/cloud_posture/controls.go b/internal/cloud_posture/controls.go
new file mode 100644
index 0000000..94de359
--- /dev/null
+++ b/internal/cloud_posture/controls.go
@@ -0,0 +1,18 @@
+package cloudposture
+
+import (
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+type policyControl struct {
+ Authority types.String `tfsdk:"authority"`
+ Code types.String `tfsdk:"code"`
+}
+
+func (p policyControl) AttributeTypes() map[string]attr.Type {
+ return map[string]attr.Type{
+ "authority": types.StringType,
+ "code": types.StringType,
+ }
+}
diff --git a/internal/cloud_posture/custom_rule_resource.go b/internal/cloud_posture/custom_rule_resource.go
new file mode 100644
index 0000000..1c59947
--- /dev/null
+++ b/internal/cloud_posture/custom_rule_resource.go
@@ -0,0 +1,877 @@
+package cloudposture
+
+import (
+ "context"
+ "fmt"
+ "regexp"
+ "strings"
+
+ "github.com/crowdstrike/gofalcon/falcon"
+ "github.com/crowdstrike/gofalcon/falcon/client"
+ "github.com/crowdstrike/gofalcon/falcon/client/cloud_policies"
+ "github.com/crowdstrike/gofalcon/falcon/models"
+ "github.com/crowdstrike/terraform-provider-crowdstrike/internal/scopes"
+ "github.com/crowdstrike/terraform-provider-crowdstrike/internal/utils"
+ "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
+ "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator"
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/setdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+)
+
+var (
+ _ resource.Resource = &cloudPostureCustomRuleResource{}
+ _ resource.ResourceWithConfigure = &cloudPostureCustomRuleResource{}
+ _ resource.ResourceWithImportState = &cloudPostureCustomRuleResource{}
+)
+
+var (
+ documentationSection string = "Cloud Posture"
+ resourceMarkdownDescription string = "This resource manages custom cloud posture rules. " +
+ "These rules can be created either by inheriting properties from a parent rule with minimal customization, or by fully customizing all attributes for maximum flexibility. " +
+ "To create a rule based on a parent rule, utilize the `crowdstrike_cloud_posture_rules` data source to gather parent rule information to use in the new custom rule. " +
+ "The `crowdstrike_cloud_compliance_framework_controls` data source can be used to query Falcon for compliance benchmark controls to associate with custom rules created with this resource. "
+ requiredScopes []scopes.Scope = cloudPostureRuleScopes
+)
+
+func NewCloudPostureCustomRuleResource() resource.Resource {
+ return &cloudPostureCustomRuleResource{}
+}
+
+type cloudPostureCustomRuleResource struct {
+ client *client.CrowdStrikeAPISpecification
+}
+
+type cloudPostureCustomRuleResourceModel struct {
+ ID types.String `tfsdk:"id"`
+ AlertInfo types.List `tfsdk:"alert_info"`
+ Controls types.Set `tfsdk:"controls"`
+ Description types.String `tfsdk:"description"`
+ Domain types.String `tfsdk:"domain"`
+ Logic types.String `tfsdk:"logic"`
+ Name types.String `tfsdk:"name"`
+ AttackTypes types.Set `tfsdk:"attack_types"`
+ ParentRuleId types.String `tfsdk:"parent_rule_id"`
+ CloudPlatform types.String `tfsdk:"cloud_platform"`
+ CloudProvider types.String `tfsdk:"cloud_provider"`
+ RemediationInfo types.List `tfsdk:"remediation_info"`
+ ResourceType types.String `tfsdk:"resource_type"`
+ Severity types.String `tfsdk:"severity"`
+ Subdomain types.String `tfsdk:"subdomain"`
+}
+
+func (r *cloudPostureCustomRuleResource) Configure(
+ ctx context.Context,
+ req resource.ConfigureRequest,
+ resp *resource.ConfigureResponse,
+) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ client, ok := req.ProviderData.(*client.CrowdStrikeAPISpecification)
+
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf(
+ "Expected *client.CrowdStrikeAPISpecification, got: %T. Please report this issue to the provider developers.",
+ req.ProviderData,
+ ),
+ )
+
+ return
+ }
+
+ r.client = client
+}
+
+func (r *cloudPostureCustomRuleResource) Metadata(
+ _ context.Context,
+ req resource.MetadataRequest,
+ resp *resource.MetadataResponse,
+) {
+ resp.TypeName = req.ProviderTypeName + "_cloud_posture_custom_rule"
+}
+
+func (r *cloudPostureCustomRuleResource) Schema(
+ _ context.Context,
+ _ resource.SchemaRequest,
+ resp *resource.SchemaResponse,
+) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: utils.MarkdownDescription(documentationSection, resourceMarkdownDescription, requiredScopes),
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Computed: true,
+ Description: "Unique identifier of the policy rule.",
+ Validators: []validator.String{
+ stringvalidator.RegexMatches(
+ regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`),
+ "must be a valid Id in the format of 7c86a274-c04b-4292-9f03-dafae42bde97",
+ ),
+ },
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "alert_info": schema.ListAttribute{
+ Optional: true,
+ Computed: true,
+ ElementType: types.StringType,
+ MarkdownDescription: "A list of the alert logic and detection criteria for rule violations. " +
+ "When `alert_info` is not defined and `parent_rule_id` is defined, this field will inherit the parent rule's `alert_info`. " +
+ "Do not include numbering within this list. The Falcon console will automatically add numbering.",
+ Validators: []validator.List{
+ listvalidator.ValueStringsAre(
+ stringvalidator.LengthAtLeast(1),
+ ),
+ },
+ },
+ "controls": schema.SetNestedAttribute{
+ Optional: true,
+ Computed: true,
+ MarkdownDescription: "Security framework and compliance rule information. " +
+ "Utilize the `crowdstrike_cloud_compliance_framework_controls` data source to obtain this information. " +
+ "When `controls` is not defined and `parent_rule_id` is defined, this field will inherit the parent rule's `controls`.",
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "authority": schema.StringAttribute{
+ Required: true,
+ Description: "The compliance framework",
+ },
+ "code": schema.StringAttribute{
+ Required: true,
+ Description: "The compliance framework rule code",
+ },
+ },
+ },
+ Default: setdefault.StaticValue(types.SetNull(types.ObjectType{
+ AttrTypes: map[string]attr.Type{
+ "authority": types.StringType,
+ "code": types.StringType,
+ },
+ })),
+ },
+ "description": schema.StringAttribute{
+ Required: true,
+ Description: "Description of the policy rule.",
+ },
+ "domain": schema.StringAttribute{
+ Computed: true,
+ Default: stringdefault.StaticString("CSPM"),
+ Description: "CrowdStrike domain for the custom rule. Default is CSPM",
+ },
+ "attack_types": schema.SetAttribute{
+ Optional: true,
+ Computed: true,
+ MarkdownDescription: "Specific attack types associated with the rule. " +
+ "Note: If `parent_rule_id` is defined, attack types will be inherited from the parent rule and cannot be specified using this field.",
+ ElementType: types.StringType,
+ },
+ "logic": schema.StringAttribute{
+ Optional: true,
+ MarkdownDescription: "Rego logic for the rule. " +
+ "If this is not defined, then parent_rule_id must be defined. " +
+ "When `parent_rule_id` is defined, `logic` from the parent rule is not visible, but it is used for triggering this rule.",
+ },
+ "name": schema.StringAttribute{
+ Required: true,
+ Description: "Name of the policy rule.",
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "parent_rule_id": schema.StringAttribute{
+ Optional: true,
+ MarkdownDescription: "Id of the parent rule to inherit properties from. " +
+ "The `crowdstrike_cloud_posture_rules` data source can be used to query Falcon for parent rule information to use in this field. " +
+ "Required if `logic` is not specified.",
+ Validators: []validator.String{
+ stringvalidator.RegexMatches(
+ regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`),
+ "must be a valid Id in the format of 7c86a274-c04b-4292-9f03-dafae42bde97",
+ ),
+ },
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "cloud_platform": schema.StringAttribute{
+ Computed: true,
+ Description: "Cloud platform for the policy rule.",
+ },
+ "cloud_provider": schema.StringAttribute{
+ Required: true,
+ Description: "Cloud provider for the policy rule.",
+ Validators: []validator.String{
+ stringvalidator.OneOf(
+ "AWS",
+ "Azure",
+ "GCP",
+ ),
+ },
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "remediation_info": schema.ListAttribute{
+ Optional: true,
+ Computed: true,
+ ElementType: types.StringType,
+ Description: "Information about how to remediate issues detected by this rule. " +
+ "Do not include numbering within this list. The Falcon console will automatically add numbering.",
+ Validators: []validator.List{
+ listvalidator.ValueStringsAre(
+ stringvalidator.LengthAtLeast(1),
+ ),
+ },
+ },
+ "resource_type": schema.StringAttribute{
+ Required: true,
+ MarkdownDescription: "The full resource type. Examples: " +
+ "`AWS::IAM::CredentialReport`, " +
+ "`Microsoft.Compute/virtualMachines`, " +
+ "`container.googleapis.com/Cluster`",
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "severity": schema.StringAttribute{
+ Optional: true,
+ Computed: true,
+ Default: stringdefault.StaticString("critical"),
+ MarkdownDescription: "Severity of the rule. Valid values are `critical`, `high`, `medium`, `informational`.",
+ Validators: []validator.String{
+ stringvalidator.OneOf("critical", "high", "medium", "informational"),
+ },
+ },
+ "subdomain": schema.StringAttribute{
+ Computed: true,
+ Description: "Subdomain for the policy rule. Valid values are 'IOM' (Indicators of Misconfiguration) or 'IAC' (Infrastructure as Code). IOM is only supported at this time.",
+ Default: stringdefault.StaticString("IOM"),
+ },
+ },
+ }
+}
+
+func (r *cloudPostureCustomRuleResource) Create(
+ ctx context.Context,
+ req resource.CreateRequest,
+ resp *resource.CreateResponse,
+) {
+ var plan cloudPostureCustomRuleResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Match Cloud Platform and Cloud Provider until IAC is implemented
+ if !utils.IsKnown(plan.CloudPlatform) {
+ plan.CloudPlatform = plan.CloudProvider
+ }
+
+ rule, diags := r.createCloudPolicyRule(ctx, &plan)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+
+ // Update state before continuing because we already created the Policy, but
+ // other operations may fail resulting in created, but not tracked resources.
+ resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), plan.ID)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(plan.wrap(ctx, rule)...)
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *cloudPostureCustomRuleResource) Read(
+ ctx context.Context,
+ req resource.ReadRequest,
+ resp *resource.ReadResponse,
+) {
+ var state cloudPostureCustomRuleResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ rule, diags := r.getCloudPolicyRule(ctx, state.ID.ValueString())
+ if diags.HasError() {
+ for _, diag := range diags {
+ if strings.Contains(diag.Detail(), "resource doesn't exist") {
+ resp.State.RemoveResource(ctx)
+ resp.Diagnostics.AddWarning(
+ "Resource Not Found",
+ fmt.Sprintf("The resource with ID %s no longer exists in Falcon and will be removed from the Terraform state.", state.ID.ValueString()),
+ )
+ return
+ }
+ }
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+
+ if rule == nil {
+ return
+ }
+
+ resp.Diagnostics.Append(state.wrap(ctx, rule)...)
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *cloudPostureCustomRuleResource) Update(
+ ctx context.Context,
+ req resource.UpdateRequest,
+ resp *resource.UpdateResponse,
+) {
+ var plan cloudPostureCustomRuleResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if !utils.IsKnown(plan.CloudPlatform) {
+ plan.CloudPlatform = plan.CloudProvider
+ }
+
+ rule, diags := r.updateCloudPolicyRule(ctx, &plan)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+
+ resp.Diagnostics.Append(plan.wrap(ctx, rule)...)
+ resp.Diagnostics.Append(resp.State.Set(ctx, plan)...)
+}
+
+func (r *cloudPostureCustomRuleResource) Delete(
+ ctx context.Context,
+ req resource.DeleteRequest,
+ resp *resource.DeleteResponse,
+) {
+ var state cloudPostureCustomRuleResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(r.deleteCloudPolicyRule(ctx, state.ID.ValueString())...)
+}
+
+func (r *cloudPostureCustomRuleResource) ImportState(
+ ctx context.Context,
+ req resource.ImportStateRequest,
+ resp *resource.ImportStateResponse,
+) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r cloudPostureCustomRuleResource) ConfigValidators(ctx context.Context) []resource.ConfigValidator {
+ return []resource.ConfigValidator{
+ resourcevalidator.ExactlyOneOf(
+ path.MatchRoot("logic"),
+ path.MatchRoot("parent_rule_id"),
+ ),
+ resourcevalidator.Conflicting(
+ path.MatchRoot("parent_rule_id"),
+ path.MatchRoot("attack_types"),
+ ),
+ }
+}
+
+func (m *cloudPostureCustomRuleResourceModel) wrap(
+ ctx context.Context,
+ rule *models.ApimodelsRule,
+) diag.Diagnostics {
+ var diags diag.Diagnostics
+
+ m.ID = types.StringPointerValue(rule.UUID)
+ m.Name = types.StringPointerValue(rule.Name)
+ m.Description = types.StringPointerValue(rule.Description)
+ m.Domain = types.StringPointerValue(rule.Domain)
+ m.Subdomain = types.StringPointerValue(rule.Subdomain)
+ m.CloudProvider = types.StringPointerValue(rule.Provider)
+ m.RemediationInfo = convertAlertRemediationInfoToTerraformState(rule.Remediation)
+
+ m.AlertInfo = convertAlertRemediationInfoToTerraformState(rule.AlertInfo)
+ if diags.HasError() {
+ return diags
+ }
+
+ if rule.Severity != nil {
+ m.Severity = types.StringValue(int32ToSeverity[int32(*rule.Severity)])
+ }
+
+ if !m.ParentRuleId.IsNull() {
+ m.ParentRuleId = types.StringValue(rule.ParentRuleShortUUID)
+ } else {
+ m.Logic = types.StringValue(rule.Logic)
+ }
+
+ m.AttackTypes = types.SetValueMust(types.StringType, []attr.Value{})
+ for _, attackType := range rule.AttackTypes {
+ m.AttackTypes, diags = types.SetValue(types.StringType, append(m.AttackTypes.Elements(), types.StringValue(attackType)))
+ if diags.HasError() {
+ return diags
+ }
+ }
+
+ var policyControls []policyControl
+ for _, control := range rule.Controls {
+ policyControls = append(policyControls, policyControl{
+ Authority: types.StringPointerValue(control.Authority),
+ Code: types.StringPointerValue(control.Code),
+ })
+ }
+
+ m.Controls, diags = types.SetValueFrom(
+ ctx,
+ types.ObjectType{AttrTypes: policyControl{}.AttributeTypes()},
+ policyControls,
+ )
+
+ if diags.HasError() {
+ return diags
+ }
+
+ if rule.RuleLogicList != nil {
+ m.CloudPlatform = types.StringPointerValue(rule.RuleLogicList[0].Platform)
+ }
+
+ if rule.ResourceTypes != nil {
+ m.ResourceType = types.StringPointerValue(rule.ResourceTypes[0].ResourceType)
+ }
+ return diags
+}
+
+func (r *cloudPostureCustomRuleResource) createCloudPolicyRule(ctx context.Context, plan *cloudPostureCustomRuleResourceModel) (*models.ApimodelsRule, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ body := &models.CommonCreateRuleRequest{
+ Description: plan.Description.ValueStringPointer(),
+ Name: plan.Name.ValueStringPointer(),
+ Platform: plan.CloudPlatform.ValueStringPointer(),
+ Provider: plan.CloudProvider.ValueStringPointer(),
+ ResourceType: plan.ResourceType.ValueStringPointer(),
+ Domain: plan.Domain.ValueStringPointer(),
+ Subdomain: plan.Subdomain.ValueStringPointer(),
+ }
+
+ if plan.ParentRuleId.IsNull() {
+
+ body.Logic = plan.Logic.ValueStringPointer()
+ body.AlertInfo, diags = convertAlertInfoToAPIFormat(ctx, plan.AlertInfo, false)
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ body.RemediationInfo, diags = convertRemediationInfoToAPIFormat(ctx, plan.RemediationInfo, false)
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ } else {
+ body.ParentRuleID = plan.ParentRuleId.ValueStringPointer()
+ body.AlertInfo, diags = convertAlertInfoToAPIFormat(ctx, plan.AlertInfo, true)
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ body.RemediationInfo, diags = convertRemediationInfoToAPIFormat(ctx, plan.RemediationInfo, true)
+ if diags.HasError() {
+ return nil, diags
+ }
+ }
+
+ var controls []policyControl
+ body.Controls = []*models.DbmodelsControlReference{}
+ diags = plan.Controls.ElementsAs(ctx, &controls, false)
+ if diags.HasError() {
+ return nil, diags
+ }
+ for _, control := range controls {
+ body.Controls = append(body.Controls, &models.DbmodelsControlReference{
+ Authority: control.Authority.ValueStringPointer(),
+ Code: control.Code.ValueStringPointer(),
+ })
+ }
+
+ severity := severityToInt32[plan.Severity.ValueString()]
+ body.Severity = &severity
+
+ attackTypes := make([]string, 0, len(plan.AttackTypes.Elements()))
+ for _, elem := range plan.AttackTypes.Elements() {
+ if str, ok := elem.(types.String); ok {
+ attackTypes = append(attackTypes, str.ValueString())
+ }
+ }
+
+ body.AttackTypes = utils.Addr(strings.Join(attackTypes, ","))
+
+ params := cloud_policies.CreateRuleParams{
+ Context: ctx,
+ Body: body,
+ }
+
+ resp, err := r.client.CloudPolicies.CreateRule(¶ms)
+ if err != nil {
+ if badRequest, ok := err.(*cloud_policies.CreateRuleBadRequest); ok {
+ diags.AddError(
+ "Error Creating Rule",
+ fmt.Sprintf("Failed to create rule (400): %+v", *badRequest.Payload.Errors[0].Message),
+ )
+ return nil, diags
+ }
+
+ if ruleConflict, ok := err.(*cloud_policies.CreateRuleConflict); ok {
+ diags.AddError(
+ "Error Creating Rule",
+ fmt.Sprintf("Failed to create rule (409): %+v", *ruleConflict.Payload.Errors[0].Message),
+ )
+ return nil, diags
+ }
+
+ if internalServerError, ok := err.(*cloud_policies.CreateRuleInternalServerError); ok {
+ diags.AddError(
+ "Error Creating Rule",
+ fmt.Sprintf("Failed to create rule (500): %+v", *internalServerError.Payload.Errors[0].Message),
+ )
+ return nil, diags
+ }
+
+ diags.AddError(
+ "Error Creating Rule",
+ fmt.Sprintf("Failed to create rule %s: %+v", plan.Name.ValueString(), err),
+ )
+
+ return nil, diags
+ }
+
+ if resp == nil || resp.Payload == nil || len(resp.Payload.Resources) == 0 {
+ diags.AddError(
+ "Error Creating Rule",
+ "Failed to create rule: Payload is empty.",
+ )
+ return nil, diags
+ }
+
+ payload := resp.GetPayload()
+
+ if err = falcon.AssertNoError(payload.Errors); err != nil {
+ diags.AddError(
+ "Error Creating Rule. Body Error",
+ fmt.Sprintf("Failed to create rule: %s", err.Error()),
+ )
+ return nil, diags
+ }
+
+ return payload.Resources[0], diags
+}
+
+func (r *cloudPostureCustomRuleResource) getCloudPolicyRule(ctx context.Context, id string) (*models.ApimodelsRule, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ params := cloud_policies.GetRuleParams{
+ Context: ctx,
+ Ids: []string{id},
+ }
+
+ resp, err := r.client.CloudPolicies.GetRule(¶ms)
+ if err != nil {
+ if notFound, ok := err.(*cloud_policies.GetRuleNotFound); ok {
+ diags.AddError(
+ "Error Retrieving Rule",
+ fmt.Sprintf("Failed to retrieve rule (404): %s, %+v", id, *notFound.Payload.Errors[0].Message),
+ )
+ return nil, diags
+ }
+
+ if internalServerError, ok := err.(*cloud_policies.GetRuleInternalServerError); ok {
+ diags.AddError(
+ "Error Retrieving Rule",
+ fmt.Sprintf("Failed to retrieve rule (500): %s, %+v", id, *internalServerError.Payload.Errors[0].Message),
+ )
+ return nil, diags
+ }
+
+ diags.AddError(
+ "Error Retrieving Rule",
+ fmt.Sprintf("Failed to retrieve rule %s: %+v", id, err),
+ )
+
+ return nil, diags
+ }
+
+ if resp == nil || resp.Payload == nil || len(resp.Payload.Resources) == 0 {
+ return nil, diags
+ }
+
+ payload := resp.GetPayload()
+
+ if err = falcon.AssertNoError(payload.Errors); err != nil {
+ diags.AddError(
+ "Error Retrieving Rule",
+ fmt.Sprintf("Failed to retrieve rule: %s", err.Error()),
+ )
+ return nil, diags
+ }
+
+ return payload.Resources[0], diags
+}
+
+func (r *cloudPostureCustomRuleResource) updateCloudPolicyRule(ctx context.Context, plan *cloudPostureCustomRuleResourceModel) (*models.ApimodelsRule, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ body := &models.CommonUpdateRuleRequest{
+ Description: plan.Description.ValueString(),
+ Name: plan.Name.ValueString(),
+ UUID: plan.ID.ValueStringPointer(),
+ }
+
+ severity := severityToInt64[plan.Severity.ValueString()]
+ body.Severity = severity
+
+ attackTypes := make([]string, 0, len(plan.AttackTypes.Elements()))
+ for _, elem := range plan.AttackTypes.Elements() {
+ if str, ok := elem.(types.String); ok {
+ attackTypes = append(attackTypes, str.ValueString())
+ }
+ }
+ body.AttackTypes = attackTypes
+
+ if !plan.Controls.IsNull() {
+ var controls []policyControl
+ body.Controls = []*models.ApimodelsControlReference{}
+ plan.Controls.ElementsAs(ctx, &controls, false)
+ for _, control := range controls {
+ body.Controls = append(body.Controls, &models.ApimodelsControlReference{
+ Authority: control.Authority.ValueStringPointer(),
+ Code: control.Code.ValueStringPointer(),
+ })
+ }
+ }
+
+ body.RuleLogicList = []*models.ApimodelsRuleLogic{
+ {
+ Platform: plan.CloudPlatform.ValueStringPointer(),
+ },
+ }
+
+ var remediationInfo, alertInfo *string
+ if plan.ParentRuleId.IsNull() {
+ alertInfo, diags = convertAlertInfoToAPIFormat(ctx, plan.AlertInfo, false)
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ remediationInfo, diags = convertRemediationInfoToAPIFormat(ctx, plan.RemediationInfo, false)
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ body.RuleLogicList[0].Logic = plan.Logic.ValueString()
+ } else {
+ alertInfo, diags = convertAlertInfoToAPIFormat(ctx, plan.AlertInfo, true)
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ remediationInfo, diags = convertRemediationInfoToAPIFormat(ctx, plan.RemediationInfo, true)
+ if diags.HasError() {
+ return nil, diags
+ }
+ }
+
+ if remediationInfo != nil {
+ body.RuleLogicList[0].RemediationInfo = remediationInfo
+ }
+
+ if alertInfo != nil {
+ body.AlertInfo = *alertInfo
+ }
+
+ params := cloud_policies.UpdateRuleParams{
+ Context: ctx,
+ Body: body,
+ }
+
+ resp, err := r.client.CloudPolicies.UpdateRule(¶ms)
+ if err != nil {
+ if badRequest, ok := err.(*cloud_policies.UpdateRuleBadRequest); ok {
+ diags.AddError(
+ "Error Updating Rule",
+ fmt.Sprintf("Failed to update rule (400): %+v", *badRequest.Payload.Errors[0].Message),
+ )
+ return nil, diags
+ }
+
+ if ruleConflict, ok := err.(*cloud_policies.UpdateRuleConflict); ok {
+ diags.AddError(
+ "Error Updating Rule",
+ fmt.Sprintf("Failed to update rule (409): %+v", *ruleConflict.Payload.Errors[0].Message),
+ )
+ return nil, diags
+ }
+
+ if internalServerError, ok := err.(*cloud_policies.UpdateRuleInternalServerError); ok {
+ diags.AddError(
+ "Error Updating Rule",
+ fmt.Sprintf("Failed to update rule (500): %+v", *internalServerError.Payload.Errors[0].Message),
+ )
+ return nil, diags
+ }
+
+ diags.AddError(
+ "Error Updating Rule",
+ fmt.Sprintf("Failed to update rule: %s", err),
+ )
+ return nil, diags
+ }
+
+ if resp == nil || resp.Payload == nil || len(resp.Payload.Resources) == 0 {
+ diags.AddError(
+ "Error Updating Rule",
+ "Failed to update rule: Payload is empty.",
+ )
+ return nil, diags
+ }
+
+ payload := resp.GetPayload()
+
+ if err = falcon.AssertNoError(payload.Errors); err != nil {
+ diags.AddError(
+ "Error Updating Rule",
+ fmt.Sprintf("Failed to update rule: %s", err.Error()),
+ )
+ return nil, diags
+ }
+
+ return payload.Resources[0], diags
+}
+
+func (r *cloudPostureCustomRuleResource) deleteCloudPolicyRule(ctx context.Context, id string) diag.Diagnostics {
+ var diags diag.Diagnostics
+
+ params := cloud_policies.DeleteRuleParams{
+ Context: ctx,
+ Ids: []string{id},
+ }
+
+ resp, err := r.client.CloudPolicies.DeleteRule(¶ms)
+ if err != nil {
+ if internalServerError, ok := err.(*cloud_policies.DeleteRuleInternalServerError); ok {
+ diags.AddError(
+ "Error Deleting Rule",
+ fmt.Sprintf("Failed to delete rule (500) %s: %+v", id, *internalServerError.Payload.Errors[0].Message),
+ )
+ return diags
+ }
+
+ diags.AddError(
+ "Error Deleting Rule",
+ fmt.Sprintf("Failed to delete rule: %s", err),
+ )
+ return diags
+ }
+
+ if resp == nil || resp.Payload == nil || len(resp.Payload.Resources) == 0 {
+ diags.AddError(
+ "Error Deleting Rule",
+ "Failed to delete rule: The API returned an empty payload. The rule may have already been deleted and will be removed from the Terraform state during the next run.",
+ )
+ return diags
+ }
+
+ payload := resp.GetPayload()
+
+ if err = falcon.AssertNoError(payload.Errors); err != nil {
+ diags.AddError(
+ "Error Deleting Rule",
+ fmt.Sprintf("Failed to delete rule: %s", err.Error()),
+ )
+ return diags
+ }
+
+ return diags
+}
+
+func convertAlertInfoToAPIFormat(ctx context.Context, alertInfo basetypes.ListValue, includeNumbering bool) (*string, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var alertInfoStrings []string
+ var convertedAlertInfo string
+
+ if alertInfo.IsNull() || alertInfo.IsUnknown() {
+ return nil, diags
+ }
+
+ // Duplicate rules require the numbering with |\n delimiters
+ // for Alert Info while custom rules only require | without
+ // newlines
+ if includeNumbering {
+ for i, elem := range alertInfo.Elements() {
+ str, ok := elem.(types.String)
+ if !ok {
+ diags.AddError(
+ "Error converting AlertInfo",
+ fmt.Sprintf("Failed to convert element %d to string", i),
+ )
+ return nil, diags
+ }
+ alertInfoStrings = append(alertInfoStrings, fmt.Sprintf("%d. %s", i+1, str.ValueString()))
+ }
+
+ convertedAlertInfo = strings.Join(alertInfoStrings, "|\n")
+ } else {
+ diags = alertInfo.ElementsAs(ctx, &alertInfoStrings, false)
+ convertedAlertInfo = strings.Join(alertInfoStrings, "|")
+ }
+ return &convertedAlertInfo, diags
+}
+
+func convertRemediationInfoToAPIFormat(ctx context.Context, info basetypes.ListValue, includeNumbering bool) (*string, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var infoStrings []string
+ var convertedInfo string
+
+ if info.IsNull() || info.IsUnknown() {
+ return nil, diags
+ }
+
+ // Duplicate rules require the numbering with |\n delimiters
+ // for Remediation info while custom rules only require | without
+ // newlines
+ if includeNumbering {
+ for i, elem := range info.Elements() {
+ str, ok := elem.(types.String)
+ if !ok {
+ diags.AddError(
+ "Error converting RemediationInfo",
+ fmt.Sprintf("Failed to convert element %d to string", i),
+ )
+ return nil, diags
+ }
+ infoStrings = append(infoStrings, fmt.Sprintf("Step %d. %s", i+1, str.ValueString()))
+ }
+ convertedInfo = strings.Join(infoStrings, "|\n")
+ } else {
+ diags = info.ElementsAs(ctx, &infoStrings, false)
+ convertedInfo = strings.Join(infoStrings, "|")
+ }
+
+ return &convertedInfo, diags
+}
diff --git a/internal/cloud_posture/custom_rule_resource_test.go b/internal/cloud_posture/custom_rule_resource_test.go
new file mode 100644
index 0000000..d6205f2
--- /dev/null
+++ b/internal/cloud_posture/custom_rule_resource_test.go
@@ -0,0 +1,358 @@
+package cloudposture_test
+
+// Check empty fields.
+// Check nil
+// Check from defined to empty or nil. In-place updates.
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ "github.com/crowdstrike/terraform-provider-crowdstrike/internal/acctest"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/hashicorp/terraform-plugin-testing/terraform"
+)
+
+type ruleBaseConfig struct {
+ ruleNamePrefix string
+ description []string
+ subdomain string
+ domain string
+ severity []string
+ remediationInfo [][]string
+ controls []control
+ logic []string
+ alertInfo [][]string
+ attackTypes [][]string
+}
+
+type ruleCustomConfig struct {
+ ruleBaseConfig
+ parentId string
+ cloudProvider string
+ cloudPlatform string
+ resourceType string
+}
+
+type control struct {
+ authority string
+ code string
+}
+
+var commonConfig = ruleBaseConfig{
+ ruleNamePrefix: "Terraform Automated Test ",
+ description: []string{
+ "This is a description",
+ "This is an updated description",
+ },
+ subdomain: "IOM",
+ domain: "CSPM",
+ severity: []string{"critical", "informational"},
+ remediationInfo: [][]string{
+ {"This is the first step", "This is the second step"},
+ {"This is the first step", "This is the second step", "This is the third step."},
+ },
+ controls: []control{
+ {
+ authority: "CIS",
+ code: "791",
+ },
+ {
+ authority: "CIS",
+ code: "98",
+ },
+ },
+ logic: []string{
+ "package crowdstrike\ndefault result = \"pass\"\nresult = \"fail\" if {\n input.tags[_] == \"catch-me\"\n }",
+ "package crowdstrike\ndefault result = \"pass\"\nresult = \"fail\" if {\n input.tags[_] == \"catch-me-again\"\n }",
+ },
+ alertInfo: [][]string{
+ {
+ "List all Auto Scaling Groups in the account.",
+ "Check if multiple instance types are included in the configuration.",
+ "Check if multiple availability zones are configured.",
+ },
+ {
+ "Check if multiple instance types are included in the configuration.",
+ "List all Auto Scaling Groups in the account.",
+ "Check if multiple availability zones are configured.",
+ "Alert when any of the above conditions are met.",
+ },
+ },
+ attackTypes: [][]string{
+ {"Look it's an attack type"},
+ {"Look it's an attack type", "This is a second attack type"},
+ },
+}
+
+var awsCopyConfig = ruleCustomConfig{
+ ruleBaseConfig: commonConfig,
+ parentId: "0473a26b-7f29-43c7-9581-105f8c9c0b7d",
+ cloudProvider: "AWS",
+ cloudPlatform: "AWS",
+ resourceType: "AWS::EC2::Instance",
+}
+
+var azureCopyConfig = ruleCustomConfig{
+ ruleBaseConfig: commonConfig,
+ parentId: "1c9516e9-490b-461c-8644-9239ff3cf0d3",
+ cloudProvider: "Azure",
+ cloudPlatform: "Azure",
+ resourceType: "Microsoft.Compute/virtualMachines",
+}
+
+var gcpCopyConfig = ruleCustomConfig{
+ ruleBaseConfig: commonConfig,
+ parentId: "0260ffa9-eb65-42f4-a02a-7456d280049a",
+ cloudProvider: "GCP",
+ cloudPlatform: "GCP",
+ resourceType: "sqladmin.googleapis.com/Instance",
+}
+
+func TestCloudPostureCustomRuleResource(t *testing.T) {
+ var steps []resource.TestStep
+
+ steps = append(steps, generateRuleCopyTests(awsCopyConfig, "AWS")...)
+ steps = append(steps, generateRuleCopyTests(azureCopyConfig, "Azure")...)
+ steps = append(steps, generateRuleCopyTests(gcpCopyConfig, "GCP")...)
+ steps = append(steps, generateRuleLogicTests(awsCopyConfig, "AWS_Rego")...)
+ steps = append(steps, generateRuleLogicTests(azureCopyConfig, "Azure_Rego")...)
+ steps = append(steps, generateRuleLogicTests(gcpCopyConfig, "GCP_Rego")...)
+ steps = append(steps, generateMinimalRuleCopyTests(awsCopyConfig, "AWS_Min")...)
+ steps = append(steps, generateMinimalRuleCopyTests(azureCopyConfig, "Azure_Min")...)
+ steps = append(steps, generateMinimalRuleCopyTests(gcpCopyConfig, "GCP_Min")...)
+ steps = append(steps, generateMinimalRuleLogicTests(awsCopyConfig, "AWS_Min_Rego")...)
+ steps = append(steps, generateMinimalRuleLogicTests(azureCopyConfig, "Azure_Min_Rego")...)
+ steps = append(steps, generateMinimalRuleLogicTests(gcpCopyConfig, "GCP_Min_Rego")...)
+ resource.ParallelTest(t, resource.TestCase{
+ ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories,
+ PreCheck: func() { acctest.PreCheck(t) },
+ Steps: steps,
+ })
+}
+
+func generateRuleCopyTests(config ruleCustomConfig, ruleName string) []resource.TestStep {
+ var steps []resource.TestStep
+ resourceName := "crowdstrike_cloud_posture_custom_rule.rule" + "_" + ruleName
+
+ for i := range 2 {
+ alertInfo := strings.Join([]string{
+ `"` + strings.Join(config.ruleBaseConfig.alertInfo[i], `","`) + `"`,
+ }, "")
+ remediationInfo := strings.Join([]string{
+ `"` + strings.Join(config.ruleBaseConfig.remediationInfo[i], `","`) + `"`,
+ }, "")
+ newStep := resource.TestStep{
+ Config: fmt.Sprintf(`
+resource "crowdstrike_cloud_posture_custom_rule" "rule_%s" {
+ resource_type = "%s"
+ name = "%s"
+ description = "%s"
+ cloud_provider = "%s"
+ severity = "%s"
+ remediation_info = [%s]
+ controls = [
+ %s
+ ]
+ parent_rule_id = "%s"
+ alert_info = [%s]
+}
+`, ruleName, config.resourceType, config.ruleBaseConfig.ruleNamePrefix+ruleName, config.ruleBaseConfig.description[i],
+ config.cloudProvider, config.severity[i], remediationInfo,
+ testGenerateControlBlock(config.ruleBaseConfig.controls[i]), config.parentId, alertInfo),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr(resourceName, "subdomain", config.ruleBaseConfig.subdomain),
+ resource.TestCheckResourceAttr(resourceName, "domain", config.ruleBaseConfig.domain),
+ resource.TestCheckResourceAttr(resourceName, "resource_type", config.resourceType),
+ resource.TestCheckResourceAttr(resourceName, "name", config.ruleBaseConfig.ruleNamePrefix+ruleName),
+ resource.TestCheckResourceAttr(resourceName, "description", config.ruleBaseConfig.description[i]),
+ resource.TestCheckResourceAttr(resourceName, "cloud_platform", config.cloudPlatform),
+ resource.TestCheckResourceAttr(resourceName, "cloud_provider", config.cloudProvider),
+ resource.TestCheckResourceAttr(resourceName, "severity", config.severity[i]),
+ resource.TestCheckResourceAttr(resourceName, "parent_rule_id", config.parentId),
+ resource.TestCheckResourceAttr(resourceName, "controls.0.authority", config.ruleBaseConfig.controls[i].authority),
+ resource.TestCheckResourceAttr(resourceName, "controls.0.code", config.ruleBaseConfig.controls[i].code),
+ resource.TestCheckResourceAttr(resourceName, fmt.Sprintf("alert_info.%d", len(config.ruleBaseConfig.alertInfo[i])-1), config.ruleBaseConfig.alertInfo[i][len(config.ruleBaseConfig.alertInfo[i])-1]),
+ resource.TestCheckResourceAttr(resourceName, fmt.Sprintf("remediation_info.%d", len(config.ruleBaseConfig.remediationInfo[i])-1), config.ruleBaseConfig.remediationInfo[i][len(config.ruleBaseConfig.remediationInfo[i])-1]),
+ resource.TestCheckResourceAttrSet(resourceName, "id"),
+ ),
+ }
+ steps = append(steps, newStep)
+ }
+
+ return steps
+}
+
+func generateRuleLogicTests(config ruleCustomConfig, ruleName string) []resource.TestStep {
+ var steps []resource.TestStep
+ resourceName := "crowdstrike_cloud_posture_custom_rule.rule" + "_" + ruleName
+
+ for i := range 2 {
+ alertInfo := strings.Join([]string{
+ `"` + strings.Join(config.ruleBaseConfig.alertInfo[i], `","`) + `"`,
+ }, "")
+ remediationInfo := strings.Join([]string{
+ `"` + strings.Join(config.ruleBaseConfig.remediationInfo[i], `","`) + `"`,
+ }, "")
+ attackTypes := strings.Join([]string{
+ `"` + strings.Join(config.ruleBaseConfig.attackTypes[i], `","`) + `"`,
+ }, "")
+ resourceStep := resource.TestStep{
+ Config: fmt.Sprintf(`
+resource "crowdstrike_cloud_posture_custom_rule" "rule_%s" {
+ resource_type = "%s"
+ name = "%s"
+ description = "%s"
+ cloud_provider = "%s"
+ severity = "%s"
+ remediation_info = [%s]
+ logic = < 0 {
+ filter = strings.Join(filters, "+")
+ }
+
+ if filter != "" {
+ queryParams.Filter = &filter
+ }
+ } else {
+ queryParams.Filter = &fql
+ }
+
+ for {
+ queryResp, err := r.client.CloudPolicies.QueryRule(&queryParams)
+
+ if err != nil {
+ if badRequest, ok := err.(*cloud_policies.QueryRuleBadRequest); ok {
+ diags.AddError(
+ "Error Querying Rules",
+ fmt.Sprintf("Failed to query rules: %s", *badRequest.Payload.Errors[0].Message),
+ )
+ return types.SetValueMust(types.ObjectType{AttrTypes: cloudPostureRulesDataSourceRuleModel{}.AttributeTypes()}, []attr.Value{}), diags
+ }
+
+ if internalServerError, ok := err.(*cloud_policies.QueryRuleInternalServerError); ok {
+ diags.AddError(
+ "Error Querying Rules",
+ fmt.Sprintf("Failed to query rules: %s", *internalServerError.Payload.Errors[0].Message),
+ )
+ return defaultResponse, diags
+ }
+
+ diags.AddError(
+ "Error Querying Rules",
+ fmt.Sprintf("Failed to query rules: %s", err),
+ )
+
+ return defaultResponse, diags
+ }
+
+ if queryResp == nil || queryResp.Payload == nil || len(queryResp.Payload.Resources) == 0 {
+ return defaultResponse, diags
+ }
+
+ queryPayload := queryResp.GetPayload()
+
+ if err = falcon.AssertNoError(queryPayload.Errors); err != nil {
+ diags.AddError(
+ "Error Querying Rules",
+ fmt.Sprintf("Failed to query rules: %s", err.Error()),
+ )
+ return defaultResponse, diags
+ }
+
+ if len(queryPayload.Resources) == 0 {
+ return defaultResponse, diags
+ }
+
+ ruleParams := cloud_policies.GetRuleParams{
+ Context: ctx,
+ Ids: queryPayload.Resources,
+ }
+
+ getRulesResp, err := r.client.CloudPolicies.GetRule(&ruleParams)
+ if err != nil {
+ if !strings.Contains(err.Error(), "rule resource doesn't exist") {
+ diags.AddError(
+ "Failed to Fetch Rule Information",
+ fmt.Sprintf("Failed to fetch rule information: %s", err),
+ )
+ }
+ return defaultResponse, diags
+ }
+
+ if getRulesResp == nil || getRulesResp.Payload == nil || len(getRulesResp.Payload.Resources) == 0 {
+ diags.AddError(
+ "Error Fetching Rule Information",
+ "Failed to fetch rule information: The API returned an empty payload.",
+ )
+ return defaultResponse, diags
+ }
+
+ getRulesPayload := getRulesResp.GetPayload()
+
+ if err = falcon.AssertNoError(getRulesPayload.Errors); err != nil {
+ diags.AddError(
+ "Error Fetching Rule Information",
+ fmt.Sprintf("Failed to fetch rule information: %s", err.Error()),
+ )
+ return defaultResponse, diags
+ }
+
+ for _, resource := range getRulesPayload.Resources {
+ rule := cloudPostureRulesDataSourceRuleModel{
+ ID: types.StringValue(*resource.UUID),
+ Description: types.StringPointerValue(resource.Description),
+ AutoRemediable: types.BoolPointerValue(resource.AutoRemediable),
+ Domain: types.StringPointerValue(resource.Domain),
+ Logic: types.StringValue(resource.Logic),
+ Name: types.StringPointerValue(resource.Name),
+ ParentRuleID: types.StringValue(resource.ParentRuleShortUUID),
+ CloudPlatform: types.StringValue(resource.Platform),
+ CloudProvider: types.StringPointerValue(resource.Provider),
+ Severity: types.StringValue(int64ToSeverity[*resource.Severity]),
+ Subdomain: types.StringPointerValue(resource.Subdomain),
+ }
+
+ var policyControls []policyControl
+ for _, control := range resource.Controls {
+ policyControls = append(policyControls, policyControl{
+ Authority: types.StringPointerValue(control.Authority),
+ Code: types.StringPointerValue(control.Code),
+ })
+ }
+
+ rule.Controls, diags = types.SetValueFrom(
+ ctx,
+ types.ObjectType{AttrTypes: policyControl{}.AttributeTypes()},
+ policyControls,
+ )
+
+ if diags.HasError() {
+ return defaultResponse, diags
+ }
+
+ rule.AttackTypes, diags = types.SetValueFrom(ctx, types.StringType, resource.AttackTypes)
+ if diags.HasError() {
+ return defaultResponse, diags
+ }
+
+ if resource.RuleLogicList != nil {
+ rule.RemediationInfo = convertAlertRemediationInfoToTerraformState(resource.RuleLogicList[0].RemediationInfo)
+ }
+
+ if resource.AlertInfo != nil {
+ rule.AlertInfo = convertAlertRemediationInfoToTerraformState(resource.AlertInfo)
+ }
+
+ if resource.ResourceTypes != nil {
+ rule.ResourceType = types.StringPointerValue(resource.ResourceTypes[0].ResourceType)
+ }
+
+ rules = append(rules, rule)
+ }
+
+ if queryPayload.Meta != nil && queryPayload.Meta.Pagination != nil {
+ pagination := queryPayload.Meta.Pagination
+ if pagination.Offset != nil && pagination.Total != nil && *pagination.Offset >= int32(*pagination.Total) {
+ tflog.Info(ctx, "Pagination complete", map[string]any{"meta": queryPayload.Meta})
+ break
+ }
+ }
+
+ offset += limit
+ }
+
+ rulesSet, diags := types.SetValueFrom(
+ ctx,
+ types.ObjectType{AttrTypes: cloudPostureRulesDataSourceRuleModel{}.AttributeTypes()},
+ rules,
+ )
+ if diags.HasError() {
+ return defaultResponse, diags
+ }
+ return rulesSet, diags
+}
diff --git a/internal/cloud_posture/rules_data_source_test.go b/internal/cloud_posture/rules_data_source_test.go
new file mode 100644
index 0000000..a4d3853
--- /dev/null
+++ b/internal/cloud_posture/rules_data_source_test.go
@@ -0,0 +1,200 @@
+package cloudposture_test
+
+import (
+ "fmt"
+ "regexp"
+ "testing"
+
+ "github.com/crowdstrike/terraform-provider-crowdstrike/internal/acctest"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/hashicorp/terraform-plugin-testing/terraform"
+)
+
+type dataRuleConfig struct {
+ cloudProvider string
+ ruleName string
+ resourceType string
+ benchmark string
+ framework string
+ service string
+}
+
+var awsConfig = dataRuleConfig{
+ cloudProvider: "AWS",
+ ruleName: "NLB/ALB configured publicly with TLS/SSL disabled",
+ resourceType: "AWS::ElasticLoadBalancingV2::LoadBalancer",
+ benchmark: "CIS*",
+ framework: "CIS",
+ service: "ELB",
+}
+
+var azureConfig = dataRuleConfig{
+ cloudProvider: "Azure",
+ ruleName: "Virtual Machine allows public internet access to Docker (port 2375/2376)",
+ resourceType: "Microsoft.Compute/virtualMachines",
+ benchmark: "CIS*",
+ framework: "CIS",
+ service: "Virtual Machines",
+}
+
+var gcpConfig = dataRuleConfig{
+ cloudProvider: "GCP",
+ ruleName: "GKE Cluster insecure kubelet read only port is enabled",
+ resourceType: "container.googleapis.com/Cluster",
+ benchmark: "CIS*",
+ framework: "CIS",
+ service: "Google Kubernetes Engine",
+}
+
+func TestCloudPostureRulesDataSource(t *testing.T) {
+ var steps []resource.TestStep
+
+ steps = append(steps, testCloudRules(awsConfig)...)
+ steps = append(steps, testCloudRules(azureConfig)...)
+ steps = append(steps, testCloudRules(gcpConfig)...)
+
+ resource.ParallelTest(t, resource.TestCase{
+ ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories,
+ PreCheck: func() { acctest.PreCheck(t) },
+ Steps: steps,
+ })
+}
+
+func testCloudRules(config dataRuleConfig) (steps []resource.TestStep) {
+ resourceName := fmt.Sprintf("data.crowdstrike_cloud_posture_rules.%s", config.cloudProvider)
+ steps = []resource.TestStep{
+ {
+ Config: fmt.Sprintf(`
+data "crowdstrike_cloud_posture_rules" "%[1]s" {
+ cloud_provider = "%[1]s"
+ resource_type = "%[2]s"
+}
+`, config.cloudProvider, config.resourceType),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttrSet(resourceName, "rules.#"),
+ resource.TestMatchResourceAttr(resourceName, "rules.#", regexp.MustCompile(`^[2-9]|\d{2,}$`)),
+ func(s *terraform.State) error {
+ rs, ok := s.RootModule().Resources[resourceName]
+ if !ok {
+ return fmt.Errorf("Not found: %s", resourceName)
+ }
+ UUIDs := []string{}
+ for i := 0; ; i++ {
+ key := fmt.Sprintf("rules.%d.id", i)
+ if v, ok := rs.Primary.Attributes[key]; ok {
+ UUIDs = append(UUIDs, v)
+ } else {
+ break
+ }
+ }
+ for i, uuid := range UUIDs {
+ if v, ok := rs.Primary.Attributes[fmt.Sprintf("rules.%d.id", i)]; !ok || v != uuid {
+ return fmt.Errorf("Expected Id %s for rule %d, got %s", uuid, i, v)
+ }
+ }
+ return nil
+ },
+ ),
+ },
+ {
+ Config: fmt.Sprintf(`
+data "crowdstrike_cloud_posture_rules" "%[1]s" {
+ cloud_provider = "%[1]s"
+ rule_name = "%[2]s"
+}
+`, config.cloudProvider, config.ruleName),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr(resourceName, "rules.#", "1"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.id"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.remediation_info.#"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.alert_info.#"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.severity"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.domain"),
+ resource.TestCheckResourceAttr(resourceName, "rules.0.name", config.ruleName),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.controls.#"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.cloud_provider"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.attack_types.#"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.resource_type"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.subdomain"),
+ func(s *terraform.State) error {
+ rs, ok := s.RootModule().Resources[resourceName]
+ if !ok {
+ return fmt.Errorf("Not found: %s", resourceName)
+ }
+ if v, ok := rs.Primary.Attributes["rules.0.id"]; !ok || v == "" {
+ return fmt.Errorf("Id not set for rule")
+ }
+ return nil
+ },
+ ),
+ },
+ {
+ Config: fmt.Sprintf(`
+data "crowdstrike_cloud_posture_rules" "%[1]s" {
+ cloud_provider = "%[1]s"
+ rule_name = "%[2]s"
+ benchmark = "%[3]s"
+ framework = "%[4]s"
+ service = "%[5]s"
+}
+`, config.cloudProvider, config.ruleName, config.benchmark, config.framework, config.service),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr(resourceName, "rules.#", "1"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.id"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.remediation_info.#"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.alert_info.#"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.severity"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.domain"),
+ resource.TestCheckResourceAttr(resourceName, "rules.0.name", config.ruleName),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.controls.#"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.cloud_provider"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.attack_types.#"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.resource_type"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.subdomain"),
+ func(s *terraform.State) error {
+ rs, ok := s.RootModule().Resources[resourceName]
+ if !ok {
+ return fmt.Errorf("Not found: %s", resourceName)
+ }
+ if v, ok := rs.Primary.Attributes["rules.0.id"]; !ok || v == "" {
+ return fmt.Errorf("Id not set for rule")
+ }
+ return nil
+ },
+ ),
+ },
+ {
+ Config: fmt.Sprintf(`
+data "crowdstrike_cloud_posture_rules" "%s" {
+ fql = "rule_name:'%s'"
+}
+`, config.cloudProvider, config.ruleName),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr(resourceName, "rules.#", "1"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.id"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.remediation_info.#"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.alert_info.#"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.severity"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.domain"),
+ resource.TestCheckResourceAttr(resourceName, "rules.0.name", config.ruleName),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.controls.#"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.cloud_provider"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.attack_types.#"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.resource_type"),
+ resource.TestCheckResourceAttrSet(resourceName, "rules.0.subdomain"),
+ func(s *terraform.State) error {
+ rs, ok := s.RootModule().Resources[resourceName]
+ if !ok {
+ return fmt.Errorf("Not found: %s", resourceName)
+ }
+ if v, ok := rs.Primary.Attributes["rules.0.id"]; !ok || v == "" {
+ return fmt.Errorf("Id not set for rule")
+ }
+ return nil
+ },
+ ),
+ },
+ }
+
+ return steps
+}
diff --git a/internal/cloud_posture/scopes.go b/internal/cloud_posture/scopes.go
new file mode 100644
index 0000000..83e9908
--- /dev/null
+++ b/internal/cloud_posture/scopes.go
@@ -0,0 +1,11 @@
+package cloudposture
+
+import "github.com/crowdstrike/terraform-provider-crowdstrike/internal/scopes"
+
+var cloudPostureRuleScopes = []scopes.Scope{
+ {
+ Name: "Cloud Security Policies",
+ Read: true,
+ Write: true,
+ },
+}
diff --git a/internal/cloud_posture/shared.go b/internal/cloud_posture/shared.go
new file mode 100644
index 0000000..69893f2
--- /dev/null
+++ b/internal/cloud_posture/shared.go
@@ -0,0 +1,74 @@
+package cloudposture
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+)
+
+const (
+ SeverityCritical = "critical"
+ SeverityHigh = "high"
+ SeverityMedium = "medium"
+ SeverityInformational = "informational"
+)
+
+var (
+ severityToInt32 = map[string]int32{
+ SeverityCritical: 0,
+ SeverityHigh: 1,
+ SeverityMedium: 2,
+ SeverityInformational: 3,
+ }
+ severityToInt64 = map[string]int64{
+ SeverityCritical: 0,
+ SeverityHigh: 1,
+ SeverityMedium: 2,
+ SeverityInformational: 3,
+ }
+ int32ToSeverity = map[int32]string{
+ 0: SeverityCritical,
+ 1: SeverityHigh,
+ 2: SeverityMedium,
+ 3: SeverityInformational,
+ }
+ int64ToSeverity = map[int64]string{
+ 0: SeverityCritical,
+ 1: SeverityHigh,
+ 2: SeverityMedium,
+ 3: SeverityInformational,
+ }
+)
+
+type fqlFilters struct {
+ value string
+ field string
+}
+
+func convertAlertRemediationInfoToTerraformState(input *string) basetypes.ListValue {
+ if input == nil {
+ return basetypes.NewListValueMust(basetypes.StringType{}, []attr.Value{})
+ }
+
+ *input = strings.TrimSpace(*input)
+ *input = strings.TrimSuffix(*input, "|")
+
+ parts := strings.Split(*input, "|")
+ values := make([]attr.Value, 0, len(parts))
+
+ for index, part := range parts {
+ trimmed := strings.TrimSpace(part)
+ if after, ok := strings.CutPrefix(trimmed, fmt.Sprintf("Step %d. ", index+1)); ok {
+ trimmed = strings.TrimSpace(after)
+ } else {
+ trimmed = strings.TrimSpace(strings.TrimPrefix(trimmed, fmt.Sprintf("%d. ", index+1)))
+ }
+ if trimmed != "" {
+ values = append(values, basetypes.NewStringValue(trimmed))
+ }
+ }
+
+ return basetypes.NewListValueMust(basetypes.StringType{}, values)
+}
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index b3af681..22a3ca4 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -7,6 +7,8 @@ import (
"os"
"github.com/crowdstrike/gofalcon/falcon"
+ cloudcompliance "github.com/crowdstrike/terraform-provider-crowdstrike/internal/cloud_compliance"
+ cloudposture "github.com/crowdstrike/terraform-provider-crowdstrike/internal/cloud_posture"
contentupdatepolicy "github.com/crowdstrike/terraform-provider-crowdstrike/internal/content_update_policy"
"github.com/crowdstrike/terraform-provider-crowdstrike/internal/fcs"
"github.com/crowdstrike/terraform-provider-crowdstrike/internal/fim"
@@ -253,6 +255,7 @@ func (p *CrowdStrikeProvider) Resources(ctx context.Context) []func() resource.R
contentupdatepolicy.NewDefaultContentUpdatePolicyResource,
contentupdatepolicy.NewContentUpdatePolicyPrecedenceResource,
sensorvisibilityexclusion.NewSensorVisibilityExclusionResource,
+ cloudposture.NewCloudPostureCustomRuleResource,
}
}
@@ -261,6 +264,8 @@ func (p *CrowdStrikeProvider) DataSources(ctx context.Context) []func() datasour
sensorupdatepolicy.NewSensorUpdateBuildsDataSource,
fcs.NewCloudAwsAccountsDataSource,
contentupdatepolicy.NewContentCategoryVersionsDataSource,
+ cloudposture.NewCloudPostureRulesDataSource,
+ cloudcompliance.NewCloudComplianceFrameworkControlDataSource,
}
}