Skip to content

Commit 8b7875c

Browse files
nick-benoittobio
andauthored
Security Detection Rule Updates (#1361)
* Add support for threat * Mark "query" as computed to support default empty string being returned from API * Add esql specific validations * Add computed to action frequency to handle kibana provided defaults * Add anomaly detection validation for anomaly_threshold * Add support for timeline_id and timeline_title * Mark threat_query as computed to handle api provided default * Update internal/kibana/security_detection_rule/models_to_api_type_utils.go Co-authored-by: Toby Brain <[email protected]> * Update internal/kibana/security_detection_rule/models_to_api_type_utils.go Co-authored-by: Toby Brain <[email protected]> * Update internal/kibana/security_detection_rule/models_to_api_type_utils.go Co-authored-by: Toby Brain <[email protected]> * Update internal/kibana/security_detection_rule/models_to_api_type_utils.go Co-authored-by: Toby Brain <[email protected]> * Update internal/kibana/security_detection_rule/models_esql.go Co-authored-by: Toby Brain <[email protected]> * Update internal/kibana/security_detection_rule/models_esql.go Co-authored-by: Toby Brain <[email protected]> * Update internal/kibana/security_detection_rule/models_machine_learning.go Co-authored-by: Toby Brain <[email protected]> * Update changelog * Add path import --------- Co-authored-by: Toby Brain <[email protected]>
1 parent 0a82d10 commit 8b7875c

14 files changed

+708
-0
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
- 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))
44
- Fixes provider crash with `elasticstack_kibana_slo` when using `kql_custom_indicator` with no `filter` set.
5+
- Updates for Security Detection Rules
6+
- Add support for `threat` property
7+
- Gracefully support `query` property not being set
8+
- Add esql specific validations to reject unsupported fields `index` and `filters`
9+
- Gracefully handle response action with no provided `frequency`
10+
- Add validation for required `anomaly_threshold` field in anomaly detection rules
11+
- Add support for `timeline_id` / `timeline_title` fields
12+
- Gracefully handle `threat_query` not being provided for `threat_match` ule
513

614
## [0.11.18] - 2025-10-10
715

