diff --git a/docs/resources/policy.md b/docs/resources/policy.md
new file mode 100644
index 0000000..efd13ec
--- /dev/null
+++ b/docs/resources/policy.md
@@ -0,0 +1,90 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "panther_policy Resource - terraform-provider-panther"
+subcategory: ""
+description: |-
+
+---
+
+# panther_policy (Resource)
+
+
+
+## Example Usage
+
+```terraform
+# Manage cloud security policy for resource compliance
+resource "panther_policy" "example" {
+ display_name = "S3 Bucket Encryption Policy"
+ body = <<-EOT
+ def policy(resource):
+ # Check if S3 bucket has encryption enabled
+ encryption = resource.get('EncryptionConfiguration', {})
+ rules = encryption.get('Rules', [])
+ return len(rules) > 0
+ EOT
+ severity = "MEDIUM"
+ description = "Ensures S3 buckets have encryption enabled"
+ enabled = true
+
+ resource_types = [
+ "AWS.S3.Bucket"
+ ]
+
+ tags = [
+ "compliance",
+ "encryption"
+ ]
+}
+```
+
+
+## Schema
+
+### Required
+
+- `body` (String) The python body of the policy
+- `severity` (String)
+
+### Optional
+
+- `description` (String) The description of the policy
+- `display_name` (String) The display name of the policy
+- `enabled` (Boolean) Determines whether or not the policy is active
+- `managed` (Boolean) Determines if the policy is managed by panther
+- `output_ids` (List of String) Destination IDs that override default alert routing based on severity
+- `reports` (Map of List of String) Reports
+- `resource_types` (List of String) Resource types
+- `suppressions` (List of String) Resources to ignore via a pattern that matches the resource id
+- `tags` (List of String) The tags for the policy
+- `tests` (Attributes List) Unit tests for the Policy. Best practice is to include a positive and negative case (see [below for nested schema](#nestedatt--tests))
+
+### Read-Only
+
+- `created_at` (String)
+- `created_by` (Attributes) The actor who created the rule (see [below for nested schema](#nestedatt--created_by))
+- `created_by_external` (String) The text of the user-provided CreatedBy field when uploaded via CI/CD
+- `id` (String) The ID of this resource.
+- `last_modified` (String)
+
+
+### Nested Schema for `tests`
+
+Required:
+
+- `expected_result` (Boolean) The expected result
+- `name` (String) name
+- `resource` (String) resource
+
+Optional:
+
+- `mocks` (List of Map of String) mocks
+
+
+
+### Nested Schema for `created_by`
+
+Read-Only:
+
+- `id` (String)
+- `type` (String)
diff --git a/docs/resources/rule.md b/docs/resources/rule.md
new file mode 100644
index 0000000..91068ac
--- /dev/null
+++ b/docs/resources/rule.md
@@ -0,0 +1,87 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "panther_rule Resource - terraform-provider-panther"
+subcategory: ""
+description: |-
+
+---
+
+# panther_rule (Resource)
+
+
+
+## Example Usage
+
+```terraform
+# Manage detection rule for log analysis
+resource "panther_rule" "example" {
+ display_name = ""
+ body = ""
+ severity = ""
+ description = ""
+ enabled = true
+ dedup_period_minutes = 60
+ log_types = [
+ ""
+ ]
+ tags = [
+ ""
+ ]
+ runbook = ""
+}
+```
+
+
+## Schema
+
+### Required
+
+- `body` (String) The python body of the rule
+- `severity` (String)
+
+### Optional
+
+- `dedup_period_minutes` (Number) The amount of time in minutes for grouping alerts
+- `description` (String) The description of the rule
+- `display_name` (String) The display name of the rule
+- `enabled` (Boolean) Determines whether or not the rule is active
+- `inline_filters` (String) The filter for the rule represented in YAML
+- `log_types` (List of String) log types
+- `managed` (Boolean) Determines if the rule is managed by panther
+- `output_ids` (List of String) Destination IDs that override default alert routing based on severity
+- `reports` (Map of List of String) reports
+- `runbook` (String) How to handle the generated alert
+- `summary_attributes` (List of String) A list of fields in the event to create top 5 summaries for
+- `tags` (List of String) The tags for the rule
+- `tests` (Attributes List) Unit tests for the Rule. Best practice is to include a positive and negative case (see [below for nested schema](#nestedatt--tests))
+- `threshold` (Number) the number of events that must match before an alert is triggered
+
+### Read-Only
+
+- `created_at` (String)
+- `created_by` (Attributes) The actor who created the rule (see [below for nested schema](#nestedatt--created_by))
+- `created_by_external` (String) The text of the user-provided CreatedBy field when uploaded via CI/CD
+- `id` (String) The ID of this resource.
+- `last_modified` (String)
+
+
+### Nested Schema for `tests`
+
+Required:
+
+- `expected_result` (Boolean) The expected result
+- `name` (String) name
+- `resource` (String) resource
+
+Optional:
+
+- `mocks` (List of Map of String) mocks
+
+
+
+### Nested Schema for `created_by`
+
+Read-Only:
+
+- `id` (String)
+- `type` (String)
diff --git a/docs/resources/scheduled_rule.md b/docs/resources/scheduled_rule.md
new file mode 100644
index 0000000..590fd99
--- /dev/null
+++ b/docs/resources/scheduled_rule.md
@@ -0,0 +1,96 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "panther_scheduled_rule Resource - terraform-provider-panther"
+subcategory: ""
+description: |-
+
+---
+
+# panther_scheduled_rule (Resource)
+
+
+
+## Example Usage
+
+```terraform
+# Manage scheduled detection rule for query results
+resource "panther_scheduled_rule" "example" {
+ display_name = "High Volume Failed Logins"
+ body = <<-EOT
+ def rule(event):
+ # Check if query results exceed threshold
+ failed_count = event.get('failed_login_count', 0)
+ return failed_count > 10
+ EOT
+ severity = "HIGH"
+ description = "Detects high volume of failed login attempts from scheduled query"
+ enabled = true
+ dedup_period_minutes = 60
+ threshold = 1
+
+ scheduled_queries = [
+ "failed-login-aggregation-query"
+ ]
+
+ tags = [
+ "authentication",
+ "security"
+ ]
+
+ runbook = "Investigate the source IPs and user accounts for potential brute force attacks"
+}
+```
+
+
+## Schema
+
+### Required
+
+- `body` (String) The python body of the scheduled rule
+- `severity` (String)
+
+### Optional
+
+- `dedup_period_minutes` (Number) The amount of time in minutes for grouping alerts
+- `description` (String) The description of the scheduled rule
+- `display_name` (String) The display name of the scheduled rule
+- `enabled` (Boolean) Determines whether or not the scheduled rule is active
+- `managed` (Boolean) Determines if the scheduled rule is managed by panther
+- `output_ids` (List of String) Destination IDs that override default alert routing based on severity
+- `reports` (Map of List of String) reports
+- `runbook` (String) How to handle the generated alert
+- `scheduled_queries` (List of String) the queries that this scheduled rule utilizes
+- `summary_attributes` (List of String) A list of fields in the event to create top 5 summaries for
+- `tags` (List of String) The tags for the scheduled rule
+- `tests` (Attributes List) Unit tests for the Rule. Best practice is to include a positive and negative case (see [below for nested schema](#nestedatt--tests))
+- `threshold` (Number) the number of events that must match before an alert is triggered
+
+### Read-Only
+
+- `created_at` (String)
+- `created_by` (Attributes) The actor who created the rule (see [below for nested schema](#nestedatt--created_by))
+- `created_by_external` (String) The text of the user-provided CreatedBy field when uploaded via CI/CD
+- `id` (String) The ID of this resource.
+- `last_modified` (String)
+
+
+### Nested Schema for `tests`
+
+Required:
+
+- `expected_result` (Boolean) The expected result
+- `name` (String) name
+- `resource` (String) resource
+
+Optional:
+
+- `mocks` (List of Map of String) mocks
+
+
+
+### Nested Schema for `created_by`
+
+Read-Only:
+
+- `id` (String)
+- `type` (String)
diff --git a/docs/resources/simple_rule.md b/docs/resources/simple_rule.md
new file mode 100644
index 0000000..273058b
--- /dev/null
+++ b/docs/resources/simple_rule.md
@@ -0,0 +1,114 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "panther_simple_rule Resource - terraform-provider-panther"
+subcategory: ""
+description: |-
+
+---
+
+# panther_simple_rule (Resource)
+
+
+
+## Example Usage
+
+```terraform
+# Manage simple detection rule with YAML-based detection
+resource "panther_simple_rule" "example" {
+ display_name = "AWS Console Login Detection"
+ detection = <<-EOT
+ MatchFilters:
+ - Key: eventName
+ Condition: Equals
+ Values:
+ - ConsoleLogin
+ - Key: userIdentity.type
+ Condition: Equals
+ Values:
+ - IAMUser
+ EOT
+ severity = "CRITICAL"
+ description = "Detects AWS console login events from IAM users"
+ enabled = true
+ dedup_period_minutes = 60
+ threshold = 1
+
+ log_types = [
+ "AWS.CloudTrail"
+ ]
+
+ tags = [
+ "authentication",
+ "aws"
+ ]
+
+ alert_title = "AWS Console Login: {{p_any_aws_account_ids}}"
+ alert_context = <<-EOT
+ User: {{userIdentity.userName}}
+ Source IP: {{sourceIPAddress}}
+ EOT
+
+ runbook = "Verify the login is legitimate and investigate if from unexpected location"
+}
+```
+
+
+## Schema
+
+### Required
+
+- `detection` (String) The yaml representation of the rule
+- `severity` (String)
+
+### Optional
+
+- `alert_context` (String) The alert context represented in YAML
+- `alert_title` (String) The alert title represented in YAML
+- `dedup_period_minutes` (Number) The amount of time in minutes for grouping alerts
+- `description` (String) The description of the rule
+- `display_name` (String) The display name of the rule
+- `dynamic_severities` (String) The dynamic severity represented in YAML
+- `enabled` (Boolean) Determines whether or not the rule is active
+- `group_by` (String) The key on an event to group by represented in YAML
+- `includepython` (Boolean) determines if associated python for the generated rule is returned
+- `inline_filters` (String) The filter for the rule represented in YAML
+- `log_types` (List of String) log types
+- `managed` (Boolean) Determines if the simple rule is managed by panther
+- `output_ids` (List of String) Destination IDs that override default alert routing based on severity
+- `python_body` (String) The python body of the rule
+- `reports` (Map of List of String) reports
+- `runbook` (String) How to handle the generated alert
+- `summary_attributes` (List of String) A list of fields in the event to create top 5 summaries for
+- `tags` (List of String) The tags for the simple rule
+- `tests` (Attributes List) Unit tests for the Rule. Best practice is to include a positive and negative case (see [below for nested schema](#nestedatt--tests))
+- `threshold` (Number) the number of events that must match before an alert is triggered
+
+### Read-Only
+
+- `created_at` (String)
+- `created_by` (Attributes) The actor who created the rule (see [below for nested schema](#nestedatt--created_by))
+- `created_by_external` (String) The text of the user-provided CreatedBy field when uploaded via CI/CD
+- `id` (String) The ID of this resource.
+- `last_modified` (String)
+
+
+### Nested Schema for `tests`
+
+Required:
+
+- `expected_result` (Boolean) The expected result
+- `name` (String) name
+- `resource` (String) resource
+
+Optional:
+
+- `mocks` (List of Map of String) mocks
+
+
+
+### Nested Schema for `created_by`
+
+Read-Only:
+
+- `id` (String)
+- `type` (String)
diff --git a/examples/full-examples/detection-rules/main.tf b/examples/full-examples/detection-rules/main.tf
new file mode 100644
index 0000000..6cd8cb0
--- /dev/null
+++ b/examples/full-examples/detection-rules/main.tf
@@ -0,0 +1,13 @@
+terraform {
+ required_version = ">= 1.0"
+ required_providers {
+ panther = {
+ source = "panther-labs/panther"
+ }
+ }
+}
+
+provider "panther" {
+ token = var.token
+ url = var.url
+}
\ No newline at end of file
diff --git a/examples/full-examples/detection-rules/outputs.tf b/examples/full-examples/detection-rules/outputs.tf
new file mode 100644
index 0000000..039649b
--- /dev/null
+++ b/examples/full-examples/detection-rules/outputs.tf
@@ -0,0 +1,9 @@
+output "console_login_rule_id" {
+ description = "ID of the console login detection rule"
+ value = panther_rule.console_login.id
+}
+
+output "failed_login_rule_id" {
+ description = "ID of the failed login detection rule"
+ value = panther_rule.failed_login.id
+}
\ No newline at end of file
diff --git a/examples/full-examples/detection-rules/rules.tf b/examples/full-examples/detection-rules/rules.tf
new file mode 100644
index 0000000..ee88a23
--- /dev/null
+++ b/examples/full-examples/detection-rules/rules.tf
@@ -0,0 +1,133 @@
+# Standard detection rule for log analysis
+resource "panther_rule" "console_login" {
+ display_name = "AWS Console Login Detection"
+ body = <<-EOT
+ def rule(event):
+ return event.get('eventName') == 'ConsoleLogin'
+ EOT
+ severity = "MEDIUM"
+ enabled = true
+
+ log_types = [
+ "AWS.CloudTrail"
+ ]
+
+ tags = [
+ "authentication",
+ "aws"
+ ]
+
+ description = "Detects AWS console login events"
+}
+
+resource "panther_rule" "failed_login" {
+ display_name = "Failed Console Login"
+ body = <<-EOT
+ def rule(event):
+ return (event.get('eventName') == 'ConsoleLogin' and
+ event.get('errorCode') == 'SigninFailure')
+ EOT
+ severity = "HIGH"
+ enabled = true
+
+ log_types = [
+ "AWS.CloudTrail"
+ ]
+
+ tags = [
+ "authentication",
+ "security"
+ ]
+
+ description = "Detects failed AWS console login attempts"
+ runbook = "Investigate the source IP and user account for potential compromise"
+}
+
+# Simple rule with YAML-based detection
+resource "panther_simple_rule" "root_login" {
+ display_name = "Root Account Login"
+ detection = <<-EOT
+ MatchFilters:
+ - Key: eventName
+ Condition: Equals
+ Values:
+ - ConsoleLogin
+ - Key: userIdentity.type
+ Condition: Equals
+ Values:
+ - Root
+ EOT
+ severity = "CRITICAL"
+ description = "Detects AWS root account console login"
+ enabled = true
+ dedup_period_minutes = 60
+ threshold = 1
+
+ log_types = [
+ "AWS.CloudTrail"
+ ]
+
+ tags = [
+ "authentication",
+ "critical"
+ ]
+
+ alert_title = "Root Login: {{p_any_aws_account_ids}}"
+ alert_context = "Source IP: {{sourceIPAddress}}"
+ runbook = "Immediately verify root account login and enable MFA if not enabled"
+}
+
+# Scheduled rule for query result analysis
+resource "panther_scheduled_rule" "brute_force_detection" {
+ display_name = "Brute Force Attack Detection"
+ body = <<-EOT
+ def rule(event):
+ # Query aggregates failed login attempts per IP
+ failed_count = event.get('failed_login_count', 0)
+ unique_users = event.get('unique_user_count', 0)
+ return failed_count > 50 and unique_users > 3
+ EOT
+ severity = "HIGH"
+ description = "Detects potential brute force attacks from scheduled query results"
+ enabled = true
+ dedup_period_minutes = 120
+ threshold = 1
+
+ scheduled_queries = [
+ "failed-login-aggregation"
+ ]
+
+ tags = [
+ "authentication",
+ "security"
+ ]
+
+ runbook = "Block source IPs showing brute force patterns and investigate affected accounts"
+}
+
+# Policy for cloud resource compliance
+resource "panther_policy" "s3_encryption" {
+ display_name = "S3 Bucket Encryption Required"
+ body = <<-EOT
+ def policy(resource):
+ encryption = resource.get('EncryptionConfiguration', {})
+ rules = encryption.get('Rules', [])
+ return len(rules) > 0 and any(
+ rule.get('ApplyServerSideEncryptionByDefault', {}).get('SSEAlgorithm')
+ for rule in rules
+ )
+ EOT
+ severity = "HIGH"
+ enabled = true
+
+ resource_types = [
+ "AWS.S3.Bucket"
+ ]
+
+ tags = [
+ "compliance",
+ "encryption"
+ ]
+
+ description = "Ensures all S3 buckets have encryption enabled"
+}
\ No newline at end of file
diff --git a/examples/full-examples/detection-rules/variables.tf b/examples/full-examples/detection-rules/variables.tf
new file mode 100644
index 0000000..153b10a
--- /dev/null
+++ b/examples/full-examples/detection-rules/variables.tf
@@ -0,0 +1,9 @@
+variable "token" {
+ description = "Panther API token"
+ type = string
+}
+
+variable "url" {
+ description = "Panther API URL"
+ type = string
+}
\ No newline at end of file
diff --git a/examples/resources/panther_policy/resource.tf b/examples/resources/panther_policy/resource.tf
new file mode 100644
index 0000000..d29d6fb
--- /dev/null
+++ b/examples/resources/panther_policy/resource.tf
@@ -0,0 +1,23 @@
+# Manage cloud security policy for resource compliance
+resource "panther_policy" "example" {
+ display_name = "S3 Bucket Encryption Policy"
+ body = <<-EOT
+ def policy(resource):
+ # Check if S3 bucket has encryption enabled
+ encryption = resource.get('EncryptionConfiguration', {})
+ rules = encryption.get('Rules', [])
+ return len(rules) > 0
+ EOT
+ severity = "MEDIUM"
+ description = "Ensures S3 buckets have encryption enabled"
+ enabled = true
+
+ resource_types = [
+ "AWS.S3.Bucket"
+ ]
+
+ tags = [
+ "compliance",
+ "encryption"
+ ]
+}
diff --git a/examples/resources/panther_rule/resource.tf b/examples/resources/panther_rule/resource.tf
new file mode 100644
index 0000000..b9db67a
--- /dev/null
+++ b/examples/resources/panther_rule/resource.tf
@@ -0,0 +1,16 @@
+# Manage detection rule for log analysis
+resource "panther_rule" "example" {
+ display_name = ""
+ body = ""
+ severity = ""
+ description = ""
+ enabled = true
+ dedup_period_minutes = 60
+ log_types = [
+ ""
+ ]
+ tags = [
+ ""
+ ]
+ runbook = ""
+}
\ No newline at end of file
diff --git a/examples/resources/panther_scheduled_rule/resource.tf b/examples/resources/panther_scheduled_rule/resource.tf
new file mode 100644
index 0000000..8738f2e
--- /dev/null
+++ b/examples/resources/panther_scheduled_rule/resource.tf
@@ -0,0 +1,26 @@
+# Manage scheduled detection rule for query results
+resource "panther_scheduled_rule" "example" {
+ display_name = "High Volume Failed Logins"
+ body = <<-EOT
+ def rule(event):
+ # Check if query results exceed threshold
+ failed_count = event.get('failed_login_count', 0)
+ return failed_count > 10
+ EOT
+ severity = "HIGH"
+ description = "Detects high volume of failed login attempts from scheduled query"
+ enabled = true
+ dedup_period_minutes = 60
+ threshold = 1
+
+ scheduled_queries = [
+ "failed-login-aggregation-query"
+ ]
+
+ tags = [
+ "authentication",
+ "security"
+ ]
+
+ runbook = "Investigate the source IPs and user accounts for potential brute force attacks"
+}
diff --git a/examples/resources/panther_simple_rule/resource.tf b/examples/resources/panther_simple_rule/resource.tf
new file mode 100644
index 0000000..6b5fb03
--- /dev/null
+++ b/examples/resources/panther_simple_rule/resource.tf
@@ -0,0 +1,37 @@
+# Manage simple detection rule with YAML-based detection
+resource "panther_simple_rule" "example" {
+ display_name = "AWS Console Login Detection"
+ detection = <<-EOT
+ MatchFilters:
+ - Key: eventName
+ Condition: Equals
+ Values:
+ - ConsoleLogin
+ - Key: userIdentity.type
+ Condition: Equals
+ Values:
+ - IAMUser
+ EOT
+ severity = "CRITICAL"
+ description = "Detects AWS console login events from IAM users"
+ enabled = true
+ dedup_period_minutes = 60
+ threshold = 1
+
+ log_types = [
+ "AWS.CloudTrail"
+ ]
+
+ tags = [
+ "authentication",
+ "aws"
+ ]
+
+ alert_title = "AWS Console Login: {{p_any_aws_account_ids}}"
+ alert_context = <<-EOT
+ User: {{userIdentity.userName}}
+ Source IP: {{sourceIPAddress}}
+ EOT
+
+ runbook = "Verify the login is legitimate and investigate if from unexpected location"
+}
diff --git a/generator_config.yml b/generator_config.yml
index 65a5630..d3811f4 100644
--- a/generator_config.yml
+++ b/generator_config.yml
@@ -17,3 +17,67 @@ resources:
schema:
ignores:
- integrationId
+ rule:
+ create:
+ path: /rules
+ method: POST
+ read:
+ path: /rules/{id}
+ method: GET
+ update:
+ path: /rules/{id}
+ method: PUT
+ delete:
+ path: /rules/{id}
+ method: DELETE
+ schema:
+ ignores:
+ - id
+ policy:
+ create:
+ path: /policies
+ method: POST
+ read:
+ path: /policies/{id}
+ method: GET
+ update:
+ path: /policies/{id}
+ method: PUT
+ delete:
+ path: /policies/{id}
+ method: DELETE
+ schema:
+ ignores:
+ - id
+ scheduled_rule:
+ create:
+ path: /scheduled-rules
+ method: POST
+ read:
+ path: /scheduled-rules/{id}
+ method: GET
+ update:
+ path: /scheduled-rules/{id}
+ method: PUT
+ delete:
+ path: /scheduled-rules/{id}
+ method: DELETE
+ schema:
+ ignores:
+ - id
+ simple_rule:
+ create:
+ path: /simple-rules
+ method: POST
+ read:
+ path: /simple-rules/{id}
+ method: GET
+ update:
+ path: /simple-rules/{id}
+ method: PUT
+ delete:
+ path: /simple-rules/{id}
+ method: DELETE
+ schema:
+ ignores:
+ - id
diff --git a/internal/client/panther.go b/internal/client/panther.go
index 9219a87..c243e3a 100644
--- a/internal/client/panther.go
+++ b/internal/client/panther.go
@@ -32,6 +32,30 @@ type RestClient interface {
UpdateHttpSource(ctx context.Context, input UpdateHttpSourceInput) (HttpSource, error)
GetHttpSource(ctx context.Context, id string) (HttpSource, error)
DeleteHttpSource(ctx context.Context, id string) error
+
+ // Rule management
+ CreateRule(ctx context.Context, input CreateRuleInput) (Rule, error)
+ UpdateRule(ctx context.Context, input UpdateRuleInput) (Rule, error)
+ GetRule(ctx context.Context, id string) (Rule, error)
+ DeleteRule(ctx context.Context, id string) error
+
+ // Policy management
+ CreatePolicy(ctx context.Context, input CreatePolicyInput) (Policy, error)
+ UpdatePolicy(ctx context.Context, input UpdatePolicyInput) (Policy, error)
+ GetPolicy(ctx context.Context, id string) (Policy, error)
+ DeletePolicy(ctx context.Context, id string) error
+
+ // Scheduled rule management
+ CreateScheduledRule(ctx context.Context, input CreateScheduledRuleInput) (ScheduledRule, error)
+ UpdateScheduledRule(ctx context.Context, input UpdateScheduledRuleInput) (ScheduledRule, error)
+ GetScheduledRule(ctx context.Context, id string) (ScheduledRule, error)
+ DeleteScheduledRule(ctx context.Context, id string) error
+
+ // Simple rule management
+ CreateSimpleRule(ctx context.Context, input CreateSimpleRuleInput) (SimpleRule, error)
+ UpdateSimpleRule(ctx context.Context, input UpdateSimpleRuleInput) (SimpleRule, error)
+ GetSimpleRule(ctx context.Context, id string) (SimpleRule, error)
+ DeleteSimpleRule(ctx context.Context, id string) error
}
// CreateS3SourceInput Input for the createS3LogSource mutation
@@ -163,3 +187,140 @@ type UpdateHttpSourceInput struct {
type HttpErrorResponse struct {
Message string
}
+
+// Rule types
+type Rule struct {
+ ID string `json:"id"`
+ CreatedAt string `json:"createdAt"`
+ UpdatedAt string `json:"updatedAt"`
+ RuleModifiableAttributes
+}
+
+type RuleModifiableAttributes struct {
+ DisplayName string `json:"displayName"`
+ Body string `json:"body"`
+ Description string `json:"description,omitempty"`
+ Severity string `json:"severity,omitempty"`
+ LogTypes []string `json:"logTypes,omitempty"`
+ Tags []string `json:"tags,omitempty"`
+ References []string `json:"references,omitempty"`
+ Runbook string `json:"runbook,omitempty"`
+ DedupPeriodMinutes int `json:"dedupPeriodMinutes,omitempty"`
+ Enabled bool `json:"enabled,omitempty"`
+}
+
+type CreateRuleInput struct {
+ ID string `json:"id"`
+ RuleModifiableAttributes
+}
+
+type UpdateRuleInput struct {
+ ID string `json:"id"`
+ RuleModifiableAttributes
+}
+
+// Policy types
+type Policy struct {
+ ID string `json:"id"`
+ CreatedAt string `json:"createdAt"`
+ UpdatedAt string `json:"lastModified"`
+ Version string `json:"version"`
+ PolicyModifiableAttributes
+}
+
+type PolicyModifiableAttributes struct {
+ DisplayName string `json:"displayName"`
+ Body string `json:"body"`
+ Description string `json:"description,omitempty"`
+ Severity string `json:"severity,omitempty"`
+ ResourceTypes []string `json:"resourceTypes,omitempty"`
+ Tags []string `json:"tags,omitempty"`
+ Runbook string `json:"runbook,omitempty"`
+ Enabled bool `json:"enabled,omitempty"`
+}
+
+type CreatePolicyInput struct {
+ ID string `json:"id"`
+ PolicyModifiableAttributes
+}
+
+type UpdatePolicyInput struct {
+ ID string `json:"id"`
+ PolicyModifiableAttributes
+}
+
+// Scheduled rule types
+type ScheduledRule struct {
+ ID string `json:"id"`
+ CreatedAt string `json:"createdAt"`
+ UpdatedAt string `json:"lastModified"`
+ ScheduledRuleModifiableAttributes
+}
+
+type ScheduledRuleModifiableAttributes struct {
+ DisplayName string `json:"displayName,omitempty"`
+ Body string `json:"body"`
+ Description string `json:"description,omitempty"`
+ Severity string `json:"severity"`
+ ScheduledQueries []string `json:"scheduledQueries,omitempty"`
+ Tags []string `json:"tags,omitempty"`
+ Runbook string `json:"runbook,omitempty"`
+ DedupPeriodMinutes int `json:"dedupPeriodMinutes,omitempty"`
+ Enabled bool `json:"enabled,omitempty"`
+ OutputIds []string `json:"outputIDs,omitempty"`
+ Reports map[string][]string `json:"reports,omitempty"`
+ SummaryAttributes []string `json:"summaryAttributes,omitempty"`
+ Threshold int `json:"threshold,omitempty"`
+ Managed bool `json:"managed,omitempty"`
+}
+
+type CreateScheduledRuleInput struct {
+ ID string `json:"id"`
+ ScheduledRuleModifiableAttributes
+}
+
+type UpdateScheduledRuleInput struct {
+ ID string `json:"id"`
+ ScheduledRuleModifiableAttributes
+}
+
+// Simple rule types
+type SimpleRule struct {
+ ID string `json:"id"`
+ CreatedAt string `json:"createdAt"`
+ UpdatedAt string `json:"lastModified"`
+ SimpleRuleModifiableAttributes
+}
+
+type SimpleRuleModifiableAttributes struct {
+ DisplayName string `json:"displayName,omitempty"`
+ Detection string `json:"detection"`
+ Description string `json:"description,omitempty"`
+ Severity string `json:"severity"`
+ LogTypes []string `json:"logTypes,omitempty"`
+ Tags []string `json:"tags,omitempty"`
+ Runbook string `json:"runbook,omitempty"`
+ DedupPeriodMinutes int `json:"dedupPeriodMinutes,omitempty"`
+ Enabled bool `json:"enabled,omitempty"`
+ AlertTitle string `json:"alertTitle,omitempty"`
+ AlertContext string `json:"alertContext,omitempty"`
+ DynamicSeverities string `json:"dynamicSeverities,omitempty"`
+ GroupBy string `json:"groupBy,omitempty"`
+ InlineFilters string `json:"inlineFilters,omitempty"`
+ OutputIds []string `json:"outputIDs,omitempty"`
+ PythonBody string `json:"pythonBody,omitempty"`
+ Reports map[string][]string `json:"reports,omitempty"`
+ SummaryAttributes []string `json:"summaryAttributes,omitempty"`
+ Threshold int `json:"threshold,omitempty"`
+ Managed bool `json:"managed,omitempty"`
+}
+
+type CreateSimpleRuleInput struct {
+ ID string `json:"id"`
+ SimpleRuleModifiableAttributes
+}
+
+type UpdateSimpleRuleInput struct {
+ ID string `json:"id"`
+ SimpleRuleModifiableAttributes
+}
diff --git a/internal/client/panther/panther.go b/internal/client/panther/panther.go
index f4a4a6b..03884ba 100644
--- a/internal/client/panther/panther.go
+++ b/internal/client/panther/panther.go
@@ -272,3 +272,249 @@ func getErrorResponseMsg(resp *http.Response) string {
return errResponse.Message
}
+
+// Generic REST helper for Rule endpoints
+func (c *RestClient) doRuleRequest(ctx context.Context, method, path string, input interface{}, expectedStatus int) ([]byte, error) {
+ // Extract base URL without the /log-sources/http suffix
+ baseURL := strings.TrimSuffix(c.url, RestHttpSourcePath)
+ fullURL := fmt.Sprintf("%s%s", baseURL, path)
+
+ var body io.Reader
+ if input != nil {
+ jsonData, err := json.Marshal(input)
+ if err != nil {
+ return nil, fmt.Errorf("error marshaling data: %w", err)
+ }
+ body = bytes.NewReader(jsonData)
+ }
+
+ req, err := http.NewRequestWithContext(ctx, method, fullURL, body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create http request: %w", err)
+ }
+
+ if input != nil {
+ req.Header.Set("Content-Type", "application/json")
+ }
+
+ resp, err := c.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("failed to make request: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != expectedStatus {
+ return nil, fmt.Errorf("failed to make request, status: %d, message: %s", resp.StatusCode, getErrorResponseMsg(resp))
+ }
+
+ responseBody, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read response body: %w", err)
+ }
+
+ return responseBody, nil
+}
+
+// Rule methods
+func (c *RestClient) CreateRule(ctx context.Context, input client.CreateRuleInput) (client.Rule, error) {
+ body, err := c.doRuleRequest(ctx, http.MethodPost, "/rules", input, http.StatusOK)
+ if err != nil {
+ return client.Rule{}, err
+ }
+
+ var response client.Rule
+ if err = json.Unmarshal(body, &response); err != nil {
+ return client.Rule{}, fmt.Errorf("failed to unmarshal response body: %w", err)
+ }
+
+ return response, nil
+}
+
+func (c *RestClient) UpdateRule(ctx context.Context, input client.UpdateRuleInput) (client.Rule, error) {
+ path := fmt.Sprintf("/rules/%s", input.ID)
+ body, err := c.doRuleRequest(ctx, http.MethodPut, path, input, http.StatusOK)
+ if err != nil {
+ return client.Rule{}, err
+ }
+
+ var response client.Rule
+ if err = json.Unmarshal(body, &response); err != nil {
+ return client.Rule{}, fmt.Errorf("failed to unmarshal response body: %w", err)
+ }
+
+ return response, nil
+}
+
+func (c *RestClient) GetRule(ctx context.Context, id string) (client.Rule, error) {
+ path := fmt.Sprintf("/rules/%s", id)
+ body, err := c.doRuleRequest(ctx, http.MethodGet, path, nil, http.StatusOK)
+ if err != nil {
+ return client.Rule{}, err
+ }
+
+ var response client.Rule
+ if err = json.Unmarshal(body, &response); err != nil {
+ return client.Rule{}, fmt.Errorf("failed to unmarshal response body: %w", err)
+ }
+
+ return response, nil
+}
+
+func (c *RestClient) DeleteRule(ctx context.Context, id string) error {
+ path := fmt.Sprintf("/rules/%s", id)
+ _, err := c.doRuleRequest(ctx, http.MethodDelete, path, nil, http.StatusNoContent)
+ return err
+}
+
+// Policy methods
+func (c *RestClient) CreatePolicy(ctx context.Context, input client.CreatePolicyInput) (client.Policy, error) {
+ body, err := c.doRuleRequest(ctx, http.MethodPost, "/policies", input, http.StatusOK)
+ if err != nil {
+ return client.Policy{}, err
+ }
+
+ var response client.Policy
+ if err = json.Unmarshal(body, &response); err != nil {
+ return client.Policy{}, fmt.Errorf("failed to unmarshal response body: %w", err)
+ }
+
+ return response, nil
+}
+
+func (c *RestClient) UpdatePolicy(ctx context.Context, input client.UpdatePolicyInput) (client.Policy, error) {
+ path := fmt.Sprintf("/policies/%s", input.ID)
+ body, err := c.doRuleRequest(ctx, http.MethodPut, path, input, http.StatusOK)
+ if err != nil {
+ return client.Policy{}, err
+ }
+
+ var response client.Policy
+ if err = json.Unmarshal(body, &response); err != nil {
+ return client.Policy{}, fmt.Errorf("failed to unmarshal response body: %w", err)
+ }
+
+ return response, nil
+}
+
+func (c *RestClient) GetPolicy(ctx context.Context, id string) (client.Policy, error) {
+ path := fmt.Sprintf("/policies/%s", id)
+ body, err := c.doRuleRequest(ctx, http.MethodGet, path, nil, http.StatusOK)
+ if err != nil {
+ return client.Policy{}, err
+ }
+
+ var response client.Policy
+ if err = json.Unmarshal(body, &response); err != nil {
+ return client.Policy{}, fmt.Errorf("failed to unmarshal response body: %w", err)
+ }
+
+ return response, nil
+}
+
+func (c *RestClient) DeletePolicy(ctx context.Context, id string) error {
+ path := fmt.Sprintf("/policies/%s", id)
+ _, err := c.doRuleRequest(ctx, http.MethodDelete, path, nil, http.StatusNoContent)
+ return err
+}
+
+// Scheduled rule methods
+func (c *RestClient) CreateScheduledRule(ctx context.Context, input client.CreateScheduledRuleInput) (client.ScheduledRule, error) {
+ body, err := c.doRuleRequest(ctx, http.MethodPost, "/scheduled-rules", input, http.StatusOK)
+ if err != nil {
+ return client.ScheduledRule{}, err
+ }
+
+ var response client.ScheduledRule
+ if err = json.Unmarshal(body, &response); err != nil {
+ return client.ScheduledRule{}, fmt.Errorf("failed to unmarshal response body: %w", err)
+ }
+
+ return response, nil
+}
+
+func (c *RestClient) UpdateScheduledRule(ctx context.Context, input client.UpdateScheduledRuleInput) (client.ScheduledRule, error) {
+ path := fmt.Sprintf("/scheduled-rules/%s", input.ID)
+ body, err := c.doRuleRequest(ctx, http.MethodPut, path, input, http.StatusOK)
+ if err != nil {
+ return client.ScheduledRule{}, err
+ }
+
+ var response client.ScheduledRule
+ if err = json.Unmarshal(body, &response); err != nil {
+ return client.ScheduledRule{}, fmt.Errorf("failed to unmarshal response body: %w", err)
+ }
+
+ return response, nil
+}
+
+func (c *RestClient) GetScheduledRule(ctx context.Context, id string) (client.ScheduledRule, error) {
+ path := fmt.Sprintf("/scheduled-rules/%s", id)
+ body, err := c.doRuleRequest(ctx, http.MethodGet, path, nil, http.StatusOK)
+ if err != nil {
+ return client.ScheduledRule{}, err
+ }
+
+ var response client.ScheduledRule
+ if err = json.Unmarshal(body, &response); err != nil {
+ return client.ScheduledRule{}, fmt.Errorf("failed to unmarshal response body: %w", err)
+ }
+
+ return response, nil
+}
+
+func (c *RestClient) DeleteScheduledRule(ctx context.Context, id string) error {
+ path := fmt.Sprintf("/scheduled-rules/%s", id)
+ _, err := c.doRuleRequest(ctx, http.MethodDelete, path, nil, http.StatusNoContent)
+ return err
+}
+
+// Simple rule methods
+func (c *RestClient) CreateSimpleRule(ctx context.Context, input client.CreateSimpleRuleInput) (client.SimpleRule, error) {
+ body, err := c.doRuleRequest(ctx, http.MethodPost, "/simple-rules", input, http.StatusOK)
+ if err != nil {
+ return client.SimpleRule{}, err
+ }
+
+ var response client.SimpleRule
+ if err = json.Unmarshal(body, &response); err != nil {
+ return client.SimpleRule{}, fmt.Errorf("failed to unmarshal response body: %w", err)
+ }
+
+ return response, nil
+}
+
+func (c *RestClient) UpdateSimpleRule(ctx context.Context, input client.UpdateSimpleRuleInput) (client.SimpleRule, error) {
+ path := fmt.Sprintf("/simple-rules/%s", input.ID)
+ body, err := c.doRuleRequest(ctx, http.MethodPut, path, input, http.StatusOK)
+ if err != nil {
+ return client.SimpleRule{}, err
+ }
+
+ var response client.SimpleRule
+ if err = json.Unmarshal(body, &response); err != nil {
+ return client.SimpleRule{}, fmt.Errorf("failed to unmarshal response body: %w", err)
+ }
+
+ return response, nil
+}
+
+func (c *RestClient) GetSimpleRule(ctx context.Context, id string) (client.SimpleRule, error) {
+ path := fmt.Sprintf("/simple-rules/%s", id)
+ body, err := c.doRuleRequest(ctx, http.MethodGet, path, nil, http.StatusOK)
+ if err != nil {
+ return client.SimpleRule{}, err
+ }
+
+ var response client.SimpleRule
+ if err = json.Unmarshal(body, &response); err != nil {
+ return client.SimpleRule{}, fmt.Errorf("failed to unmarshal response body: %w", err)
+ }
+
+ return response, nil
+}
+
+func (c *RestClient) DeleteSimpleRule(ctx context.Context, id string) error {
+ path := fmt.Sprintf("/simple-rules/%s", id)
+ _, err := c.doRuleRequest(ctx, http.MethodDelete, path, nil, http.StatusNoContent)
+ return err
+}
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index b6b8535..10ddb89 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -130,6 +130,10 @@ func (p *PantherProvider) Resources(ctx context.Context) []func() resource.Resou
return []func() resource.Resource{
NewS3SourceResource,
NewHttpsourceResource,
+ NewRuleResource,
+ NewPolicyResource,
+ NewScheduledRuleResource,
+ NewSimpleRuleResource,
}
}
diff --git a/internal/provider/resource_policy.go b/internal/provider/resource_policy.go
new file mode 100644
index 0000000..05fe4af
--- /dev/null
+++ b/internal/provider/resource_policy.go
@@ -0,0 +1,403 @@
+/*
+Copyright 2023 Panther Labs, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package provider
+
+import (
+ "context"
+ "fmt"
+ "terraform-provider-panther/internal/client"
+ "terraform-provider-panther/internal/client/panther"
+ "terraform-provider-panther/internal/provider/resource_policy"
+
+ "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/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+)
+
+var (
+ _ resource.Resource = (*policyResource)(nil)
+ _ resource.ResourceWithConfigure = (*policyResource)(nil)
+ _ resource.ResourceWithImportState = (*policyResource)(nil)
+)
+
+func NewPolicyResource() resource.Resource {
+ return &policyResource{}
+}
+
+type policyResource struct {
+ client client.RestClient
+}
+
+func (r *policyResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_policy"
+}
+
+func (r *policyResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+ // Use the generated schema
+ generatedSchema := resource_policy.PolicyResourceSchema(ctx)
+
+ // Add the ID field with UseStateForUnknown as required by limitations
+ if generatedSchema.Attributes == nil {
+ generatedSchema.Attributes = make(map[string]schema.Attribute)
+ }
+
+ generatedSchema.Attributes["id"] = schema.StringAttribute{
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ }
+
+ resp.Schema = generatedSchema
+}
+
+func (r *policyResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ apiClient, ok := req.ProviderData.(*panther.APIClient)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *panther.APIClient, got: %T", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = apiClient.RestClient
+}
+
+func (r *policyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data resource_policy.PolicyModel
+
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Convert from generated model to our client types
+ input := client.CreatePolicyInput{
+ ID: data.DisplayName.ValueString(), // Using display_name as ID
+ PolicyModifiableAttributes: client.PolicyModifiableAttributes{
+ DisplayName: data.DisplayName.ValueString(),
+ Body: data.Body.ValueString(),
+ Description: data.Description.ValueString(),
+ Severity: data.Severity.ValueString(),
+ Enabled: data.Enabled.ValueBool(),
+ },
+ }
+
+ // Convert resource types
+ if !data.ResourceTypes.IsNull() && !data.ResourceTypes.IsUnknown() {
+ resourceTypes := make([]string, 0, len(data.ResourceTypes.Elements()))
+ for _, elem := range data.ResourceTypes.Elements() {
+ if strVal, ok := elem.(types.String); ok {
+ resourceTypes = append(resourceTypes, strVal.ValueString())
+ }
+ }
+ input.ResourceTypes = resourceTypes
+ }
+
+ // Convert tags
+ if !data.Tags.IsNull() && !data.Tags.IsUnknown() {
+ tags := make([]string, 0, len(data.Tags.Elements()))
+ for _, elem := range data.Tags.Elements() {
+ if strVal, ok := elem.(types.String); ok {
+ tags = append(tags, strVal.ValueString())
+ }
+ }
+ input.Tags = tags
+ }
+
+ result, err := r.client.CreatePolicy(ctx, input)
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create policy, got error: %s", err))
+ return
+ }
+
+ // Update the model with the result
+ data.Id = types.StringValue(result.ID)
+ data.DisplayName = types.StringValue(result.DisplayName)
+ data.Body = types.StringValue(result.Body)
+ data.Description = types.StringValue(result.Description)
+ data.Severity = types.StringValue(result.Severity)
+ data.Enabled = types.BoolValue(result.Enabled)
+ data.CreatedAt = types.StringValue(result.CreatedAt)
+ data.LastModified = types.StringValue(result.UpdatedAt)
+
+ // Convert resource types back to list
+ if len(result.ResourceTypes) > 0 {
+ elements := make([]types.String, len(result.ResourceTypes))
+ for i, resourceType := range result.ResourceTypes {
+ elements[i] = types.StringValue(resourceType)
+ }
+ resourceTypesList, diags := types.ListValueFrom(ctx, types.StringType, elements)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ data.ResourceTypes = resourceTypesList
+ } else {
+ data.ResourceTypes = types.ListNull(types.StringType)
+ }
+
+ // Set other computed fields to null/empty for now
+ data.CreatedBy = resource_policy.NewCreatedByValueNull()
+ data.CreatedByExternal = types.StringNull()
+ data.Managed = types.BoolNull()
+ data.OutputIds = types.ListNull(types.StringType)
+ data.Reports = types.MapNull(types.ListType{ElemType: types.StringType})
+ data.Tests = types.ListNull(resource_policy.TestsType{
+ ObjectType: types.ObjectType{
+ AttrTypes: resource_policy.TestsValue{}.AttributeTypes(ctx),
+ },
+ })
+ data.Suppressions = types.ListNull(types.StringType)
+
+ tflog.Debug(ctx, "Created Policy", map[string]any{
+ "id": result.ID,
+ })
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *policyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data resource_policy.PolicyModel
+
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Use ID if available, otherwise fall back to DisplayName for backward compatibility
+ policyID := data.Id.ValueString()
+ if policyID == "" {
+ policyID = data.DisplayName.ValueString()
+ }
+ policy, err := r.client.GetPolicy(ctx, policyID)
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read policy, got error: %s", err))
+ return
+ }
+
+ // Update model with API response
+ data.Id = types.StringValue(policy.ID)
+ data.DisplayName = types.StringValue(policy.DisplayName)
+ data.Body = types.StringValue(policy.Body)
+ data.Description = types.StringValue(policy.Description)
+ data.Severity = types.StringValue(policy.Severity)
+ data.Enabled = types.BoolValue(policy.Enabled)
+ data.CreatedAt = types.StringValue(policy.CreatedAt)
+ data.LastModified = types.StringValue(policy.UpdatedAt)
+
+ // Convert resource types back to list
+ if len(policy.ResourceTypes) > 0 {
+ elements := make([]types.String, len(policy.ResourceTypes))
+ for i, resourceType := range policy.ResourceTypes {
+ elements[i] = types.StringValue(resourceType)
+ }
+ resourceTypesList, diags := types.ListValueFrom(ctx, types.StringType, elements)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ data.ResourceTypes = resourceTypesList
+ } else {
+ data.ResourceTypes = types.ListNull(types.StringType)
+ }
+
+ // Only update tags if they have actually changed (content-wise, ignoring order)
+ if len(policy.Tags) > 0 {
+ // Get current tags from state
+ currentTags := make([]string, 0)
+ if !data.Tags.IsNull() && !data.Tags.IsUnknown() {
+ for _, elem := range data.Tags.Elements() {
+ if strVal, ok := elem.(types.String); ok {
+ currentTags = append(currentTags, strVal.ValueString())
+ }
+ }
+ }
+
+ // Check if tag content has changed (ignore order)
+ tagsChanged := len(currentTags) != len(policy.Tags)
+ if !tagsChanged {
+ apiTagsMap := make(map[string]bool)
+ for _, tag := range policy.Tags {
+ apiTagsMap[tag] = true
+ }
+ for _, tag := range currentTags {
+ if !apiTagsMap[tag] {
+ tagsChanged = true
+ break
+ }
+ }
+ }
+
+ // Only update tags if content changed, preserving existing order
+ if tagsChanged {
+ elements := make([]types.String, len(policy.Tags))
+ for i, tag := range policy.Tags {
+ elements[i] = types.StringValue(tag)
+ }
+ tagsList, diags := types.ListValueFrom(ctx, types.StringType, elements)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ data.Tags = tagsList
+ }
+ } else if !data.Tags.IsNull() {
+ // API returned no tags but state has tags - clear them
+ data.Tags = types.ListNull(types.StringType)
+ }
+
+ // Set other computed fields to null/empty for now
+ data.CreatedBy = resource_policy.NewCreatedByValueNull()
+ data.CreatedByExternal = types.StringNull()
+ data.Managed = types.BoolNull()
+ data.OutputIds = types.ListNull(types.StringType)
+ data.Reports = types.MapNull(types.ListType{ElemType: types.StringType})
+ data.Tests = types.ListNull(resource_policy.TestsType{
+ ObjectType: types.ObjectType{
+ AttrTypes: resource_policy.TestsValue{}.AttributeTypes(ctx),
+ },
+ })
+ data.Suppressions = types.ListNull(types.StringType)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *policyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var data resource_policy.PolicyModel
+
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ input := client.UpdatePolicyInput{
+ ID: data.Id.ValueString(),
+ PolicyModifiableAttributes: client.PolicyModifiableAttributes{
+ DisplayName: data.DisplayName.ValueString(),
+ Body: data.Body.ValueString(),
+ Description: data.Description.ValueString(),
+ Severity: data.Severity.ValueString(),
+ Enabled: data.Enabled.ValueBool(),
+ },
+ }
+
+ // Convert resource types
+ if !data.ResourceTypes.IsNull() && !data.ResourceTypes.IsUnknown() {
+ resourceTypes := make([]string, 0, len(data.ResourceTypes.Elements()))
+ for _, elem := range data.ResourceTypes.Elements() {
+ if strVal, ok := elem.(types.String); ok {
+ resourceTypes = append(resourceTypes, strVal.ValueString())
+ }
+ }
+ input.ResourceTypes = resourceTypes
+ }
+
+ // Convert tags
+ if !data.Tags.IsNull() && !data.Tags.IsUnknown() {
+ tags := make([]string, 0, len(data.Tags.Elements()))
+ for _, elem := range data.Tags.Elements() {
+ if strVal, ok := elem.(types.String); ok {
+ tags = append(tags, strVal.ValueString())
+ }
+ }
+ input.Tags = tags
+ }
+
+ result, err := r.client.UpdatePolicy(ctx, input)
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update policy, got error: %s", err))
+ return
+ }
+
+ // Update the model with the result
+ data.Id = types.StringValue(result.ID)
+ data.DisplayName = types.StringValue(result.DisplayName)
+ data.Body = types.StringValue(result.Body)
+ data.Description = types.StringValue(result.Description)
+ data.Severity = types.StringValue(result.Severity)
+ data.Enabled = types.BoolValue(result.Enabled)
+ data.CreatedAt = types.StringValue(result.CreatedAt)
+ data.LastModified = types.StringValue(result.UpdatedAt)
+
+ // Convert resource types back to list
+ if len(result.ResourceTypes) > 0 {
+ elements := make([]types.String, len(result.ResourceTypes))
+ for i, resourceType := range result.ResourceTypes {
+ elements[i] = types.StringValue(resourceType)
+ }
+ resourceTypesList, diags := types.ListValueFrom(ctx, types.StringType, elements)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ data.ResourceTypes = resourceTypesList
+ } else {
+ data.ResourceTypes = types.ListNull(types.StringType)
+ }
+
+ // Set other computed fields to null/empty for now
+ data.CreatedBy = resource_policy.NewCreatedByValueNull()
+ data.CreatedByExternal = types.StringNull()
+ data.Managed = types.BoolNull()
+ data.OutputIds = types.ListNull(types.StringType)
+ data.Reports = types.MapNull(types.ListType{ElemType: types.StringType})
+ data.Tests = types.ListNull(resource_policy.TestsType{
+ ObjectType: types.ObjectType{
+ AttrTypes: resource_policy.TestsValue{}.AttributeTypes(ctx),
+ },
+ })
+ data.Suppressions = types.ListNull(types.StringType)
+
+ tflog.Debug(ctx, "Updated Policy", map[string]any{
+ "id": result.ID,
+ })
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *policyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var data resource_policy.PolicyModel
+
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ err := r.client.DeletePolicy(ctx, data.Id.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete policy, got error: %s", err))
+ return
+ }
+
+ tflog.Debug(ctx, "Deleted Policy", map[string]any{
+ "id": data.Id.ValueString(),
+ })
+}
+
+func (r *policyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
diff --git a/internal/provider/resource_policy/policy_resource_gen.go b/internal/provider/resource_policy/policy_resource_gen.go
new file mode 100644
index 0000000..622446c
--- /dev/null
+++ b/internal/provider/resource_policy/policy_resource_gen.go
@@ -0,0 +1,1102 @@
+// Code generated by terraform-plugin-framework-generator DO NOT EDIT.
+
+package resource_policy
+
+import (
+ "context"
+ "fmt"
+ "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/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+ "github.com/hashicorp/terraform-plugin-go/tftypes"
+ "strings"
+
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+)
+
+func PolicyResourceSchema(ctx context.Context) schema.Schema {
+ return schema.Schema{
+ Attributes: map[string]schema.Attribute{
+ "body": schema.StringAttribute{
+ Required: true,
+ Description: "The python body of the policy",
+ MarkdownDescription: "The python body of the policy",
+ },
+ "created_at": schema.StringAttribute{
+ Computed: true,
+ },
+ "created_by": schema.SingleNestedAttribute{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Computed: true,
+ },
+ "type": schema.StringAttribute{
+ Computed: true,
+ },
+ },
+ CustomType: CreatedByType{
+ ObjectType: types.ObjectType{
+ AttrTypes: CreatedByValue{}.AttributeTypes(ctx),
+ },
+ },
+ Computed: true,
+ Description: "The actor who created the rule",
+ MarkdownDescription: "The actor who created the rule",
+ },
+ "created_by_external": schema.StringAttribute{
+ Computed: true,
+ Description: "The text of the user-provided CreatedBy field when uploaded via CI/CD",
+ MarkdownDescription: "The text of the user-provided CreatedBy field when uploaded via CI/CD",
+ },
+ "description": schema.StringAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "The description of the policy",
+ MarkdownDescription: "The description of the policy",
+ },
+ "display_name": schema.StringAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "The display name of the policy",
+ MarkdownDescription: "The display name of the policy",
+ },
+ "enabled": schema.BoolAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "Determines whether or not the policy is active",
+ MarkdownDescription: "Determines whether or not the policy is active",
+ },
+ "last_modified": schema.StringAttribute{
+ Computed: true,
+ },
+ "managed": schema.BoolAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "Determines if the policy is managed by panther",
+ MarkdownDescription: "Determines if the policy is managed by panther",
+ },
+ "output_ids": schema.ListAttribute{
+ ElementType: types.StringType,
+ Optional: true,
+ Computed: true,
+ Description: "Destination IDs that override default alert routing based on severity",
+ MarkdownDescription: "Destination IDs that override default alert routing based on severity",
+ },
+ "reports": schema.MapAttribute{
+ ElementType: types.ListType{
+ ElemType: types.StringType,
+ },
+ Optional: true,
+ Computed: true,
+ Description: "Reports",
+ MarkdownDescription: "Reports",
+ },
+ "resource_types": schema.ListAttribute{
+ ElementType: types.StringType,
+ Optional: true,
+ Computed: true,
+ Description: "Resource types",
+ MarkdownDescription: "Resource types",
+ },
+ "severity": schema.StringAttribute{
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf(
+ "INFO",
+ "LOW",
+ "MEDIUM",
+ "HIGH",
+ "CRITICAL",
+ ),
+ },
+ },
+ "suppressions": schema.ListAttribute{
+ ElementType: types.StringType,
+ Optional: true,
+ Computed: true,
+ Description: "Resources to ignore via a pattern that matches the resource id",
+ MarkdownDescription: "Resources to ignore via a pattern that matches the resource id",
+ },
+ "tags": schema.ListAttribute{
+ ElementType: types.StringType,
+ Optional: true,
+ Computed: true,
+ Description: "The tags for the policy",
+ MarkdownDescription: "The tags for the policy",
+ },
+ "tests": schema.ListNestedAttribute{
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "expected_result": schema.BoolAttribute{
+ Required: true,
+ Description: "The expected result",
+ MarkdownDescription: "The expected result",
+ },
+ "mocks": schema.ListAttribute{
+ ElementType: types.MapType{
+ ElemType: types.StringType,
+ },
+ Optional: true,
+ Computed: true,
+ Description: "mocks",
+ MarkdownDescription: "mocks",
+ },
+ "name": schema.StringAttribute{
+ Required: true,
+ Description: "name",
+ MarkdownDescription: "name",
+ },
+ "resource": schema.StringAttribute{
+ Required: true,
+ Description: "resource",
+ MarkdownDescription: "resource",
+ },
+ },
+ CustomType: TestsType{
+ ObjectType: types.ObjectType{
+ AttrTypes: TestsValue{}.AttributeTypes(ctx),
+ },
+ },
+ },
+ Optional: true,
+ Computed: true,
+ Description: "Unit tests for the Policy. Best practice is to include a positive and negative case",
+ MarkdownDescription: "Unit tests for the Policy. Best practice is to include a positive and negative case",
+ },
+ },
+ }
+}
+
+type PolicyModel struct {
+ Id types.String `tfsdk:"id"`
+ Body types.String `tfsdk:"body"`
+ CreatedAt types.String `tfsdk:"created_at"`
+ CreatedBy CreatedByValue `tfsdk:"created_by"`
+ CreatedByExternal types.String `tfsdk:"created_by_external"`
+ Description types.String `tfsdk:"description"`
+ DisplayName types.String `tfsdk:"display_name"`
+ Enabled types.Bool `tfsdk:"enabled"`
+ LastModified types.String `tfsdk:"last_modified"`
+ Managed types.Bool `tfsdk:"managed"`
+ OutputIds types.List `tfsdk:"output_ids"`
+ Reports types.Map `tfsdk:"reports"`
+ ResourceTypes types.List `tfsdk:"resource_types"`
+ Severity types.String `tfsdk:"severity"`
+ Suppressions types.List `tfsdk:"suppressions"`
+ Tags types.List `tfsdk:"tags"`
+ Tests types.List `tfsdk:"tests"`
+}
+
+var _ basetypes.ObjectTypable = CreatedByType{}
+
+type CreatedByType struct {
+ basetypes.ObjectType
+}
+
+func (t CreatedByType) Equal(o attr.Type) bool {
+ other, ok := o.(CreatedByType)
+
+ if !ok {
+ return false
+ }
+
+ return t.ObjectType.Equal(other.ObjectType)
+}
+
+func (t CreatedByType) String() string {
+ return "CreatedByType"
+}
+
+func (t CreatedByType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ attributes := in.Attributes()
+
+ idAttribute, ok := attributes["id"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `id is missing from object`)
+
+ return nil, diags
+ }
+
+ idVal, ok := idAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`id expected to be basetypes.StringValue, was: %T`, idAttribute))
+ }
+
+ typeAttribute, ok := attributes["type"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `type is missing from object`)
+
+ return nil, diags
+ }
+
+ typeVal, ok := typeAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`type expected to be basetypes.StringValue, was: %T`, typeAttribute))
+ }
+
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ return CreatedByValue{
+ Id: idVal,
+ CreatedByType: typeVal,
+ state: attr.ValueStateKnown,
+ }, diags
+}
+
+func NewCreatedByValueNull() CreatedByValue {
+ return CreatedByValue{
+ state: attr.ValueStateNull,
+ }
+}
+
+func NewCreatedByValueUnknown() CreatedByValue {
+ return CreatedByValue{
+ state: attr.ValueStateUnknown,
+ }
+}
+
+func NewCreatedByValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (CreatedByValue, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521
+ ctx := context.Background()
+
+ for name, attributeType := range attributeTypes {
+ attribute, ok := attributes[name]
+
+ if !ok {
+ diags.AddError(
+ "Missing CreatedByValue Attribute Value",
+ "While creating a CreatedByValue value, a missing attribute value was detected. "+
+ "A CreatedByValue must contain values for all attributes, even if null or unknown. "+
+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+
+ fmt.Sprintf("CreatedByValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()),
+ )
+
+ continue
+ }
+
+ if !attributeType.Equal(attribute.Type(ctx)) {
+ diags.AddError(
+ "Invalid CreatedByValue Attribute Type",
+ "While creating a CreatedByValue value, an invalid attribute value was detected. "+
+ "A CreatedByValue must use a matching attribute type for the value. "+
+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+
+ fmt.Sprintf("CreatedByValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+
+ fmt.Sprintf("CreatedByValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)),
+ )
+ }
+ }
+
+ for name := range attributes {
+ _, ok := attributeTypes[name]
+
+ if !ok {
+ diags.AddError(
+ "Extra CreatedByValue Attribute Value",
+ "While creating a CreatedByValue value, an extra attribute value was detected. "+
+ "A CreatedByValue must not contain values beyond the expected attribute types. "+
+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+
+ fmt.Sprintf("Extra CreatedByValue Attribute Name: %s", name),
+ )
+ }
+ }
+
+ if diags.HasError() {
+ return NewCreatedByValueUnknown(), diags
+ }
+
+ idAttribute, ok := attributes["id"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `id is missing from object`)
+
+ return NewCreatedByValueUnknown(), diags
+ }
+
+ idVal, ok := idAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`id expected to be basetypes.StringValue, was: %T`, idAttribute))
+ }
+
+ typeAttribute, ok := attributes["type"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `type is missing from object`)
+
+ return NewCreatedByValueUnknown(), diags
+ }
+
+ typeVal, ok := typeAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`type expected to be basetypes.StringValue, was: %T`, typeAttribute))
+ }
+
+ if diags.HasError() {
+ return NewCreatedByValueUnknown(), diags
+ }
+
+ return CreatedByValue{
+ Id: idVal,
+ CreatedByType: typeVal,
+ state: attr.ValueStateKnown,
+ }, diags
+}
+
+func NewCreatedByValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) CreatedByValue {
+ object, diags := NewCreatedByValue(attributeTypes, attributes)
+
+ if diags.HasError() {
+ // This could potentially be added to the diag package.
+ diagsStrings := make([]string, 0, len(diags))
+
+ for _, diagnostic := range diags {
+ diagsStrings = append(diagsStrings, fmt.Sprintf(
+ "%s | %s | %s",
+ diagnostic.Severity(),
+ diagnostic.Summary(),
+ diagnostic.Detail()))
+ }
+
+ panic("NewCreatedByValueMust received error(s): " + strings.Join(diagsStrings, "\n"))
+ }
+
+ return object
+}
+
+func (t CreatedByType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
+ if in.Type() == nil {
+ return NewCreatedByValueNull(), nil
+ }
+
+ if !in.Type().Equal(t.TerraformType(ctx)) {
+ return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type())
+ }
+
+ if !in.IsKnown() {
+ return NewCreatedByValueUnknown(), nil
+ }
+
+ if in.IsNull() {
+ return NewCreatedByValueNull(), nil
+ }
+
+ attributes := map[string]attr.Value{}
+
+ val := map[string]tftypes.Value{}
+
+ err := in.As(&val)
+
+ if err != nil {
+ return nil, err
+ }
+
+ for k, v := range val {
+ a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v)
+
+ if err != nil {
+ return nil, err
+ }
+
+ attributes[k] = a
+ }
+
+ return NewCreatedByValueMust(CreatedByValue{}.AttributeTypes(ctx), attributes), nil
+}
+
+func (t CreatedByType) ValueType(ctx context.Context) attr.Value {
+ return CreatedByValue{}
+}
+
+var _ basetypes.ObjectValuable = CreatedByValue{}
+
+type CreatedByValue struct {
+ Id basetypes.StringValue `tfsdk:"id"`
+ CreatedByType basetypes.StringValue `tfsdk:"type"`
+ state attr.ValueState
+}
+
+func (v CreatedByValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
+ attrTypes := make(map[string]tftypes.Type, 2)
+
+ var val tftypes.Value
+ var err error
+
+ attrTypes["id"] = basetypes.StringType{}.TerraformType(ctx)
+ attrTypes["type"] = basetypes.StringType{}.TerraformType(ctx)
+
+ objectType := tftypes.Object{AttributeTypes: attrTypes}
+
+ switch v.state {
+ case attr.ValueStateKnown:
+ vals := make(map[string]tftypes.Value, 2)
+
+ val, err = v.Id.ToTerraformValue(ctx)
+
+ if err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ vals["id"] = val
+
+ val, err = v.CreatedByType.ToTerraformValue(ctx)
+
+ if err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ vals["type"] = val
+
+ if err := tftypes.ValidateValue(objectType, vals); err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ return tftypes.NewValue(objectType, vals), nil
+ case attr.ValueStateNull:
+ return tftypes.NewValue(objectType, nil), nil
+ case attr.ValueStateUnknown:
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), nil
+ default:
+ panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state))
+ }
+}
+
+func (v CreatedByValue) IsNull() bool {
+ return v.state == attr.ValueStateNull
+}
+
+func (v CreatedByValue) IsUnknown() bool {
+ return v.state == attr.ValueStateUnknown
+}
+
+func (v CreatedByValue) String() string {
+ return "CreatedByValue"
+}
+
+func (v CreatedByValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ attributeTypes := map[string]attr.Type{
+ "id": basetypes.StringType{},
+ "type": basetypes.StringType{},
+ }
+
+ if v.IsNull() {
+ return types.ObjectNull(attributeTypes), diags
+ }
+
+ if v.IsUnknown() {
+ return types.ObjectUnknown(attributeTypes), diags
+ }
+
+ objVal, diags := types.ObjectValue(
+ attributeTypes,
+ map[string]attr.Value{
+ "id": v.Id,
+ "type": v.CreatedByType,
+ })
+
+ return objVal, diags
+}
+
+func (v CreatedByValue) Equal(o attr.Value) bool {
+ other, ok := o.(CreatedByValue)
+
+ if !ok {
+ return false
+ }
+
+ if v.state != other.state {
+ return false
+ }
+
+ if v.state != attr.ValueStateKnown {
+ return true
+ }
+
+ if !v.Id.Equal(other.Id) {
+ return false
+ }
+
+ if !v.CreatedByType.Equal(other.CreatedByType) {
+ return false
+ }
+
+ return true
+}
+
+func (v CreatedByValue) Type(ctx context.Context) attr.Type {
+ return CreatedByType{
+ basetypes.ObjectType{
+ AttrTypes: v.AttributeTypes(ctx),
+ },
+ }
+}
+
+func (v CreatedByValue) AttributeTypes(ctx context.Context) map[string]attr.Type {
+ return map[string]attr.Type{
+ "id": basetypes.StringType{},
+ "type": basetypes.StringType{},
+ }
+}
+
+var _ basetypes.ObjectTypable = TestsType{}
+
+type TestsType struct {
+ basetypes.ObjectType
+}
+
+func (t TestsType) Equal(o attr.Type) bool {
+ other, ok := o.(TestsType)
+
+ if !ok {
+ return false
+ }
+
+ return t.ObjectType.Equal(other.ObjectType)
+}
+
+func (t TestsType) String() string {
+ return "TestsType"
+}
+
+func (t TestsType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ attributes := in.Attributes()
+
+ expectedResultAttribute, ok := attributes["expected_result"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `expected_result is missing from object`)
+
+ return nil, diags
+ }
+
+ expectedResultVal, ok := expectedResultAttribute.(basetypes.BoolValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`expected_result expected to be basetypes.BoolValue, was: %T`, expectedResultAttribute))
+ }
+
+ mocksAttribute, ok := attributes["mocks"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `mocks is missing from object`)
+
+ return nil, diags
+ }
+
+ mocksVal, ok := mocksAttribute.(basetypes.ListValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`mocks expected to be basetypes.ListValue, was: %T`, mocksAttribute))
+ }
+
+ nameAttribute, ok := attributes["name"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `name is missing from object`)
+
+ return nil, diags
+ }
+
+ nameVal, ok := nameAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`name expected to be basetypes.StringValue, was: %T`, nameAttribute))
+ }
+
+ resourceAttribute, ok := attributes["resource"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `resource is missing from object`)
+
+ return nil, diags
+ }
+
+ resourceVal, ok := resourceAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`resource expected to be basetypes.StringValue, was: %T`, resourceAttribute))
+ }
+
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ return TestsValue{
+ ExpectedResult: expectedResultVal,
+ Mocks: mocksVal,
+ Name: nameVal,
+ Resource: resourceVal,
+ state: attr.ValueStateKnown,
+ }, diags
+}
+
+func NewTestsValueNull() TestsValue {
+ return TestsValue{
+ state: attr.ValueStateNull,
+ }
+}
+
+func NewTestsValueUnknown() TestsValue {
+ return TestsValue{
+ state: attr.ValueStateUnknown,
+ }
+}
+
+func NewTestsValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (TestsValue, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521
+ ctx := context.Background()
+
+ for name, attributeType := range attributeTypes {
+ attribute, ok := attributes[name]
+
+ if !ok {
+ diags.AddError(
+ "Missing TestsValue Attribute Value",
+ "While creating a TestsValue value, a missing attribute value was detected. "+
+ "A TestsValue must contain values for all attributes, even if null or unknown. "+
+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+
+ fmt.Sprintf("TestsValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()),
+ )
+
+ continue
+ }
+
+ if !attributeType.Equal(attribute.Type(ctx)) {
+ diags.AddError(
+ "Invalid TestsValue Attribute Type",
+ "While creating a TestsValue value, an invalid attribute value was detected. "+
+ "A TestsValue must use a matching attribute type for the value. "+
+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+
+ fmt.Sprintf("TestsValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+
+ fmt.Sprintf("TestsValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)),
+ )
+ }
+ }
+
+ for name := range attributes {
+ _, ok := attributeTypes[name]
+
+ if !ok {
+ diags.AddError(
+ "Extra TestsValue Attribute Value",
+ "While creating a TestsValue value, an extra attribute value was detected. "+
+ "A TestsValue must not contain values beyond the expected attribute types. "+
+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+
+ fmt.Sprintf("Extra TestsValue Attribute Name: %s", name),
+ )
+ }
+ }
+
+ if diags.HasError() {
+ return NewTestsValueUnknown(), diags
+ }
+
+ expectedResultAttribute, ok := attributes["expected_result"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `expected_result is missing from object`)
+
+ return NewTestsValueUnknown(), diags
+ }
+
+ expectedResultVal, ok := expectedResultAttribute.(basetypes.BoolValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`expected_result expected to be basetypes.BoolValue, was: %T`, expectedResultAttribute))
+ }
+
+ mocksAttribute, ok := attributes["mocks"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `mocks is missing from object`)
+
+ return NewTestsValueUnknown(), diags
+ }
+
+ mocksVal, ok := mocksAttribute.(basetypes.ListValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`mocks expected to be basetypes.ListValue, was: %T`, mocksAttribute))
+ }
+
+ nameAttribute, ok := attributes["name"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `name is missing from object`)
+
+ return NewTestsValueUnknown(), diags
+ }
+
+ nameVal, ok := nameAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`name expected to be basetypes.StringValue, was: %T`, nameAttribute))
+ }
+
+ resourceAttribute, ok := attributes["resource"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `resource is missing from object`)
+
+ return NewTestsValueUnknown(), diags
+ }
+
+ resourceVal, ok := resourceAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`resource expected to be basetypes.StringValue, was: %T`, resourceAttribute))
+ }
+
+ if diags.HasError() {
+ return NewTestsValueUnknown(), diags
+ }
+
+ return TestsValue{
+ ExpectedResult: expectedResultVal,
+ Mocks: mocksVal,
+ Name: nameVal,
+ Resource: resourceVal,
+ state: attr.ValueStateKnown,
+ }, diags
+}
+
+func NewTestsValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) TestsValue {
+ object, diags := NewTestsValue(attributeTypes, attributes)
+
+ if diags.HasError() {
+ // This could potentially be added to the diag package.
+ diagsStrings := make([]string, 0, len(diags))
+
+ for _, diagnostic := range diags {
+ diagsStrings = append(diagsStrings, fmt.Sprintf(
+ "%s | %s | %s",
+ diagnostic.Severity(),
+ diagnostic.Summary(),
+ diagnostic.Detail()))
+ }
+
+ panic("NewTestsValueMust received error(s): " + strings.Join(diagsStrings, "\n"))
+ }
+
+ return object
+}
+
+func (t TestsType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
+ if in.Type() == nil {
+ return NewTestsValueNull(), nil
+ }
+
+ if !in.Type().Equal(t.TerraformType(ctx)) {
+ return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type())
+ }
+
+ if !in.IsKnown() {
+ return NewTestsValueUnknown(), nil
+ }
+
+ if in.IsNull() {
+ return NewTestsValueNull(), nil
+ }
+
+ attributes := map[string]attr.Value{}
+
+ val := map[string]tftypes.Value{}
+
+ err := in.As(&val)
+
+ if err != nil {
+ return nil, err
+ }
+
+ for k, v := range val {
+ a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v)
+
+ if err != nil {
+ return nil, err
+ }
+
+ attributes[k] = a
+ }
+
+ return NewTestsValueMust(TestsValue{}.AttributeTypes(ctx), attributes), nil
+}
+
+func (t TestsType) ValueType(ctx context.Context) attr.Value {
+ return TestsValue{}
+}
+
+var _ basetypes.ObjectValuable = TestsValue{}
+
+type TestsValue struct {
+ ExpectedResult basetypes.BoolValue `tfsdk:"expected_result"`
+ Mocks basetypes.ListValue `tfsdk:"mocks"`
+ Name basetypes.StringValue `tfsdk:"name"`
+ Resource basetypes.StringValue `tfsdk:"resource"`
+ state attr.ValueState
+}
+
+func (v TestsValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
+ attrTypes := make(map[string]tftypes.Type, 4)
+
+ var val tftypes.Value
+ var err error
+
+ attrTypes["expected_result"] = basetypes.BoolType{}.TerraformType(ctx)
+ attrTypes["mocks"] = basetypes.ListType{
+ ElemType: types.MapType{
+ ElemType: types.StringType,
+ },
+ }.TerraformType(ctx)
+ attrTypes["name"] = basetypes.StringType{}.TerraformType(ctx)
+ attrTypes["resource"] = basetypes.StringType{}.TerraformType(ctx)
+
+ objectType := tftypes.Object{AttributeTypes: attrTypes}
+
+ switch v.state {
+ case attr.ValueStateKnown:
+ vals := make(map[string]tftypes.Value, 4)
+
+ val, err = v.ExpectedResult.ToTerraformValue(ctx)
+
+ if err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ vals["expected_result"] = val
+
+ val, err = v.Mocks.ToTerraformValue(ctx)
+
+ if err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ vals["mocks"] = val
+
+ val, err = v.Name.ToTerraformValue(ctx)
+
+ if err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ vals["name"] = val
+
+ val, err = v.Resource.ToTerraformValue(ctx)
+
+ if err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ vals["resource"] = val
+
+ if err := tftypes.ValidateValue(objectType, vals); err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ return tftypes.NewValue(objectType, vals), nil
+ case attr.ValueStateNull:
+ return tftypes.NewValue(objectType, nil), nil
+ case attr.ValueStateUnknown:
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), nil
+ default:
+ panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state))
+ }
+}
+
+func (v TestsValue) IsNull() bool {
+ return v.state == attr.ValueStateNull
+}
+
+func (v TestsValue) IsUnknown() bool {
+ return v.state == attr.ValueStateUnknown
+}
+
+func (v TestsValue) String() string {
+ return "TestsValue"
+}
+
+func (v TestsValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ var mocksVal basetypes.ListValue
+ switch {
+ case v.Mocks.IsUnknown():
+ mocksVal = types.ListUnknown(types.MapType{
+ ElemType: types.StringType,
+ })
+ case v.Mocks.IsNull():
+ mocksVal = types.ListNull(types.MapType{
+ ElemType: types.StringType,
+ })
+ default:
+ var d diag.Diagnostics
+ mocksVal, d = types.ListValue(types.MapType{
+ ElemType: types.StringType,
+ }, v.Mocks.Elements())
+ diags.Append(d...)
+ }
+
+ if diags.HasError() {
+ return types.ObjectUnknown(map[string]attr.Type{
+ "expected_result": basetypes.BoolType{},
+ "mocks": basetypes.ListType{
+ ElemType: types.MapType{
+ ElemType: types.StringType,
+ },
+ },
+ "name": basetypes.StringType{},
+ "resource": basetypes.StringType{},
+ }), diags
+ }
+
+ attributeTypes := map[string]attr.Type{
+ "expected_result": basetypes.BoolType{},
+ "mocks": basetypes.ListType{
+ ElemType: types.MapType{
+ ElemType: types.StringType,
+ },
+ },
+ "name": basetypes.StringType{},
+ "resource": basetypes.StringType{},
+ }
+
+ if v.IsNull() {
+ return types.ObjectNull(attributeTypes), diags
+ }
+
+ if v.IsUnknown() {
+ return types.ObjectUnknown(attributeTypes), diags
+ }
+
+ objVal, diags := types.ObjectValue(
+ attributeTypes,
+ map[string]attr.Value{
+ "expected_result": v.ExpectedResult,
+ "mocks": mocksVal,
+ "name": v.Name,
+ "resource": v.Resource,
+ })
+
+ return objVal, diags
+}
+
+func (v TestsValue) Equal(o attr.Value) bool {
+ other, ok := o.(TestsValue)
+
+ if !ok {
+ return false
+ }
+
+ if v.state != other.state {
+ return false
+ }
+
+ if v.state != attr.ValueStateKnown {
+ return true
+ }
+
+ if !v.ExpectedResult.Equal(other.ExpectedResult) {
+ return false
+ }
+
+ if !v.Mocks.Equal(other.Mocks) {
+ return false
+ }
+
+ if !v.Name.Equal(other.Name) {
+ return false
+ }
+
+ if !v.Resource.Equal(other.Resource) {
+ return false
+ }
+
+ return true
+}
+
+func (v TestsValue) Type(ctx context.Context) attr.Type {
+ return TestsType{
+ basetypes.ObjectType{
+ AttrTypes: v.AttributeTypes(ctx),
+ },
+ }
+}
+
+func (v TestsValue) AttributeTypes(ctx context.Context) map[string]attr.Type {
+ return map[string]attr.Type{
+ "expected_result": basetypes.BoolType{},
+ "mocks": basetypes.ListType{
+ ElemType: types.MapType{
+ ElemType: types.StringType,
+ },
+ },
+ "name": basetypes.StringType{},
+ "resource": basetypes.StringType{},
+ }
+}
diff --git a/internal/provider/resource_policy_test.go b/internal/provider/resource_policy_test.go
new file mode 100644
index 0000000..6220704
--- /dev/null
+++ b/internal/provider/resource_policy_test.go
@@ -0,0 +1,76 @@
+/*
+Copyright 2023 Panther Labs, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package provider
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ "github.com/google/uuid"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+)
+
+func TestPolicyResource(t *testing.T) {
+ policyName := strings.ReplaceAll(uuid.NewString(), "-", "")
+ policyUpdatedName := strings.ReplaceAll(uuid.NewString(), "-", "")
+
+ resource.Test(t, resource.TestCase{
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ // Create and Read testing
+ {
+ Config: providerConfig + testAccPolicyResourceConfig(policyName),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("panther_policy.test", "display_name", policyName),
+ resource.TestCheckResourceAttr("panther_policy.test", "enabled", "true"),
+ resource.TestCheckResourceAttr("panther_policy.test", "severity", "MEDIUM"),
+ resource.TestCheckResourceAttr("panther_policy.test", "resource_types.#", "1"),
+ resource.TestCheckResourceAttr("panther_policy.test", "resource_types.0", "AWS.S3.Bucket"),
+ resource.TestCheckResourceAttrSet("panther_policy.test", "id"),
+ ),
+ },
+ // ImportState testing
+ {
+ ResourceName: "panther_policy.test",
+ ImportState: true,
+ ImportStateVerify: true,
+ ImportStateVerifyIgnore: []string{"tags"},
+ },
+ // Update and Read testing
+ {
+ Config: providerConfig + testAccPolicyResourceConfig(policyUpdatedName),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("panther_policy.test", "display_name", policyUpdatedName),
+ ),
+ },
+ },
+ })
+}
+
+func testAccPolicyResourceConfig(name string) string {
+ return fmt.Sprintf(`
+resource "panther_policy" "test" {
+ display_name = %[1]q
+ body = "def policy(resource): return True"
+ enabled = true
+ resource_types = ["AWS.S3.Bucket"]
+ severity = "MEDIUM"
+ tags = ["test", "terraform"]
+}
+`, name)
+}
diff --git a/internal/provider/resource_rule.go b/internal/provider/resource_rule.go
new file mode 100644
index 0000000..45cc508
--- /dev/null
+++ b/internal/provider/resource_rule.go
@@ -0,0 +1,426 @@
+/*
+Copyright 2023 Panther Labs, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package provider
+
+import (
+ "context"
+ "fmt"
+ "terraform-provider-panther/internal/client"
+ "terraform-provider-panther/internal/client/panther"
+ "terraform-provider-panther/internal/provider/resource_rule"
+
+ "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/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+)
+
+var (
+ _ resource.Resource = (*ruleResource)(nil)
+ _ resource.ResourceWithConfigure = (*ruleResource)(nil)
+ _ resource.ResourceWithImportState = (*ruleResource)(nil)
+)
+
+func NewRuleResource() resource.Resource {
+ return &ruleResource{}
+}
+
+type ruleResource struct {
+ client client.RestClient
+}
+
+func (r *ruleResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_rule"
+}
+
+func (r *ruleResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+ // Use the generated schema
+ generatedSchema := resource_rule.RuleResourceSchema(ctx)
+
+ // Add the ID field with UseStateForUnknown as required by limitations
+ if generatedSchema.Attributes == nil {
+ generatedSchema.Attributes = make(map[string]schema.Attribute)
+ }
+
+ generatedSchema.Attributes["id"] = schema.StringAttribute{
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ }
+
+ resp.Schema = generatedSchema
+}
+
+func (r *ruleResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ apiClient, ok := req.ProviderData.(*panther.APIClient)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *panther.APIClient, got: %T", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = apiClient.RestClient
+}
+
+func (r *ruleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data resource_rule.RuleModel
+
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Convert from generated model to our client types
+ input := client.CreateRuleInput{
+ ID: data.DisplayName.ValueString(), // Using display_name as ID
+ RuleModifiableAttributes: client.RuleModifiableAttributes{
+ DisplayName: data.DisplayName.ValueString(),
+ Body: data.Body.ValueString(),
+ Description: data.Description.ValueString(),
+ Severity: data.Severity.ValueString(),
+ Enabled: data.Enabled.ValueBool(),
+ DedupPeriodMinutes: int(data.DedupPeriodMinutes.ValueInt64()),
+ Runbook: data.Runbook.ValueString(),
+ },
+ }
+
+ // Convert log types
+ if !data.LogTypes.IsNull() && !data.LogTypes.IsUnknown() {
+ logTypes := make([]string, 0, len(data.LogTypes.Elements()))
+ for _, elem := range data.LogTypes.Elements() {
+ if strVal, ok := elem.(types.String); ok {
+ logTypes = append(logTypes, strVal.ValueString())
+ }
+ }
+ input.LogTypes = logTypes
+ }
+
+ // Convert tags
+ if !data.Tags.IsNull() && !data.Tags.IsUnknown() {
+ tags := make([]string, 0, len(data.Tags.Elements()))
+ for _, elem := range data.Tags.Elements() {
+ if strVal, ok := elem.(types.String); ok {
+ tags = append(tags, strVal.ValueString())
+ }
+ }
+ input.Tags = tags
+ }
+
+ result, err := r.client.CreateRule(ctx, input)
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create rule, got error: %s", err))
+ return
+ }
+
+ // Update the model with the result - populate all fields including computed ones
+ data.Id = types.StringValue(result.ID)
+ data.DisplayName = types.StringValue(result.DisplayName)
+ data.Body = types.StringValue(result.Body)
+ data.Description = types.StringValue(result.Description)
+ data.Severity = types.StringValue(result.Severity)
+ data.Enabled = types.BoolValue(result.Enabled)
+ data.DedupPeriodMinutes = types.Int64Value(int64(result.DedupPeriodMinutes))
+ data.Runbook = types.StringValue(result.Runbook)
+ data.CreatedAt = types.StringValue(result.CreatedAt)
+ data.LastModified = types.StringValue(result.UpdatedAt)
+
+ // Convert log types back to list
+ if len(result.LogTypes) > 0 {
+ elements := make([]types.String, len(result.LogTypes))
+ for i, logType := range result.LogTypes {
+ elements[i] = types.StringValue(logType)
+ }
+ logTypesList, diags := types.ListValueFrom(ctx, types.StringType, elements)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ data.LogTypes = logTypesList
+ } else {
+ data.LogTypes = types.ListNull(types.StringType)
+ }
+
+ // Preserve original tag order from the plan (Terraform expects consistent ordering)
+ // The data.Tags already has the correct values from the plan, so we don't need to overwrite it
+
+ // Set other computed fields to null/empty for now since they're not returned by the API
+ data.CreatedBy = resource_rule.NewCreatedByValueNull()
+ data.CreatedByExternal = types.StringNull()
+ data.InlineFilters = types.StringNull()
+ data.Managed = types.BoolNull()
+ data.OutputIds = types.ListNull(types.StringType)
+ data.Reports = types.MapNull(types.ListType{ElemType: types.StringType})
+ data.SummaryAttributes = types.ListNull(types.StringType)
+ data.Tests = types.ListNull(resource_rule.TestsType{
+ ObjectType: types.ObjectType{
+ AttrTypes: resource_rule.TestsValue{}.AttributeTypes(ctx),
+ },
+ })
+ data.Threshold = types.Int64Value(1) // Use default value
+
+ tflog.Debug(ctx, "Created Rule", map[string]any{
+ "id": result.ID,
+ })
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ruleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data resource_rule.RuleModel
+
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Use ID if available, otherwise fall back to DisplayName for backward compatibility
+ ruleID := data.Id.ValueString()
+ if ruleID == "" {
+ ruleID = data.DisplayName.ValueString()
+ }
+ rule, err := r.client.GetRule(ctx, ruleID)
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read rule, got error: %s", err))
+ return
+ }
+
+ // Update model with API response - populate all fields including computed ones
+ data.Id = types.StringValue(rule.ID)
+ data.DisplayName = types.StringValue(rule.DisplayName)
+ data.Body = types.StringValue(rule.Body)
+ data.Description = types.StringValue(rule.Description)
+ data.Severity = types.StringValue(rule.Severity)
+ data.Enabled = types.BoolValue(rule.Enabled)
+ data.DedupPeriodMinutes = types.Int64Value(int64(rule.DedupPeriodMinutes))
+ data.Runbook = types.StringValue(rule.Runbook)
+ data.CreatedAt = types.StringValue(rule.CreatedAt)
+ data.LastModified = types.StringValue(rule.UpdatedAt)
+
+ // Convert log types back to list
+ if len(rule.LogTypes) > 0 {
+ elements := make([]types.String, len(rule.LogTypes))
+ for i, logType := range rule.LogTypes {
+ elements[i] = types.StringValue(logType)
+ }
+ logTypesList, diags := types.ListValueFrom(ctx, types.StringType, elements)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ data.LogTypes = logTypesList
+ } else {
+ data.LogTypes = types.ListNull(types.StringType)
+ }
+
+ // Only update tags if they have actually changed (content-wise, ignoring order)
+ // to preserve the order from the original plan/state
+ if len(rule.Tags) > 0 {
+ // Get current tags from state
+ currentTags := make([]string, 0)
+ if !data.Tags.IsNull() && !data.Tags.IsUnknown() {
+ for _, elem := range data.Tags.Elements() {
+ if strVal, ok := elem.(types.String); ok {
+ currentTags = append(currentTags, strVal.ValueString())
+ }
+ }
+ }
+
+ // Check if tag content has changed (ignore order)
+ tagsChanged := len(currentTags) != len(rule.Tags)
+ if !tagsChanged {
+ apiTagsMap := make(map[string]bool)
+ for _, tag := range rule.Tags {
+ apiTagsMap[tag] = true
+ }
+ for _, tag := range currentTags {
+ if !apiTagsMap[tag] {
+ tagsChanged = true
+ break
+ }
+ }
+ }
+
+ // Only update tags if content changed, preserving existing order
+ if tagsChanged {
+ elements := make([]types.String, len(rule.Tags))
+ for i, tag := range rule.Tags {
+ elements[i] = types.StringValue(tag)
+ }
+ tagsList, diags := types.ListValueFrom(ctx, types.StringType, elements)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ data.Tags = tagsList
+ }
+ } else if !data.Tags.IsNull() {
+ // API returned no tags but state has tags - clear them
+ data.Tags = types.ListNull(types.StringType)
+ }
+
+ // Set other computed fields to null/empty for now since they're not returned by the API
+ data.CreatedBy = resource_rule.NewCreatedByValueNull()
+ data.CreatedByExternal = types.StringNull()
+ data.InlineFilters = types.StringNull()
+ data.Managed = types.BoolNull()
+ data.OutputIds = types.ListNull(types.StringType)
+ data.Reports = types.MapNull(types.ListType{ElemType: types.StringType})
+ data.SummaryAttributes = types.ListNull(types.StringType)
+ data.Tests = types.ListNull(resource_rule.TestsType{
+ ObjectType: types.ObjectType{
+ AttrTypes: resource_rule.TestsValue{}.AttributeTypes(ctx),
+ },
+ })
+ data.Threshold = types.Int64Value(1) // Use default value
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ruleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var data resource_rule.RuleModel
+
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ input := client.UpdateRuleInput{
+ ID: data.Id.ValueString(),
+ RuleModifiableAttributes: client.RuleModifiableAttributes{
+ DisplayName: data.DisplayName.ValueString(),
+ Body: data.Body.ValueString(),
+ Description: data.Description.ValueString(),
+ Severity: data.Severity.ValueString(),
+ Enabled: data.Enabled.ValueBool(),
+ DedupPeriodMinutes: int(data.DedupPeriodMinutes.ValueInt64()),
+ Runbook: data.Runbook.ValueString(),
+ },
+ }
+
+ // Convert log types
+ if !data.LogTypes.IsNull() && !data.LogTypes.IsUnknown() {
+ logTypes := make([]string, 0, len(data.LogTypes.Elements()))
+ for _, elem := range data.LogTypes.Elements() {
+ if strVal, ok := elem.(types.String); ok {
+ logTypes = append(logTypes, strVal.ValueString())
+ }
+ }
+ input.LogTypes = logTypes
+ }
+
+ // Convert tags
+ if !data.Tags.IsNull() && !data.Tags.IsUnknown() {
+ tags := make([]string, 0, len(data.Tags.Elements()))
+ for _, elem := range data.Tags.Elements() {
+ if strVal, ok := elem.(types.String); ok {
+ tags = append(tags, strVal.ValueString())
+ }
+ }
+ input.Tags = tags
+ }
+
+ result, err := r.client.UpdateRule(ctx, input)
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update rule, got error: %s", err))
+ return
+ }
+
+ // Update the model with the result - populate all fields including computed ones
+ data.Id = types.StringValue(result.ID)
+ data.DisplayName = types.StringValue(result.DisplayName)
+ data.Body = types.StringValue(result.Body)
+ data.Description = types.StringValue(result.Description)
+ data.Severity = types.StringValue(result.Severity)
+ data.Enabled = types.BoolValue(result.Enabled)
+ data.DedupPeriodMinutes = types.Int64Value(int64(result.DedupPeriodMinutes))
+ data.Runbook = types.StringValue(result.Runbook)
+ data.CreatedAt = types.StringValue(result.CreatedAt)
+ data.LastModified = types.StringValue(result.UpdatedAt)
+
+ // Convert log types back to list
+ if len(result.LogTypes) > 0 {
+ elements := make([]types.String, len(result.LogTypes))
+ for i, logType := range result.LogTypes {
+ elements[i] = types.StringValue(logType)
+ }
+ logTypesList, diags := types.ListValueFrom(ctx, types.StringType, elements)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ data.LogTypes = logTypesList
+ } else {
+ data.LogTypes = types.ListNull(types.StringType)
+ }
+
+ // Preserve original tag order from the plan (Terraform expects consistent ordering)
+ // The data.Tags already has the correct values from the plan, so we don't need to overwrite it
+
+ // Set other computed fields to null/empty for now since they're not returned by the API
+ data.CreatedBy = resource_rule.NewCreatedByValueNull()
+ data.CreatedByExternal = types.StringNull()
+ data.InlineFilters = types.StringNull()
+ data.Managed = types.BoolNull()
+ data.OutputIds = types.ListNull(types.StringType)
+ data.Reports = types.MapNull(types.ListType{ElemType: types.StringType})
+ data.SummaryAttributes = types.ListNull(types.StringType)
+ data.Tests = types.ListNull(resource_rule.TestsType{
+ ObjectType: types.ObjectType{
+ AttrTypes: resource_rule.TestsValue{}.AttributeTypes(ctx),
+ },
+ })
+ data.Threshold = types.Int64Value(1) // Use default value
+
+ tflog.Debug(ctx, "Updated Rule", map[string]any{
+ "id": result.ID,
+ })
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ruleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var data resource_rule.RuleModel
+
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ err := r.client.DeleteRule(ctx, data.Id.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete rule, got error: %s", err))
+ return
+ }
+
+ tflog.Debug(ctx, "Deleted Rule", map[string]any{
+ "id": data.Id.ValueString(),
+ })
+}
+
+func (r *ruleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
\ No newline at end of file
diff --git a/internal/provider/resource_rule/rule_resource_gen.go b/internal/provider/resource_rule/rule_resource_gen.go
new file mode 100644
index 0000000..498ade4
--- /dev/null
+++ b/internal/provider/resource_rule/rule_resource_gen.go
@@ -0,0 +1,1140 @@
+// Code generated by terraform-plugin-framework-generator DO NOT EDIT.
+
+package resource_rule
+
+import (
+ "context"
+ "fmt"
+ "github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+ "github.com/hashicorp/terraform-plugin-go/tftypes"
+ "strings"
+
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+)
+
+func RuleResourceSchema(ctx context.Context) schema.Schema {
+ return schema.Schema{
+ Attributes: map[string]schema.Attribute{
+ "body": schema.StringAttribute{
+ Required: true,
+ Description: "The python body of the rule",
+ MarkdownDescription: "The python body of the rule",
+ },
+ "created_at": schema.StringAttribute{
+ Computed: true,
+ },
+ "created_by": schema.SingleNestedAttribute{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Computed: true,
+ },
+ "type": schema.StringAttribute{
+ Computed: true,
+ },
+ },
+ CustomType: CreatedByType{
+ ObjectType: types.ObjectType{
+ AttrTypes: CreatedByValue{}.AttributeTypes(ctx),
+ },
+ },
+ Computed: true,
+ Description: "The actor who created the rule",
+ MarkdownDescription: "The actor who created the rule",
+ },
+ "created_by_external": schema.StringAttribute{
+ Computed: true,
+ Description: "The text of the user-provided CreatedBy field when uploaded via CI/CD",
+ MarkdownDescription: "The text of the user-provided CreatedBy field when uploaded via CI/CD",
+ },
+ "dedup_period_minutes": schema.Int64Attribute{
+ Optional: true,
+ Computed: true,
+ Description: "The amount of time in minutes for grouping alerts",
+ MarkdownDescription: "The amount of time in minutes for grouping alerts",
+ Validators: []validator.Int64{
+ int64validator.AtLeast(1),
+ },
+ Default: int64default.StaticInt64(60),
+ },
+ "description": schema.StringAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "The description of the rule",
+ MarkdownDescription: "The description of the rule",
+ },
+ "display_name": schema.StringAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "The display name of the rule",
+ MarkdownDescription: "The display name of the rule",
+ },
+ "enabled": schema.BoolAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "Determines whether or not the rule is active",
+ MarkdownDescription: "Determines whether or not the rule is active",
+ },
+ "inline_filters": schema.StringAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "The filter for the rule represented in YAML",
+ MarkdownDescription: "The filter for the rule represented in YAML",
+ },
+ "last_modified": schema.StringAttribute{
+ Computed: true,
+ },
+ "log_types": schema.ListAttribute{
+ ElementType: types.StringType,
+ Optional: true,
+ Computed: true,
+ Description: "log types",
+ MarkdownDescription: "log types",
+ },
+ "managed": schema.BoolAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "Determines if the rule is managed by panther",
+ MarkdownDescription: "Determines if the rule is managed by panther",
+ },
+ "output_ids": schema.ListAttribute{
+ ElementType: types.StringType,
+ Optional: true,
+ Computed: true,
+ Description: "Destination IDs that override default alert routing based on severity",
+ MarkdownDescription: "Destination IDs that override default alert routing based on severity",
+ },
+ "reports": schema.MapAttribute{
+ ElementType: types.ListType{
+ ElemType: types.StringType,
+ },
+ Optional: true,
+ Computed: true,
+ Description: "reports",
+ MarkdownDescription: "reports",
+ },
+ "runbook": schema.StringAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "How to handle the generated alert",
+ MarkdownDescription: "How to handle the generated alert",
+ },
+ "severity": schema.StringAttribute{
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf(
+ "INFO",
+ "LOW",
+ "MEDIUM",
+ "HIGH",
+ "CRITICAL",
+ ),
+ },
+ },
+ "summary_attributes": schema.ListAttribute{
+ ElementType: types.StringType,
+ Optional: true,
+ Computed: true,
+ Description: "A list of fields in the event to create top 5 summaries for",
+ MarkdownDescription: "A list of fields in the event to create top 5 summaries for",
+ },
+ "tags": schema.ListAttribute{
+ ElementType: types.StringType,
+ Optional: true,
+ Computed: true,
+ Description: "The tags for the rule",
+ MarkdownDescription: "The tags for the rule",
+ },
+ "tests": schema.ListNestedAttribute{
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "expected_result": schema.BoolAttribute{
+ Required: true,
+ Description: "The expected result",
+ MarkdownDescription: "The expected result",
+ },
+ "mocks": schema.ListAttribute{
+ ElementType: types.MapType{
+ ElemType: types.StringType,
+ },
+ Optional: true,
+ Computed: true,
+ Description: "mocks",
+ MarkdownDescription: "mocks",
+ },
+ "name": schema.StringAttribute{
+ Required: true,
+ Description: "name",
+ MarkdownDescription: "name",
+ },
+ "resource": schema.StringAttribute{
+ Required: true,
+ Description: "resource",
+ MarkdownDescription: "resource",
+ },
+ },
+ CustomType: TestsType{
+ ObjectType: types.ObjectType{
+ AttrTypes: TestsValue{}.AttributeTypes(ctx),
+ },
+ },
+ },
+ Optional: true,
+ Computed: true,
+ Description: "Unit tests for the Rule. Best practice is to include a positive and negative case",
+ MarkdownDescription: "Unit tests for the Rule. Best practice is to include a positive and negative case",
+ },
+ "threshold": schema.Int64Attribute{
+ Optional: true,
+ Computed: true,
+ Description: "the number of events that must match before an alert is triggered",
+ MarkdownDescription: "the number of events that must match before an alert is triggered",
+ Validators: []validator.Int64{
+ int64validator.AtLeast(1),
+ },
+ Default: int64default.StaticInt64(1),
+ },
+ },
+ }
+}
+
+type RuleModel struct {
+ Id types.String `tfsdk:"id"`
+ Body types.String `tfsdk:"body"`
+ CreatedAt types.String `tfsdk:"created_at"`
+ CreatedBy CreatedByValue `tfsdk:"created_by"`
+ CreatedByExternal types.String `tfsdk:"created_by_external"`
+ DedupPeriodMinutes types.Int64 `tfsdk:"dedup_period_minutes"`
+ Description types.String `tfsdk:"description"`
+ DisplayName types.String `tfsdk:"display_name"`
+ Enabled types.Bool `tfsdk:"enabled"`
+ InlineFilters types.String `tfsdk:"inline_filters"`
+ LastModified types.String `tfsdk:"last_modified"`
+ LogTypes types.List `tfsdk:"log_types"`
+ Managed types.Bool `tfsdk:"managed"`
+ OutputIds types.List `tfsdk:"output_ids"`
+ Reports types.Map `tfsdk:"reports"`
+ Runbook types.String `tfsdk:"runbook"`
+ Severity types.String `tfsdk:"severity"`
+ SummaryAttributes types.List `tfsdk:"summary_attributes"`
+ Tags types.List `tfsdk:"tags"`
+ Tests types.List `tfsdk:"tests"`
+ Threshold types.Int64 `tfsdk:"threshold"`
+}
+
+var _ basetypes.ObjectTypable = CreatedByType{}
+
+type CreatedByType struct {
+ basetypes.ObjectType
+}
+
+func (t CreatedByType) Equal(o attr.Type) bool {
+ other, ok := o.(CreatedByType)
+
+ if !ok {
+ return false
+ }
+
+ return t.ObjectType.Equal(other.ObjectType)
+}
+
+func (t CreatedByType) String() string {
+ return "CreatedByType"
+}
+
+func (t CreatedByType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ attributes := in.Attributes()
+
+ idAttribute, ok := attributes["id"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `id is missing from object`)
+
+ return nil, diags
+ }
+
+ idVal, ok := idAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`id expected to be basetypes.StringValue, was: %T`, idAttribute))
+ }
+
+ typeAttribute, ok := attributes["type"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `type is missing from object`)
+
+ return nil, diags
+ }
+
+ typeVal, ok := typeAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`type expected to be basetypes.StringValue, was: %T`, typeAttribute))
+ }
+
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ return CreatedByValue{
+ Id: idVal,
+ CreatedByType: typeVal,
+ state: attr.ValueStateKnown,
+ }, diags
+}
+
+func NewCreatedByValueNull() CreatedByValue {
+ return CreatedByValue{
+ state: attr.ValueStateNull,
+ }
+}
+
+func NewCreatedByValueUnknown() CreatedByValue {
+ return CreatedByValue{
+ state: attr.ValueStateUnknown,
+ }
+}
+
+func NewCreatedByValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (CreatedByValue, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521
+ ctx := context.Background()
+
+ for name, attributeType := range attributeTypes {
+ attribute, ok := attributes[name]
+
+ if !ok {
+ diags.AddError(
+ "Missing CreatedByValue Attribute Value",
+ "While creating a CreatedByValue value, a missing attribute value was detected. "+
+ "A CreatedByValue must contain values for all attributes, even if null or unknown. "+
+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+
+ fmt.Sprintf("CreatedByValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()),
+ )
+
+ continue
+ }
+
+ if !attributeType.Equal(attribute.Type(ctx)) {
+ diags.AddError(
+ "Invalid CreatedByValue Attribute Type",
+ "While creating a CreatedByValue value, an invalid attribute value was detected. "+
+ "A CreatedByValue must use a matching attribute type for the value. "+
+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+
+ fmt.Sprintf("CreatedByValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+
+ fmt.Sprintf("CreatedByValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)),
+ )
+ }
+ }
+
+ for name := range attributes {
+ _, ok := attributeTypes[name]
+
+ if !ok {
+ diags.AddError(
+ "Extra CreatedByValue Attribute Value",
+ "While creating a CreatedByValue value, an extra attribute value was detected. "+
+ "A CreatedByValue must not contain values beyond the expected attribute types. "+
+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+
+ fmt.Sprintf("Extra CreatedByValue Attribute Name: %s", name),
+ )
+ }
+ }
+
+ if diags.HasError() {
+ return NewCreatedByValueUnknown(), diags
+ }
+
+ idAttribute, ok := attributes["id"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `id is missing from object`)
+
+ return NewCreatedByValueUnknown(), diags
+ }
+
+ idVal, ok := idAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`id expected to be basetypes.StringValue, was: %T`, idAttribute))
+ }
+
+ typeAttribute, ok := attributes["type"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `type is missing from object`)
+
+ return NewCreatedByValueUnknown(), diags
+ }
+
+ typeVal, ok := typeAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`type expected to be basetypes.StringValue, was: %T`, typeAttribute))
+ }
+
+ if diags.HasError() {
+ return NewCreatedByValueUnknown(), diags
+ }
+
+ return CreatedByValue{
+ Id: idVal,
+ CreatedByType: typeVal,
+ state: attr.ValueStateKnown,
+ }, diags
+}
+
+func NewCreatedByValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) CreatedByValue {
+ object, diags := NewCreatedByValue(attributeTypes, attributes)
+
+ if diags.HasError() {
+ // This could potentially be added to the diag package.
+ diagsStrings := make([]string, 0, len(diags))
+
+ for _, diagnostic := range diags {
+ diagsStrings = append(diagsStrings, fmt.Sprintf(
+ "%s | %s | %s",
+ diagnostic.Severity(),
+ diagnostic.Summary(),
+ diagnostic.Detail()))
+ }
+
+ panic("NewCreatedByValueMust received error(s): " + strings.Join(diagsStrings, "\n"))
+ }
+
+ return object
+}
+
+func (t CreatedByType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
+ if in.Type() == nil {
+ return NewCreatedByValueNull(), nil
+ }
+
+ if !in.Type().Equal(t.TerraformType(ctx)) {
+ return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type())
+ }
+
+ if !in.IsKnown() {
+ return NewCreatedByValueUnknown(), nil
+ }
+
+ if in.IsNull() {
+ return NewCreatedByValueNull(), nil
+ }
+
+ attributes := map[string]attr.Value{}
+
+ val := map[string]tftypes.Value{}
+
+ err := in.As(&val)
+
+ if err != nil {
+ return nil, err
+ }
+
+ for k, v := range val {
+ a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v)
+
+ if err != nil {
+ return nil, err
+ }
+
+ attributes[k] = a
+ }
+
+ return NewCreatedByValueMust(CreatedByValue{}.AttributeTypes(ctx), attributes), nil
+}
+
+func (t CreatedByType) ValueType(ctx context.Context) attr.Value {
+ return CreatedByValue{}
+}
+
+var _ basetypes.ObjectValuable = CreatedByValue{}
+
+type CreatedByValue struct {
+ Id basetypes.StringValue `tfsdk:"id"`
+ CreatedByType basetypes.StringValue `tfsdk:"type"`
+ state attr.ValueState
+}
+
+func (v CreatedByValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
+ attrTypes := make(map[string]tftypes.Type, 2)
+
+ var val tftypes.Value
+ var err error
+
+ attrTypes["id"] = basetypes.StringType{}.TerraformType(ctx)
+ attrTypes["type"] = basetypes.StringType{}.TerraformType(ctx)
+
+ objectType := tftypes.Object{AttributeTypes: attrTypes}
+
+ switch v.state {
+ case attr.ValueStateKnown:
+ vals := make(map[string]tftypes.Value, 2)
+
+ val, err = v.Id.ToTerraformValue(ctx)
+
+ if err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ vals["id"] = val
+
+ val, err = v.CreatedByType.ToTerraformValue(ctx)
+
+ if err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ vals["type"] = val
+
+ if err := tftypes.ValidateValue(objectType, vals); err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ return tftypes.NewValue(objectType, vals), nil
+ case attr.ValueStateNull:
+ return tftypes.NewValue(objectType, nil), nil
+ case attr.ValueStateUnknown:
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), nil
+ default:
+ panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state))
+ }
+}
+
+func (v CreatedByValue) IsNull() bool {
+ return v.state == attr.ValueStateNull
+}
+
+func (v CreatedByValue) IsUnknown() bool {
+ return v.state == attr.ValueStateUnknown
+}
+
+func (v CreatedByValue) String() string {
+ return "CreatedByValue"
+}
+
+func (v CreatedByValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ attributeTypes := map[string]attr.Type{
+ "id": basetypes.StringType{},
+ "type": basetypes.StringType{},
+ }
+
+ if v.IsNull() {
+ return types.ObjectNull(attributeTypes), diags
+ }
+
+ if v.IsUnknown() {
+ return types.ObjectUnknown(attributeTypes), diags
+ }
+
+ objVal, diags := types.ObjectValue(
+ attributeTypes,
+ map[string]attr.Value{
+ "id": v.Id,
+ "type": v.CreatedByType,
+ })
+
+ return objVal, diags
+}
+
+func (v CreatedByValue) Equal(o attr.Value) bool {
+ other, ok := o.(CreatedByValue)
+
+ if !ok {
+ return false
+ }
+
+ if v.state != other.state {
+ return false
+ }
+
+ if v.state != attr.ValueStateKnown {
+ return true
+ }
+
+ if !v.Id.Equal(other.Id) {
+ return false
+ }
+
+ if !v.CreatedByType.Equal(other.CreatedByType) {
+ return false
+ }
+
+ return true
+}
+
+func (v CreatedByValue) Type(ctx context.Context) attr.Type {
+ return CreatedByType{
+ basetypes.ObjectType{
+ AttrTypes: v.AttributeTypes(ctx),
+ },
+ }
+}
+
+func (v CreatedByValue) AttributeTypes(ctx context.Context) map[string]attr.Type {
+ return map[string]attr.Type{
+ "id": basetypes.StringType{},
+ "type": basetypes.StringType{},
+ }
+}
+
+var _ basetypes.ObjectTypable = TestsType{}
+
+type TestsType struct {
+ basetypes.ObjectType
+}
+
+func (t TestsType) Equal(o attr.Type) bool {
+ other, ok := o.(TestsType)
+
+ if !ok {
+ return false
+ }
+
+ return t.ObjectType.Equal(other.ObjectType)
+}
+
+func (t TestsType) String() string {
+ return "TestsType"
+}
+
+func (t TestsType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ attributes := in.Attributes()
+
+ expectedResultAttribute, ok := attributes["expected_result"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `expected_result is missing from object`)
+
+ return nil, diags
+ }
+
+ expectedResultVal, ok := expectedResultAttribute.(basetypes.BoolValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`expected_result expected to be basetypes.BoolValue, was: %T`, expectedResultAttribute))
+ }
+
+ mocksAttribute, ok := attributes["mocks"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `mocks is missing from object`)
+
+ return nil, diags
+ }
+
+ mocksVal, ok := mocksAttribute.(basetypes.ListValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`mocks expected to be basetypes.ListValue, was: %T`, mocksAttribute))
+ }
+
+ nameAttribute, ok := attributes["name"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `name is missing from object`)
+
+ return nil, diags
+ }
+
+ nameVal, ok := nameAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`name expected to be basetypes.StringValue, was: %T`, nameAttribute))
+ }
+
+ resourceAttribute, ok := attributes["resource"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `resource is missing from object`)
+
+ return nil, diags
+ }
+
+ resourceVal, ok := resourceAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`resource expected to be basetypes.StringValue, was: %T`, resourceAttribute))
+ }
+
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ return TestsValue{
+ ExpectedResult: expectedResultVal,
+ Mocks: mocksVal,
+ Name: nameVal,
+ Resource: resourceVal,
+ state: attr.ValueStateKnown,
+ }, diags
+}
+
+func NewTestsValueNull() TestsValue {
+ return TestsValue{
+ state: attr.ValueStateNull,
+ }
+}
+
+func NewTestsValueUnknown() TestsValue {
+ return TestsValue{
+ state: attr.ValueStateUnknown,
+ }
+}
+
+func NewTestsValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (TestsValue, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521
+ ctx := context.Background()
+
+ for name, attributeType := range attributeTypes {
+ attribute, ok := attributes[name]
+
+ if !ok {
+ diags.AddError(
+ "Missing TestsValue Attribute Value",
+ "While creating a TestsValue value, a missing attribute value was detected. "+
+ "A TestsValue must contain values for all attributes, even if null or unknown. "+
+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+
+ fmt.Sprintf("TestsValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()),
+ )
+
+ continue
+ }
+
+ if !attributeType.Equal(attribute.Type(ctx)) {
+ diags.AddError(
+ "Invalid TestsValue Attribute Type",
+ "While creating a TestsValue value, an invalid attribute value was detected. "+
+ "A TestsValue must use a matching attribute type for the value. "+
+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+
+ fmt.Sprintf("TestsValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+
+ fmt.Sprintf("TestsValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)),
+ )
+ }
+ }
+
+ for name := range attributes {
+ _, ok := attributeTypes[name]
+
+ if !ok {
+ diags.AddError(
+ "Extra TestsValue Attribute Value",
+ "While creating a TestsValue value, an extra attribute value was detected. "+
+ "A TestsValue must not contain values beyond the expected attribute types. "+
+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+
+ fmt.Sprintf("Extra TestsValue Attribute Name: %s", name),
+ )
+ }
+ }
+
+ if diags.HasError() {
+ return NewTestsValueUnknown(), diags
+ }
+
+ expectedResultAttribute, ok := attributes["expected_result"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `expected_result is missing from object`)
+
+ return NewTestsValueUnknown(), diags
+ }
+
+ expectedResultVal, ok := expectedResultAttribute.(basetypes.BoolValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`expected_result expected to be basetypes.BoolValue, was: %T`, expectedResultAttribute))
+ }
+
+ mocksAttribute, ok := attributes["mocks"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `mocks is missing from object`)
+
+ return NewTestsValueUnknown(), diags
+ }
+
+ mocksVal, ok := mocksAttribute.(basetypes.ListValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`mocks expected to be basetypes.ListValue, was: %T`, mocksAttribute))
+ }
+
+ nameAttribute, ok := attributes["name"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `name is missing from object`)
+
+ return NewTestsValueUnknown(), diags
+ }
+
+ nameVal, ok := nameAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`name expected to be basetypes.StringValue, was: %T`, nameAttribute))
+ }
+
+ resourceAttribute, ok := attributes["resource"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `resource is missing from object`)
+
+ return NewTestsValueUnknown(), diags
+ }
+
+ resourceVal, ok := resourceAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`resource expected to be basetypes.StringValue, was: %T`, resourceAttribute))
+ }
+
+ if diags.HasError() {
+ return NewTestsValueUnknown(), diags
+ }
+
+ return TestsValue{
+ ExpectedResult: expectedResultVal,
+ Mocks: mocksVal,
+ Name: nameVal,
+ Resource: resourceVal,
+ state: attr.ValueStateKnown,
+ }, diags
+}
+
+func NewTestsValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) TestsValue {
+ object, diags := NewTestsValue(attributeTypes, attributes)
+
+ if diags.HasError() {
+ // This could potentially be added to the diag package.
+ diagsStrings := make([]string, 0, len(diags))
+
+ for _, diagnostic := range diags {
+ diagsStrings = append(diagsStrings, fmt.Sprintf(
+ "%s | %s | %s",
+ diagnostic.Severity(),
+ diagnostic.Summary(),
+ diagnostic.Detail()))
+ }
+
+ panic("NewTestsValueMust received error(s): " + strings.Join(diagsStrings, "\n"))
+ }
+
+ return object
+}
+
+func (t TestsType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
+ if in.Type() == nil {
+ return NewTestsValueNull(), nil
+ }
+
+ if !in.Type().Equal(t.TerraformType(ctx)) {
+ return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type())
+ }
+
+ if !in.IsKnown() {
+ return NewTestsValueUnknown(), nil
+ }
+
+ if in.IsNull() {
+ return NewTestsValueNull(), nil
+ }
+
+ attributes := map[string]attr.Value{}
+
+ val := map[string]tftypes.Value{}
+
+ err := in.As(&val)
+
+ if err != nil {
+ return nil, err
+ }
+
+ for k, v := range val {
+ a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v)
+
+ if err != nil {
+ return nil, err
+ }
+
+ attributes[k] = a
+ }
+
+ return NewTestsValueMust(TestsValue{}.AttributeTypes(ctx), attributes), nil
+}
+
+func (t TestsType) ValueType(ctx context.Context) attr.Value {
+ return TestsValue{}
+}
+
+var _ basetypes.ObjectValuable = TestsValue{}
+
+type TestsValue struct {
+ ExpectedResult basetypes.BoolValue `tfsdk:"expected_result"`
+ Mocks basetypes.ListValue `tfsdk:"mocks"`
+ Name basetypes.StringValue `tfsdk:"name"`
+ Resource basetypes.StringValue `tfsdk:"resource"`
+ state attr.ValueState
+}
+
+func (v TestsValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
+ attrTypes := make(map[string]tftypes.Type, 4)
+
+ var val tftypes.Value
+ var err error
+
+ attrTypes["expected_result"] = basetypes.BoolType{}.TerraformType(ctx)
+ attrTypes["mocks"] = basetypes.ListType{
+ ElemType: types.MapType{
+ ElemType: types.StringType,
+ },
+ }.TerraformType(ctx)
+ attrTypes["name"] = basetypes.StringType{}.TerraformType(ctx)
+ attrTypes["resource"] = basetypes.StringType{}.TerraformType(ctx)
+
+ objectType := tftypes.Object{AttributeTypes: attrTypes}
+
+ switch v.state {
+ case attr.ValueStateKnown:
+ vals := make(map[string]tftypes.Value, 4)
+
+ val, err = v.ExpectedResult.ToTerraformValue(ctx)
+
+ if err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ vals["expected_result"] = val
+
+ val, err = v.Mocks.ToTerraformValue(ctx)
+
+ if err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ vals["mocks"] = val
+
+ val, err = v.Name.ToTerraformValue(ctx)
+
+ if err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ vals["name"] = val
+
+ val, err = v.Resource.ToTerraformValue(ctx)
+
+ if err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ vals["resource"] = val
+
+ if err := tftypes.ValidateValue(objectType, vals); err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ return tftypes.NewValue(objectType, vals), nil
+ case attr.ValueStateNull:
+ return tftypes.NewValue(objectType, nil), nil
+ case attr.ValueStateUnknown:
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), nil
+ default:
+ panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state))
+ }
+}
+
+func (v TestsValue) IsNull() bool {
+ return v.state == attr.ValueStateNull
+}
+
+func (v TestsValue) IsUnknown() bool {
+ return v.state == attr.ValueStateUnknown
+}
+
+func (v TestsValue) String() string {
+ return "TestsValue"
+}
+
+func (v TestsValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ var mocksVal basetypes.ListValue
+ switch {
+ case v.Mocks.IsUnknown():
+ mocksVal = types.ListUnknown(types.MapType{
+ ElemType: types.StringType,
+ })
+ case v.Mocks.IsNull():
+ mocksVal = types.ListNull(types.MapType{
+ ElemType: types.StringType,
+ })
+ default:
+ var d diag.Diagnostics
+ mocksVal, d = types.ListValue(types.MapType{
+ ElemType: types.StringType,
+ }, v.Mocks.Elements())
+ diags.Append(d...)
+ }
+
+ if diags.HasError() {
+ return types.ObjectUnknown(map[string]attr.Type{
+ "expected_result": basetypes.BoolType{},
+ "mocks": basetypes.ListType{
+ ElemType: types.MapType{
+ ElemType: types.StringType,
+ },
+ },
+ "name": basetypes.StringType{},
+ "resource": basetypes.StringType{},
+ }), diags
+ }
+
+ attributeTypes := map[string]attr.Type{
+ "expected_result": basetypes.BoolType{},
+ "mocks": basetypes.ListType{
+ ElemType: types.MapType{
+ ElemType: types.StringType,
+ },
+ },
+ "name": basetypes.StringType{},
+ "resource": basetypes.StringType{},
+ }
+
+ if v.IsNull() {
+ return types.ObjectNull(attributeTypes), diags
+ }
+
+ if v.IsUnknown() {
+ return types.ObjectUnknown(attributeTypes), diags
+ }
+
+ objVal, diags := types.ObjectValue(
+ attributeTypes,
+ map[string]attr.Value{
+ "expected_result": v.ExpectedResult,
+ "mocks": mocksVal,
+ "name": v.Name,
+ "resource": v.Resource,
+ })
+
+ return objVal, diags
+}
+
+func (v TestsValue) Equal(o attr.Value) bool {
+ other, ok := o.(TestsValue)
+
+ if !ok {
+ return false
+ }
+
+ if v.state != other.state {
+ return false
+ }
+
+ if v.state != attr.ValueStateKnown {
+ return true
+ }
+
+ if !v.ExpectedResult.Equal(other.ExpectedResult) {
+ return false
+ }
+
+ if !v.Mocks.Equal(other.Mocks) {
+ return false
+ }
+
+ if !v.Name.Equal(other.Name) {
+ return false
+ }
+
+ if !v.Resource.Equal(other.Resource) {
+ return false
+ }
+
+ return true
+}
+
+func (v TestsValue) Type(ctx context.Context) attr.Type {
+ return TestsType{
+ basetypes.ObjectType{
+ AttrTypes: v.AttributeTypes(ctx),
+ },
+ }
+}
+
+func (v TestsValue) AttributeTypes(ctx context.Context) map[string]attr.Type {
+ return map[string]attr.Type{
+ "expected_result": basetypes.BoolType{},
+ "mocks": basetypes.ListType{
+ ElemType: types.MapType{
+ ElemType: types.StringType,
+ },
+ },
+ "name": basetypes.StringType{},
+ "resource": basetypes.StringType{},
+ }
+}
diff --git a/internal/provider/resource_rule_test.go b/internal/provider/resource_rule_test.go
new file mode 100644
index 0000000..43071cb
--- /dev/null
+++ b/internal/provider/resource_rule_test.go
@@ -0,0 +1,77 @@
+/*
+Copyright 2023 Panther Labs, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package provider
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ "github.com/google/uuid"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+)
+
+func TestRuleResource(t *testing.T) {
+ ruleName := strings.ReplaceAll(uuid.NewString(), "-", "")
+ ruleUpdatedName := strings.ReplaceAll(uuid.NewString(), "-", "")
+
+ resource.Test(t, resource.TestCase{
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ // Create and Read testing
+ {
+ Config: providerConfig + testAccRuleResourceConfig(ruleName),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("panther_rule.test", "display_name", ruleName),
+ resource.TestCheckResourceAttr("panther_rule.test", "enabled", "true"),
+ resource.TestCheckResourceAttr("panther_rule.test", "severity", "HIGH"),
+ resource.TestCheckResourceAttr("panther_rule.test", "log_types.#", "1"),
+ resource.TestCheckResourceAttr("panther_rule.test", "log_types.0", "AWS.VPCFlow"),
+ resource.TestCheckResourceAttrSet("panther_rule.test", "id"),
+ ),
+ },
+ // ImportState testing
+ {
+ ResourceName: "panther_rule.test",
+ ImportState: true,
+ ImportStateVerify: true,
+ // Ignore tags as the API may return them in a different order
+ ImportStateVerifyIgnore: []string{"tags"},
+ },
+ // Update and Read testing
+ {
+ Config: providerConfig + testAccRuleResourceConfig(ruleUpdatedName),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("panther_rule.test", "display_name", ruleUpdatedName),
+ ),
+ },
+ },
+ })
+}
+
+func testAccRuleResourceConfig(name string) string {
+ return fmt.Sprintf(`
+resource "panther_rule" "test" {
+ display_name = %[1]q
+ body = "def rule(event): return True"
+ enabled = true
+ log_types = ["AWS.VPCFlow"]
+ severity = "HIGH"
+ tags = ["test", "terraform"]
+}
+`, name)
+}
\ No newline at end of file
diff --git a/internal/provider/resource_scheduled_rule.go b/internal/provider/resource_scheduled_rule.go
new file mode 100644
index 0000000..e3774f6
--- /dev/null
+++ b/internal/provider/resource_scheduled_rule.go
@@ -0,0 +1,407 @@
+/*
+Copyright 2023 Panther Labs, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package provider
+
+import (
+ "context"
+ "fmt"
+ "terraform-provider-panther/internal/client"
+ "terraform-provider-panther/internal/client/panther"
+ "terraform-provider-panther/internal/provider/resource_scheduled_rule"
+
+ "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/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+)
+
+var (
+ _ resource.Resource = (*scheduledRuleResource)(nil)
+ _ resource.ResourceWithConfigure = (*scheduledRuleResource)(nil)
+ _ resource.ResourceWithImportState = (*scheduledRuleResource)(nil)
+)
+
+func NewScheduledRuleResource() resource.Resource {
+ return &scheduledRuleResource{}
+}
+
+type scheduledRuleResource struct {
+ client client.RestClient
+}
+
+func (r *scheduledRuleResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_scheduled_rule"
+}
+
+func (r *scheduledRuleResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+ generatedSchema := resource_scheduled_rule.ScheduledRuleResourceSchema(ctx)
+
+ if generatedSchema.Attributes == nil {
+ generatedSchema.Attributes = make(map[string]schema.Attribute)
+ }
+
+ generatedSchema.Attributes["id"] = schema.StringAttribute{
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ }
+
+ resp.Schema = generatedSchema
+}
+
+func (r *scheduledRuleResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ apiClient, ok := req.ProviderData.(*panther.APIClient)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *panther.APIClient, got: %T", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = apiClient.RestClient
+}
+
+func (r *scheduledRuleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data resource_scheduled_rule.ScheduledRuleModel
+
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ input := client.CreateScheduledRuleInput{
+ ID: data.DisplayName.ValueString(),
+ ScheduledRuleModifiableAttributes: client.ScheduledRuleModifiableAttributes{
+ DisplayName: data.DisplayName.ValueString(),
+ Body: data.Body.ValueString(),
+ Description: data.Description.ValueString(),
+ Severity: data.Severity.ValueString(),
+ Enabled: data.Enabled.ValueBool(),
+ DedupPeriodMinutes: int(data.DedupPeriodMinutes.ValueInt64()),
+ Runbook: data.Runbook.ValueString(),
+ Threshold: int(data.Threshold.ValueInt64()),
+ },
+ }
+
+ // Convert scheduled queries
+ if !data.ScheduledQueries.IsNull() && !data.ScheduledQueries.IsUnknown() {
+ scheduledQueries := make([]string, 0, len(data.ScheduledQueries.Elements()))
+ for _, elem := range data.ScheduledQueries.Elements() {
+ if strVal, ok := elem.(types.String); ok {
+ scheduledQueries = append(scheduledQueries, strVal.ValueString())
+ }
+ }
+ input.ScheduledQueries = scheduledQueries
+ }
+
+ // Convert tags
+ if !data.Tags.IsNull() && !data.Tags.IsUnknown() {
+ tags := make([]string, 0, len(data.Tags.Elements()))
+ for _, elem := range data.Tags.Elements() {
+ if strVal, ok := elem.(types.String); ok {
+ tags = append(tags, strVal.ValueString())
+ }
+ }
+ input.Tags = tags
+ }
+
+ result, err := r.client.CreateScheduledRule(ctx, input)
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create scheduled_rule, got error: %s", err))
+ return
+ }
+
+ data.Id = types.StringValue(result.ID)
+ data.DisplayName = types.StringValue(result.DisplayName)
+ data.Body = types.StringValue(result.Body)
+ data.Description = types.StringValue(result.Description)
+ data.Severity = types.StringValue(result.Severity)
+ data.Enabled = types.BoolValue(result.Enabled)
+ data.DedupPeriodMinutes = types.Int64Value(int64(result.DedupPeriodMinutes))
+ data.Runbook = types.StringValue(result.Runbook)
+ data.Threshold = types.Int64Value(int64(result.Threshold))
+ data.CreatedAt = types.StringValue(result.CreatedAt)
+ data.LastModified = types.StringValue(result.UpdatedAt)
+
+ // Convert scheduled queries back to list
+ if len(result.ScheduledQueries) > 0 {
+ elements := make([]types.String, len(result.ScheduledQueries))
+ for i, query := range result.ScheduledQueries {
+ elements[i] = types.StringValue(query)
+ }
+ queriesList, diags := types.ListValueFrom(ctx, types.StringType, elements)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ data.ScheduledQueries = queriesList
+ } else {
+ data.ScheduledQueries = types.ListNull(types.StringType)
+ }
+
+ // Set computed fields
+ data.CreatedBy = resource_scheduled_rule.NewCreatedByValueNull()
+ data.CreatedByExternal = types.StringNull()
+ data.Managed = types.BoolNull()
+ data.OutputIds = types.ListNull(types.StringType)
+ data.Reports = types.MapNull(types.ListType{ElemType: types.StringType})
+ data.SummaryAttributes = types.ListNull(types.StringType)
+ data.Tests = types.ListNull(resource_scheduled_rule.TestsType{
+ ObjectType: types.ObjectType{
+ AttrTypes: resource_scheduled_rule.TestsValue{}.AttributeTypes(ctx),
+ },
+ })
+
+ tflog.Debug(ctx, "Created ScheduledRule", map[string]any{
+ "id": result.ID,
+ })
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *scheduledRuleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data resource_scheduled_rule.ScheduledRuleModel
+
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ scheduledRuleID := data.Id.ValueString()
+ if scheduledRuleID == "" {
+ scheduledRuleID = data.DisplayName.ValueString()
+ }
+ scheduledRule, err := r.client.GetScheduledRule(ctx, scheduledRuleID)
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read scheduled_rule, got error: %s", err))
+ return
+ }
+
+ data.Id = types.StringValue(scheduledRule.ID)
+ data.DisplayName = types.StringValue(scheduledRule.DisplayName)
+ data.Body = types.StringValue(scheduledRule.Body)
+ data.Description = types.StringValue(scheduledRule.Description)
+ data.Severity = types.StringValue(scheduledRule.Severity)
+ data.Enabled = types.BoolValue(scheduledRule.Enabled)
+ data.DedupPeriodMinutes = types.Int64Value(int64(scheduledRule.DedupPeriodMinutes))
+ data.Runbook = types.StringValue(scheduledRule.Runbook)
+ data.Threshold = types.Int64Value(int64(scheduledRule.Threshold))
+ data.CreatedAt = types.StringValue(scheduledRule.CreatedAt)
+ data.LastModified = types.StringValue(scheduledRule.UpdatedAt)
+
+ // Convert scheduled queries back to list
+ if len(scheduledRule.ScheduledQueries) > 0 {
+ elements := make([]types.String, len(scheduledRule.ScheduledQueries))
+ for i, query := range scheduledRule.ScheduledQueries {
+ elements[i] = types.StringValue(query)
+ }
+ queriesList, diags := types.ListValueFrom(ctx, types.StringType, elements)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ data.ScheduledQueries = queriesList
+ } else {
+ data.ScheduledQueries = types.ListNull(types.StringType)
+ }
+
+ // Handle tags with order preservation
+ if len(scheduledRule.Tags) > 0 {
+ currentTags := make([]string, 0)
+ if !data.Tags.IsNull() && !data.Tags.IsUnknown() {
+ for _, elem := range data.Tags.Elements() {
+ if strVal, ok := elem.(types.String); ok {
+ currentTags = append(currentTags, strVal.ValueString())
+ }
+ }
+ }
+
+ tagsChanged := len(currentTags) != len(scheduledRule.Tags)
+ if !tagsChanged {
+ apiTagsMap := make(map[string]bool)
+ for _, tag := range scheduledRule.Tags {
+ apiTagsMap[tag] = true
+ }
+ for _, tag := range currentTags {
+ if !apiTagsMap[tag] {
+ tagsChanged = true
+ break
+ }
+ }
+ }
+
+ if tagsChanged {
+ elements := make([]types.String, len(scheduledRule.Tags))
+ for i, tag := range scheduledRule.Tags {
+ elements[i] = types.StringValue(tag)
+ }
+ tagsList, diags := types.ListValueFrom(ctx, types.StringType, elements)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ data.Tags = tagsList
+ }
+ } else if !data.Tags.IsNull() {
+ data.Tags = types.ListNull(types.StringType)
+ }
+
+ // Set computed fields
+ data.CreatedBy = resource_scheduled_rule.NewCreatedByValueNull()
+ data.CreatedByExternal = types.StringNull()
+ data.Managed = types.BoolNull()
+ data.OutputIds = types.ListNull(types.StringType)
+ data.Reports = types.MapNull(types.ListType{ElemType: types.StringType})
+ data.SummaryAttributes = types.ListNull(types.StringType)
+ data.Tests = types.ListNull(resource_scheduled_rule.TestsType{
+ ObjectType: types.ObjectType{
+ AttrTypes: resource_scheduled_rule.TestsValue{}.AttributeTypes(ctx),
+ },
+ })
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *scheduledRuleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var data resource_scheduled_rule.ScheduledRuleModel
+
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ input := client.UpdateScheduledRuleInput{
+ ID: data.Id.ValueString(),
+ ScheduledRuleModifiableAttributes: client.ScheduledRuleModifiableAttributes{
+ DisplayName: data.DisplayName.ValueString(),
+ Body: data.Body.ValueString(),
+ Description: data.Description.ValueString(),
+ Severity: data.Severity.ValueString(),
+ Enabled: data.Enabled.ValueBool(),
+ DedupPeriodMinutes: int(data.DedupPeriodMinutes.ValueInt64()),
+ Runbook: data.Runbook.ValueString(),
+ Threshold: int(data.Threshold.ValueInt64()),
+ },
+ }
+
+ // Convert scheduled queries
+ if !data.ScheduledQueries.IsNull() && !data.ScheduledQueries.IsUnknown() {
+ scheduledQueries := make([]string, 0, len(data.ScheduledQueries.Elements()))
+ for _, elem := range data.ScheduledQueries.Elements() {
+ if strVal, ok := elem.(types.String); ok {
+ scheduledQueries = append(scheduledQueries, strVal.ValueString())
+ }
+ }
+ input.ScheduledQueries = scheduledQueries
+ }
+
+ // Convert tags
+ if !data.Tags.IsNull() && !data.Tags.IsUnknown() {
+ tags := make([]string, 0, len(data.Tags.Elements()))
+ for _, elem := range data.Tags.Elements() {
+ if strVal, ok := elem.(types.String); ok {
+ tags = append(tags, strVal.ValueString())
+ }
+ }
+ input.Tags = tags
+ }
+
+ result, err := r.client.UpdateScheduledRule(ctx, input)
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update scheduled_rule, got error: %s", err))
+ return
+ }
+
+ data.Id = types.StringValue(result.ID)
+ data.DisplayName = types.StringValue(result.DisplayName)
+ data.Body = types.StringValue(result.Body)
+ data.Description = types.StringValue(result.Description)
+ data.Severity = types.StringValue(result.Severity)
+ data.Enabled = types.BoolValue(result.Enabled)
+ data.DedupPeriodMinutes = types.Int64Value(int64(result.DedupPeriodMinutes))
+ data.Runbook = types.StringValue(result.Runbook)
+ data.Threshold = types.Int64Value(int64(result.Threshold))
+ data.CreatedAt = types.StringValue(result.CreatedAt)
+ data.LastModified = types.StringValue(result.UpdatedAt)
+
+ // Convert scheduled queries back to list
+ if len(result.ScheduledQueries) > 0 {
+ elements := make([]types.String, len(result.ScheduledQueries))
+ for i, query := range result.ScheduledQueries {
+ elements[i] = types.StringValue(query)
+ }
+ queriesList, diags := types.ListValueFrom(ctx, types.StringType, elements)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ data.ScheduledQueries = queriesList
+ } else {
+ data.ScheduledQueries = types.ListNull(types.StringType)
+ }
+
+ // Set computed fields
+ data.CreatedBy = resource_scheduled_rule.NewCreatedByValueNull()
+ data.CreatedByExternal = types.StringNull()
+ data.Managed = types.BoolNull()
+ data.OutputIds = types.ListNull(types.StringType)
+ data.Reports = types.MapNull(types.ListType{ElemType: types.StringType})
+ data.SummaryAttributes = types.ListNull(types.StringType)
+ data.Tests = types.ListNull(resource_scheduled_rule.TestsType{
+ ObjectType: types.ObjectType{
+ AttrTypes: resource_scheduled_rule.TestsValue{}.AttributeTypes(ctx),
+ },
+ })
+
+ tflog.Debug(ctx, "Updated ScheduledRule", map[string]any{
+ "id": result.ID,
+ })
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *scheduledRuleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var data resource_scheduled_rule.ScheduledRuleModel
+
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ err := r.client.DeleteScheduledRule(ctx, data.Id.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete scheduled_rule, got error: %s", err))
+ return
+ }
+
+ tflog.Debug(ctx, "Deleted ScheduledRule", map[string]any{
+ "id": data.Id.ValueString(),
+ })
+}
+
+func (r *scheduledRuleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
diff --git a/internal/provider/resource_scheduled_rule/scheduled_rule_resource_gen.go b/internal/provider/resource_scheduled_rule/scheduled_rule_resource_gen.go
new file mode 100644
index 0000000..ec8d431
--- /dev/null
+++ b/internal/provider/resource_scheduled_rule/scheduled_rule_resource_gen.go
@@ -0,0 +1,1133 @@
+// Code generated by terraform-plugin-framework-generator DO NOT EDIT.
+
+package resource_scheduled_rule
+
+import (
+ "context"
+ "fmt"
+ "github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+ "github.com/hashicorp/terraform-plugin-go/tftypes"
+ "strings"
+
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+)
+
+func ScheduledRuleResourceSchema(ctx context.Context) schema.Schema {
+ return schema.Schema{
+ Attributes: map[string]schema.Attribute{
+ "body": schema.StringAttribute{
+ Required: true,
+ Description: "The python body of the scheduled rule",
+ MarkdownDescription: "The python body of the scheduled rule",
+ },
+ "created_at": schema.StringAttribute{
+ Computed: true,
+ },
+ "created_by": schema.SingleNestedAttribute{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Computed: true,
+ },
+ "type": schema.StringAttribute{
+ Computed: true,
+ },
+ },
+ CustomType: CreatedByType{
+ ObjectType: types.ObjectType{
+ AttrTypes: CreatedByValue{}.AttributeTypes(ctx),
+ },
+ },
+ Computed: true,
+ Description: "The actor who created the rule",
+ MarkdownDescription: "The actor who created the rule",
+ },
+ "created_by_external": schema.StringAttribute{
+ Computed: true,
+ Description: "The text of the user-provided CreatedBy field when uploaded via CI/CD",
+ MarkdownDescription: "The text of the user-provided CreatedBy field when uploaded via CI/CD",
+ },
+ "dedup_period_minutes": schema.Int64Attribute{
+ Optional: true,
+ Computed: true,
+ Description: "The amount of time in minutes for grouping alerts",
+ MarkdownDescription: "The amount of time in minutes for grouping alerts",
+ Validators: []validator.Int64{
+ int64validator.AtLeast(1),
+ },
+ Default: int64default.StaticInt64(60),
+ },
+ "description": schema.StringAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "The description of the scheduled rule",
+ MarkdownDescription: "The description of the scheduled rule",
+ },
+ "display_name": schema.StringAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "The display name of the scheduled rule",
+ MarkdownDescription: "The display name of the scheduled rule",
+ },
+ "enabled": schema.BoolAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "Determines whether or not the scheduled rule is active",
+ MarkdownDescription: "Determines whether or not the scheduled rule is active",
+ },
+ "last_modified": schema.StringAttribute{
+ Computed: true,
+ },
+ "managed": schema.BoolAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "Determines if the scheduled rule is managed by panther",
+ MarkdownDescription: "Determines if the scheduled rule is managed by panther",
+ },
+ "output_ids": schema.ListAttribute{
+ ElementType: types.StringType,
+ Optional: true,
+ Computed: true,
+ Description: "Destination IDs that override default alert routing based on severity",
+ MarkdownDescription: "Destination IDs that override default alert routing based on severity",
+ },
+ "reports": schema.MapAttribute{
+ ElementType: types.ListType{
+ ElemType: types.StringType,
+ },
+ Optional: true,
+ Computed: true,
+ Description: "reports",
+ MarkdownDescription: "reports",
+ },
+ "runbook": schema.StringAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "How to handle the generated alert",
+ MarkdownDescription: "How to handle the generated alert",
+ },
+ "scheduled_queries": schema.ListAttribute{
+ ElementType: types.StringType,
+ Optional: true,
+ Computed: true,
+ Description: "the queries that this scheduled rule utilizes",
+ MarkdownDescription: "the queries that this scheduled rule utilizes",
+ },
+ "severity": schema.StringAttribute{
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf(
+ "INFO",
+ "LOW",
+ "MEDIUM",
+ "HIGH",
+ "CRITICAL",
+ ),
+ },
+ },
+ "summary_attributes": schema.ListAttribute{
+ ElementType: types.StringType,
+ Optional: true,
+ Computed: true,
+ Description: "A list of fields in the event to create top 5 summaries for",
+ MarkdownDescription: "A list of fields in the event to create top 5 summaries for",
+ },
+ "tags": schema.ListAttribute{
+ ElementType: types.StringType,
+ Optional: true,
+ Computed: true,
+ Description: "The tags for the scheduled rule",
+ MarkdownDescription: "The tags for the scheduled rule",
+ },
+ "tests": schema.ListNestedAttribute{
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "expected_result": schema.BoolAttribute{
+ Required: true,
+ Description: "The expected result",
+ MarkdownDescription: "The expected result",
+ },
+ "mocks": schema.ListAttribute{
+ ElementType: types.MapType{
+ ElemType: types.StringType,
+ },
+ Optional: true,
+ Computed: true,
+ Description: "mocks",
+ MarkdownDescription: "mocks",
+ },
+ "name": schema.StringAttribute{
+ Required: true,
+ Description: "name",
+ MarkdownDescription: "name",
+ },
+ "resource": schema.StringAttribute{
+ Required: true,
+ Description: "resource",
+ MarkdownDescription: "resource",
+ },
+ },
+ CustomType: TestsType{
+ ObjectType: types.ObjectType{
+ AttrTypes: TestsValue{}.AttributeTypes(ctx),
+ },
+ },
+ },
+ Optional: true,
+ Computed: true,
+ Description: "Unit tests for the Rule. Best practice is to include a positive and negative case",
+ MarkdownDescription: "Unit tests for the Rule. Best practice is to include a positive and negative case",
+ },
+ "threshold": schema.Int64Attribute{
+ Optional: true,
+ Computed: true,
+ Description: "the number of events that must match before an alert is triggered",
+ MarkdownDescription: "the number of events that must match before an alert is triggered",
+ Validators: []validator.Int64{
+ int64validator.AtLeast(1),
+ },
+ Default: int64default.StaticInt64(1),
+ },
+ },
+ }
+}
+
+type ScheduledRuleModel struct {
+ Id types.String `tfsdk:"id"`
+ Body types.String `tfsdk:"body"`
+ CreatedAt types.String `tfsdk:"created_at"`
+ CreatedBy CreatedByValue `tfsdk:"created_by"`
+ CreatedByExternal types.String `tfsdk:"created_by_external"`
+ DedupPeriodMinutes types.Int64 `tfsdk:"dedup_period_minutes"`
+ Description types.String `tfsdk:"description"`
+ DisplayName types.String `tfsdk:"display_name"`
+ Enabled types.Bool `tfsdk:"enabled"`
+ LastModified types.String `tfsdk:"last_modified"`
+ Managed types.Bool `tfsdk:"managed"`
+ OutputIds types.List `tfsdk:"output_ids"`
+ Reports types.Map `tfsdk:"reports"`
+ Runbook types.String `tfsdk:"runbook"`
+ ScheduledQueries types.List `tfsdk:"scheduled_queries"`
+ Severity types.String `tfsdk:"severity"`
+ SummaryAttributes types.List `tfsdk:"summary_attributes"`
+ Tags types.List `tfsdk:"tags"`
+ Tests types.List `tfsdk:"tests"`
+ Threshold types.Int64 `tfsdk:"threshold"`
+}
+
+var _ basetypes.ObjectTypable = CreatedByType{}
+
+type CreatedByType struct {
+ basetypes.ObjectType
+}
+
+func (t CreatedByType) Equal(o attr.Type) bool {
+ other, ok := o.(CreatedByType)
+
+ if !ok {
+ return false
+ }
+
+ return t.ObjectType.Equal(other.ObjectType)
+}
+
+func (t CreatedByType) String() string {
+ return "CreatedByType"
+}
+
+func (t CreatedByType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ attributes := in.Attributes()
+
+ idAttribute, ok := attributes["id"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `id is missing from object`)
+
+ return nil, diags
+ }
+
+ idVal, ok := idAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`id expected to be basetypes.StringValue, was: %T`, idAttribute))
+ }
+
+ typeAttribute, ok := attributes["type"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `type is missing from object`)
+
+ return nil, diags
+ }
+
+ typeVal, ok := typeAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`type expected to be basetypes.StringValue, was: %T`, typeAttribute))
+ }
+
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ return CreatedByValue{
+ Id: idVal,
+ CreatedByType: typeVal,
+ state: attr.ValueStateKnown,
+ }, diags
+}
+
+func NewCreatedByValueNull() CreatedByValue {
+ return CreatedByValue{
+ state: attr.ValueStateNull,
+ }
+}
+
+func NewCreatedByValueUnknown() CreatedByValue {
+ return CreatedByValue{
+ state: attr.ValueStateUnknown,
+ }
+}
+
+func NewCreatedByValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (CreatedByValue, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521
+ ctx := context.Background()
+
+ for name, attributeType := range attributeTypes {
+ attribute, ok := attributes[name]
+
+ if !ok {
+ diags.AddError(
+ "Missing CreatedByValue Attribute Value",
+ "While creating a CreatedByValue value, a missing attribute value was detected. "+
+ "A CreatedByValue must contain values for all attributes, even if null or unknown. "+
+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+
+ fmt.Sprintf("CreatedByValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()),
+ )
+
+ continue
+ }
+
+ if !attributeType.Equal(attribute.Type(ctx)) {
+ diags.AddError(
+ "Invalid CreatedByValue Attribute Type",
+ "While creating a CreatedByValue value, an invalid attribute value was detected. "+
+ "A CreatedByValue must use a matching attribute type for the value. "+
+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+
+ fmt.Sprintf("CreatedByValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+
+ fmt.Sprintf("CreatedByValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)),
+ )
+ }
+ }
+
+ for name := range attributes {
+ _, ok := attributeTypes[name]
+
+ if !ok {
+ diags.AddError(
+ "Extra CreatedByValue Attribute Value",
+ "While creating a CreatedByValue value, an extra attribute value was detected. "+
+ "A CreatedByValue must not contain values beyond the expected attribute types. "+
+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+
+ fmt.Sprintf("Extra CreatedByValue Attribute Name: %s", name),
+ )
+ }
+ }
+
+ if diags.HasError() {
+ return NewCreatedByValueUnknown(), diags
+ }
+
+ idAttribute, ok := attributes["id"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `id is missing from object`)
+
+ return NewCreatedByValueUnknown(), diags
+ }
+
+ idVal, ok := idAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`id expected to be basetypes.StringValue, was: %T`, idAttribute))
+ }
+
+ typeAttribute, ok := attributes["type"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `type is missing from object`)
+
+ return NewCreatedByValueUnknown(), diags
+ }
+
+ typeVal, ok := typeAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`type expected to be basetypes.StringValue, was: %T`, typeAttribute))
+ }
+
+ if diags.HasError() {
+ return NewCreatedByValueUnknown(), diags
+ }
+
+ return CreatedByValue{
+ Id: idVal,
+ CreatedByType: typeVal,
+ state: attr.ValueStateKnown,
+ }, diags
+}
+
+func NewCreatedByValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) CreatedByValue {
+ object, diags := NewCreatedByValue(attributeTypes, attributes)
+
+ if diags.HasError() {
+ // This could potentially be added to the diag package.
+ diagsStrings := make([]string, 0, len(diags))
+
+ for _, diagnostic := range diags {
+ diagsStrings = append(diagsStrings, fmt.Sprintf(
+ "%s | %s | %s",
+ diagnostic.Severity(),
+ diagnostic.Summary(),
+ diagnostic.Detail()))
+ }
+
+ panic("NewCreatedByValueMust received error(s): " + strings.Join(diagsStrings, "\n"))
+ }
+
+ return object
+}
+
+func (t CreatedByType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
+ if in.Type() == nil {
+ return NewCreatedByValueNull(), nil
+ }
+
+ if !in.Type().Equal(t.TerraformType(ctx)) {
+ return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type())
+ }
+
+ if !in.IsKnown() {
+ return NewCreatedByValueUnknown(), nil
+ }
+
+ if in.IsNull() {
+ return NewCreatedByValueNull(), nil
+ }
+
+ attributes := map[string]attr.Value{}
+
+ val := map[string]tftypes.Value{}
+
+ err := in.As(&val)
+
+ if err != nil {
+ return nil, err
+ }
+
+ for k, v := range val {
+ a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v)
+
+ if err != nil {
+ return nil, err
+ }
+
+ attributes[k] = a
+ }
+
+ return NewCreatedByValueMust(CreatedByValue{}.AttributeTypes(ctx), attributes), nil
+}
+
+func (t CreatedByType) ValueType(ctx context.Context) attr.Value {
+ return CreatedByValue{}
+}
+
+var _ basetypes.ObjectValuable = CreatedByValue{}
+
+type CreatedByValue struct {
+ Id basetypes.StringValue `tfsdk:"id"`
+ CreatedByType basetypes.StringValue `tfsdk:"type"`
+ state attr.ValueState
+}
+
+func (v CreatedByValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
+ attrTypes := make(map[string]tftypes.Type, 2)
+
+ var val tftypes.Value
+ var err error
+
+ attrTypes["id"] = basetypes.StringType{}.TerraformType(ctx)
+ attrTypes["type"] = basetypes.StringType{}.TerraformType(ctx)
+
+ objectType := tftypes.Object{AttributeTypes: attrTypes}
+
+ switch v.state {
+ case attr.ValueStateKnown:
+ vals := make(map[string]tftypes.Value, 2)
+
+ val, err = v.Id.ToTerraformValue(ctx)
+
+ if err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ vals["id"] = val
+
+ val, err = v.CreatedByType.ToTerraformValue(ctx)
+
+ if err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ vals["type"] = val
+
+ if err := tftypes.ValidateValue(objectType, vals); err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ return tftypes.NewValue(objectType, vals), nil
+ case attr.ValueStateNull:
+ return tftypes.NewValue(objectType, nil), nil
+ case attr.ValueStateUnknown:
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), nil
+ default:
+ panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state))
+ }
+}
+
+func (v CreatedByValue) IsNull() bool {
+ return v.state == attr.ValueStateNull
+}
+
+func (v CreatedByValue) IsUnknown() bool {
+ return v.state == attr.ValueStateUnknown
+}
+
+func (v CreatedByValue) String() string {
+ return "CreatedByValue"
+}
+
+func (v CreatedByValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ attributeTypes := map[string]attr.Type{
+ "id": basetypes.StringType{},
+ "type": basetypes.StringType{},
+ }
+
+ if v.IsNull() {
+ return types.ObjectNull(attributeTypes), diags
+ }
+
+ if v.IsUnknown() {
+ return types.ObjectUnknown(attributeTypes), diags
+ }
+
+ objVal, diags := types.ObjectValue(
+ attributeTypes,
+ map[string]attr.Value{
+ "id": v.Id,
+ "type": v.CreatedByType,
+ })
+
+ return objVal, diags
+}
+
+func (v CreatedByValue) Equal(o attr.Value) bool {
+ other, ok := o.(CreatedByValue)
+
+ if !ok {
+ return false
+ }
+
+ if v.state != other.state {
+ return false
+ }
+
+ if v.state != attr.ValueStateKnown {
+ return true
+ }
+
+ if !v.Id.Equal(other.Id) {
+ return false
+ }
+
+ if !v.CreatedByType.Equal(other.CreatedByType) {
+ return false
+ }
+
+ return true
+}
+
+func (v CreatedByValue) Type(ctx context.Context) attr.Type {
+ return CreatedByType{
+ basetypes.ObjectType{
+ AttrTypes: v.AttributeTypes(ctx),
+ },
+ }
+}
+
+func (v CreatedByValue) AttributeTypes(ctx context.Context) map[string]attr.Type {
+ return map[string]attr.Type{
+ "id": basetypes.StringType{},
+ "type": basetypes.StringType{},
+ }
+}
+
+var _ basetypes.ObjectTypable = TestsType{}
+
+type TestsType struct {
+ basetypes.ObjectType
+}
+
+func (t TestsType) Equal(o attr.Type) bool {
+ other, ok := o.(TestsType)
+
+ if !ok {
+ return false
+ }
+
+ return t.ObjectType.Equal(other.ObjectType)
+}
+
+func (t TestsType) String() string {
+ return "TestsType"
+}
+
+func (t TestsType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ attributes := in.Attributes()
+
+ expectedResultAttribute, ok := attributes["expected_result"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `expected_result is missing from object`)
+
+ return nil, diags
+ }
+
+ expectedResultVal, ok := expectedResultAttribute.(basetypes.BoolValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`expected_result expected to be basetypes.BoolValue, was: %T`, expectedResultAttribute))
+ }
+
+ mocksAttribute, ok := attributes["mocks"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `mocks is missing from object`)
+
+ return nil, diags
+ }
+
+ mocksVal, ok := mocksAttribute.(basetypes.ListValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`mocks expected to be basetypes.ListValue, was: %T`, mocksAttribute))
+ }
+
+ nameAttribute, ok := attributes["name"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `name is missing from object`)
+
+ return nil, diags
+ }
+
+ nameVal, ok := nameAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`name expected to be basetypes.StringValue, was: %T`, nameAttribute))
+ }
+
+ resourceAttribute, ok := attributes["resource"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `resource is missing from object`)
+
+ return nil, diags
+ }
+
+ resourceVal, ok := resourceAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`resource expected to be basetypes.StringValue, was: %T`, resourceAttribute))
+ }
+
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ return TestsValue{
+ ExpectedResult: expectedResultVal,
+ Mocks: mocksVal,
+ Name: nameVal,
+ Resource: resourceVal,
+ state: attr.ValueStateKnown,
+ }, diags
+}
+
+func NewTestsValueNull() TestsValue {
+ return TestsValue{
+ state: attr.ValueStateNull,
+ }
+}
+
+func NewTestsValueUnknown() TestsValue {
+ return TestsValue{
+ state: attr.ValueStateUnknown,
+ }
+}
+
+func NewTestsValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (TestsValue, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521
+ ctx := context.Background()
+
+ for name, attributeType := range attributeTypes {
+ attribute, ok := attributes[name]
+
+ if !ok {
+ diags.AddError(
+ "Missing TestsValue Attribute Value",
+ "While creating a TestsValue value, a missing attribute value was detected. "+
+ "A TestsValue must contain values for all attributes, even if null or unknown. "+
+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+
+ fmt.Sprintf("TestsValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()),
+ )
+
+ continue
+ }
+
+ if !attributeType.Equal(attribute.Type(ctx)) {
+ diags.AddError(
+ "Invalid TestsValue Attribute Type",
+ "While creating a TestsValue value, an invalid attribute value was detected. "+
+ "A TestsValue must use a matching attribute type for the value. "+
+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+
+ fmt.Sprintf("TestsValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+
+ fmt.Sprintf("TestsValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)),
+ )
+ }
+ }
+
+ for name := range attributes {
+ _, ok := attributeTypes[name]
+
+ if !ok {
+ diags.AddError(
+ "Extra TestsValue Attribute Value",
+ "While creating a TestsValue value, an extra attribute value was detected. "+
+ "A TestsValue must not contain values beyond the expected attribute types. "+
+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+
+ fmt.Sprintf("Extra TestsValue Attribute Name: %s", name),
+ )
+ }
+ }
+
+ if diags.HasError() {
+ return NewTestsValueUnknown(), diags
+ }
+
+ expectedResultAttribute, ok := attributes["expected_result"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `expected_result is missing from object`)
+
+ return NewTestsValueUnknown(), diags
+ }
+
+ expectedResultVal, ok := expectedResultAttribute.(basetypes.BoolValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`expected_result expected to be basetypes.BoolValue, was: %T`, expectedResultAttribute))
+ }
+
+ mocksAttribute, ok := attributes["mocks"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `mocks is missing from object`)
+
+ return NewTestsValueUnknown(), diags
+ }
+
+ mocksVal, ok := mocksAttribute.(basetypes.ListValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`mocks expected to be basetypes.ListValue, was: %T`, mocksAttribute))
+ }
+
+ nameAttribute, ok := attributes["name"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `name is missing from object`)
+
+ return NewTestsValueUnknown(), diags
+ }
+
+ nameVal, ok := nameAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`name expected to be basetypes.StringValue, was: %T`, nameAttribute))
+ }
+
+ resourceAttribute, ok := attributes["resource"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `resource is missing from object`)
+
+ return NewTestsValueUnknown(), diags
+ }
+
+ resourceVal, ok := resourceAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`resource expected to be basetypes.StringValue, was: %T`, resourceAttribute))
+ }
+
+ if diags.HasError() {
+ return NewTestsValueUnknown(), diags
+ }
+
+ return TestsValue{
+ ExpectedResult: expectedResultVal,
+ Mocks: mocksVal,
+ Name: nameVal,
+ Resource: resourceVal,
+ state: attr.ValueStateKnown,
+ }, diags
+}
+
+func NewTestsValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) TestsValue {
+ object, diags := NewTestsValue(attributeTypes, attributes)
+
+ if diags.HasError() {
+ // This could potentially be added to the diag package.
+ diagsStrings := make([]string, 0, len(diags))
+
+ for _, diagnostic := range diags {
+ diagsStrings = append(diagsStrings, fmt.Sprintf(
+ "%s | %s | %s",
+ diagnostic.Severity(),
+ diagnostic.Summary(),
+ diagnostic.Detail()))
+ }
+
+ panic("NewTestsValueMust received error(s): " + strings.Join(diagsStrings, "\n"))
+ }
+
+ return object
+}
+
+func (t TestsType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
+ if in.Type() == nil {
+ return NewTestsValueNull(), nil
+ }
+
+ if !in.Type().Equal(t.TerraformType(ctx)) {
+ return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type())
+ }
+
+ if !in.IsKnown() {
+ return NewTestsValueUnknown(), nil
+ }
+
+ if in.IsNull() {
+ return NewTestsValueNull(), nil
+ }
+
+ attributes := map[string]attr.Value{}
+
+ val := map[string]tftypes.Value{}
+
+ err := in.As(&val)
+
+ if err != nil {
+ return nil, err
+ }
+
+ for k, v := range val {
+ a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v)
+
+ if err != nil {
+ return nil, err
+ }
+
+ attributes[k] = a
+ }
+
+ return NewTestsValueMust(TestsValue{}.AttributeTypes(ctx), attributes), nil
+}
+
+func (t TestsType) ValueType(ctx context.Context) attr.Value {
+ return TestsValue{}
+}
+
+var _ basetypes.ObjectValuable = TestsValue{}
+
+type TestsValue struct {
+ ExpectedResult basetypes.BoolValue `tfsdk:"expected_result"`
+ Mocks basetypes.ListValue `tfsdk:"mocks"`
+ Name basetypes.StringValue `tfsdk:"name"`
+ Resource basetypes.StringValue `tfsdk:"resource"`
+ state attr.ValueState
+}
+
+func (v TestsValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
+ attrTypes := make(map[string]tftypes.Type, 4)
+
+ var val tftypes.Value
+ var err error
+
+ attrTypes["expected_result"] = basetypes.BoolType{}.TerraformType(ctx)
+ attrTypes["mocks"] = basetypes.ListType{
+ ElemType: types.MapType{
+ ElemType: types.StringType,
+ },
+ }.TerraformType(ctx)
+ attrTypes["name"] = basetypes.StringType{}.TerraformType(ctx)
+ attrTypes["resource"] = basetypes.StringType{}.TerraformType(ctx)
+
+ objectType := tftypes.Object{AttributeTypes: attrTypes}
+
+ switch v.state {
+ case attr.ValueStateKnown:
+ vals := make(map[string]tftypes.Value, 4)
+
+ val, err = v.ExpectedResult.ToTerraformValue(ctx)
+
+ if err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ vals["expected_result"] = val
+
+ val, err = v.Mocks.ToTerraformValue(ctx)
+
+ if err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ vals["mocks"] = val
+
+ val, err = v.Name.ToTerraformValue(ctx)
+
+ if err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ vals["name"] = val
+
+ val, err = v.Resource.ToTerraformValue(ctx)
+
+ if err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ vals["resource"] = val
+
+ if err := tftypes.ValidateValue(objectType, vals); err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ return tftypes.NewValue(objectType, vals), nil
+ case attr.ValueStateNull:
+ return tftypes.NewValue(objectType, nil), nil
+ case attr.ValueStateUnknown:
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), nil
+ default:
+ panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state))
+ }
+}
+
+func (v TestsValue) IsNull() bool {
+ return v.state == attr.ValueStateNull
+}
+
+func (v TestsValue) IsUnknown() bool {
+ return v.state == attr.ValueStateUnknown
+}
+
+func (v TestsValue) String() string {
+ return "TestsValue"
+}
+
+func (v TestsValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ var mocksVal basetypes.ListValue
+ switch {
+ case v.Mocks.IsUnknown():
+ mocksVal = types.ListUnknown(types.MapType{
+ ElemType: types.StringType,
+ })
+ case v.Mocks.IsNull():
+ mocksVal = types.ListNull(types.MapType{
+ ElemType: types.StringType,
+ })
+ default:
+ var d diag.Diagnostics
+ mocksVal, d = types.ListValue(types.MapType{
+ ElemType: types.StringType,
+ }, v.Mocks.Elements())
+ diags.Append(d...)
+ }
+
+ if diags.HasError() {
+ return types.ObjectUnknown(map[string]attr.Type{
+ "expected_result": basetypes.BoolType{},
+ "mocks": basetypes.ListType{
+ ElemType: types.MapType{
+ ElemType: types.StringType,
+ },
+ },
+ "name": basetypes.StringType{},
+ "resource": basetypes.StringType{},
+ }), diags
+ }
+
+ attributeTypes := map[string]attr.Type{
+ "expected_result": basetypes.BoolType{},
+ "mocks": basetypes.ListType{
+ ElemType: types.MapType{
+ ElemType: types.StringType,
+ },
+ },
+ "name": basetypes.StringType{},
+ "resource": basetypes.StringType{},
+ }
+
+ if v.IsNull() {
+ return types.ObjectNull(attributeTypes), diags
+ }
+
+ if v.IsUnknown() {
+ return types.ObjectUnknown(attributeTypes), diags
+ }
+
+ objVal, diags := types.ObjectValue(
+ attributeTypes,
+ map[string]attr.Value{
+ "expected_result": v.ExpectedResult,
+ "mocks": mocksVal,
+ "name": v.Name,
+ "resource": v.Resource,
+ })
+
+ return objVal, diags
+}
+
+func (v TestsValue) Equal(o attr.Value) bool {
+ other, ok := o.(TestsValue)
+
+ if !ok {
+ return false
+ }
+
+ if v.state != other.state {
+ return false
+ }
+
+ if v.state != attr.ValueStateKnown {
+ return true
+ }
+
+ if !v.ExpectedResult.Equal(other.ExpectedResult) {
+ return false
+ }
+
+ if !v.Mocks.Equal(other.Mocks) {
+ return false
+ }
+
+ if !v.Name.Equal(other.Name) {
+ return false
+ }
+
+ if !v.Resource.Equal(other.Resource) {
+ return false
+ }
+
+ return true
+}
+
+func (v TestsValue) Type(ctx context.Context) attr.Type {
+ return TestsType{
+ basetypes.ObjectType{
+ AttrTypes: v.AttributeTypes(ctx),
+ },
+ }
+}
+
+func (v TestsValue) AttributeTypes(ctx context.Context) map[string]attr.Type {
+ return map[string]attr.Type{
+ "expected_result": basetypes.BoolType{},
+ "mocks": basetypes.ListType{
+ ElemType: types.MapType{
+ ElemType: types.StringType,
+ },
+ },
+ "name": basetypes.StringType{},
+ "resource": basetypes.StringType{},
+ }
+}
diff --git a/internal/provider/resource_scheduled_rule_test.go b/internal/provider/resource_scheduled_rule_test.go
new file mode 100644
index 0000000..06c02a6
--- /dev/null
+++ b/internal/provider/resource_scheduled_rule_test.go
@@ -0,0 +1,80 @@
+/*
+Copyright 2023 Panther Labs, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package provider
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ "github.com/google/uuid"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+)
+
+func TestScheduledRuleResource(t *testing.T) {
+ scheduledRuleName := strings.ReplaceAll(uuid.NewString(), "-", "")
+ scheduledRuleUpdatedName := strings.ReplaceAll(uuid.NewString(), "-", "")
+
+ resource.Test(t, resource.TestCase{
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ // Create and Read testing
+ {
+ Config: providerConfig + testAccScheduledRuleResourceConfig(scheduledRuleName),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("panther_scheduled_rule.test", "display_name", scheduledRuleName),
+ resource.TestCheckResourceAttr("panther_scheduled_rule.test", "enabled", "true"),
+ resource.TestCheckResourceAttr("panther_scheduled_rule.test", "severity", "HIGH"),
+ resource.TestCheckResourceAttr("panther_scheduled_rule.test", "scheduled_queries.#", "1"),
+ resource.TestCheckResourceAttr("panther_scheduled_rule.test", "scheduled_queries.0", "test-query"),
+ resource.TestCheckResourceAttr("panther_scheduled_rule.test", "dedup_period_minutes", "60"),
+ resource.TestCheckResourceAttr("panther_scheduled_rule.test", "threshold", "1"),
+ resource.TestCheckResourceAttrSet("panther_scheduled_rule.test", "id"),
+ ),
+ },
+ // ImportState testing
+ {
+ ResourceName: "panther_scheduled_rule.test",
+ ImportState: true,
+ ImportStateVerify: true,
+ ImportStateVerifyIgnore: []string{"tags"},
+ },
+ // Update and Read testing
+ {
+ Config: providerConfig + testAccScheduledRuleResourceConfig(scheduledRuleUpdatedName),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("panther_scheduled_rule.test", "display_name", scheduledRuleUpdatedName),
+ ),
+ },
+ },
+ })
+}
+
+func testAccScheduledRuleResourceConfig(name string) string {
+ return fmt.Sprintf(`
+resource "panther_scheduled_rule" "test" {
+ display_name = %[1]q
+ body = "def rule(event): return True"
+ enabled = true
+ scheduled_queries = ["test-query"]
+ severity = "HIGH"
+ dedup_period_minutes = 60
+ threshold = 1
+ tags = ["test", "terraform"]
+}
+`, name)
+}
diff --git a/internal/provider/resource_simple_rule.go b/internal/provider/resource_simple_rule.go
new file mode 100644
index 0000000..547b3a7
--- /dev/null
+++ b/internal/provider/resource_simple_rule.go
@@ -0,0 +1,437 @@
+/*
+Copyright 2023 Panther Labs, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package provider
+
+import (
+ "context"
+ "fmt"
+ "terraform-provider-panther/internal/client"
+ "terraform-provider-panther/internal/client/panther"
+ "terraform-provider-panther/internal/provider/resource_simple_rule"
+
+ "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/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+)
+
+var (
+ _ resource.Resource = (*simpleRuleResource)(nil)
+ _ resource.ResourceWithConfigure = (*simpleRuleResource)(nil)
+ _ resource.ResourceWithImportState = (*simpleRuleResource)(nil)
+)
+
+func NewSimpleRuleResource() resource.Resource {
+ return &simpleRuleResource{}
+}
+
+type simpleRuleResource struct {
+ client client.RestClient
+}
+
+func (r *simpleRuleResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_simple_rule"
+}
+
+func (r *simpleRuleResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+ generatedSchema := resource_simple_rule.SimpleRuleResourceSchema(ctx)
+
+ if generatedSchema.Attributes == nil {
+ generatedSchema.Attributes = make(map[string]schema.Attribute)
+ }
+
+ generatedSchema.Attributes["id"] = schema.StringAttribute{
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ }
+
+ resp.Schema = generatedSchema
+}
+
+func (r *simpleRuleResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ apiClient, ok := req.ProviderData.(*panther.APIClient)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *panther.APIClient, got: %T", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = apiClient.RestClient
+}
+
+func (r *simpleRuleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data resource_simple_rule.SimpleRuleModel
+
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ input := client.CreateSimpleRuleInput{
+ ID: data.DisplayName.ValueString(),
+ SimpleRuleModifiableAttributes: client.SimpleRuleModifiableAttributes{
+ DisplayName: data.DisplayName.ValueString(),
+ Detection: data.Detection.ValueString(),
+ Description: data.Description.ValueString(),
+ Severity: data.Severity.ValueString(),
+ Enabled: data.Enabled.ValueBool(),
+ DedupPeriodMinutes: int(data.DedupPeriodMinutes.ValueInt64()),
+ Runbook: data.Runbook.ValueString(),
+ Threshold: int(data.Threshold.ValueInt64()),
+ AlertContext: data.AlertContext.ValueString(),
+ AlertTitle: data.AlertTitle.ValueString(),
+ DynamicSeverities: data.DynamicSeverities.ValueString(),
+ GroupBy: data.GroupBy.ValueString(),
+ InlineFilters: data.InlineFilters.ValueString(),
+ PythonBody: data.PythonBody.ValueString(),
+ },
+ }
+
+ // Convert log types
+ if !data.LogTypes.IsNull() && !data.LogTypes.IsUnknown() {
+ logTypes := make([]string, 0, len(data.LogTypes.Elements()))
+ for _, elem := range data.LogTypes.Elements() {
+ if strVal, ok := elem.(types.String); ok {
+ logTypes = append(logTypes, strVal.ValueString())
+ }
+ }
+ input.LogTypes = logTypes
+ }
+
+ // Convert tags
+ if !data.Tags.IsNull() && !data.Tags.IsUnknown() {
+ tags := make([]string, 0, len(data.Tags.Elements()))
+ for _, elem := range data.Tags.Elements() {
+ if strVal, ok := elem.(types.String); ok {
+ tags = append(tags, strVal.ValueString())
+ }
+ }
+ input.Tags = tags
+ }
+
+ result, err := r.client.CreateSimpleRule(ctx, input)
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create simple_rule, got error: %s", err))
+ return
+ }
+
+ data.Id = types.StringValue(result.ID)
+ data.DisplayName = types.StringValue(result.DisplayName)
+ data.Detection = types.StringValue(result.Detection)
+ data.Description = types.StringValue(result.Description)
+ data.Severity = types.StringValue(result.Severity)
+ data.Enabled = types.BoolValue(result.Enabled)
+ data.DedupPeriodMinutes = types.Int64Value(int64(result.DedupPeriodMinutes))
+ data.Runbook = types.StringValue(result.Runbook)
+ data.Threshold = types.Int64Value(int64(result.Threshold))
+ data.AlertContext = types.StringValue(result.AlertContext)
+ data.AlertTitle = types.StringValue(result.AlertTitle)
+ data.DynamicSeverities = types.StringValue(result.DynamicSeverities)
+ data.GroupBy = types.StringValue(result.GroupBy)
+ data.InlineFilters = types.StringValue(result.InlineFilters)
+ data.PythonBody = types.StringValue(result.PythonBody)
+ data.CreatedAt = types.StringValue(result.CreatedAt)
+ data.LastModified = types.StringValue(result.UpdatedAt)
+
+ // Convert log types back to list
+ if len(result.LogTypes) > 0 {
+ elements := make([]types.String, len(result.LogTypes))
+ for i, logType := range result.LogTypes {
+ elements[i] = types.StringValue(logType)
+ }
+ logTypesList, diags := types.ListValueFrom(ctx, types.StringType, elements)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ data.LogTypes = logTypesList
+ } else {
+ data.LogTypes = types.ListNull(types.StringType)
+ }
+
+ // Set computed fields
+ data.CreatedBy = resource_simple_rule.NewCreatedByValueNull()
+ data.CreatedByExternal = types.StringNull()
+ data.Managed = types.BoolNull()
+ data.OutputIds = types.ListNull(types.StringType)
+ data.Reports = types.MapNull(types.ListType{ElemType: types.StringType})
+ data.SummaryAttributes = types.ListNull(types.StringType)
+ data.Tests = types.ListNull(resource_simple_rule.TestsType{
+ ObjectType: types.ObjectType{
+ AttrTypes: resource_simple_rule.TestsValue{}.AttributeTypes(ctx),
+ },
+ })
+
+ tflog.Debug(ctx, "Created SimpleRule", map[string]any{
+ "id": result.ID,
+ })
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *simpleRuleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data resource_simple_rule.SimpleRuleModel
+
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ simpleRuleID := data.Id.ValueString()
+ if simpleRuleID == "" {
+ simpleRuleID = data.DisplayName.ValueString()
+ }
+ simpleRule, err := r.client.GetSimpleRule(ctx, simpleRuleID)
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read simple_rule, got error: %s", err))
+ return
+ }
+
+ data.Id = types.StringValue(simpleRule.ID)
+ data.DisplayName = types.StringValue(simpleRule.DisplayName)
+ data.Detection = types.StringValue(simpleRule.Detection)
+ data.Description = types.StringValue(simpleRule.Description)
+ data.Severity = types.StringValue(simpleRule.Severity)
+ data.Enabled = types.BoolValue(simpleRule.Enabled)
+ data.DedupPeriodMinutes = types.Int64Value(int64(simpleRule.DedupPeriodMinutes))
+ data.Runbook = types.StringValue(simpleRule.Runbook)
+ data.Threshold = types.Int64Value(int64(simpleRule.Threshold))
+ data.AlertContext = types.StringValue(simpleRule.AlertContext)
+ data.AlertTitle = types.StringValue(simpleRule.AlertTitle)
+ data.DynamicSeverities = types.StringValue(simpleRule.DynamicSeverities)
+ data.GroupBy = types.StringValue(simpleRule.GroupBy)
+ data.InlineFilters = types.StringValue(simpleRule.InlineFilters)
+ data.PythonBody = types.StringValue(simpleRule.PythonBody)
+ data.CreatedAt = types.StringValue(simpleRule.CreatedAt)
+ data.LastModified = types.StringValue(simpleRule.UpdatedAt)
+
+ // Convert log types back to list
+ if len(simpleRule.LogTypes) > 0 {
+ elements := make([]types.String, len(simpleRule.LogTypes))
+ for i, logType := range simpleRule.LogTypes {
+ elements[i] = types.StringValue(logType)
+ }
+ logTypesList, diags := types.ListValueFrom(ctx, types.StringType, elements)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ data.LogTypes = logTypesList
+ } else {
+ data.LogTypes = types.ListNull(types.StringType)
+ }
+
+ // Handle tags with order preservation
+ if len(simpleRule.Tags) > 0 {
+ currentTags := make([]string, 0)
+ if !data.Tags.IsNull() && !data.Tags.IsUnknown() {
+ for _, elem := range data.Tags.Elements() {
+ if strVal, ok := elem.(types.String); ok {
+ currentTags = append(currentTags, strVal.ValueString())
+ }
+ }
+ }
+
+ tagsChanged := len(currentTags) != len(simpleRule.Tags)
+ if !tagsChanged {
+ apiTagsMap := make(map[string]bool)
+ for _, tag := range simpleRule.Tags {
+ apiTagsMap[tag] = true
+ }
+ for _, tag := range currentTags {
+ if !apiTagsMap[tag] {
+ tagsChanged = true
+ break
+ }
+ }
+ }
+
+ if tagsChanged {
+ elements := make([]types.String, len(simpleRule.Tags))
+ for i, tag := range simpleRule.Tags {
+ elements[i] = types.StringValue(tag)
+ }
+ tagsList, diags := types.ListValueFrom(ctx, types.StringType, elements)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ data.Tags = tagsList
+ }
+ } else if !data.Tags.IsNull() {
+ data.Tags = types.ListNull(types.StringType)
+ }
+
+ // Set computed fields
+ data.CreatedBy = resource_simple_rule.NewCreatedByValueNull()
+ data.CreatedByExternal = types.StringNull()
+ data.Managed = types.BoolNull()
+ data.OutputIds = types.ListNull(types.StringType)
+ data.Reports = types.MapNull(types.ListType{ElemType: types.StringType})
+ data.SummaryAttributes = types.ListNull(types.StringType)
+ data.Tests = types.ListNull(resource_simple_rule.TestsType{
+ ObjectType: types.ObjectType{
+ AttrTypes: resource_simple_rule.TestsValue{}.AttributeTypes(ctx),
+ },
+ })
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *simpleRuleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var data resource_simple_rule.SimpleRuleModel
+
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ input := client.UpdateSimpleRuleInput{
+ ID: data.Id.ValueString(),
+ SimpleRuleModifiableAttributes: client.SimpleRuleModifiableAttributes{
+ DisplayName: data.DisplayName.ValueString(),
+ Detection: data.Detection.ValueString(),
+ Description: data.Description.ValueString(),
+ Severity: data.Severity.ValueString(),
+ Enabled: data.Enabled.ValueBool(),
+ DedupPeriodMinutes: int(data.DedupPeriodMinutes.ValueInt64()),
+ Runbook: data.Runbook.ValueString(),
+ Threshold: int(data.Threshold.ValueInt64()),
+ AlertContext: data.AlertContext.ValueString(),
+ AlertTitle: data.AlertTitle.ValueString(),
+ DynamicSeverities: data.DynamicSeverities.ValueString(),
+ GroupBy: data.GroupBy.ValueString(),
+ InlineFilters: data.InlineFilters.ValueString(),
+ PythonBody: data.PythonBody.ValueString(),
+ },
+ }
+
+ // Convert log types
+ if !data.LogTypes.IsNull() && !data.LogTypes.IsUnknown() {
+ logTypes := make([]string, 0, len(data.LogTypes.Elements()))
+ for _, elem := range data.LogTypes.Elements() {
+ if strVal, ok := elem.(types.String); ok {
+ logTypes = append(logTypes, strVal.ValueString())
+ }
+ }
+ input.LogTypes = logTypes
+ }
+
+ // Convert tags
+ if !data.Tags.IsNull() && !data.Tags.IsUnknown() {
+ tags := make([]string, 0, len(data.Tags.Elements()))
+ for _, elem := range data.Tags.Elements() {
+ if strVal, ok := elem.(types.String); ok {
+ tags = append(tags, strVal.ValueString())
+ }
+ }
+ input.Tags = tags
+ }
+
+ result, err := r.client.UpdateSimpleRule(ctx, input)
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update simple_rule, got error: %s", err))
+ return
+ }
+
+ data.Id = types.StringValue(result.ID)
+ data.DisplayName = types.StringValue(result.DisplayName)
+ data.Detection = types.StringValue(result.Detection)
+ data.Description = types.StringValue(result.Description)
+ data.Severity = types.StringValue(result.Severity)
+ data.Enabled = types.BoolValue(result.Enabled)
+ data.DedupPeriodMinutes = types.Int64Value(int64(result.DedupPeriodMinutes))
+ data.Runbook = types.StringValue(result.Runbook)
+ data.Threshold = types.Int64Value(int64(result.Threshold))
+ data.AlertContext = types.StringValue(result.AlertContext)
+ data.AlertTitle = types.StringValue(result.AlertTitle)
+ data.DynamicSeverities = types.StringValue(result.DynamicSeverities)
+ data.GroupBy = types.StringValue(result.GroupBy)
+ data.InlineFilters = types.StringValue(result.InlineFilters)
+ data.PythonBody = types.StringValue(result.PythonBody)
+ data.CreatedAt = types.StringValue(result.CreatedAt)
+ data.LastModified = types.StringValue(result.UpdatedAt)
+
+ // Convert log types back to list
+ if len(result.LogTypes) > 0 {
+ elements := make([]types.String, len(result.LogTypes))
+ for i, logType := range result.LogTypes {
+ elements[i] = types.StringValue(logType)
+ }
+ logTypesList, diags := types.ListValueFrom(ctx, types.StringType, elements)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ data.LogTypes = logTypesList
+ } else {
+ data.LogTypes = types.ListNull(types.StringType)
+ }
+
+ // Set computed fields
+ data.CreatedBy = resource_simple_rule.NewCreatedByValueNull()
+ data.CreatedByExternal = types.StringNull()
+ data.Managed = types.BoolNull()
+ data.OutputIds = types.ListNull(types.StringType)
+ data.Reports = types.MapNull(types.ListType{ElemType: types.StringType})
+ data.SummaryAttributes = types.ListNull(types.StringType)
+ data.Tests = types.ListNull(resource_simple_rule.TestsType{
+ ObjectType: types.ObjectType{
+ AttrTypes: resource_simple_rule.TestsValue{}.AttributeTypes(ctx),
+ },
+ })
+
+ tflog.Debug(ctx, "Updated SimpleRule", map[string]any{
+ "id": result.ID,
+ })
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *simpleRuleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var data resource_simple_rule.SimpleRuleModel
+
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ err := r.client.DeleteSimpleRule(ctx, data.Id.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete simple_rule, got error: %s", err))
+ return
+ }
+
+ tflog.Debug(ctx, "Deleted SimpleRule", map[string]any{
+ "id": data.Id.ValueString(),
+ })
+}
+
+func (r *simpleRuleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
diff --git a/internal/provider/resource_simple_rule/simple_rule_resource_gen.go b/internal/provider/resource_simple_rule/simple_rule_resource_gen.go
new file mode 100644
index 0000000..3e94c0d
--- /dev/null
+++ b/internal/provider/resource_simple_rule/simple_rule_resource_gen.go
@@ -0,0 +1,1184 @@
+// Code generated by terraform-plugin-framework-generator DO NOT EDIT.
+
+package resource_simple_rule
+
+import (
+ "context"
+ "fmt"
+ "github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+ "github.com/hashicorp/terraform-plugin-go/tftypes"
+ "strings"
+
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+)
+
+func SimpleRuleResourceSchema(ctx context.Context) schema.Schema {
+ return schema.Schema{
+ Attributes: map[string]schema.Attribute{
+ "alert_context": schema.StringAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "The alert context represented in YAML",
+ MarkdownDescription: "The alert context represented in YAML",
+ },
+ "alert_title": schema.StringAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "The alert title represented in YAML",
+ MarkdownDescription: "The alert title represented in YAML",
+ },
+ "created_at": schema.StringAttribute{
+ Computed: true,
+ },
+ "created_by": schema.SingleNestedAttribute{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Computed: true,
+ },
+ "type": schema.StringAttribute{
+ Computed: true,
+ },
+ },
+ CustomType: CreatedByType{
+ ObjectType: types.ObjectType{
+ AttrTypes: CreatedByValue{}.AttributeTypes(ctx),
+ },
+ },
+ Computed: true,
+ Description: "The actor who created the rule",
+ MarkdownDescription: "The actor who created the rule",
+ },
+ "created_by_external": schema.StringAttribute{
+ Computed: true,
+ Description: "The text of the user-provided CreatedBy field when uploaded via CI/CD",
+ MarkdownDescription: "The text of the user-provided CreatedBy field when uploaded via CI/CD",
+ },
+ "dedup_period_minutes": schema.Int64Attribute{
+ Optional: true,
+ Computed: true,
+ Description: "The amount of time in minutes for grouping alerts",
+ MarkdownDescription: "The amount of time in minutes for grouping alerts",
+ Validators: []validator.Int64{
+ int64validator.AtLeast(1),
+ },
+ Default: int64default.StaticInt64(60),
+ },
+ "description": schema.StringAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "The description of the rule",
+ MarkdownDescription: "The description of the rule",
+ },
+ "detection": schema.StringAttribute{
+ Required: true,
+ Description: "The yaml representation of the rule",
+ MarkdownDescription: "The yaml representation of the rule",
+ },
+ "display_name": schema.StringAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "The display name of the rule",
+ MarkdownDescription: "The display name of the rule",
+ },
+ "dynamic_severities": schema.StringAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "The dynamic severity represented in YAML",
+ MarkdownDescription: "The dynamic severity represented in YAML",
+ },
+ "enabled": schema.BoolAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "Determines whether or not the rule is active",
+ MarkdownDescription: "Determines whether or not the rule is active",
+ },
+ "group_by": schema.StringAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "The key on an event to group by represented in YAML",
+ MarkdownDescription: "The key on an event to group by represented in YAML",
+ },
+ "includepython": schema.BoolAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "determines if associated python for the generated rule is returned",
+ MarkdownDescription: "determines if associated python for the generated rule is returned",
+ Default: booldefault.StaticBool(false),
+ },
+ "inline_filters": schema.StringAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "The filter for the rule represented in YAML",
+ MarkdownDescription: "The filter for the rule represented in YAML",
+ },
+ "last_modified": schema.StringAttribute{
+ Computed: true,
+ },
+ "log_types": schema.ListAttribute{
+ ElementType: types.StringType,
+ Optional: true,
+ Computed: true,
+ Description: "log types",
+ MarkdownDescription: "log types",
+ },
+ "managed": schema.BoolAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "Determines if the simple rule is managed by panther",
+ MarkdownDescription: "Determines if the simple rule is managed by panther",
+ },
+ "output_ids": schema.ListAttribute{
+ ElementType: types.StringType,
+ Optional: true,
+ Computed: true,
+ Description: "Destination IDs that override default alert routing based on severity",
+ MarkdownDescription: "Destination IDs that override default alert routing based on severity",
+ },
+ "python_body": schema.StringAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "The python body of the rule",
+ MarkdownDescription: "The python body of the rule",
+ },
+ "reports": schema.MapAttribute{
+ ElementType: types.ListType{
+ ElemType: types.StringType,
+ },
+ Optional: true,
+ Computed: true,
+ Description: "reports",
+ MarkdownDescription: "reports",
+ },
+ "runbook": schema.StringAttribute{
+ Optional: true,
+ Computed: true,
+ Description: "How to handle the generated alert",
+ MarkdownDescription: "How to handle the generated alert",
+ },
+ "severity": schema.StringAttribute{
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf(
+ "INFO",
+ "LOW",
+ "MEDIUM",
+ "HIGH",
+ "CRITICAL",
+ ),
+ },
+ },
+ "summary_attributes": schema.ListAttribute{
+ ElementType: types.StringType,
+ Optional: true,
+ Computed: true,
+ Description: "A list of fields in the event to create top 5 summaries for",
+ MarkdownDescription: "A list of fields in the event to create top 5 summaries for",
+ },
+ "tags": schema.ListAttribute{
+ ElementType: types.StringType,
+ Optional: true,
+ Computed: true,
+ Description: "The tags for the simple rule",
+ MarkdownDescription: "The tags for the simple rule",
+ },
+ "tests": schema.ListNestedAttribute{
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "expected_result": schema.BoolAttribute{
+ Required: true,
+ Description: "The expected result",
+ MarkdownDescription: "The expected result",
+ },
+ "mocks": schema.ListAttribute{
+ ElementType: types.MapType{
+ ElemType: types.StringType,
+ },
+ Optional: true,
+ Computed: true,
+ Description: "mocks",
+ MarkdownDescription: "mocks",
+ },
+ "name": schema.StringAttribute{
+ Required: true,
+ Description: "name",
+ MarkdownDescription: "name",
+ },
+ "resource": schema.StringAttribute{
+ Required: true,
+ Description: "resource",
+ MarkdownDescription: "resource",
+ },
+ },
+ CustomType: TestsType{
+ ObjectType: types.ObjectType{
+ AttrTypes: TestsValue{}.AttributeTypes(ctx),
+ },
+ },
+ },
+ Optional: true,
+ Computed: true,
+ Description: "Unit tests for the Rule. Best practice is to include a positive and negative case",
+ MarkdownDescription: "Unit tests for the Rule. Best practice is to include a positive and negative case",
+ },
+ "threshold": schema.Int64Attribute{
+ Optional: true,
+ Computed: true,
+ Description: "the number of events that must match before an alert is triggered",
+ MarkdownDescription: "the number of events that must match before an alert is triggered",
+ Validators: []validator.Int64{
+ int64validator.AtLeast(1),
+ },
+ Default: int64default.StaticInt64(1),
+ },
+ },
+ }
+}
+
+type SimpleRuleModel struct {
+ Id types.String `tfsdk:"id"`
+ AlertContext types.String `tfsdk:"alert_context"`
+ AlertTitle types.String `tfsdk:"alert_title"`
+ CreatedAt types.String `tfsdk:"created_at"`
+ CreatedBy CreatedByValue `tfsdk:"created_by"`
+ CreatedByExternal types.String `tfsdk:"created_by_external"`
+ DedupPeriodMinutes types.Int64 `tfsdk:"dedup_period_minutes"`
+ Description types.String `tfsdk:"description"`
+ Detection types.String `tfsdk:"detection"`
+ DisplayName types.String `tfsdk:"display_name"`
+ DynamicSeverities types.String `tfsdk:"dynamic_severities"`
+ Enabled types.Bool `tfsdk:"enabled"`
+ GroupBy types.String `tfsdk:"group_by"`
+ Includepython types.Bool `tfsdk:"includepython"`
+ InlineFilters types.String `tfsdk:"inline_filters"`
+ LastModified types.String `tfsdk:"last_modified"`
+ LogTypes types.List `tfsdk:"log_types"`
+ Managed types.Bool `tfsdk:"managed"`
+ OutputIds types.List `tfsdk:"output_ids"`
+ PythonBody types.String `tfsdk:"python_body"`
+ Reports types.Map `tfsdk:"reports"`
+ Runbook types.String `tfsdk:"runbook"`
+ Severity types.String `tfsdk:"severity"`
+ SummaryAttributes types.List `tfsdk:"summary_attributes"`
+ Tags types.List `tfsdk:"tags"`
+ Tests types.List `tfsdk:"tests"`
+ Threshold types.Int64 `tfsdk:"threshold"`
+}
+
+var _ basetypes.ObjectTypable = CreatedByType{}
+
+type CreatedByType struct {
+ basetypes.ObjectType
+}
+
+func (t CreatedByType) Equal(o attr.Type) bool {
+ other, ok := o.(CreatedByType)
+
+ if !ok {
+ return false
+ }
+
+ return t.ObjectType.Equal(other.ObjectType)
+}
+
+func (t CreatedByType) String() string {
+ return "CreatedByType"
+}
+
+func (t CreatedByType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ attributes := in.Attributes()
+
+ idAttribute, ok := attributes["id"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `id is missing from object`)
+
+ return nil, diags
+ }
+
+ idVal, ok := idAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`id expected to be basetypes.StringValue, was: %T`, idAttribute))
+ }
+
+ typeAttribute, ok := attributes["type"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `type is missing from object`)
+
+ return nil, diags
+ }
+
+ typeVal, ok := typeAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`type expected to be basetypes.StringValue, was: %T`, typeAttribute))
+ }
+
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ return CreatedByValue{
+ Id: idVal,
+ CreatedByType: typeVal,
+ state: attr.ValueStateKnown,
+ }, diags
+}
+
+func NewCreatedByValueNull() CreatedByValue {
+ return CreatedByValue{
+ state: attr.ValueStateNull,
+ }
+}
+
+func NewCreatedByValueUnknown() CreatedByValue {
+ return CreatedByValue{
+ state: attr.ValueStateUnknown,
+ }
+}
+
+func NewCreatedByValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (CreatedByValue, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521
+ ctx := context.Background()
+
+ for name, attributeType := range attributeTypes {
+ attribute, ok := attributes[name]
+
+ if !ok {
+ diags.AddError(
+ "Missing CreatedByValue Attribute Value",
+ "While creating a CreatedByValue value, a missing attribute value was detected. "+
+ "A CreatedByValue must contain values for all attributes, even if null or unknown. "+
+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+
+ fmt.Sprintf("CreatedByValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()),
+ )
+
+ continue
+ }
+
+ if !attributeType.Equal(attribute.Type(ctx)) {
+ diags.AddError(
+ "Invalid CreatedByValue Attribute Type",
+ "While creating a CreatedByValue value, an invalid attribute value was detected. "+
+ "A CreatedByValue must use a matching attribute type for the value. "+
+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+
+ fmt.Sprintf("CreatedByValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+
+ fmt.Sprintf("CreatedByValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)),
+ )
+ }
+ }
+
+ for name := range attributes {
+ _, ok := attributeTypes[name]
+
+ if !ok {
+ diags.AddError(
+ "Extra CreatedByValue Attribute Value",
+ "While creating a CreatedByValue value, an extra attribute value was detected. "+
+ "A CreatedByValue must not contain values beyond the expected attribute types. "+
+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+
+ fmt.Sprintf("Extra CreatedByValue Attribute Name: %s", name),
+ )
+ }
+ }
+
+ if diags.HasError() {
+ return NewCreatedByValueUnknown(), diags
+ }
+
+ idAttribute, ok := attributes["id"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `id is missing from object`)
+
+ return NewCreatedByValueUnknown(), diags
+ }
+
+ idVal, ok := idAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`id expected to be basetypes.StringValue, was: %T`, idAttribute))
+ }
+
+ typeAttribute, ok := attributes["type"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `type is missing from object`)
+
+ return NewCreatedByValueUnknown(), diags
+ }
+
+ typeVal, ok := typeAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`type expected to be basetypes.StringValue, was: %T`, typeAttribute))
+ }
+
+ if diags.HasError() {
+ return NewCreatedByValueUnknown(), diags
+ }
+
+ return CreatedByValue{
+ Id: idVal,
+ CreatedByType: typeVal,
+ state: attr.ValueStateKnown,
+ }, diags
+}
+
+func NewCreatedByValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) CreatedByValue {
+ object, diags := NewCreatedByValue(attributeTypes, attributes)
+
+ if diags.HasError() {
+ // This could potentially be added to the diag package.
+ diagsStrings := make([]string, 0, len(diags))
+
+ for _, diagnostic := range diags {
+ diagsStrings = append(diagsStrings, fmt.Sprintf(
+ "%s | %s | %s",
+ diagnostic.Severity(),
+ diagnostic.Summary(),
+ diagnostic.Detail()))
+ }
+
+ panic("NewCreatedByValueMust received error(s): " + strings.Join(diagsStrings, "\n"))
+ }
+
+ return object
+}
+
+func (t CreatedByType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
+ if in.Type() == nil {
+ return NewCreatedByValueNull(), nil
+ }
+
+ if !in.Type().Equal(t.TerraformType(ctx)) {
+ return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type())
+ }
+
+ if !in.IsKnown() {
+ return NewCreatedByValueUnknown(), nil
+ }
+
+ if in.IsNull() {
+ return NewCreatedByValueNull(), nil
+ }
+
+ attributes := map[string]attr.Value{}
+
+ val := map[string]tftypes.Value{}
+
+ err := in.As(&val)
+
+ if err != nil {
+ return nil, err
+ }
+
+ for k, v := range val {
+ a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v)
+
+ if err != nil {
+ return nil, err
+ }
+
+ attributes[k] = a
+ }
+
+ return NewCreatedByValueMust(CreatedByValue{}.AttributeTypes(ctx), attributes), nil
+}
+
+func (t CreatedByType) ValueType(ctx context.Context) attr.Value {
+ return CreatedByValue{}
+}
+
+var _ basetypes.ObjectValuable = CreatedByValue{}
+
+type CreatedByValue struct {
+ Id basetypes.StringValue `tfsdk:"id"`
+ CreatedByType basetypes.StringValue `tfsdk:"type"`
+ state attr.ValueState
+}
+
+func (v CreatedByValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
+ attrTypes := make(map[string]tftypes.Type, 2)
+
+ var val tftypes.Value
+ var err error
+
+ attrTypes["id"] = basetypes.StringType{}.TerraformType(ctx)
+ attrTypes["type"] = basetypes.StringType{}.TerraformType(ctx)
+
+ objectType := tftypes.Object{AttributeTypes: attrTypes}
+
+ switch v.state {
+ case attr.ValueStateKnown:
+ vals := make(map[string]tftypes.Value, 2)
+
+ val, err = v.Id.ToTerraformValue(ctx)
+
+ if err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ vals["id"] = val
+
+ val, err = v.CreatedByType.ToTerraformValue(ctx)
+
+ if err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ vals["type"] = val
+
+ if err := tftypes.ValidateValue(objectType, vals); err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ return tftypes.NewValue(objectType, vals), nil
+ case attr.ValueStateNull:
+ return tftypes.NewValue(objectType, nil), nil
+ case attr.ValueStateUnknown:
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), nil
+ default:
+ panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state))
+ }
+}
+
+func (v CreatedByValue) IsNull() bool {
+ return v.state == attr.ValueStateNull
+}
+
+func (v CreatedByValue) IsUnknown() bool {
+ return v.state == attr.ValueStateUnknown
+}
+
+func (v CreatedByValue) String() string {
+ return "CreatedByValue"
+}
+
+func (v CreatedByValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ attributeTypes := map[string]attr.Type{
+ "id": basetypes.StringType{},
+ "type": basetypes.StringType{},
+ }
+
+ if v.IsNull() {
+ return types.ObjectNull(attributeTypes), diags
+ }
+
+ if v.IsUnknown() {
+ return types.ObjectUnknown(attributeTypes), diags
+ }
+
+ objVal, diags := types.ObjectValue(
+ attributeTypes,
+ map[string]attr.Value{
+ "id": v.Id,
+ "type": v.CreatedByType,
+ })
+
+ return objVal, diags
+}
+
+func (v CreatedByValue) Equal(o attr.Value) bool {
+ other, ok := o.(CreatedByValue)
+
+ if !ok {
+ return false
+ }
+
+ if v.state != other.state {
+ return false
+ }
+
+ if v.state != attr.ValueStateKnown {
+ return true
+ }
+
+ if !v.Id.Equal(other.Id) {
+ return false
+ }
+
+ if !v.CreatedByType.Equal(other.CreatedByType) {
+ return false
+ }
+
+ return true
+}
+
+func (v CreatedByValue) Type(ctx context.Context) attr.Type {
+ return CreatedByType{
+ basetypes.ObjectType{
+ AttrTypes: v.AttributeTypes(ctx),
+ },
+ }
+}
+
+func (v CreatedByValue) AttributeTypes(ctx context.Context) map[string]attr.Type {
+ return map[string]attr.Type{
+ "id": basetypes.StringType{},
+ "type": basetypes.StringType{},
+ }
+}
+
+var _ basetypes.ObjectTypable = TestsType{}
+
+type TestsType struct {
+ basetypes.ObjectType
+}
+
+func (t TestsType) Equal(o attr.Type) bool {
+ other, ok := o.(TestsType)
+
+ if !ok {
+ return false
+ }
+
+ return t.ObjectType.Equal(other.ObjectType)
+}
+
+func (t TestsType) String() string {
+ return "TestsType"
+}
+
+func (t TestsType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ attributes := in.Attributes()
+
+ expectedResultAttribute, ok := attributes["expected_result"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `expected_result is missing from object`)
+
+ return nil, diags
+ }
+
+ expectedResultVal, ok := expectedResultAttribute.(basetypes.BoolValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`expected_result expected to be basetypes.BoolValue, was: %T`, expectedResultAttribute))
+ }
+
+ mocksAttribute, ok := attributes["mocks"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `mocks is missing from object`)
+
+ return nil, diags
+ }
+
+ mocksVal, ok := mocksAttribute.(basetypes.ListValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`mocks expected to be basetypes.ListValue, was: %T`, mocksAttribute))
+ }
+
+ nameAttribute, ok := attributes["name"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `name is missing from object`)
+
+ return nil, diags
+ }
+
+ nameVal, ok := nameAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`name expected to be basetypes.StringValue, was: %T`, nameAttribute))
+ }
+
+ resourceAttribute, ok := attributes["resource"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `resource is missing from object`)
+
+ return nil, diags
+ }
+
+ resourceVal, ok := resourceAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`resource expected to be basetypes.StringValue, was: %T`, resourceAttribute))
+ }
+
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ return TestsValue{
+ ExpectedResult: expectedResultVal,
+ Mocks: mocksVal,
+ Name: nameVal,
+ Resource: resourceVal,
+ state: attr.ValueStateKnown,
+ }, diags
+}
+
+func NewTestsValueNull() TestsValue {
+ return TestsValue{
+ state: attr.ValueStateNull,
+ }
+}
+
+func NewTestsValueUnknown() TestsValue {
+ return TestsValue{
+ state: attr.ValueStateUnknown,
+ }
+}
+
+func NewTestsValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (TestsValue, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521
+ ctx := context.Background()
+
+ for name, attributeType := range attributeTypes {
+ attribute, ok := attributes[name]
+
+ if !ok {
+ diags.AddError(
+ "Missing TestsValue Attribute Value",
+ "While creating a TestsValue value, a missing attribute value was detected. "+
+ "A TestsValue must contain values for all attributes, even if null or unknown. "+
+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+
+ fmt.Sprintf("TestsValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()),
+ )
+
+ continue
+ }
+
+ if !attributeType.Equal(attribute.Type(ctx)) {
+ diags.AddError(
+ "Invalid TestsValue Attribute Type",
+ "While creating a TestsValue value, an invalid attribute value was detected. "+
+ "A TestsValue must use a matching attribute type for the value. "+
+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+
+ fmt.Sprintf("TestsValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+
+ fmt.Sprintf("TestsValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)),
+ )
+ }
+ }
+
+ for name := range attributes {
+ _, ok := attributeTypes[name]
+
+ if !ok {
+ diags.AddError(
+ "Extra TestsValue Attribute Value",
+ "While creating a TestsValue value, an extra attribute value was detected. "+
+ "A TestsValue must not contain values beyond the expected attribute types. "+
+ "This is always an issue with the provider and should be reported to the provider developers.\n\n"+
+ fmt.Sprintf("Extra TestsValue Attribute Name: %s", name),
+ )
+ }
+ }
+
+ if diags.HasError() {
+ return NewTestsValueUnknown(), diags
+ }
+
+ expectedResultAttribute, ok := attributes["expected_result"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `expected_result is missing from object`)
+
+ return NewTestsValueUnknown(), diags
+ }
+
+ expectedResultVal, ok := expectedResultAttribute.(basetypes.BoolValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`expected_result expected to be basetypes.BoolValue, was: %T`, expectedResultAttribute))
+ }
+
+ mocksAttribute, ok := attributes["mocks"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `mocks is missing from object`)
+
+ return NewTestsValueUnknown(), diags
+ }
+
+ mocksVal, ok := mocksAttribute.(basetypes.ListValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`mocks expected to be basetypes.ListValue, was: %T`, mocksAttribute))
+ }
+
+ nameAttribute, ok := attributes["name"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `name is missing from object`)
+
+ return NewTestsValueUnknown(), diags
+ }
+
+ nameVal, ok := nameAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`name expected to be basetypes.StringValue, was: %T`, nameAttribute))
+ }
+
+ resourceAttribute, ok := attributes["resource"]
+
+ if !ok {
+ diags.AddError(
+ "Attribute Missing",
+ `resource is missing from object`)
+
+ return NewTestsValueUnknown(), diags
+ }
+
+ resourceVal, ok := resourceAttribute.(basetypes.StringValue)
+
+ if !ok {
+ diags.AddError(
+ "Attribute Wrong Type",
+ fmt.Sprintf(`resource expected to be basetypes.StringValue, was: %T`, resourceAttribute))
+ }
+
+ if diags.HasError() {
+ return NewTestsValueUnknown(), diags
+ }
+
+ return TestsValue{
+ ExpectedResult: expectedResultVal,
+ Mocks: mocksVal,
+ Name: nameVal,
+ Resource: resourceVal,
+ state: attr.ValueStateKnown,
+ }, diags
+}
+
+func NewTestsValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) TestsValue {
+ object, diags := NewTestsValue(attributeTypes, attributes)
+
+ if diags.HasError() {
+ // This could potentially be added to the diag package.
+ diagsStrings := make([]string, 0, len(diags))
+
+ for _, diagnostic := range diags {
+ diagsStrings = append(diagsStrings, fmt.Sprintf(
+ "%s | %s | %s",
+ diagnostic.Severity(),
+ diagnostic.Summary(),
+ diagnostic.Detail()))
+ }
+
+ panic("NewTestsValueMust received error(s): " + strings.Join(diagsStrings, "\n"))
+ }
+
+ return object
+}
+
+func (t TestsType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
+ if in.Type() == nil {
+ return NewTestsValueNull(), nil
+ }
+
+ if !in.Type().Equal(t.TerraformType(ctx)) {
+ return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type())
+ }
+
+ if !in.IsKnown() {
+ return NewTestsValueUnknown(), nil
+ }
+
+ if in.IsNull() {
+ return NewTestsValueNull(), nil
+ }
+
+ attributes := map[string]attr.Value{}
+
+ val := map[string]tftypes.Value{}
+
+ err := in.As(&val)
+
+ if err != nil {
+ return nil, err
+ }
+
+ for k, v := range val {
+ a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v)
+
+ if err != nil {
+ return nil, err
+ }
+
+ attributes[k] = a
+ }
+
+ return NewTestsValueMust(TestsValue{}.AttributeTypes(ctx), attributes), nil
+}
+
+func (t TestsType) ValueType(ctx context.Context) attr.Value {
+ return TestsValue{}
+}
+
+var _ basetypes.ObjectValuable = TestsValue{}
+
+type TestsValue struct {
+ ExpectedResult basetypes.BoolValue `tfsdk:"expected_result"`
+ Mocks basetypes.ListValue `tfsdk:"mocks"`
+ Name basetypes.StringValue `tfsdk:"name"`
+ Resource basetypes.StringValue `tfsdk:"resource"`
+ state attr.ValueState
+}
+
+func (v TestsValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
+ attrTypes := make(map[string]tftypes.Type, 4)
+
+ var val tftypes.Value
+ var err error
+
+ attrTypes["expected_result"] = basetypes.BoolType{}.TerraformType(ctx)
+ attrTypes["mocks"] = basetypes.ListType{
+ ElemType: types.MapType{
+ ElemType: types.StringType,
+ },
+ }.TerraformType(ctx)
+ attrTypes["name"] = basetypes.StringType{}.TerraformType(ctx)
+ attrTypes["resource"] = basetypes.StringType{}.TerraformType(ctx)
+
+ objectType := tftypes.Object{AttributeTypes: attrTypes}
+
+ switch v.state {
+ case attr.ValueStateKnown:
+ vals := make(map[string]tftypes.Value, 4)
+
+ val, err = v.ExpectedResult.ToTerraformValue(ctx)
+
+ if err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ vals["expected_result"] = val
+
+ val, err = v.Mocks.ToTerraformValue(ctx)
+
+ if err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ vals["mocks"] = val
+
+ val, err = v.Name.ToTerraformValue(ctx)
+
+ if err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ vals["name"] = val
+
+ val, err = v.Resource.ToTerraformValue(ctx)
+
+ if err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ vals["resource"] = val
+
+ if err := tftypes.ValidateValue(objectType, vals); err != nil {
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), err
+ }
+
+ return tftypes.NewValue(objectType, vals), nil
+ case attr.ValueStateNull:
+ return tftypes.NewValue(objectType, nil), nil
+ case attr.ValueStateUnknown:
+ return tftypes.NewValue(objectType, tftypes.UnknownValue), nil
+ default:
+ panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state))
+ }
+}
+
+func (v TestsValue) IsNull() bool {
+ return v.state == attr.ValueStateNull
+}
+
+func (v TestsValue) IsUnknown() bool {
+ return v.state == attr.ValueStateUnknown
+}
+
+func (v TestsValue) String() string {
+ return "TestsValue"
+}
+
+func (v TestsValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ var mocksVal basetypes.ListValue
+ switch {
+ case v.Mocks.IsUnknown():
+ mocksVal = types.ListUnknown(types.MapType{
+ ElemType: types.StringType,
+ })
+ case v.Mocks.IsNull():
+ mocksVal = types.ListNull(types.MapType{
+ ElemType: types.StringType,
+ })
+ default:
+ var d diag.Diagnostics
+ mocksVal, d = types.ListValue(types.MapType{
+ ElemType: types.StringType,
+ }, v.Mocks.Elements())
+ diags.Append(d...)
+ }
+
+ if diags.HasError() {
+ return types.ObjectUnknown(map[string]attr.Type{
+ "expected_result": basetypes.BoolType{},
+ "mocks": basetypes.ListType{
+ ElemType: types.MapType{
+ ElemType: types.StringType,
+ },
+ },
+ "name": basetypes.StringType{},
+ "resource": basetypes.StringType{},
+ }), diags
+ }
+
+ attributeTypes := map[string]attr.Type{
+ "expected_result": basetypes.BoolType{},
+ "mocks": basetypes.ListType{
+ ElemType: types.MapType{
+ ElemType: types.StringType,
+ },
+ },
+ "name": basetypes.StringType{},
+ "resource": basetypes.StringType{},
+ }
+
+ if v.IsNull() {
+ return types.ObjectNull(attributeTypes), diags
+ }
+
+ if v.IsUnknown() {
+ return types.ObjectUnknown(attributeTypes), diags
+ }
+
+ objVal, diags := types.ObjectValue(
+ attributeTypes,
+ map[string]attr.Value{
+ "expected_result": v.ExpectedResult,
+ "mocks": mocksVal,
+ "name": v.Name,
+ "resource": v.Resource,
+ })
+
+ return objVal, diags
+}
+
+func (v TestsValue) Equal(o attr.Value) bool {
+ other, ok := o.(TestsValue)
+
+ if !ok {
+ return false
+ }
+
+ if v.state != other.state {
+ return false
+ }
+
+ if v.state != attr.ValueStateKnown {
+ return true
+ }
+
+ if !v.ExpectedResult.Equal(other.ExpectedResult) {
+ return false
+ }
+
+ if !v.Mocks.Equal(other.Mocks) {
+ return false
+ }
+
+ if !v.Name.Equal(other.Name) {
+ return false
+ }
+
+ if !v.Resource.Equal(other.Resource) {
+ return false
+ }
+
+ return true
+}
+
+func (v TestsValue) Type(ctx context.Context) attr.Type {
+ return TestsType{
+ basetypes.ObjectType{
+ AttrTypes: v.AttributeTypes(ctx),
+ },
+ }
+}
+
+func (v TestsValue) AttributeTypes(ctx context.Context) map[string]attr.Type {
+ return map[string]attr.Type{
+ "expected_result": basetypes.BoolType{},
+ "mocks": basetypes.ListType{
+ ElemType: types.MapType{
+ ElemType: types.StringType,
+ },
+ },
+ "name": basetypes.StringType{},
+ "resource": basetypes.StringType{},
+ }
+}
diff --git a/internal/provider/resource_simple_rule_test.go b/internal/provider/resource_simple_rule_test.go
new file mode 100644
index 0000000..37b55c2
--- /dev/null
+++ b/internal/provider/resource_simple_rule_test.go
@@ -0,0 +1,87 @@
+/*
+Copyright 2023 Panther Labs, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package provider
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ "github.com/google/uuid"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+)
+
+func TestSimpleRuleResource(t *testing.T) {
+ simpleRuleName := strings.ReplaceAll(uuid.NewString(), "-", "")
+ simpleRuleUpdatedName := strings.ReplaceAll(uuid.NewString(), "-", "")
+
+ resource.Test(t, resource.TestCase{
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ // Create and Read testing
+ {
+ Config: providerConfig + testAccSimpleRuleResourceConfig(simpleRuleName),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("panther_simple_rule.test", "display_name", simpleRuleName),
+ resource.TestCheckResourceAttr("panther_simple_rule.test", "enabled", "true"),
+ resource.TestCheckResourceAttr("panther_simple_rule.test", "severity", "CRITICAL"),
+ resource.TestCheckResourceAttr("panther_simple_rule.test", "log_types.#", "1"),
+ resource.TestCheckResourceAttr("panther_simple_rule.test", "log_types.0", "AWS.CloudTrail"),
+ resource.TestCheckResourceAttr("panther_simple_rule.test", "dedup_period_minutes", "60"),
+ resource.TestCheckResourceAttr("panther_simple_rule.test", "threshold", "1"),
+ resource.TestCheckResourceAttrSet("panther_simple_rule.test", "id"),
+ resource.TestCheckResourceAttrSet("panther_simple_rule.test", "detection"),
+ ),
+ },
+ // ImportState testing
+ {
+ ResourceName: "panther_simple_rule.test",
+ ImportState: true,
+ ImportStateVerify: true,
+ ImportStateVerifyIgnore: []string{"tags"},
+ },
+ // Update and Read testing
+ {
+ Config: providerConfig + testAccSimpleRuleResourceConfig(simpleRuleUpdatedName),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("panther_simple_rule.test", "display_name", simpleRuleUpdatedName),
+ ),
+ },
+ },
+ })
+}
+
+func testAccSimpleRuleResourceConfig(name string) string {
+ return fmt.Sprintf(`
+resource "panther_simple_rule" "test" {
+ display_name = %[1]q
+ detection = <<-EOT
+ MatchFilters:
+ - Key: eventName
+ Condition: Equals
+ Values:
+ - ConsoleLogin
+ EOT
+ enabled = true
+ log_types = ["AWS.CloudTrail"]
+ severity = "CRITICAL"
+ dedup_period_minutes = 60
+ threshold = 1
+ tags = ["test", "terraform"]
+}
+`, name)
+}
diff --git a/provider-code-spec.json b/provider-code-spec.json
index eadc0dc..833d712 100644
--- a/provider-code-spec.json
+++ b/provider-code-spec.json
@@ -105,6 +105,13 @@
"computed_optional_required": "computed_optional",
"description": "Path to the array value to extract elements from, only applicable if logStreamType is JsonArray. Leave empty if the input JSON is an array itself"
}
+ },
+ {
+ "name": "xml_root_element",
+ "string": {
+ "computed_optional_required": "computed_optional",
+ "description": "The root element name for XML streams, only applicable if logStreamType is XML. Leave empty if the XML events are not enclosed in a root element"
+ }
}
]
}
@@ -128,6 +135,1018 @@
}
]
}
+ },
+ {
+ "name": "policy",
+ "schema": {
+ "attributes": [
+ {
+ "name": "body",
+ "string": {
+ "computed_optional_required": "required",
+ "description": "The python body of the policy"
+ }
+ },
+ {
+ "name": "description",
+ "string": {
+ "computed_optional_required": "computed_optional",
+ "description": "The description of the policy"
+ }
+ },
+ {
+ "name": "display_name",
+ "string": {
+ "computed_optional_required": "computed_optional",
+ "description": "The display name of the policy"
+ }
+ },
+ {
+ "name": "enabled",
+ "bool": {
+ "computed_optional_required": "computed_optional",
+ "description": "Determines whether or not the policy is active"
+ }
+ },
+ {
+ "name": "managed",
+ "bool": {
+ "computed_optional_required": "computed_optional",
+ "description": "Determines if the policy is managed by panther"
+ }
+ },
+ {
+ "name": "output_ids",
+ "list": {
+ "computed_optional_required": "computed_optional",
+ "element_type": {
+ "string": {}
+ },
+ "description": "Destination IDs that override default alert routing based on severity"
+ }
+ },
+ {
+ "name": "reports",
+ "map": {
+ "computed_optional_required": "computed_optional",
+ "element_type": {
+ "list": {
+ "element_type": {
+ "string": {}
+ }
+ }
+ },
+ "description": "Reports"
+ }
+ },
+ {
+ "name": "resource_types",
+ "list": {
+ "computed_optional_required": "computed_optional",
+ "element_type": {
+ "string": {}
+ },
+ "description": "Resource types"
+ }
+ },
+ {
+ "name": "severity",
+ "string": {
+ "computed_optional_required": "required",
+ "validators": [
+ {
+ "custom": {
+ "imports": [
+ {
+ "path": "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ }
+ ],
+ "schema_definition": "stringvalidator.OneOf(\n\"INFO\",\n\"LOW\",\n\"MEDIUM\",\n\"HIGH\",\n\"CRITICAL\",\n)"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "name": "suppressions",
+ "list": {
+ "computed_optional_required": "computed_optional",
+ "element_type": {
+ "string": {}
+ },
+ "description": "Resources to ignore via a pattern that matches the resource id"
+ }
+ },
+ {
+ "name": "tags",
+ "list": {
+ "computed_optional_required": "computed_optional",
+ "element_type": {
+ "string": {}
+ },
+ "description": "The tags for the policy"
+ }
+ },
+ {
+ "name": "tests",
+ "list_nested": {
+ "computed_optional_required": "computed_optional",
+ "nested_object": {
+ "attributes": [
+ {
+ "name": "expected_result",
+ "bool": {
+ "computed_optional_required": "required",
+ "description": "The expected result"
+ }
+ },
+ {
+ "name": "mocks",
+ "list": {
+ "computed_optional_required": "computed_optional",
+ "element_type": {
+ "map": {
+ "element_type": {
+ "string": {}
+ }
+ }
+ },
+ "description": "mocks"
+ }
+ },
+ {
+ "name": "name",
+ "string": {
+ "computed_optional_required": "required",
+ "description": "name"
+ }
+ },
+ {
+ "name": "resource",
+ "string": {
+ "computed_optional_required": "required",
+ "description": "resource"
+ }
+ }
+ ]
+ },
+ "description": "Unit tests for the Policy. Best practice is to include a positive and negative case"
+ }
+ },
+ {
+ "name": "created_at",
+ "string": {
+ "computed_optional_required": "computed"
+ }
+ },
+ {
+ "name": "created_by",
+ "single_nested": {
+ "computed_optional_required": "computed",
+ "attributes": [
+ {
+ "name": "id",
+ "string": {
+ "computed_optional_required": "computed"
+ }
+ },
+ {
+ "name": "type",
+ "string": {
+ "computed_optional_required": "computed"
+ }
+ }
+ ],
+ "description": "The actor who created the rule"
+ }
+ },
+ {
+ "name": "created_by_external",
+ "string": {
+ "computed_optional_required": "computed",
+ "description": "The text of the user-provided CreatedBy field when uploaded via CI/CD"
+ }
+ },
+ {
+ "name": "last_modified",
+ "string": {
+ "computed_optional_required": "computed"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "name": "rule",
+ "schema": {
+ "attributes": [
+ {
+ "name": "body",
+ "string": {
+ "computed_optional_required": "required",
+ "description": "The python body of the rule"
+ }
+ },
+ {
+ "name": "dedup_period_minutes",
+ "int64": {
+ "computed_optional_required": "computed_optional",
+ "default": {
+ "static": 60
+ },
+ "description": "The amount of time in minutes for grouping alerts",
+ "validators": [
+ {
+ "custom": {
+ "imports": [
+ {
+ "path": "github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
+ }
+ ],
+ "schema_definition": "int64validator.AtLeast(1)"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "name": "description",
+ "string": {
+ "computed_optional_required": "computed_optional",
+ "description": "The description of the rule"
+ }
+ },
+ {
+ "name": "display_name",
+ "string": {
+ "computed_optional_required": "computed_optional",
+ "description": "The display name of the rule"
+ }
+ },
+ {
+ "name": "enabled",
+ "bool": {
+ "computed_optional_required": "computed_optional",
+ "description": "Determines whether or not the rule is active"
+ }
+ },
+ {
+ "name": "inline_filters",
+ "string": {
+ "computed_optional_required": "computed_optional",
+ "description": "The filter for the rule represented in YAML"
+ }
+ },
+ {
+ "name": "log_types",
+ "list": {
+ "computed_optional_required": "computed_optional",
+ "element_type": {
+ "string": {}
+ },
+ "description": "log types"
+ }
+ },
+ {
+ "name": "managed",
+ "bool": {
+ "computed_optional_required": "computed_optional",
+ "description": "Determines if the rule is managed by panther"
+ }
+ },
+ {
+ "name": "output_ids",
+ "list": {
+ "computed_optional_required": "computed_optional",
+ "element_type": {
+ "string": {}
+ },
+ "description": "Destination IDs that override default alert routing based on severity"
+ }
+ },
+ {
+ "name": "reports",
+ "map": {
+ "computed_optional_required": "computed_optional",
+ "element_type": {
+ "list": {
+ "element_type": {
+ "string": {}
+ }
+ }
+ },
+ "description": "reports"
+ }
+ },
+ {
+ "name": "runbook",
+ "string": {
+ "computed_optional_required": "computed_optional",
+ "description": "How to handle the generated alert"
+ }
+ },
+ {
+ "name": "severity",
+ "string": {
+ "computed_optional_required": "required",
+ "validators": [
+ {
+ "custom": {
+ "imports": [
+ {
+ "path": "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ }
+ ],
+ "schema_definition": "stringvalidator.OneOf(\n\"INFO\",\n\"LOW\",\n\"MEDIUM\",\n\"HIGH\",\n\"CRITICAL\",\n)"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "name": "summary_attributes",
+ "list": {
+ "computed_optional_required": "computed_optional",
+ "element_type": {
+ "string": {}
+ },
+ "description": "A list of fields in the event to create top 5 summaries for"
+ }
+ },
+ {
+ "name": "tags",
+ "list": {
+ "computed_optional_required": "computed_optional",
+ "element_type": {
+ "string": {}
+ },
+ "description": "The tags for the rule"
+ }
+ },
+ {
+ "name": "tests",
+ "list_nested": {
+ "computed_optional_required": "computed_optional",
+ "nested_object": {
+ "attributes": [
+ {
+ "name": "expected_result",
+ "bool": {
+ "computed_optional_required": "required",
+ "description": "The expected result"
+ }
+ },
+ {
+ "name": "mocks",
+ "list": {
+ "computed_optional_required": "computed_optional",
+ "element_type": {
+ "map": {
+ "element_type": {
+ "string": {}
+ }
+ }
+ },
+ "description": "mocks"
+ }
+ },
+ {
+ "name": "name",
+ "string": {
+ "computed_optional_required": "required",
+ "description": "name"
+ }
+ },
+ {
+ "name": "resource",
+ "string": {
+ "computed_optional_required": "required",
+ "description": "resource"
+ }
+ }
+ ]
+ },
+ "description": "Unit tests for the Rule. Best practice is to include a positive and negative case"
+ }
+ },
+ {
+ "name": "threshold",
+ "int64": {
+ "computed_optional_required": "computed_optional",
+ "default": {
+ "static": 1
+ },
+ "description": "the number of events that must match before an alert is triggered",
+ "validators": [
+ {
+ "custom": {
+ "imports": [
+ {
+ "path": "github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
+ }
+ ],
+ "schema_definition": "int64validator.AtLeast(1)"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "name": "created_at",
+ "string": {
+ "computed_optional_required": "computed"
+ }
+ },
+ {
+ "name": "created_by",
+ "single_nested": {
+ "computed_optional_required": "computed",
+ "attributes": [
+ {
+ "name": "id",
+ "string": {
+ "computed_optional_required": "computed"
+ }
+ },
+ {
+ "name": "type",
+ "string": {
+ "computed_optional_required": "computed"
+ }
+ }
+ ],
+ "description": "The actor who created the rule"
+ }
+ },
+ {
+ "name": "created_by_external",
+ "string": {
+ "computed_optional_required": "computed",
+ "description": "The text of the user-provided CreatedBy field when uploaded via CI/CD"
+ }
+ },
+ {
+ "name": "last_modified",
+ "string": {
+ "computed_optional_required": "computed"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "name": "scheduled_rule",
+ "schema": {
+ "attributes": [
+ {
+ "name": "body",
+ "string": {
+ "computed_optional_required": "required",
+ "description": "The python body of the scheduled rule"
+ }
+ },
+ {
+ "name": "dedup_period_minutes",
+ "int64": {
+ "computed_optional_required": "computed_optional",
+ "default": {
+ "static": 60
+ },
+ "description": "The amount of time in minutes for grouping alerts",
+ "validators": [
+ {
+ "custom": {
+ "imports": [
+ {
+ "path": "github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
+ }
+ ],
+ "schema_definition": "int64validator.AtLeast(1)"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "name": "description",
+ "string": {
+ "computed_optional_required": "computed_optional",
+ "description": "The description of the scheduled rule"
+ }
+ },
+ {
+ "name": "display_name",
+ "string": {
+ "computed_optional_required": "computed_optional",
+ "description": "The display name of the scheduled rule"
+ }
+ },
+ {
+ "name": "enabled",
+ "bool": {
+ "computed_optional_required": "computed_optional",
+ "description": "Determines whether or not the scheduled rule is active"
+ }
+ },
+ {
+ "name": "managed",
+ "bool": {
+ "computed_optional_required": "computed_optional",
+ "description": "Determines if the scheduled rule is managed by panther"
+ }
+ },
+ {
+ "name": "output_ids",
+ "list": {
+ "computed_optional_required": "computed_optional",
+ "element_type": {
+ "string": {}
+ },
+ "description": "Destination IDs that override default alert routing based on severity"
+ }
+ },
+ {
+ "name": "reports",
+ "map": {
+ "computed_optional_required": "computed_optional",
+ "element_type": {
+ "list": {
+ "element_type": {
+ "string": {}
+ }
+ }
+ },
+ "description": "reports"
+ }
+ },
+ {
+ "name": "runbook",
+ "string": {
+ "computed_optional_required": "computed_optional",
+ "description": "How to handle the generated alert"
+ }
+ },
+ {
+ "name": "scheduled_queries",
+ "list": {
+ "computed_optional_required": "computed_optional",
+ "element_type": {
+ "string": {}
+ },
+ "description": "the queries that this scheduled rule utilizes"
+ }
+ },
+ {
+ "name": "severity",
+ "string": {
+ "computed_optional_required": "required",
+ "validators": [
+ {
+ "custom": {
+ "imports": [
+ {
+ "path": "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ }
+ ],
+ "schema_definition": "stringvalidator.OneOf(\n\"INFO\",\n\"LOW\",\n\"MEDIUM\",\n\"HIGH\",\n\"CRITICAL\",\n)"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "name": "summary_attributes",
+ "list": {
+ "computed_optional_required": "computed_optional",
+ "element_type": {
+ "string": {}
+ },
+ "description": "A list of fields in the event to create top 5 summaries for"
+ }
+ },
+ {
+ "name": "tags",
+ "list": {
+ "computed_optional_required": "computed_optional",
+ "element_type": {
+ "string": {}
+ },
+ "description": "The tags for the scheduled rule"
+ }
+ },
+ {
+ "name": "tests",
+ "list_nested": {
+ "computed_optional_required": "computed_optional",
+ "nested_object": {
+ "attributes": [
+ {
+ "name": "expected_result",
+ "bool": {
+ "computed_optional_required": "required",
+ "description": "The expected result"
+ }
+ },
+ {
+ "name": "mocks",
+ "list": {
+ "computed_optional_required": "computed_optional",
+ "element_type": {
+ "map": {
+ "element_type": {
+ "string": {}
+ }
+ }
+ },
+ "description": "mocks"
+ }
+ },
+ {
+ "name": "name",
+ "string": {
+ "computed_optional_required": "required",
+ "description": "name"
+ }
+ },
+ {
+ "name": "resource",
+ "string": {
+ "computed_optional_required": "required",
+ "description": "resource"
+ }
+ }
+ ]
+ },
+ "description": "Unit tests for the Rule. Best practice is to include a positive and negative case"
+ }
+ },
+ {
+ "name": "threshold",
+ "int64": {
+ "computed_optional_required": "computed_optional",
+ "default": {
+ "static": 1
+ },
+ "description": "the number of events that must match before an alert is triggered",
+ "validators": [
+ {
+ "custom": {
+ "imports": [
+ {
+ "path": "github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
+ }
+ ],
+ "schema_definition": "int64validator.AtLeast(1)"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "name": "created_at",
+ "string": {
+ "computed_optional_required": "computed"
+ }
+ },
+ {
+ "name": "created_by",
+ "single_nested": {
+ "computed_optional_required": "computed",
+ "attributes": [
+ {
+ "name": "id",
+ "string": {
+ "computed_optional_required": "computed"
+ }
+ },
+ {
+ "name": "type",
+ "string": {
+ "computed_optional_required": "computed"
+ }
+ }
+ ],
+ "description": "The actor who created the rule"
+ }
+ },
+ {
+ "name": "created_by_external",
+ "string": {
+ "computed_optional_required": "computed",
+ "description": "The text of the user-provided CreatedBy field when uploaded via CI/CD"
+ }
+ },
+ {
+ "name": "last_modified",
+ "string": {
+ "computed_optional_required": "computed"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "name": "simple_rule",
+ "schema": {
+ "attributes": [
+ {
+ "name": "alert_context",
+ "string": {
+ "computed_optional_required": "computed_optional",
+ "description": "The alert context represented in YAML"
+ }
+ },
+ {
+ "name": "alert_title",
+ "string": {
+ "computed_optional_required": "computed_optional",
+ "description": "The alert title represented in YAML"
+ }
+ },
+ {
+ "name": "dedup_period_minutes",
+ "int64": {
+ "computed_optional_required": "computed_optional",
+ "default": {
+ "static": 60
+ },
+ "description": "The amount of time in minutes for grouping alerts",
+ "validators": [
+ {
+ "custom": {
+ "imports": [
+ {
+ "path": "github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
+ }
+ ],
+ "schema_definition": "int64validator.AtLeast(1)"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "name": "description",
+ "string": {
+ "computed_optional_required": "computed_optional",
+ "description": "The description of the rule"
+ }
+ },
+ {
+ "name": "detection",
+ "string": {
+ "computed_optional_required": "required",
+ "description": "The yaml representation of the rule"
+ }
+ },
+ {
+ "name": "display_name",
+ "string": {
+ "computed_optional_required": "computed_optional",
+ "description": "The display name of the rule"
+ }
+ },
+ {
+ "name": "dynamic_severities",
+ "string": {
+ "computed_optional_required": "computed_optional",
+ "description": "The dynamic severity represented in YAML"
+ }
+ },
+ {
+ "name": "enabled",
+ "bool": {
+ "computed_optional_required": "computed_optional",
+ "description": "Determines whether or not the rule is active"
+ }
+ },
+ {
+ "name": "group_by",
+ "string": {
+ "computed_optional_required": "computed_optional",
+ "description": "The key on an event to group by represented in YAML"
+ }
+ },
+ {
+ "name": "inline_filters",
+ "string": {
+ "computed_optional_required": "computed_optional",
+ "description": "The filter for the rule represented in YAML"
+ }
+ },
+ {
+ "name": "log_types",
+ "list": {
+ "computed_optional_required": "computed_optional",
+ "element_type": {
+ "string": {}
+ },
+ "description": "log types"
+ }
+ },
+ {
+ "name": "managed",
+ "bool": {
+ "computed_optional_required": "computed_optional",
+ "description": "Determines if the simple rule is managed by panther"
+ }
+ },
+ {
+ "name": "output_ids",
+ "list": {
+ "computed_optional_required": "computed_optional",
+ "element_type": {
+ "string": {}
+ },
+ "description": "Destination IDs that override default alert routing based on severity"
+ }
+ },
+ {
+ "name": "python_body",
+ "string": {
+ "computed_optional_required": "computed_optional",
+ "description": "The python body of the rule"
+ }
+ },
+ {
+ "name": "reports",
+ "map": {
+ "computed_optional_required": "computed_optional",
+ "element_type": {
+ "list": {
+ "element_type": {
+ "string": {}
+ }
+ }
+ },
+ "description": "reports"
+ }
+ },
+ {
+ "name": "runbook",
+ "string": {
+ "computed_optional_required": "computed_optional",
+ "description": "How to handle the generated alert"
+ }
+ },
+ {
+ "name": "severity",
+ "string": {
+ "computed_optional_required": "required",
+ "validators": [
+ {
+ "custom": {
+ "imports": [
+ {
+ "path": "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ }
+ ],
+ "schema_definition": "stringvalidator.OneOf(\n\"INFO\",\n\"LOW\",\n\"MEDIUM\",\n\"HIGH\",\n\"CRITICAL\",\n)"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "name": "summary_attributes",
+ "list": {
+ "computed_optional_required": "computed_optional",
+ "element_type": {
+ "string": {}
+ },
+ "description": "A list of fields in the event to create top 5 summaries for"
+ }
+ },
+ {
+ "name": "tags",
+ "list": {
+ "computed_optional_required": "computed_optional",
+ "element_type": {
+ "string": {}
+ },
+ "description": "The tags for the simple rule"
+ }
+ },
+ {
+ "name": "tests",
+ "list_nested": {
+ "computed_optional_required": "computed_optional",
+ "nested_object": {
+ "attributes": [
+ {
+ "name": "expected_result",
+ "bool": {
+ "computed_optional_required": "required",
+ "description": "The expected result"
+ }
+ },
+ {
+ "name": "mocks",
+ "list": {
+ "computed_optional_required": "computed_optional",
+ "element_type": {
+ "map": {
+ "element_type": {
+ "string": {}
+ }
+ }
+ },
+ "description": "mocks"
+ }
+ },
+ {
+ "name": "name",
+ "string": {
+ "computed_optional_required": "required",
+ "description": "name"
+ }
+ },
+ {
+ "name": "resource",
+ "string": {
+ "computed_optional_required": "required",
+ "description": "resource"
+ }
+ }
+ ]
+ },
+ "description": "Unit tests for the Rule. Best practice is to include a positive and negative case"
+ }
+ },
+ {
+ "name": "threshold",
+ "int64": {
+ "computed_optional_required": "computed_optional",
+ "default": {
+ "static": 1
+ },
+ "description": "the number of events that must match before an alert is triggered",
+ "validators": [
+ {
+ "custom": {
+ "imports": [
+ {
+ "path": "github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
+ }
+ ],
+ "schema_definition": "int64validator.AtLeast(1)"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "name": "created_at",
+ "string": {
+ "computed_optional_required": "computed"
+ }
+ },
+ {
+ "name": "created_by",
+ "single_nested": {
+ "computed_optional_required": "computed",
+ "attributes": [
+ {
+ "name": "id",
+ "string": {
+ "computed_optional_required": "computed"
+ }
+ },
+ {
+ "name": "type",
+ "string": {
+ "computed_optional_required": "computed"
+ }
+ }
+ ],
+ "description": "The actor who created the rule"
+ }
+ },
+ {
+ "name": "created_by_external",
+ "string": {
+ "computed_optional_required": "computed",
+ "description": "The text of the user-provided CreatedBy field when uploaded via CI/CD"
+ }
+ },
+ {
+ "name": "last_modified",
+ "string": {
+ "computed_optional_required": "computed"
+ }
+ },
+ {
+ "name": "includepython",
+ "bool": {
+ "computed_optional_required": "computed_optional",
+ "default": {
+ "static": false
+ },
+ "description": "determines if associated python for the generated rule is returned"
+ }
+ }
+ ]
+ }
}
],
"version": "0.1"