From 9937ac5ee26f3248eb26f113daba6acc6249d925 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Mon, 13 Oct 2025 11:53:36 -0700 Subject: [PATCH 01/16] Add support for threat --- .../security_detection_rule/acc_test.go | 249 ++++++++++++++++++ .../kibana/security_detection_rule/models.go | 45 ++++ .../security_detection_rule/models_eql.go | 6 + .../security_detection_rule/models_esql.go | 6 + .../models_from_api_type_utils.go | 99 +++++++ .../models_machine_learning.go | 7 + .../models_new_terms.go | 6 + .../security_detection_rule/models_query.go | 6 + .../models_saved_query.go | 6 + .../models_threat_match.go | 6 + .../models_threshold.go | 6 + .../models_to_api_type_utils.go | 84 ++++++ .../kibana/security_detection_rule/schema.go | 16 ++ 13 files changed, 542 insertions(+) diff --git a/internal/kibana/security_detection_rule/acc_test.go b/internal/kibana/security_detection_rule/acc_test.go index 9ba1f4a25..3f4a21f56 100644 --- a/internal/kibana/security_detection_rule/acc_test.go +++ b/internal/kibana/security_detection_rule/acc_test.go @@ -4522,3 +4522,252 @@ resource "elasticstack_kibana_security_detection_rule" "test" { } `, name) } + +func TestAccResourceSecurityDetectionRule_QueryWithMitreThreat(t *testing.T) { + resourceName := "elasticstack_kibana_security_detection_rule.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.Providers, + CheckDestroy: testAccCheckSecurityDetectionRuleDestroy, + Steps: []resource.TestStep{ + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionSupport), + Config: testAccSecurityDetectionRuleConfig_queryWithMitreThreat("test-query-mitre-rule"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", "test-query-mitre-rule"), + resource.TestCheckResourceAttr(resourceName, "type", "query"), + resource.TestCheckResourceAttr(resourceName, "query", "process.parent.name:(EXCEL.EXE OR WINWORD.EXE OR POWERPNT.EXE OR OUTLOOK.EXE)"), + resource.TestCheckResourceAttr(resourceName, "language", "kuery"), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "description", "Detects processes started by MS Office programs"), + resource.TestCheckResourceAttr(resourceName, "severity", "low"), + resource.TestCheckResourceAttr(resourceName, "risk_score", "50"), + resource.TestCheckResourceAttr(resourceName, "from", "now-70m"), + resource.TestCheckResourceAttr(resourceName, "to", "now"), + resource.TestCheckResourceAttr(resourceName, "interval", "1h"), + resource.TestCheckResourceAttr(resourceName, "index.0", "logs-*"), + resource.TestCheckResourceAttr(resourceName, "index.1", "winlogbeat-*"), + resource.TestCheckResourceAttr(resourceName, "max_signals", "100"), + + // Check tags + resource.TestCheckResourceAttr(resourceName, "tags.#", "3"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "child process"), + resource.TestCheckResourceAttr(resourceName, "tags.1", "ms office"), + resource.TestCheckResourceAttr(resourceName, "tags.2", "terraform-test"), + + // Check references + resource.TestCheckResourceAttr(resourceName, "references.#", "1"), + resource.TestCheckResourceAttr(resourceName, "references.0", "https://attack.mitre.org/techniques/T1566/001/"), + + // Check false positives + resource.TestCheckResourceAttr(resourceName, "false_positives.#", "1"), + resource.TestCheckResourceAttr(resourceName, "false_positives.0", "Legitimate corporate macros"), + + // Check author + resource.TestCheckResourceAttr(resourceName, "author.#", "1"), + resource.TestCheckResourceAttr(resourceName, "author.0", "Security Team"), + + // Check license + resource.TestCheckResourceAttr(resourceName, "license", "Elastic License v2"), + + // Check note + resource.TestCheckResourceAttr(resourceName, "note", "Investigate parent process and command line"), + + // Check threat (MITRE ATT&CK) + resource.TestCheckResourceAttr(resourceName, "threat.#", "1"), + resource.TestCheckResourceAttr(resourceName, "threat.0.framework", "MITRE ATT&CK"), + resource.TestCheckResourceAttr(resourceName, "threat.0.tactic.id", "TA0009"), + resource.TestCheckResourceAttr(resourceName, "threat.0.tactic.name", "Collection"), + resource.TestCheckResourceAttr(resourceName, "threat.0.tactic.reference", "https://attack.mitre.org/tactics/TA0009"), + resource.TestCheckResourceAttr(resourceName, "threat.0.technique.#", "1"), + resource.TestCheckResourceAttr(resourceName, "threat.0.technique.0.id", "T1123"), + resource.TestCheckResourceAttr(resourceName, "threat.0.technique.0.name", "Audio Capture"), + resource.TestCheckResourceAttr(resourceName, "threat.0.technique.0.reference", "https://attack.mitre.org/techniques/T1123"), + + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttrSet(resourceName, "rule_id"), + resource.TestCheckResourceAttrSet(resourceName, "created_at"), + resource.TestCheckResourceAttrSet(resourceName, "created_by"), + ), + }, + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionSupport), + Config: testAccSecurityDetectionRuleConfig_queryWithMitreThreatUpdate("test-query-mitre-rule-updated"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", "test-query-mitre-rule-updated"), + resource.TestCheckResourceAttr(resourceName, "description", "Updated detection rule for processes started by MS Office programs"), + resource.TestCheckResourceAttr(resourceName, "severity", "medium"), + resource.TestCheckResourceAttr(resourceName, "risk_score", "75"), + resource.TestCheckResourceAttr(resourceName, "from", "now-2h"), + resource.TestCheckResourceAttr(resourceName, "interval", "30m"), + resource.TestCheckResourceAttr(resourceName, "max_signals", "200"), + + // Check updated tags + resource.TestCheckResourceAttr(resourceName, "tags.#", "4"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "child process"), + resource.TestCheckResourceAttr(resourceName, "tags.1", "ms office"), + resource.TestCheckResourceAttr(resourceName, "tags.2", "terraform-test"), + resource.TestCheckResourceAttr(resourceName, "tags.3", "updated"), + + // Check updated references + resource.TestCheckResourceAttr(resourceName, "references.#", "2"), + resource.TestCheckResourceAttr(resourceName, "references.0", "https://attack.mitre.org/techniques/T1566/001/"), + resource.TestCheckResourceAttr(resourceName, "references.1", "https://attack.mitre.org/techniques/T1204/002/"), + + // Check updated false positives + resource.TestCheckResourceAttr(resourceName, "false_positives.#", "2"), + resource.TestCheckResourceAttr(resourceName, "false_positives.0", "Legitimate corporate macros"), + resource.TestCheckResourceAttr(resourceName, "false_positives.1", "Authorized office automation"), + + // Check updated author + resource.TestCheckResourceAttr(resourceName, "author.#", "2"), + resource.TestCheckResourceAttr(resourceName, "author.0", "Security Team"), + resource.TestCheckResourceAttr(resourceName, "author.1", "SOC Team"), + + // Check updated note + resource.TestCheckResourceAttr(resourceName, "note", "Investigate parent process and command line. Check for malicious documents."), + + // Check updated threat - multiple techniques + resource.TestCheckResourceAttr(resourceName, "threat.#", "1"), + resource.TestCheckResourceAttr(resourceName, "threat.0.framework", "MITRE ATT&CK"), + resource.TestCheckResourceAttr(resourceName, "threat.0.tactic.id", "TA0002"), + resource.TestCheckResourceAttr(resourceName, "threat.0.tactic.name", "Execution"), + resource.TestCheckResourceAttr(resourceName, "threat.0.tactic.reference", "https://attack.mitre.org/tactics/TA0002"), + resource.TestCheckResourceAttr(resourceName, "threat.0.technique.#", "2"), + resource.TestCheckResourceAttr(resourceName, "threat.0.technique.0.id", "T1566"), + resource.TestCheckResourceAttr(resourceName, "threat.0.technique.0.name", "Phishing"), + resource.TestCheckResourceAttr(resourceName, "threat.0.technique.0.reference", "https://attack.mitre.org/techniques/T1566"), + resource.TestCheckResourceAttr(resourceName, "threat.0.technique.0.subtechnique.#", "1"), + resource.TestCheckResourceAttr(resourceName, "threat.0.technique.0.subtechnique.0.id", "T1566.001"), + resource.TestCheckResourceAttr(resourceName, "threat.0.technique.0.subtechnique.0.name", "Spearphishing Attachment"), + resource.TestCheckResourceAttr(resourceName, "threat.0.technique.0.subtechnique.0.reference", "https://attack.mitre.org/techniques/T1566/001"), + resource.TestCheckResourceAttr(resourceName, "threat.0.technique.1.id", "T1204"), + resource.TestCheckResourceAttr(resourceName, "threat.0.technique.1.name", "User Execution"), + resource.TestCheckResourceAttr(resourceName, "threat.0.technique.1.reference", "https://attack.mitre.org/techniques/T1204"), + resource.TestCheckResourceAttr(resourceName, "threat.0.technique.1.subtechnique.#", "1"), + resource.TestCheckResourceAttr(resourceName, "threat.0.technique.1.subtechnique.0.id", "T1204.002"), + resource.TestCheckResourceAttr(resourceName, "threat.0.technique.1.subtechnique.0.name", "Malicious File"), + resource.TestCheckResourceAttr(resourceName, "threat.0.technique.1.subtechnique.0.reference", "https://attack.mitre.org/techniques/T1204/002"), + ), + }, + }, + }) +} + +func testAccSecurityDetectionRuleConfig_queryWithMitreThreat(name string) string { + return fmt.Sprintf(` +provider "elasticstack" { + kibana {} +} + +resource "elasticstack_kibana_security_detection_rule" "test" { + name = "%s" + type = "query" + query = "process.parent.name:(EXCEL.EXE OR WINWORD.EXE OR POWERPNT.EXE OR OUTLOOK.EXE)" + language = "kuery" + enabled = true + description = "Detects processes started by MS Office programs" + severity = "low" + risk_score = 50 + from = "now-70m" + to = "now" + interval = "1h" + index = ["logs-*", "winlogbeat-*"] + + tags = ["child process", "ms office", "terraform-test"] + references = ["https://attack.mitre.org/techniques/T1566/001/"] + false_positives = ["Legitimate corporate macros"] + author = ["Security Team"] + license = "Elastic License v2" + note = "Investigate parent process and command line" + max_signals = 100 + + threat = [ + { + framework = "MITRE ATT&CK" + tactic = { + id = "TA0009" + name = "Collection" + reference = "https://attack.mitre.org/tactics/TA0009" + } + technique = [ + { + id = "T1123" + name = "Audio Capture" + reference = "https://attack.mitre.org/techniques/T1123" + } + ] + } + ] +} +`, name) +} + +func testAccSecurityDetectionRuleConfig_queryWithMitreThreatUpdate(name string) string { + return fmt.Sprintf(` +provider "elasticstack" { + kibana {} +} + +resource "elasticstack_kibana_security_detection_rule" "test" { + name = "%s" + type = "query" + query = "process.parent.name:(EXCEL.EXE OR WINWORD.EXE OR POWERPNT.EXE OR OUTLOOK.EXE)" + language = "kuery" + enabled = true + description = "Updated detection rule for processes started by MS Office programs" + severity = "medium" + risk_score = 75 + from = "now-2h" + to = "now" + interval = "30m" + index = ["logs-*", "winlogbeat-*", "sysmon-*"] + + tags = ["child process", "ms office", "terraform-test", "updated"] + references = ["https://attack.mitre.org/techniques/T1566/001/", "https://attack.mitre.org/techniques/T1204/002/"] + false_positives = ["Legitimate corporate macros", "Authorized office automation"] + author = ["Security Team", "SOC Team"] + license = "Elastic License v2" + note = "Investigate parent process and command line. Check for malicious documents." + max_signals = 200 + + threat = [ + { + framework = "MITRE ATT&CK" + tactic = { + id = "TA0002" + name = "Execution" + reference = "https://attack.mitre.org/tactics/TA0002" + } + technique = [ + { + id = "T1566" + name = "Phishing" + reference = "https://attack.mitre.org/techniques/T1566" + subtechnique = [ + { + id = "T1566.001" + name = "Spearphishing Attachment" + reference = "https://attack.mitre.org/techniques/T1566/001" + } + ] + }, + { + id = "T1204" + name = "User Execution" + reference = "https://attack.mitre.org/techniques/T1204" + subtechnique = [ + { + id = "T1204.002" + name = "Malicious File" + reference = "https://attack.mitre.org/techniques/T1204/002" + } + ] + } + ] + } + ] +} +`, name) +} diff --git a/internal/kibana/security_detection_rule/models.go b/internal/kibana/security_detection_rule/models.go index f8a7791af..69116a4a2 100644 --- a/internal/kibana/security_detection_rule/models.go +++ b/internal/kibana/security_detection_rule/models.go @@ -240,6 +240,31 @@ type SeverityMappingModel struct { Severity types.String `tfsdk:"severity"` } +type ThreatModel struct { + Framework types.String `tfsdk:"framework"` + Tactic types.Object `tfsdk:"tactic"` + Technique types.List `tfsdk:"technique"` +} + +type ThreatTacticModel struct { + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Reference types.String `tfsdk:"reference"` +} + +type ThreatTechniqueModel struct { + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Reference types.String `tfsdk:"reference"` + Subtechnique types.List `tfsdk:"subtechnique"` +} + +type ThreatSubtechniqueModel struct { + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Reference types.String `tfsdk:"reference"` +} + // CommonCreateProps holds all the field pointers for setting common create properties type CommonCreateProps struct { Actions **[]kbapi.SecurityDetectionsAPIRuleAction @@ -273,6 +298,7 @@ type CommonCreateProps struct { TimestampOverrideFallbackDisabled **kbapi.SecurityDetectionsAPITimestampOverrideFallbackDisabled InvestigationFields **kbapi.SecurityDetectionsAPIInvestigationFields Filters **kbapi.SecurityDetectionsAPIRuleFilterArray + Threat **kbapi.SecurityDetectionsAPIThreatArray } // CommonUpdateProps holds all the field pointers for setting common update properties @@ -308,6 +334,7 @@ type CommonUpdateProps struct { TimestampOverrideFallbackDisabled **kbapi.SecurityDetectionsAPITimestampOverrideFallbackDisabled InvestigationFields **kbapi.SecurityDetectionsAPIInvestigationFields Filters **kbapi.SecurityDetectionsAPIRuleFilterArray + Threat **kbapi.SecurityDetectionsAPIThreatArray } // Helper function to set common properties across all rule types @@ -538,6 +565,15 @@ func (d SecurityDetectionRuleData) setCommonCreateProps( *props.AlertSuppression = alertSuppression } } + + // Set threat (MITRE ATT&CK framework) + if props.Threat != nil && utils.IsKnown(d.Threat) { + threat, threatDiags := d.threatToApi(ctx) + diags.Append(threatDiags...) + if !threatDiags.HasError() && len(threat) > 0 { + *props.Threat = &threat + } + } } // Helper function to set common update properties across all rule types @@ -762,6 +798,15 @@ func (d SecurityDetectionRuleData) setCommonUpdateProps( *props.AlertSuppression = alertSuppression } } + + // Set threat (MITRE ATT&CK framework) + if props.Threat != nil && utils.IsKnown(d.Threat) { + threat, threatDiags := d.threatToApi(ctx) + diags.Append(threatDiags...) + if !threatDiags.HasError() && len(threat) > 0 { + *props.Threat = &threat + } + } } // Helper function to initialize fields that should be set to default values for all rule types diff --git a/internal/kibana/security_detection_rule/models_eql.go b/internal/kibana/security_detection_rule/models_eql.go index 96f9aa9ca..993419b55 100644 --- a/internal/kibana/security_detection_rule/models_eql.go +++ b/internal/kibana/security_detection_rule/models_eql.go @@ -103,6 +103,7 @@ func toEqlRuleCreateProps(ctx context.Context, client clients.MinVersionEnforcea TimestampOverrideFallbackDisabled: &eqlRule.TimestampOverrideFallbackDisabled, InvestigationFields: &eqlRule.InvestigationFields, Filters: &eqlRule.Filters, + Threat: &eqlRule.Threat, }, &diags, client) // Set EQL-specific fields @@ -187,6 +188,7 @@ func toEqlRuleUpdateProps(ctx context.Context, client clients.MinVersionEnforcea TimestampOverrideFallbackDisabled: &eqlRule.TimestampOverrideFallbackDisabled, InvestigationFields: &eqlRule.InvestigationFields, Filters: &eqlRule.Filters, + Threat: &eqlRule.Threat, }, &diags, client) // Set EQL-specific fields @@ -295,6 +297,10 @@ func updateFromEqlRule(ctx context.Context, rule *kbapi.SecurityDetectionsAPIEql filtersDiags := d.updateFiltersFromApi(ctx, rule.Filters) diags.Append(filtersDiags...) + // Update threat + threatDiags := d.updateThreatFromApi(ctx, &rule.Threat) + diags.Append(threatDiags...) + // Update severity mapping severityMappingDiags := d.updateSeverityMappingFromApi(ctx, &rule.SeverityMapping) diags.Append(severityMappingDiags...) diff --git a/internal/kibana/security_detection_rule/models_esql.go b/internal/kibana/security_detection_rule/models_esql.go index c7229e390..30d50ebb9 100644 --- a/internal/kibana/security_detection_rule/models_esql.go +++ b/internal/kibana/security_detection_rule/models_esql.go @@ -104,6 +104,7 @@ func (d SecurityDetectionRuleData) toEsqlRuleCreateProps(ctx context.Context, cl TimestampOverrideFallbackDisabled: &esqlRule.TimestampOverrideFallbackDisabled, InvestigationFields: &esqlRule.InvestigationFields, Filters: nil, // ESQL rules don't support this field + Threat: &esqlRule.Threat, }, &diags, client) // ESQL rules don't use index patterns as they use FROM clause in the query @@ -185,6 +186,7 @@ func (d SecurityDetectionRuleData) toEsqlRuleUpdateProps(ctx context.Context, cl TimestampOverrideFallbackDisabled: &esqlRule.TimestampOverrideFallbackDisabled, InvestigationFields: &esqlRule.InvestigationFields, Filters: nil, // ESQL rules don't have Filters + Threat: &esqlRule.Threat, }, &diags, client) // ESQL rules don't use index patterns as they use FROM clause in the query @@ -278,6 +280,10 @@ func (d *SecurityDetectionRuleData) updateFromEsqlRule(ctx context.Context, rule investigationFieldsDiags := d.updateInvestigationFieldsFromApi(ctx, rule.InvestigationFields) diags.Append(investigationFieldsDiags...) + // Update threat + threatDiags := d.updateThreatFromApi(ctx, &rule.Threat) + diags.Append(threatDiags...) + // Update severity mapping severityMappingDiags := d.updateSeverityMappingFromApi(ctx, &rule.SeverityMapping) diags.Append(severityMappingDiags...) diff --git a/internal/kibana/security_detection_rule/models_from_api_type_utils.go b/internal/kibana/security_detection_rule/models_from_api_type_utils.go index 20053e2db..3c6ec36ef 100644 --- a/internal/kibana/security_detection_rule/models_from_api_type_utils.go +++ b/internal/kibana/security_detection_rule/models_from_api_type_utils.go @@ -988,6 +988,105 @@ func (d *SecurityDetectionRuleData) updateRequiredFieldsFromApi(ctx context.Cont return diags } +// convertThreatToModel converts kbapi.SecurityDetectionsAPIThreatArray to Terraform model +func convertThreatToModel(ctx context.Context, apiThreats *kbapi.SecurityDetectionsAPIThreatArray) (types.List, diag.Diagnostics) { + var diags diag.Diagnostics + + if apiThreats == nil || len(*apiThreats) == 0 { + return types.ListNull(getThreatElementType()), diags + } + + threats := make([]ThreatModel, 0) + + for _, apiThreat := range *apiThreats { + threat := ThreatModel{ + Framework: types.StringValue(apiThreat.Framework), + } + + // Convert tactic + tacticModel := ThreatTacticModel{ + Id: types.StringValue(apiThreat.Tactic.Id), + Name: types.StringValue(apiThreat.Tactic.Name), + Reference: types.StringValue(apiThreat.Tactic.Reference), + } + + tacticObj, tacticDiags := types.ObjectValueFrom(ctx, getThreatTacticType(), tacticModel) + diags.Append(tacticDiags...) + if tacticDiags.HasError() { + continue + } + threat.Tactic = tacticObj + + // Convert techniques (optional) + if apiThreat.Technique != nil && len(*apiThreat.Technique) > 0 { + techniques := make([]ThreatTechniqueModel, 0) + + for _, apiTechnique := range *apiThreat.Technique { + technique := ThreatTechniqueModel{ + Id: types.StringValue(apiTechnique.Id), + Name: types.StringValue(apiTechnique.Name), + Reference: types.StringValue(apiTechnique.Reference), + } + + // Convert subtechniques (optional) + if apiTechnique.Subtechnique != nil && len(*apiTechnique.Subtechnique) > 0 { + subtechniques := make([]ThreatSubtechniqueModel, 0) + + for _, apiSubtechnique := range *apiTechnique.Subtechnique { + subtechnique := ThreatSubtechniqueModel{ + Id: types.StringValue(apiSubtechnique.Id), + Name: types.StringValue(apiSubtechnique.Name), + Reference: types.StringValue(apiSubtechnique.Reference), + } + subtechniques = append(subtechniques, subtechnique) + } + + subtechniquesList, subtechniquesListDiags := types.ListValueFrom(ctx, getThreatSubtechniqueElementType(), subtechniques) + diags.Append(subtechniquesListDiags...) + if !subtechniquesListDiags.HasError() { + technique.Subtechnique = subtechniquesList + } + } else { + technique.Subtechnique = types.ListNull(getThreatSubtechniqueElementType()) + } + + techniques = append(techniques, technique) + } + + techniquesList, techniquesListDiags := types.ListValueFrom(ctx, getThreatTechniqueElementType(), techniques) + diags.Append(techniquesListDiags...) + if !techniquesListDiags.HasError() { + threat.Technique = techniquesList + } + } else { + threat.Technique = types.ListNull(getThreatTechniqueElementType()) + } + + threats = append(threats, threat) + } + + listValue, listDiags := types.ListValueFrom(ctx, getThreatElementType(), threats) + diags.Append(listDiags...) + return listValue, diags +} + +// Helper function to update threat from API response +func (d *SecurityDetectionRuleData) updateThreatFromApi(ctx context.Context, threat *kbapi.SecurityDetectionsAPIThreatArray) diag.Diagnostics { + var diags diag.Diagnostics + + if threat != nil && len(*threat) > 0 { + threatValue, threatDiags := convertThreatToModel(ctx, threat) + diags.Append(threatDiags...) + if !threatDiags.HasError() { + d.Threat = threatValue + } + } else { + d.Threat = types.ListNull(getThreatElementType()) + } + + return diags +} + // parseDurationFromApi converts an API duration to customtypes.Duration func parseDurationFromApi(apiDuration kbapi.SecurityDetectionsAPIAlertSuppressionDuration) customtypes.Duration { // Convert the API's Value + Unit format back to a duration string diff --git a/internal/kibana/security_detection_rule/models_machine_learning.go b/internal/kibana/security_detection_rule/models_machine_learning.go index f41b61282..584501caa 100644 --- a/internal/kibana/security_detection_rule/models_machine_learning.go +++ b/internal/kibana/security_detection_rule/models_machine_learning.go @@ -117,6 +117,8 @@ func (d SecurityDetectionRuleData) toMachineLearningRuleCreateProps(ctx context. TimestampOverride: &mlRule.TimestampOverride, TimestampOverrideFallbackDisabled: &mlRule.TimestampOverrideFallbackDisabled, InvestigationFields: &mlRule.InvestigationFields, + Filters: nil, // ML rules don't have Filters + Threat: &mlRule.Threat, }, &diags, client) // ML rules don't use index patterns or query @@ -210,6 +212,7 @@ func (d SecurityDetectionRuleData) toMachineLearningRuleUpdateProps(ctx context. TimestampOverrideFallbackDisabled: &mlRule.TimestampOverrideFallbackDisabled, InvestigationFields: &mlRule.InvestigationFields, Filters: nil, // ML rules don't have Filters + Threat: &mlRule.Threat, }, &diags, client) // ML rules don't use index patterns or query @@ -323,6 +326,10 @@ func (d *SecurityDetectionRuleData) updateFromMachineLearningRule(ctx context.Co investigationFieldsDiags := d.updateInvestigationFieldsFromApi(ctx, rule.InvestigationFields) diags.Append(investigationFieldsDiags...) + // Update threat + threatDiags := d.updateThreatFromApi(ctx, &rule.Threat) + diags.Append(threatDiags...) + // Update severity mapping severityMappingDiags := d.updateSeverityMappingFromApi(ctx, &rule.SeverityMapping) diags.Append(severityMappingDiags...) diff --git a/internal/kibana/security_detection_rule/models_new_terms.go b/internal/kibana/security_detection_rule/models_new_terms.go index 0223f9d7d..2ab2ac2ff 100644 --- a/internal/kibana/security_detection_rule/models_new_terms.go +++ b/internal/kibana/security_detection_rule/models_new_terms.go @@ -113,6 +113,7 @@ func (d SecurityDetectionRuleData) toNewTermsRuleCreateProps(ctx context.Context TimestampOverrideFallbackDisabled: &newTermsRule.TimestampOverrideFallbackDisabled, InvestigationFields: &newTermsRule.InvestigationFields, Filters: &newTermsRule.Filters, + Threat: &newTermsRule.Threat, }, &diags, client) // Set query language @@ -202,6 +203,7 @@ func (d SecurityDetectionRuleData) toNewTermsRuleUpdateProps(ctx context.Context TimestampOverride: &newTermsRule.TimestampOverride, TimestampOverrideFallbackDisabled: &newTermsRule.TimestampOverrideFallbackDisabled, Filters: &newTermsRule.Filters, + Threat: &newTermsRule.Threat, }, &diags, client) // Set query language @@ -308,6 +310,10 @@ func (d *SecurityDetectionRuleData) updateFromNewTermsRule(ctx context.Context, filtersDiags := d.updateFiltersFromApi(ctx, rule.Filters) diags.Append(filtersDiags...) + // Update threat + threatDiags := d.updateThreatFromApi(ctx, &rule.Threat) + diags.Append(threatDiags...) + // Update severity mapping severityMappingDiags := d.updateSeverityMappingFromApi(ctx, &rule.SeverityMapping) diags.Append(severityMappingDiags...) diff --git a/internal/kibana/security_detection_rule/models_query.go b/internal/kibana/security_detection_rule/models_query.go index 1f880a615..73d16550c 100644 --- a/internal/kibana/security_detection_rule/models_query.go +++ b/internal/kibana/security_detection_rule/models_query.go @@ -103,6 +103,7 @@ func toQueryRuleCreateProps(ctx context.Context, client clients.MinVersionEnforc TimestampOverrideFallbackDisabled: &queryRule.TimestampOverrideFallbackDisabled, InvestigationFields: &queryRule.InvestigationFields, Filters: &queryRule.Filters, + Threat: &queryRule.Threat, }, &diags, client) // Set query-specific fields @@ -191,6 +192,7 @@ func toQueryRuleUpdateProps(ctx context.Context, client clients.MinVersionEnforc TimestampOverrideFallbackDisabled: &queryRule.TimestampOverrideFallbackDisabled, InvestigationFields: &queryRule.InvestigationFields, Filters: &queryRule.Filters, + Threat: &queryRule.Threat, }, &diags, client) // Set query-specific fields @@ -264,6 +266,10 @@ func updateFromQueryRule(ctx context.Context, rule *kbapi.SecurityDetectionsAPIQ d.UpdatedBy = types.StringValue(rule.UpdatedBy) d.Revision = types.Int64Value(int64(rule.Revision)) + // Update threat + threatDiags := d.updateThreatFromApi(ctx, &rule.Threat) + diags.Append(threatDiags...) + // Update index patterns indexDiags := d.updateIndexFromApi(ctx, rule.Index) diags.Append(indexDiags...) diff --git a/internal/kibana/security_detection_rule/models_saved_query.go b/internal/kibana/security_detection_rule/models_saved_query.go index 55037531c..f333c1b6a 100644 --- a/internal/kibana/security_detection_rule/models_saved_query.go +++ b/internal/kibana/security_detection_rule/models_saved_query.go @@ -102,6 +102,7 @@ func (d SecurityDetectionRuleData) toSavedQueryRuleCreateProps(ctx context.Conte TimestampOverrideFallbackDisabled: &savedQueryRule.TimestampOverrideFallbackDisabled, InvestigationFields: &savedQueryRule.InvestigationFields, Filters: &savedQueryRule.Filters, + Threat: &savedQueryRule.Threat, }, &diags, client) // Set optional query for saved query rules @@ -188,6 +189,7 @@ func (d SecurityDetectionRuleData) toSavedQueryRuleUpdateProps(ctx context.Conte TimestampOverride: &savedQueryRule.TimestampOverride, TimestampOverrideFallbackDisabled: &savedQueryRule.TimestampOverrideFallbackDisabled, Filters: &savedQueryRule.Filters, + Threat: &savedQueryRule.Threat, }, &diags, client) // Set optional query for saved query rules @@ -298,6 +300,10 @@ func (d *SecurityDetectionRuleData) updateFromSavedQueryRule(ctx context.Context filtersDiags := d.updateFiltersFromApi(ctx, rule.Filters) diags.Append(filtersDiags...) + // Update threat + threatDiags := d.updateThreatFromApi(ctx, &rule.Threat) + diags.Append(threatDiags...) + // Update severity mapping severityMappingDiags := d.updateSeverityMappingFromApi(ctx, &rule.SeverityMapping) diags.Append(severityMappingDiags...) diff --git a/internal/kibana/security_detection_rule/models_threat_match.go b/internal/kibana/security_detection_rule/models_threat_match.go index f0c73b330..28ed987d2 100644 --- a/internal/kibana/security_detection_rule/models_threat_match.go +++ b/internal/kibana/security_detection_rule/models_threat_match.go @@ -120,6 +120,7 @@ func (d SecurityDetectionRuleData) toThreatMatchRuleCreateProps(ctx context.Cont TimestampOverrideFallbackDisabled: &threatMatchRule.TimestampOverrideFallbackDisabled, InvestigationFields: &threatMatchRule.InvestigationFields, Filters: &threatMatchRule.Filters, + Threat: &threatMatchRule.Threat, }, &diags, client) // Set threat-specific fields @@ -241,6 +242,7 @@ func (d SecurityDetectionRuleData) toThreatMatchRuleUpdateProps(ctx context.Cont TimestampOverride: &threatMatchRule.TimestampOverride, TimestampOverrideFallbackDisabled: &threatMatchRule.TimestampOverrideFallbackDisabled, Filters: &threatMatchRule.Filters, + Threat: &threatMatchRule.Threat, }, &diags, client) // Set threat-specific fields @@ -324,6 +326,10 @@ func (d *SecurityDetectionRuleData) updateFromThreatMatchRule(ctx context.Contex d.UpdatedBy = types.StringValue(rule.UpdatedBy) d.Revision = types.Int64Value(int64(rule.Revision)) + // Update threat + threatDiags := d.updateThreatFromApi(ctx, &rule.Threat) + diags.Append(threatDiags...) + // Update index patterns diags.Append(d.updateIndexFromApi(ctx, rule.Index)...) diff --git a/internal/kibana/security_detection_rule/models_threshold.go b/internal/kibana/security_detection_rule/models_threshold.go index 3590c8071..75a098f17 100644 --- a/internal/kibana/security_detection_rule/models_threshold.go +++ b/internal/kibana/security_detection_rule/models_threshold.go @@ -107,6 +107,7 @@ func (d SecurityDetectionRuleData) toThresholdRuleCreateProps(ctx context.Contex TimestampOverrideFallbackDisabled: &thresholdRule.TimestampOverrideFallbackDisabled, InvestigationFields: &thresholdRule.InvestigationFields, Filters: &thresholdRule.Filters, + Threat: &thresholdRule.Threat, AlertSuppression: nil, // Handle specially for threshold rule }, &diags, client) @@ -206,6 +207,7 @@ func (d SecurityDetectionRuleData) toThresholdRuleUpdateProps(ctx context.Contex TimestampOverride: &thresholdRule.TimestampOverride, TimestampOverrideFallbackDisabled: &thresholdRule.TimestampOverrideFallbackDisabled, Filters: &thresholdRule.Filters, + Threat: &thresholdRule.Threat, AlertSuppression: nil, // Handle specially for threshold rule }, &diags, client) @@ -279,6 +281,10 @@ func (d *SecurityDetectionRuleData) updateFromThresholdRule(ctx context.Context, d.UpdatedBy = types.StringValue(rule.UpdatedBy) d.Revision = types.Int64Value(int64(rule.Revision)) + // Update threat + threatDiags := d.updateThreatFromApi(ctx, &rule.Threat) + diags.Append(threatDiags...) + // Update index patterns diags.Append(d.updateIndexFromApi(ctx, rule.Index)...) diff --git a/internal/kibana/security_detection_rule/models_to_api_type_utils.go b/internal/kibana/security_detection_rule/models_to_api_type_utils.go index 241419a7e..7aca36ce3 100644 --- a/internal/kibana/security_detection_rule/models_to_api_type_utils.go +++ b/internal/kibana/security_detection_rule/models_to_api_type_utils.go @@ -391,6 +391,90 @@ func (d SecurityDetectionRuleData) threatMappingToApi(ctx context.Context) (kbap return apiThreatMapping, diags } +// Helper function to convert MITRE ATT&CK threat data from Terraform to API format +func (d SecurityDetectionRuleData) threatToApi(ctx context.Context) (kbapi.SecurityDetectionsAPIThreatArray, diag.Diagnostics) { + var diags diag.Diagnostics + + if !utils.IsKnown(d.Threat) || len(d.Threat.Elements()) == 0 { + return nil, diags + } + + threats := make([]ThreatModel, len(d.Threat.Elements())) + threatDiags := d.Threat.ElementsAs(ctx, &threats, false) + if threatDiags.HasError() { + diags.Append(threatDiags...) + return nil, diags + } + + apiThreats := make(kbapi.SecurityDetectionsAPIThreatArray, 0) + for _, threat := range threats { + apiThreat := kbapi.SecurityDetectionsAPIThreat{ + Framework: threat.Framework.ValueString(), + } + + // Convert tactic + var tacticModel ThreatTacticModel + tacticDiags := threat.Tactic.As(ctx, &tacticModel, basetypes.ObjectAsOptions{}) + if tacticDiags.HasError() { + diags.Append(tacticDiags...) + continue + } + + apiThreat.Tactic = kbapi.SecurityDetectionsAPIThreatTactic{ + Id: tacticModel.Id.ValueString(), + Name: tacticModel.Name.ValueString(), + Reference: tacticModel.Reference.ValueString(), + } + + // Convert techniques (optional) + if utils.IsKnown(threat.Technique) && len(threat.Technique.Elements()) > 0 { + techniques := make([]ThreatTechniqueModel, len(threat.Technique.Elements())) + techniqueDiags := threat.Technique.ElementsAs(ctx, &techniques, false) + if techniqueDiags.HasError() { + diags.Append(techniqueDiags...) + continue + } + + apiTechniques := make([]kbapi.SecurityDetectionsAPIThreatTechnique, 0) + for _, technique := range techniques { + apiTechnique := kbapi.SecurityDetectionsAPIThreatTechnique{ + Id: technique.Id.ValueString(), + Name: technique.Name.ValueString(), + Reference: technique.Reference.ValueString(), + } + + // Convert subtechniques (optional) + if utils.IsKnown(technique.Subtechnique) && len(technique.Subtechnique.Elements()) > 0 { + subtechniques := make([]ThreatSubtechniqueModel, len(technique.Subtechnique.Elements())) + subtechniqueDiags := technique.Subtechnique.ElementsAs(ctx, &subtechniques, false) + if subtechniqueDiags.HasError() { + diags.Append(subtechniqueDiags...) + continue + } + + apiSubtechniques := make([]kbapi.SecurityDetectionsAPIThreatSubtechnique, 0) + for _, subtechnique := range subtechniques { + apiSubtechnique := kbapi.SecurityDetectionsAPIThreatSubtechnique{ + Id: subtechnique.Id.ValueString(), + Name: subtechnique.Name.ValueString(), + Reference: subtechnique.Reference.ValueString(), + } + apiSubtechniques = append(apiSubtechniques, apiSubtechnique) + } + apiTechnique.Subtechnique = &apiSubtechniques + } + + apiTechniques = append(apiTechniques, apiTechnique) + } + apiThreat.Technique = &apiTechniques + } + + apiThreats = append(apiThreats, apiThreat) + } + + return apiThreats, diags +} + // Helper function to process response actions configuration for all rule types func (d SecurityDetectionRuleData) responseActionsToApi(ctx context.Context, client clients.MinVersionEnforceable) ([]kbapi.SecurityDetectionsAPIResponseAction, diag.Diagnostics) { var diags diag.Diagnostics diff --git a/internal/kibana/security_detection_rule/schema.go b/internal/kibana/security_detection_rule/schema.go index 0a8b5696a..9fdd56959 100644 --- a/internal/kibana/security_detection_rule/schema.go +++ b/internal/kibana/security_detection_rule/schema.go @@ -894,3 +894,19 @@ func getRequiredFieldElementType() attr.Type { func getSeverityMappingElementType() attr.Type { return GetSchema().Attributes["severity_mapping"].GetType().(attr.TypeWithElementType).ElementType() } + +func getThreatTacticType() map[string]attr.Type { + threatType := GetSchema().Attributes["threat"].GetType().(attr.TypeWithElementType).ElementType().(attr.TypeWithAttributeTypes) + return threatType.AttributeTypes()["tactic"].(attr.TypeWithAttributeTypes).AttributeTypes() +} + +func getThreatTechniqueElementType() attr.Type { + threatType := GetSchema().Attributes["threat"].GetType().(attr.TypeWithElementType).ElementType().(attr.TypeWithAttributeTypes) + return threatType.AttributeTypes()["technique"].(attr.TypeWithElementType).ElementType() +} + +func getThreatSubtechniqueElementType() attr.Type { + threatType := GetSchema().Attributes["threat"].GetType().(attr.TypeWithElementType).ElementType().(attr.TypeWithAttributeTypes) + techniqueType := threatType.AttributeTypes()["technique"].(attr.TypeWithElementType).ElementType().(attr.TypeWithAttributeTypes) + return techniqueType.AttributeTypes()["subtechnique"].(attr.TypeWithElementType).ElementType() +} From d5f89717811c19e39916abf46e6cbcad4a050a2b Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Mon, 13 Oct 2025 18:07:35 -0700 Subject: [PATCH 02/16] Mark "query" as computed to support default empty string being returned from API --- internal/kibana/security_detection_rule/schema.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/kibana/security_detection_rule/schema.go b/internal/kibana/security_detection_rule/schema.go index 9fdd56959..ba214ccde 100644 --- a/internal/kibana/security_detection_rule/schema.go +++ b/internal/kibana/security_detection_rule/schema.go @@ -93,6 +93,7 @@ func GetSchema() schema.Schema { "query": schema.StringAttribute{ MarkdownDescription: "The query language definition.", Optional: true, + Computed: true, }, "language": schema.StringAttribute{ MarkdownDescription: "The query language (KQL or Lucene).", From d31c3f6558da1bf5a11d7e111bf08c41b18ce5e0 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Mon, 13 Oct 2025 18:35:45 -0700 Subject: [PATCH 03/16] Add esql specific validations --- .../security_detection_rule/models_esql.go | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/internal/kibana/security_detection_rule/models_esql.go b/internal/kibana/security_detection_rule/models_esql.go index 30d50ebb9..49a168e0d 100644 --- a/internal/kibana/security_detection_rule/models_esql.go +++ b/internal/kibana/security_detection_rule/models_esql.go @@ -58,10 +58,33 @@ func (e EsqlRuleProcessor) ExtractId(response any) (string, diag.Diagnostics) { return value.Id.String(), diags } +// applyEsqlValidations validates that ESQL-specific constraints are met +func (d SecurityDetectionRuleData) applyEsqlValidations(diags *diag.Diagnostics) { + if utils.IsKnown(d.Index) { + diags.AddError( + "Invalid attribute 'index'", + "ESQL rules do not use index patterns. Please remove the 'index' attribute.", + ) + } + + if utils.IsKnown(d.Filters) { + diags.AddError( + "Invalid attribute 'filters'", + "ESQL rules do not support filters. Please remove the 'filters' attribute.", + ) + } +} + func (d SecurityDetectionRuleData) toEsqlRuleCreateProps(ctx context.Context, client clients.MinVersionEnforceable) (kbapi.SecurityDetectionsAPIRuleCreateProps, diag.Diagnostics) { var diags diag.Diagnostics var createProps kbapi.SecurityDetectionsAPIRuleCreateProps + // Apply ESQL-specific validations + d.applyEsqlValidations(&diags) + if diags.HasError() { + return createProps, diags + } + esqlRule := kbapi.SecurityDetectionsAPIEsqlRuleCreateProps{ Name: kbapi.SecurityDetectionsAPIRuleName(d.Name.ValueString()), Description: kbapi.SecurityDetectionsAPIRuleDescription(d.Description.ValueString()), @@ -125,6 +148,12 @@ func (d SecurityDetectionRuleData) toEsqlRuleUpdateProps(ctx context.Context, cl var diags diag.Diagnostics var updateProps kbapi.SecurityDetectionsAPIRuleUpdateProps + // Apply ESQL-specific validations + d.applyEsqlValidations(&diags) + if diags.HasError() { + return updateProps, diags + } + // Parse ID to get space_id and rule_id compId, resourceIdDiags := clients.CompositeIdFromStrFw(d.Id.ValueString()) diags.Append(resourceIdDiags...) From 6894eb4ecdebe2f95d4d5e9d4404ad9cfbc46039 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Mon, 13 Oct 2025 19:04:09 -0700 Subject: [PATCH 04/16] Add computed to action frequency to handle kibana provided defaults --- internal/kibana/security_detection_rule/schema.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/kibana/security_detection_rule/schema.go b/internal/kibana/security_detection_rule/schema.go index ba214ccde..dc5105cd5 100644 --- a/internal/kibana/security_detection_rule/schema.go +++ b/internal/kibana/security_detection_rule/schema.go @@ -366,6 +366,7 @@ func GetSchema() schema.Schema { "frequency": schema.SingleNestedAttribute{ MarkdownDescription: "The action frequency defines when the action runs.", Optional: true, + Computed: true, Attributes: map[string]schema.Attribute{ "notify_when": schema.StringAttribute{ MarkdownDescription: "Defines how often rules run actions. Valid values: onActionGroupChange, onActiveAlert, onThrottleInterval.", From b05275f46a50883e24a5687e042616dc8a3da688 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Mon, 13 Oct 2025 19:36:43 -0700 Subject: [PATCH 05/16] Add anomaly detection validation for anomaly_threshold --- .../models_machine_learning.go | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/internal/kibana/security_detection_rule/models_machine_learning.go b/internal/kibana/security_detection_rule/models_machine_learning.go index 584501caa..52f8eec86 100644 --- a/internal/kibana/security_detection_rule/models_machine_learning.go +++ b/internal/kibana/security_detection_rule/models_machine_learning.go @@ -59,10 +59,25 @@ func (m MachineLearningRuleProcessor) ExtractId(response any) (string, diag.Diag return value.Id.String(), diags } +// applyMachineLearningValidations validates that Machine learning-specific constraints are met +func (d SecurityDetectionRuleData) applyMachineLearningValidations(diags *diag.Diagnostics) { + if !utils.IsKnown(d.AnomalyThreshold) { + diags.AddError( + "Missing attribute 'anomaly_threshold'", + "Machine learning rules require an 'anomaly_threshold' attribute.", + ) + } +} + func (d SecurityDetectionRuleData) toMachineLearningRuleCreateProps(ctx context.Context, client clients.MinVersionEnforceable) (kbapi.SecurityDetectionsAPIRuleCreateProps, diag.Diagnostics) { var diags diag.Diagnostics var createProps kbapi.SecurityDetectionsAPIRuleCreateProps + d.applyMachineLearningValidations(&diags) + if diags.HasError() { + return createProps, diags + } + mlRule := kbapi.SecurityDetectionsAPIMachineLearningRuleCreateProps{ Name: kbapi.SecurityDetectionsAPIRuleName(d.Name.ValueString()), Description: kbapi.SecurityDetectionsAPIRuleDescription(d.Description.ValueString()), @@ -138,6 +153,11 @@ func (d SecurityDetectionRuleData) toMachineLearningRuleUpdateProps(ctx context. var diags diag.Diagnostics var updateProps kbapi.SecurityDetectionsAPIRuleUpdateProps + d.applyMachineLearningValidations(&diags) + if diags.HasError() { + return updateProps, diags + } + // Parse ID to get space_id and rule_id compId, resourceIdDiags := clients.CompositeIdFromStrFw(d.Id.ValueString()) diags.Append(resourceIdDiags...) From 60ae113ca72279aa61053997934495606f6be970 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Mon, 13 Oct 2025 19:37:28 -0700 Subject: [PATCH 06/16] Add support for timeline_id and timeline_title --- .../kibana/security_detection_rule/models.go | 28 +++++++++++++++++++ .../security_detection_rule/models_eql.go | 6 ++++ .../security_detection_rule/models_esql.go | 6 ++++ .../models_from_api_type_utils.go | 26 +++++++++++++++++ .../models_machine_learning.go | 6 ++++ .../models_new_terms.go | 6 ++++ .../security_detection_rule/models_query.go | 6 ++++ .../models_saved_query.go | 6 ++++ .../models_threat_match.go | 6 ++++ .../models_threshold.go | 6 ++++ 10 files changed, 102 insertions(+) diff --git a/internal/kibana/security_detection_rule/models.go b/internal/kibana/security_detection_rule/models.go index 69116a4a2..e0e3888ec 100644 --- a/internal/kibana/security_detection_rule/models.go +++ b/internal/kibana/security_detection_rule/models.go @@ -299,6 +299,8 @@ type CommonCreateProps struct { InvestigationFields **kbapi.SecurityDetectionsAPIInvestigationFields Filters **kbapi.SecurityDetectionsAPIRuleFilterArray Threat **kbapi.SecurityDetectionsAPIThreatArray + TimelineId **kbapi.SecurityDetectionsAPITimelineTemplateId + TimelineTitle **kbapi.SecurityDetectionsAPITimelineTemplateTitle } // CommonUpdateProps holds all the field pointers for setting common update properties @@ -335,6 +337,8 @@ type CommonUpdateProps struct { InvestigationFields **kbapi.SecurityDetectionsAPIInvestigationFields Filters **kbapi.SecurityDetectionsAPIRuleFilterArray Threat **kbapi.SecurityDetectionsAPIThreatArray + TimelineId **kbapi.SecurityDetectionsAPITimelineTemplateId + TimelineTitle **kbapi.SecurityDetectionsAPITimelineTemplateTitle } // Helper function to set common properties across all rule types @@ -574,6 +578,18 @@ func (d SecurityDetectionRuleData) setCommonCreateProps( *props.Threat = &threat } } + + // Set timeline ID + if props.TimelineId != nil && utils.IsKnown(d.TimelineId) { + timelineId := kbapi.SecurityDetectionsAPITimelineTemplateId(d.TimelineId.ValueString()) + *props.TimelineId = &timelineId + } + + // Set timeline title + if props.TimelineTitle != nil && utils.IsKnown(d.TimelineTitle) { + timelineTitle := kbapi.SecurityDetectionsAPITimelineTemplateTitle(d.TimelineTitle.ValueString()) + *props.TimelineTitle = &timelineTitle + } } // Helper function to set common update properties across all rule types @@ -807,6 +823,18 @@ func (d SecurityDetectionRuleData) setCommonUpdateProps( *props.Threat = &threat } } + + // Set timeline ID + if props.TimelineId != nil && utils.IsKnown(d.TimelineId) { + timelineId := kbapi.SecurityDetectionsAPITimelineTemplateId(d.TimelineId.ValueString()) + *props.TimelineId = &timelineId + } + + // Set timeline title + if props.TimelineTitle != nil && utils.IsKnown(d.TimelineTitle) { + timelineTitle := kbapi.SecurityDetectionsAPITimelineTemplateTitle(d.TimelineTitle.ValueString()) + *props.TimelineTitle = &timelineTitle + } } // Helper function to initialize fields that should be set to default values for all rule types diff --git a/internal/kibana/security_detection_rule/models_eql.go b/internal/kibana/security_detection_rule/models_eql.go index 993419b55..795a9fe85 100644 --- a/internal/kibana/security_detection_rule/models_eql.go +++ b/internal/kibana/security_detection_rule/models_eql.go @@ -104,6 +104,8 @@ func toEqlRuleCreateProps(ctx context.Context, client clients.MinVersionEnforcea InvestigationFields: &eqlRule.InvestigationFields, Filters: &eqlRule.Filters, Threat: &eqlRule.Threat, + TimelineId: &eqlRule.TimelineId, + TimelineTitle: &eqlRule.TimelineTitle, }, &diags, client) // Set EQL-specific fields @@ -189,6 +191,8 @@ func toEqlRuleUpdateProps(ctx context.Context, client clients.MinVersionEnforcea InvestigationFields: &eqlRule.InvestigationFields, Filters: &eqlRule.Filters, Threat: &eqlRule.Threat, + TimelineId: &eqlRule.TimelineId, + TimelineTitle: &eqlRule.TimelineTitle, }, &diags, client) // Set EQL-specific fields @@ -222,6 +226,8 @@ func updateFromEqlRule(ctx context.Context, rule *kbapi.SecurityDetectionsAPIEql d.Type = types.StringValue(string(rule.Type)) // Update common fields + diags.Append(d.updateTimelineIdFromApi(ctx, rule.TimelineId)...) + diags.Append(d.updateTimelineTitleFromApi(ctx, rule.TimelineTitle)...) diags.Append(d.updateDataViewIdFromApi(ctx, rule.DataViewId)...) diags.Append(d.updateNamespaceFromApi(ctx, rule.Namespace)...) diags.Append(d.updateRuleNameOverrideFromApi(ctx, rule.RuleNameOverride)...) diff --git a/internal/kibana/security_detection_rule/models_esql.go b/internal/kibana/security_detection_rule/models_esql.go index 49a168e0d..7655c3839 100644 --- a/internal/kibana/security_detection_rule/models_esql.go +++ b/internal/kibana/security_detection_rule/models_esql.go @@ -128,6 +128,8 @@ func (d SecurityDetectionRuleData) toEsqlRuleCreateProps(ctx context.Context, cl InvestigationFields: &esqlRule.InvestigationFields, Filters: nil, // ESQL rules don't support this field Threat: &esqlRule.Threat, + TimelineId: &esqlRule.TimelineId, + TimelineTitle: &esqlRule.TimelineTitle, }, &diags, client) // ESQL rules don't use index patterns as they use FROM clause in the query @@ -216,6 +218,8 @@ func (d SecurityDetectionRuleData) toEsqlRuleUpdateProps(ctx context.Context, cl InvestigationFields: &esqlRule.InvestigationFields, Filters: nil, // ESQL rules don't have Filters Threat: &esqlRule.Threat, + TimelineId: &esqlRule.TimelineId, + TimelineTitle: &esqlRule.TimelineTitle, }, &diags, client) // ESQL rules don't use index patterns as they use FROM clause in the query @@ -246,6 +250,8 @@ func (d *SecurityDetectionRuleData) updateFromEsqlRule(ctx context.Context, rule // Update common fields (ESQL doesn't support DataViewId) d.DataViewId = types.StringNull() + diags.Append(d.updateTimelineIdFromApi(ctx, rule.TimelineId)...) + diags.Append(d.updateTimelineTitleFromApi(ctx, rule.TimelineTitle)...) diags.Append(d.updateNamespaceFromApi(ctx, rule.Namespace)...) diags.Append(d.updateRuleNameOverrideFromApi(ctx, rule.RuleNameOverride)...) diags.Append(d.updateTimestampOverrideFromApi(ctx, rule.TimestampOverride)...) diff --git a/internal/kibana/security_detection_rule/models_from_api_type_utils.go b/internal/kibana/security_detection_rule/models_from_api_type_utils.go index 3c6ec36ef..296eaa70e 100644 --- a/internal/kibana/security_detection_rule/models_from_api_type_utils.go +++ b/internal/kibana/security_detection_rule/models_from_api_type_utils.go @@ -699,6 +699,32 @@ func (d *SecurityDetectionRuleData) updateDataViewIdFromApi(ctx context.Context, return diags } +// Helper function to update timeline ID from API response +func (d *SecurityDetectionRuleData) updateTimelineIdFromApi(ctx context.Context, timelineId *kbapi.SecurityDetectionsAPITimelineTemplateId) diag.Diagnostics { + var diags diag.Diagnostics + + if timelineId != nil { + d.TimelineId = types.StringValue(string(*timelineId)) + } else { + d.TimelineId = types.StringNull() + } + + return diags +} + +// Helper function to update timeline title from API response +func (d *SecurityDetectionRuleData) updateTimelineTitleFromApi(ctx context.Context, timelineTitle *kbapi.SecurityDetectionsAPITimelineTemplateTitle) diag.Diagnostics { + var diags diag.Diagnostics + + if timelineTitle != nil { + d.TimelineTitle = types.StringValue(string(*timelineTitle)) + } else { + d.TimelineTitle = types.StringNull() + } + + return diags +} + // Helper function to update namespace from API response func (d *SecurityDetectionRuleData) updateNamespaceFromApi(ctx context.Context, namespace *kbapi.SecurityDetectionsAPIAlertsIndexNamespace) diag.Diagnostics { var diags diag.Diagnostics diff --git a/internal/kibana/security_detection_rule/models_machine_learning.go b/internal/kibana/security_detection_rule/models_machine_learning.go index 52f8eec86..caa196b2c 100644 --- a/internal/kibana/security_detection_rule/models_machine_learning.go +++ b/internal/kibana/security_detection_rule/models_machine_learning.go @@ -134,6 +134,8 @@ func (d SecurityDetectionRuleData) toMachineLearningRuleCreateProps(ctx context. InvestigationFields: &mlRule.InvestigationFields, Filters: nil, // ML rules don't have Filters Threat: &mlRule.Threat, + TimelineId: &mlRule.TimelineId, + TimelineTitle: &mlRule.TimelineTitle, }, &diags, client) // ML rules don't use index patterns or query @@ -233,6 +235,8 @@ func (d SecurityDetectionRuleData) toMachineLearningRuleUpdateProps(ctx context. InvestigationFields: &mlRule.InvestigationFields, Filters: nil, // ML rules don't have Filters Threat: &mlRule.Threat, + TimelineId: &mlRule.TimelineId, + TimelineTitle: &mlRule.TimelineTitle, }, &diags, client) // ML rules don't use index patterns or query @@ -264,6 +268,8 @@ func (d *SecurityDetectionRuleData) updateFromMachineLearningRule(ctx context.Co // Update common fields (ML doesn't support DataViewId) d.DataViewId = types.StringNull() + diags.Append(d.updateTimelineIdFromApi(ctx, rule.TimelineId)...) + diags.Append(d.updateTimelineTitleFromApi(ctx, rule.TimelineTitle)...) diags.Append(d.updateNamespaceFromApi(ctx, rule.Namespace)...) diags.Append(d.updateRuleNameOverrideFromApi(ctx, rule.RuleNameOverride)...) diags.Append(d.updateTimestampOverrideFromApi(ctx, rule.TimestampOverride)...) diff --git a/internal/kibana/security_detection_rule/models_new_terms.go b/internal/kibana/security_detection_rule/models_new_terms.go index 2ab2ac2ff..989d07136 100644 --- a/internal/kibana/security_detection_rule/models_new_terms.go +++ b/internal/kibana/security_detection_rule/models_new_terms.go @@ -114,6 +114,8 @@ func (d SecurityDetectionRuleData) toNewTermsRuleCreateProps(ctx context.Context InvestigationFields: &newTermsRule.InvestigationFields, Filters: &newTermsRule.Filters, Threat: &newTermsRule.Threat, + TimelineId: &newTermsRule.TimelineId, + TimelineTitle: &newTermsRule.TimelineTitle, }, &diags, client) // Set query language @@ -204,6 +206,8 @@ func (d SecurityDetectionRuleData) toNewTermsRuleUpdateProps(ctx context.Context TimestampOverrideFallbackDisabled: &newTermsRule.TimestampOverrideFallbackDisabled, Filters: &newTermsRule.Filters, Threat: &newTermsRule.Threat, + TimelineId: &newTermsRule.TimelineId, + TimelineTitle: &newTermsRule.TimelineTitle, }, &diags, client) // Set query language @@ -234,6 +238,8 @@ func (d *SecurityDetectionRuleData) updateFromNewTermsRule(ctx context.Context, d.Type = types.StringValue(string(rule.Type)) // Update common fields + diags.Append(d.updateTimelineIdFromApi(ctx, rule.TimelineId)...) + diags.Append(d.updateTimelineTitleFromApi(ctx, rule.TimelineTitle)...) diags.Append(d.updateDataViewIdFromApi(ctx, rule.DataViewId)...) diags.Append(d.updateNamespaceFromApi(ctx, rule.Namespace)...) diags.Append(d.updateRuleNameOverrideFromApi(ctx, rule.RuleNameOverride)...) diff --git a/internal/kibana/security_detection_rule/models_query.go b/internal/kibana/security_detection_rule/models_query.go index 73d16550c..d5c4235d8 100644 --- a/internal/kibana/security_detection_rule/models_query.go +++ b/internal/kibana/security_detection_rule/models_query.go @@ -104,6 +104,8 @@ func toQueryRuleCreateProps(ctx context.Context, client clients.MinVersionEnforc InvestigationFields: &queryRule.InvestigationFields, Filters: &queryRule.Filters, Threat: &queryRule.Threat, + TimelineId: &queryRule.TimelineId, + TimelineTitle: &queryRule.TimelineTitle, }, &diags, client) // Set query-specific fields @@ -193,6 +195,8 @@ func toQueryRuleUpdateProps(ctx context.Context, client clients.MinVersionEnforc InvestigationFields: &queryRule.InvestigationFields, Filters: &queryRule.Filters, Threat: &queryRule.Threat, + TimelineId: &queryRule.TimelineId, + TimelineTitle: &queryRule.TimelineTitle, }, &diags, client) // Set query-specific fields @@ -228,6 +232,8 @@ func updateFromQueryRule(ctx context.Context, rule *kbapi.SecurityDetectionsAPIQ d.Type = types.StringValue(string(rule.Type)) // Update common fields + diags.Append(d.updateTimelineIdFromApi(ctx, rule.TimelineId)...) + diags.Append(d.updateTimelineTitleFromApi(ctx, rule.TimelineTitle)...) dataViewIdDiags := d.updateDataViewIdFromApi(ctx, rule.DataViewId) diags.Append(dataViewIdDiags...) diff --git a/internal/kibana/security_detection_rule/models_saved_query.go b/internal/kibana/security_detection_rule/models_saved_query.go index f333c1b6a..2e703312c 100644 --- a/internal/kibana/security_detection_rule/models_saved_query.go +++ b/internal/kibana/security_detection_rule/models_saved_query.go @@ -103,6 +103,8 @@ func (d SecurityDetectionRuleData) toSavedQueryRuleCreateProps(ctx context.Conte InvestigationFields: &savedQueryRule.InvestigationFields, Filters: &savedQueryRule.Filters, Threat: &savedQueryRule.Threat, + TimelineId: &savedQueryRule.TimelineId, + TimelineTitle: &savedQueryRule.TimelineTitle, }, &diags, client) // Set optional query for saved query rules @@ -190,6 +192,8 @@ func (d SecurityDetectionRuleData) toSavedQueryRuleUpdateProps(ctx context.Conte TimestampOverrideFallbackDisabled: &savedQueryRule.TimestampOverrideFallbackDisabled, Filters: &savedQueryRule.Filters, Threat: &savedQueryRule.Threat, + TimelineId: &savedQueryRule.TimelineId, + TimelineTitle: &savedQueryRule.TimelineTitle, }, &diags, client) // Set optional query for saved query rules @@ -227,6 +231,8 @@ func (d *SecurityDetectionRuleData) updateFromSavedQueryRule(ctx context.Context d.Type = types.StringValue(string(rule.Type)) // Update common fields + diags.Append(d.updateTimelineIdFromApi(ctx, rule.TimelineId)...) + diags.Append(d.updateTimelineTitleFromApi(ctx, rule.TimelineTitle)...) diags.Append(d.updateDataViewIdFromApi(ctx, rule.DataViewId)...) diags.Append(d.updateNamespaceFromApi(ctx, rule.Namespace)...) diags.Append(d.updateRuleNameOverrideFromApi(ctx, rule.RuleNameOverride)...) diff --git a/internal/kibana/security_detection_rule/models_threat_match.go b/internal/kibana/security_detection_rule/models_threat_match.go index 28ed987d2..911bfe43d 100644 --- a/internal/kibana/security_detection_rule/models_threat_match.go +++ b/internal/kibana/security_detection_rule/models_threat_match.go @@ -121,6 +121,8 @@ func (d SecurityDetectionRuleData) toThreatMatchRuleCreateProps(ctx context.Cont InvestigationFields: &threatMatchRule.InvestigationFields, Filters: &threatMatchRule.Filters, Threat: &threatMatchRule.Threat, + TimelineId: &threatMatchRule.TimelineId, + TimelineTitle: &threatMatchRule.TimelineTitle, }, &diags, client) // Set threat-specific fields @@ -243,6 +245,8 @@ func (d SecurityDetectionRuleData) toThreatMatchRuleUpdateProps(ctx context.Cont TimestampOverrideFallbackDisabled: &threatMatchRule.TimestampOverrideFallbackDisabled, Filters: &threatMatchRule.Filters, Threat: &threatMatchRule.Threat, + TimelineId: &threatMatchRule.TimelineId, + TimelineTitle: &threatMatchRule.TimelineTitle, }, &diags, client) // Set threat-specific fields @@ -299,6 +303,8 @@ func (d *SecurityDetectionRuleData) updateFromThreatMatchRule(ctx context.Contex d.Type = types.StringValue(string(rule.Type)) // Update common fields + diags.Append(d.updateTimelineIdFromApi(ctx, rule.TimelineId)...) + diags.Append(d.updateTimelineTitleFromApi(ctx, rule.TimelineTitle)...) diags.Append(d.updateDataViewIdFromApi(ctx, rule.DataViewId)...) diags.Append(d.updateNamespaceFromApi(ctx, rule.Namespace)...) diags.Append(d.updateRuleNameOverrideFromApi(ctx, rule.RuleNameOverride)...) diff --git a/internal/kibana/security_detection_rule/models_threshold.go b/internal/kibana/security_detection_rule/models_threshold.go index 75a098f17..df74d5641 100644 --- a/internal/kibana/security_detection_rule/models_threshold.go +++ b/internal/kibana/security_detection_rule/models_threshold.go @@ -109,6 +109,8 @@ func (d SecurityDetectionRuleData) toThresholdRuleCreateProps(ctx context.Contex Filters: &thresholdRule.Filters, Threat: &thresholdRule.Threat, AlertSuppression: nil, // Handle specially for threshold rule + TimelineId: &thresholdRule.TimelineId, + TimelineTitle: &thresholdRule.TimelineTitle, }, &diags, client) // Handle threshold-specific alert suppression @@ -209,6 +211,8 @@ func (d SecurityDetectionRuleData) toThresholdRuleUpdateProps(ctx context.Contex Filters: &thresholdRule.Filters, Threat: &thresholdRule.Threat, AlertSuppression: nil, // Handle specially for threshold rule + TimelineId: &thresholdRule.TimelineId, + TimelineTitle: &thresholdRule.TimelineTitle, }, &diags, client) // Handle threshold-specific alert suppression @@ -253,6 +257,8 @@ func (d *SecurityDetectionRuleData) updateFromThresholdRule(ctx context.Context, d.Type = types.StringValue(string(rule.Type)) // Update common fields + diags.Append(d.updateTimelineIdFromApi(ctx, rule.TimelineId)...) + diags.Append(d.updateTimelineTitleFromApi(ctx, rule.TimelineTitle)...) diags.Append(d.updateDataViewIdFromApi(ctx, rule.DataViewId)...) diags.Append(d.updateNamespaceFromApi(ctx, rule.Namespace)...) diags.Append(d.updateRuleNameOverrideFromApi(ctx, rule.RuleNameOverride)...) From b3650b12b753ff093905b66f19c66d1922a5b80e Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Mon, 13 Oct 2025 19:46:20 -0700 Subject: [PATCH 07/16] Mark threat_query as computed to handle api provided default --- internal/kibana/security_detection_rule/schema.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/kibana/security_detection_rule/schema.go b/internal/kibana/security_detection_rule/schema.go index dc5105cd5..c131f78fc 100644 --- a/internal/kibana/security_detection_rule/schema.go +++ b/internal/kibana/security_detection_rule/schema.go @@ -637,6 +637,7 @@ func GetSchema() schema.Schema { "threat_query": schema.StringAttribute{ MarkdownDescription: "Query used to filter threat intelligence data. Optional for threat_match rules.", Optional: true, + Computed: true, }, "threat_mapping": schema.ListNestedAttribute{ MarkdownDescription: "Array of threat mappings that specify how to match events with threat intelligence. Required for threat_match rules.", From a679357716e70d8bd2a326e4668cafbd552e0067 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Mon, 13 Oct 2025 21:05:44 -0700 Subject: [PATCH 08/16] Update internal/kibana/security_detection_rule/models_to_api_type_utils.go Co-authored-by: Toby Brain --- .../kibana/security_detection_rule/models_to_api_type_utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/kibana/security_detection_rule/models_to_api_type_utils.go b/internal/kibana/security_detection_rule/models_to_api_type_utils.go index 7aca36ce3..c3f403868 100644 --- a/internal/kibana/security_detection_rule/models_to_api_type_utils.go +++ b/internal/kibana/security_detection_rule/models_to_api_type_utils.go @@ -401,8 +401,8 @@ func (d SecurityDetectionRuleData) threatToApi(ctx context.Context) (kbapi.Secur threats := make([]ThreatModel, len(d.Threat.Elements())) threatDiags := d.Threat.ElementsAs(ctx, &threats, false) + diags.Append(threatDiags...) if threatDiags.HasError() { - diags.Append(threatDiags...) return nil, diags } From 1d4184999068f60349ee27646f175fe39e27e7c3 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Mon, 13 Oct 2025 21:05:50 -0700 Subject: [PATCH 09/16] Update internal/kibana/security_detection_rule/models_to_api_type_utils.go Co-authored-by: Toby Brain --- .../kibana/security_detection_rule/models_to_api_type_utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/kibana/security_detection_rule/models_to_api_type_utils.go b/internal/kibana/security_detection_rule/models_to_api_type_utils.go index c3f403868..37247c7de 100644 --- a/internal/kibana/security_detection_rule/models_to_api_type_utils.go +++ b/internal/kibana/security_detection_rule/models_to_api_type_utils.go @@ -430,8 +430,8 @@ func (d SecurityDetectionRuleData) threatToApi(ctx context.Context) (kbapi.Secur if utils.IsKnown(threat.Technique) && len(threat.Technique.Elements()) > 0 { techniques := make([]ThreatTechniqueModel, len(threat.Technique.Elements())) techniqueDiags := threat.Technique.ElementsAs(ctx, &techniques, false) + diags.Append(techniqueDiags...) if techniqueDiags.HasError() { - diags.Append(techniqueDiags...) continue } From b0d7855a78a92b9a25f00d25e47b81d43862447c Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Mon, 13 Oct 2025 21:06:00 -0700 Subject: [PATCH 10/16] Update internal/kibana/security_detection_rule/models_to_api_type_utils.go Co-authored-by: Toby Brain --- .../kibana/security_detection_rule/models_to_api_type_utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/kibana/security_detection_rule/models_to_api_type_utils.go b/internal/kibana/security_detection_rule/models_to_api_type_utils.go index 37247c7de..36e48e589 100644 --- a/internal/kibana/security_detection_rule/models_to_api_type_utils.go +++ b/internal/kibana/security_detection_rule/models_to_api_type_utils.go @@ -415,8 +415,8 @@ func (d SecurityDetectionRuleData) threatToApi(ctx context.Context) (kbapi.Secur // Convert tactic var tacticModel ThreatTacticModel tacticDiags := threat.Tactic.As(ctx, &tacticModel, basetypes.ObjectAsOptions{}) + diags.Append(tacticDiags...) if tacticDiags.HasError() { - diags.Append(tacticDiags...) continue } From 2661016b8f505a00c4253321c33da23f3f773b58 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Mon, 13 Oct 2025 21:06:07 -0700 Subject: [PATCH 11/16] Update internal/kibana/security_detection_rule/models_to_api_type_utils.go Co-authored-by: Toby Brain --- .../kibana/security_detection_rule/models_to_api_type_utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/kibana/security_detection_rule/models_to_api_type_utils.go b/internal/kibana/security_detection_rule/models_to_api_type_utils.go index 36e48e589..6ae6bb2f2 100644 --- a/internal/kibana/security_detection_rule/models_to_api_type_utils.go +++ b/internal/kibana/security_detection_rule/models_to_api_type_utils.go @@ -447,8 +447,8 @@ func (d SecurityDetectionRuleData) threatToApi(ctx context.Context) (kbapi.Secur if utils.IsKnown(technique.Subtechnique) && len(technique.Subtechnique.Elements()) > 0 { subtechniques := make([]ThreatSubtechniqueModel, len(technique.Subtechnique.Elements())) subtechniqueDiags := technique.Subtechnique.ElementsAs(ctx, &subtechniques, false) + diags.Append(subtechniqueDiags...) if subtechniqueDiags.HasError() { - diags.Append(subtechniqueDiags...) continue } From dbc2f7c9045cfe186f82f5a41dfdb301d4f109dd Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Mon, 13 Oct 2025 21:06:15 -0700 Subject: [PATCH 12/16] Update internal/kibana/security_detection_rule/models_esql.go Co-authored-by: Toby Brain --- internal/kibana/security_detection_rule/models_esql.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/kibana/security_detection_rule/models_esql.go b/internal/kibana/security_detection_rule/models_esql.go index 7655c3839..3453e5599 100644 --- a/internal/kibana/security_detection_rule/models_esql.go +++ b/internal/kibana/security_detection_rule/models_esql.go @@ -61,7 +61,8 @@ func (e EsqlRuleProcessor) ExtractId(response any) (string, diag.Diagnostics) { // applyEsqlValidations validates that ESQL-specific constraints are met func (d SecurityDetectionRuleData) applyEsqlValidations(diags *diag.Diagnostics) { if utils.IsKnown(d.Index) { - diags.AddError( + diags.AddAttributeError( + path.Root("index"), "Invalid attribute 'index'", "ESQL rules do not use index patterns. Please remove the 'index' attribute.", ) From ad42408d4668420e79f14e433cf1c60c3d5ae756 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Mon, 13 Oct 2025 21:06:22 -0700 Subject: [PATCH 13/16] Update internal/kibana/security_detection_rule/models_esql.go Co-authored-by: Toby Brain --- internal/kibana/security_detection_rule/models_esql.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/kibana/security_detection_rule/models_esql.go b/internal/kibana/security_detection_rule/models_esql.go index 3453e5599..5f563ef7e 100644 --- a/internal/kibana/security_detection_rule/models_esql.go +++ b/internal/kibana/security_detection_rule/models_esql.go @@ -69,7 +69,8 @@ func (d SecurityDetectionRuleData) applyEsqlValidations(diags *diag.Diagnostics) } if utils.IsKnown(d.Filters) { - diags.AddError( + diags.AddAttributeError( + path.Root("filters"), "Invalid attribute 'filters'", "ESQL rules do not support filters. Please remove the 'filters' attribute.", ) From f1efd7392bd34dca309b602e0359bfbdf06a2e51 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Mon, 13 Oct 2025 21:06:28 -0700 Subject: [PATCH 14/16] Update internal/kibana/security_detection_rule/models_machine_learning.go Co-authored-by: Toby Brain --- .../kibana/security_detection_rule/models_machine_learning.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/kibana/security_detection_rule/models_machine_learning.go b/internal/kibana/security_detection_rule/models_machine_learning.go index caa196b2c..783adbbbe 100644 --- a/internal/kibana/security_detection_rule/models_machine_learning.go +++ b/internal/kibana/security_detection_rule/models_machine_learning.go @@ -62,7 +62,8 @@ func (m MachineLearningRuleProcessor) ExtractId(response any) (string, diag.Diag // applyMachineLearningValidations validates that Machine learning-specific constraints are met func (d SecurityDetectionRuleData) applyMachineLearningValidations(diags *diag.Diagnostics) { if !utils.IsKnown(d.AnomalyThreshold) { - diags.AddError( + diags.AddAttributeError( + path.Root("anomaly_threshold"), "Missing attribute 'anomaly_threshold'", "Machine learning rules require an 'anomaly_threshold' attribute.", ) From 4e9621ac26e08cae46d72cfa0ff2fcc77b3ff7c4 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Mon, 13 Oct 2025 21:12:58 -0700 Subject: [PATCH 15/16] Update changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index efb1a5399..8e9a38196 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ - Fix provider crash with `elasticstack_kibana_action_connector` when `config` or `secrets` was unset in 0.11.17 ([#1355](https://github.com/elastic/terraform-provider-elasticstack/pull/1355)) - Fixes provider crash with `elasticstack_kibana_slo` when using `kql_custom_indicator` with no `filter` set. +- Updates for Security Detection Rules + - Add support for `threat` property + - Gracefully support `query` property not being set + - Add esql specific validations to reject unsupported fields `index` and `filters` + - Gracefully handle response action with no provided `frequency` + - Add validation for required `anomaly_threshold` field in anomaly detection rules + - Add support for `timeline_id` / `timeline_title` fields + - Gracefully handle `threat_query` not being provided for `threat_match` ule ## [0.11.18] - 2025-10-10 From f60f7c3a6fcb4db8d26646406fb2c686e0c99c96 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Mon, 13 Oct 2025 21:18:07 -0700 Subject: [PATCH 16/16] Add path import --- internal/kibana/security_detection_rule/models_esql.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/kibana/security_detection_rule/models_esql.go b/internal/kibana/security_detection_rule/models_esql.go index 5f563ef7e..a55eaeb0f 100644 --- a/internal/kibana/security_detection_rule/models_esql.go +++ b/internal/kibana/security_detection_rule/models_esql.go @@ -9,6 +9,7 @@ import ( "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/types" )