internal/kibana/security_detection_rule/acc_test.go

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4522,3 +4522,252 @@ resource "elasticstack_kibana_security_detection_rule" "test" {
45224522
}
45234523
`, name)
45244524
}
4525+
4526+
func TestAccResourceSecurityDetectionRule_QueryWithMitreThreat(t *testing.T) {
4527+
resourceName := "elasticstack_kibana_security_detection_rule.test"
4528+
4529+
resource.Test(t, resource.TestCase{
4530+
PreCheck: func() { acctest.PreCheck(t) },
4531+
ProtoV6ProviderFactories: acctest.Providers,
4532+
CheckDestroy: testAccCheckSecurityDetectionRuleDestroy,
4533+
Steps: []resource.TestStep{
4534+
{
4535+
SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionSupport),
4536+
Config: testAccSecurityDetectionRuleConfig_queryWithMitreThreat("test-query-mitre-rule"),
4537+
Check: resource.ComposeTestCheckFunc(
4538+
resource.TestCheckResourceAttr(resourceName, "name", "test-query-mitre-rule"),
4539+
resource.TestCheckResourceAttr(resourceName, "type", "query"),
4540+
resource.TestCheckResourceAttr(resourceName, "query", "process.parent.name:(EXCEL.EXE OR WINWORD.EXE OR POWERPNT.EXE OR OUTLOOK.EXE)"),
4541+
resource.TestCheckResourceAttr(resourceName, "language", "kuery"),
4542+
resource.TestCheckResourceAttr(resourceName, "enabled", "true"),
4543+
resource.TestCheckResourceAttr(resourceName, "description", "Detects processes started by MS Office programs"),
4544+
resource.TestCheckResourceAttr(resourceName, "severity", "low"),
4545+
resource.TestCheckResourceAttr(resourceName, "risk_score", "50"),
4546+
resource.TestCheckResourceAttr(resourceName, "from", "now-70m"),
4547+
resource.TestCheckResourceAttr(resourceName, "to", "now"),
4548+
resource.TestCheckResourceAttr(resourceName, "interval", "1h"),
4549+
resource.TestCheckResourceAttr(resourceName, "index.0", "logs-*"),
4550+
resource.TestCheckResourceAttr(resourceName, "index.1", "winlogbeat-*"),
4551+
resource.TestCheckResourceAttr(resourceName, "max_signals", "100"),
4552+
4553+
// Check tags
4554+
resource.TestCheckResourceAttr(resourceName, "tags.#", "3"),
4555+
resource.TestCheckResourceAttr(resourceName, "tags.0", "child process"),
4556+
resource.TestCheckResourceAttr(resourceName, "tags.1", "ms office"),
4557+
resource.TestCheckResourceAttr(resourceName, "tags.2", "terraform-test"),
4558+
4559+
// Check references
4560+
resource.TestCheckResourceAttr(resourceName, "references.#", "1"),
4561+
resource.TestCheckResourceAttr(resourceName, "references.0", "https://attack.mitre.org/techniques/T1566/001/"),
4562+
4563+
// Check false positives
4564+
resource.TestCheckResourceAttr(resourceName, "false_positives.#", "1"),
4565+
resource.TestCheckResourceAttr(resourceName, "false_positives.0", "Legitimate corporate macros"),
4566+
4567+
// Check author
4568+
resource.TestCheckResourceAttr(resourceName, "author.#", "1"),
4569+
resource.TestCheckResourceAttr(resourceName, "author.0", "Security Team"),
4570+
4571+
// Check license
4572+
resource.TestCheckResourceAttr(resourceName, "license", "Elastic License v2"),
4573+
4574+
// Check note
4575+
resource.TestCheckResourceAttr(resourceName, "note", "Investigate parent process and command line"),
4576+
4577+
// Check threat (MITRE ATT&CK)
4578+
resource.TestCheckResourceAttr(resourceName, "threat.#", "1"),
4579+
resource.TestCheckResourceAttr(resourceName, "threat.0.framework", "MITRE ATT&CK"),
4580+
resource.TestCheckResourceAttr(resourceName, "threat.0.tactic.id", "TA0009"),
4581+
resource.TestCheckResourceAttr(resourceName, "threat.0.tactic.name", "Collection"),
4582+
resource.TestCheckResourceAttr(resourceName, "threat.0.tactic.reference", "https://attack.mitre.org/tactics/TA0009"),
4583+
resource.TestCheckResourceAttr(resourceName, "threat.0.technique.#", "1"),
4584+
resource.TestCheckResourceAttr(resourceName, "threat.0.technique.0.id", "T1123"),
4585+
resource.TestCheckResourceAttr(resourceName, "threat.0.technique.0.name", "Audio Capture"),
4586+
resource.TestCheckResourceAttr(resourceName, "threat.0.technique.0.reference", "https://attack.mitre.org/techniques/T1123"),
4587+
4588+
resource.TestCheckResourceAttrSet(resourceName, "id"),
4589+
resource.TestCheckResourceAttrSet(resourceName, "rule_id"),
4590+
resource.TestCheckResourceAttrSet(resourceName, "created_at"),
4591+
resource.TestCheckResourceAttrSet(resourceName, "created_by"),
4592+
),
4593+
},
4594+
{
4595+
SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionSupport),
4596+
Config: testAccSecurityDetectionRuleConfig_queryWithMitreThreatUpdate("test-query-mitre-rule-updated"),
4597+
Check: resource.ComposeTestCheckFunc(
4598+
resource.TestCheckResourceAttr(resourceName, "name", "test-query-mitre-rule-updated"),
4599+
resource.TestCheckResourceAttr(resourceName, "description", "Updated detection rule for processes started by MS Office programs"),
4600+
resource.TestCheckResourceAttr(resourceName, "severity", "medium"),
4601+
resource.TestCheckResourceAttr(resourceName, "risk_score", "75"),
4602+
resource.TestCheckResourceAttr(resourceName, "from", "now-2h"),
4603+
resource.TestCheckResourceAttr(resourceName, "interval", "30m"),
4604+
resource.TestCheckResourceAttr(resourceName, "max_signals", "200"),
4605+
4606+
// Check updated tags
4607+
resource.TestCheckResourceAttr(resourceName, "tags.#", "4"),
4608+
resource.TestCheckResourceAttr(resourceName, "tags.0", "child process"),
4609+
resource.TestCheckResourceAttr(resourceName, "tags.1", "ms office"),
4610+
resource.TestCheckResourceAttr(resourceName, "tags.2", "terraform-test"),
4611+
resource.TestCheckResourceAttr(resourceName, "tags.3", "updated"),
4612+
4613+
// Check updated references
4614+
resource.TestCheckResourceAttr(resourceName, "references.#", "2"),
4615+
resource.TestCheckResourceAttr(resourceName, "references.0", "https://attack.mitre.org/techniques/T1566/001/"),
4616+
resource.TestCheckResourceAttr(resourceName, "references.1", "https://attack.mitre.org/techniques/T1204/002/"),
4617+
4618+
// Check updated false positives
4619+
resource.TestCheckResourceAttr(resourceName, "false_positives.#", "2"),
4620+
resource.TestCheckResourceAttr(resourceName, "false_positives.0", "Legitimate corporate macros"),
4621+
resource.TestCheckResourceAttr(resourceName, "false_positives.1", "Authorized office automation"),
4622+
4623+
// Check updated author
4624+
resource.TestCheckResourceAttr(resourceName, "author.#", "2"),
4625+
resource.TestCheckResourceAttr(resourceName, "author.0", "Security Team"),
4626+
resource.TestCheckResourceAttr(resourceName, "author.1", "SOC Team"),
4627+
4628+
// Check updated note
4629+
resource.TestCheckResourceAttr(resourceName, "note", "Investigate parent process and command line. Check for malicious documents."),
4630+
4631+
// Check updated threat - multiple techniques
4632+
resource.TestCheckResourceAttr(resourceName, "threat.#", "1"),
4633+
resource.TestCheckResourceAttr(resourceName, "threat.0.framework", "MITRE ATT&CK"),
4634+
resource.TestCheckResourceAttr(resourceName, "threat.0.tactic.id", "TA0002"),
4635+
resource.TestCheckResourceAttr(resourceName, "threat.0.tactic.name", "Execution"),
4636+
resource.TestCheckResourceAttr(resourceName, "threat.0.tactic.reference", "https://attack.mitre.org/tactics/TA0002"),
4637+
resource.TestCheckResourceAttr(resourceName, "threat.0.technique.#", "2"),
4638+
resource.TestCheckResourceAttr(resourceName, "threat.0.technique.0.id", "T1566"),
4639+
resource.TestCheckResourceAttr(resourceName, "threat.0.technique.0.name", "Phishing"),
4640+
resource.TestCheckResourceAttr(resourceName, "threat.0.technique.0.reference", "https://attack.mitre.org/techniques/T1566"),
4641+
resource.TestCheckResourceAttr(resourceName, "threat.0.technique.0.subtechnique.#", "1"),
4642+
resource.TestCheckResourceAttr(resourceName, "threat.0.technique.0.subtechnique.0.id", "T1566.001"),
4643+
resource.TestCheckResourceAttr(resourceName, "threat.0.technique.0.subtechnique.0.name", "Spearphishing Attachment"),
4644+
resource.TestCheckResourceAttr(resourceName, "threat.0.technique.0.subtechnique.0.reference", "https://attack.mitre.org/techniques/T1566/001"),
4645+
resource.TestCheckResourceAttr(resourceName, "threat.0.technique.1.id", "T1204"),
4646+
resource.TestCheckResourceAttr(resourceName, "threat.0.technique.1.name", "User Execution"),
4647+
resource.TestCheckResourceAttr(resourceName, "threat.0.technique.1.reference", "https://attack.mitre.org/techniques/T1204"),
4648+
resource.TestCheckResourceAttr(resourceName, "threat.0.technique.1.subtechnique.#", "1"),
4649+
resource.TestCheckResourceAttr(resourceName, "threat.0.technique.1.subtechnique.0.id", "T1204.002"),
4650+
resource.TestCheckResourceAttr(resourceName, "threat.0.technique.1.subtechnique.0.name", "Malicious File"),
4651+
resource.TestCheckResourceAttr(resourceName, "threat.0.technique.1.subtechnique.0.reference", "https://attack.mitre.org/techniques/T1204/002"),
4652+
),
4653+
},
4654+
},
4655+
})
4656+
}
4657+
4658+
func testAccSecurityDetectionRuleConfig_queryWithMitreThreat(name string) string {
4659+
return fmt.Sprintf(`
4660+
provider "elasticstack" {
4661+
kibana {}
4662+
}
4663+
4664+
resource "elasticstack_kibana_security_detection_rule" "test" {
4665+
name = "%s"
4666+
type = "query"
4667+
query = "process.parent.name:(EXCEL.EXE OR WINWORD.EXE OR POWERPNT.EXE OR OUTLOOK.EXE)"
4668+
language = "kuery"
4669+
enabled = true
4670+
description = "Detects processes started by MS Office programs"
4671+
severity = "low"
4672+
risk_score = 50
4673+
from = "now-70m"
4674+
to = "now"
4675+
interval = "1h"
4676+
index = ["logs-*", "winlogbeat-*"]
4677+
4678+
tags = ["child process", "ms office", "terraform-test"]
4679+
references = ["https://attack.mitre.org/techniques/T1566/001/"]
4680+
false_positives = ["Legitimate corporate macros"]
4681+
author = ["Security Team"]
4682+
license = "Elastic License v2"
4683+
note = "Investigate parent process and command line"
4684+
max_signals = 100
4685+
4686+
threat = [
4687+
{
4688+
framework = "MITRE ATT&CK"
4689+
tactic = {
4690+
id = "TA0009"
4691+
name = "Collection"
4692+
reference = "https://attack.mitre.org/tactics/TA0009"
4693+
}
4694+
technique = [
4695+
{
4696+
id = "T1123"
4697+
name = "Audio Capture"
4698+
reference = "https://attack.mitre.org/techniques/T1123"
4699+
}
4700+
]
4701+
}
4702+
]
4703+
}
4704+
`, name)
4705+
}
4706+
4707+
func testAccSecurityDetectionRuleConfig_queryWithMitreThreatUpdate(name string) string {
4708+
return fmt.Sprintf(`
4709+
provider "elasticstack" {
4710+
kibana {}
4711+
}
4712+
4713+
resource "elasticstack_kibana_security_detection_rule" "test" {
4714+
name = "%s"
4715+
type = "query"
4716+
query = "process.parent.name:(EXCEL.EXE OR WINWORD.EXE OR POWERPNT.EXE OR OUTLOOK.EXE)"
4717+
language = "kuery"
4718+
enabled = true
4719+
description = "Updated detection rule for processes started by MS Office programs"
4720+
severity = "medium"
4721+
risk_score = 75
4722+
from = "now-2h"
4723+
to = "now"
4724+
interval = "30m"
4725+
index = ["logs-*", "winlogbeat-*", "sysmon-*"]
4726+
4727+
tags = ["child process", "ms office", "terraform-test", "updated"]
4728+
references = ["https://attack.mitre.org/techniques/T1566/001/", "https://attack.mitre.org/techniques/T1204/002/"]
4729+
false_positives = ["Legitimate corporate macros", "Authorized office automation"]
4730+
author = ["Security Team", "SOC Team"]
4731+
license = "Elastic License v2"
4732+
note = "Investigate parent process and command line. Check for malicious documents."
4733+
max_signals = 200
4734+
4735+
threat = [
4736+
{
4737+
framework = "MITRE ATT&CK"
4738+
tactic = {
4739+
id = "TA0002"
4740+
name = "Execution"
4741+
reference = "https://attack.mitre.org/tactics/TA0002"
4742+
}
4743+
technique = [
4744+
{
4745+
id = "T1566"
4746+
name = "Phishing"
4747+
reference = "https://attack.mitre.org/techniques/T1566"
4748+
subtechnique = [
4749+
{
4750+
id = "T1566.001"
4751+
name = "Spearphishing Attachment"
4752+
reference = "https://attack.mitre.org/techniques/T1566/001"
4753+
}
4754+
]
4755+
},
4756+
{
4757+
id = "T1204"
4758+
name = "User Execution"
4759+
reference = "https://attack.mitre.org/techniques/T1204"
4760+
subtechnique = [
4761+
{
4762+
id = "T1204.002"
4763+
name = "Malicious File"
4764+
reference = "https://attack.mitre.org/techniques/T1204/002"
4765+
}
4766+
]
4767+
}
4768+
]
4769+
}
4770+
]
4771+
}
4772+
`, name)
4773+
}

internal/kibana/security_detection_rule/models.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,31 @@ type SeverityMappingModel struct {
240240
Severity types.String `tfsdk:"severity"`
241241
}
242242

243+
type ThreatModel struct {
244+
Framework types.String `tfsdk:"framework"`
245+
Tactic types.Object `tfsdk:"tactic"`
246+
Technique types.List `tfsdk:"technique"`
247+
}
248+
249+
type ThreatTacticModel struct {
250+
Id types.String `tfsdk:"id"`
251+
Name types.String `tfsdk:"name"`
252+
Reference types.String `tfsdk:"reference"`
253+
}
254+
255+
type ThreatTechniqueModel struct {
256+
Id types.String `tfsdk:"id"`
257+
Name types.String `tfsdk:"name"`
258+
Reference types.String `tfsdk:"reference"`
259+
Subtechnique types.List `tfsdk:"subtechnique"`
260+
}
261+
262+
type ThreatSubtechniqueModel struct {
263+
Id types.String `tfsdk:"id"`
264+
Name types.String `tfsdk:"name"`
265+
Reference types.String `tfsdk:"reference"`
266+
}
267+
243268
// CommonCreateProps holds all the field pointers for setting common create properties
244269
type CommonCreateProps struct {
245270
Actions **[]kbapi.SecurityDetectionsAPIRuleAction
@@ -273,6 +298,9 @@ type CommonCreateProps struct {
273298
TimestampOverrideFallbackDisabled **kbapi.SecurityDetectionsAPITimestampOverrideFallbackDisabled
274299
InvestigationFields **kbapi.SecurityDetectionsAPIInvestigationFields
275300
Filters **kbapi.SecurityDetectionsAPIRuleFilterArray
301+
Threat **kbapi.SecurityDetectionsAPIThreatArray
302+
TimelineId **kbapi.SecurityDetectionsAPITimelineTemplateId
303+
TimelineTitle **kbapi.SecurityDetectionsAPITimelineTemplateTitle
276304
}
277305

278306
// CommonUpdateProps holds all the field pointers for setting common update properties
@@ -308,6 +336,9 @@ type CommonUpdateProps struct {
308336
TimestampOverrideFallbackDisabled **kbapi.SecurityDetectionsAPITimestampOverrideFallbackDisabled
309337
InvestigationFields **kbapi.SecurityDetectionsAPIInvestigationFields
310338
Filters **kbapi.SecurityDetectionsAPIRuleFilterArray
339+
Threat **kbapi.SecurityDetectionsAPIThreatArray
340+
TimelineId **kbapi.SecurityDetectionsAPITimelineTemplateId
341+
TimelineTitle **kbapi.SecurityDetectionsAPITimelineTemplateTitle
311342
}
312343

313344
// Helper function to set common properties across all rule types
@@ -538,6 +569,27 @@ func (d SecurityDetectionRuleData) setCommonCreateProps(
538569
*props.AlertSuppression = alertSuppression
539570
}
540571
}
572+
573+
// Set threat (MITRE ATT&CK framework)
574+
if props.Threat != nil && utils.IsKnown(d.Threat) {
575+
threat, threatDiags := d.threatToApi(ctx)
576+
diags.Append(threatDiags...)
577+
if !threatDiags.HasError() && len(threat) > 0 {
578+
*props.Threat = &threat
579+
}
580+
}
581+
582+
// Set timeline ID
583+
if props.TimelineId != nil && utils.IsKnown(d.TimelineId) {
584+
timelineId := kbapi.SecurityDetectionsAPITimelineTemplateId(d.TimelineId.ValueString())
585+
*props.TimelineId = &timelineId
586+
}
587+
588+
// Set timeline title
589+
if props.TimelineTitle != nil && utils.IsKnown(d.TimelineTitle) {
590+
timelineTitle := kbapi.SecurityDetectionsAPITimelineTemplateTitle(d.TimelineTitle.ValueString())
591+
*props.TimelineTitle = &timelineTitle
592+
}
541593
}
542594

543595
// Helper function to set common update properties across all rule types
@@ -762,6 +814,27 @@ func (d SecurityDetectionRuleData) setCommonUpdateProps(
762814
*props.AlertSuppression = alertSuppression
763815
}
764816
}
817+
818+
// Set threat (MITRE ATT&CK framework)
819+
if props.Threat != nil && utils.IsKnown(d.Threat) {
820+
threat, threatDiags := d.threatToApi(ctx)
821+
diags.Append(threatDiags...)
822+
if !threatDiags.HasError() && len(threat) > 0 {
823+
*props.Threat = &threat
824+
}
825+
}
826+
827+
// Set timeline ID
828+
if props.TimelineId != nil && utils.IsKnown(d.TimelineId) {
829+
timelineId := kbapi.SecurityDetectionsAPITimelineTemplateId(d.TimelineId.ValueString())
830+
*props.TimelineId = &timelineId
831+
}
832+
833+
// Set timeline title
834+
if props.TimelineTitle != nil && utils.IsKnown(d.TimelineTitle) {
835+
timelineTitle := kbapi.SecurityDetectionsAPITimelineTemplateTitle(d.TimelineTitle.ValueString())
836+
*props.TimelineTitle = &timelineTitle
837+
}
765838
}
766839

767840
// Helper function to initialize fields that should be set to default values for all rule types

0 commit comments

Comments
 (0)