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"