diff --git a/internal/services/sentinel/sentinel_alert_rule_fusion_resource.go b/internal/services/sentinel/sentinel_alert_rule_fusion_resource.go index 18260ea62883..0f2e8af07111 100644 --- a/internal/services/sentinel/sentinel_alert_rule_fusion_resource.go +++ b/internal/services/sentinel/sentinel_alert_rule_fusion_resource.go @@ -62,6 +62,62 @@ func resourceSentinelAlertRuleFusion() *pluginsdk.Resource { Optional: true, Default: true, }, + + "source": { + Type: pluginsdk.TypeList, + Optional: true, + // Service will auto-fill this if not given in request, based on the "alert_rule_template_guid". + Computed: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + "enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, + }, + "sub_type": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + "enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, + }, + "severities_allowed": { + Type: pluginsdk.TypeSet, + Required: true, + MinItems: 1, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringInSlice( + []string{ + string(securityinsight.AlertSeverityHigh), + string(securityinsight.AlertSeverityMedium), + string(securityinsight.AlertSeverityLow), + string(securityinsight.AlertSeverityInformational), + }, + false, + ), + }, + }, + }, + }, + }, + }, + }, + }, }, } } @@ -98,6 +154,7 @@ func resourceSentinelAlertRuleFusionCreateUpdate(d *pluginsdk.ResourceData, meta FusionAlertRuleProperties: &securityinsight.FusionAlertRuleProperties{ AlertRuleTemplateName: utils.String(d.Get("alert_rule_template_guid").(string)), Enabled: utils.Bool(d.Get("enabled").(bool)), + SourceSettings: expandFusionSourceSettings(d.Get("source").([]interface{})), }, } @@ -155,6 +212,9 @@ func resourceSentinelAlertRuleFusionRead(d *pluginsdk.ResourceData, meta interfa if prop := rule.FusionAlertRuleProperties; prop != nil { d.Set("enabled", prop.Enabled) d.Set("alert_rule_template_guid", prop.AlertRuleTemplateName) + if err := d.Set("source", flattenFusionSourceSettings(prop.SourceSettings)); err != nil { + return fmt.Errorf("setting `source`: %v", err) + } } return nil @@ -176,3 +236,151 @@ func resourceSentinelAlertRuleFusionDelete(d *pluginsdk.ResourceData, meta inter return nil } + +func expandFusionSourceSettings(input []interface{}) *[]securityinsight.FusionSourceSettings { + if len(input) == 0 { + return nil + } + + result := make([]securityinsight.FusionSourceSettings, 0) + + for _, e := range input { + e := e.(map[string]interface{}) + setting := securityinsight.FusionSourceSettings{ + Enabled: utils.Bool(e["enabled"].(bool)), + SourceName: utils.String(e["name"].(string)), + SourceSubTypes: expandFusionSourceSubTypes(e["sub_type"].([]interface{})), + } + result = append(result, setting) + } + + return &result +} + +func expandFusionSourceSubTypes(input []interface{}) *[]securityinsight.FusionSourceSubTypeSetting { + if len(input) == 0 { + return nil + } + + result := make([]securityinsight.FusionSourceSubTypeSetting, 0) + + for _, e := range input { + e := e.(map[string]interface{}) + setting := securityinsight.FusionSourceSubTypeSetting{ + Enabled: utils.Bool(e["enabled"].(bool)), + SourceSubTypeName: utils.String(e["name"].(string)), + SeverityFilters: &securityinsight.FusionSubTypeSeverityFilter{ + Filters: expandFusionSubTypeSeverityFiltersItems(e["severities_allowed"].(*pluginsdk.Set).List()), + }, + } + result = append(result, setting) + } + + return &result +} + +func expandFusionSubTypeSeverityFiltersItems(input []interface{}) *[]securityinsight.FusionSubTypeSeverityFiltersItem { + if len(input) == 0 { + return nil + } + + result := make([]securityinsight.FusionSubTypeSeverityFiltersItem, 0) + + // We can't simply remove the disabled properties in the request, as that will be reflected to the backend model (i.e. those unspecified severity will be absent also). + // As any absent severity then will not be shown in the Portal when users try to edit the alert rule. The drop down menu won't show these absent severities... + filters := map[string]bool{} + for _, e := range securityinsight.PossibleAlertSeverityValues() { + filters[string(e)] = false + } + + for _, e := range input { + filters[e.(string)] = true + } + + for severity, enabled := range filters { + item := securityinsight.FusionSubTypeSeverityFiltersItem{ + Enabled: utils.Bool(enabled), + Severity: securityinsight.AlertSeverity(severity), + } + result = append(result, item) + } + + return &result +} + +func flattenFusionSourceSettings(input *[]securityinsight.FusionSourceSettings) []interface{} { + if input == nil { + return []interface{}{} + } + + output := make([]interface{}, 0) + + for _, e := range *input { + var name string + if e.SourceName != nil { + name = *e.SourceName + } + + var enabled bool + if e.Enabled != nil { + enabled = *e.Enabled + } + + output = append(output, map[string]interface{}{ + "name": name, + "enabled": enabled, + "sub_type": flattenFusionSourceSubTypes(e.SourceSubTypes), + }) + } + + return output +} + +func flattenFusionSourceSubTypes(input *[]securityinsight.FusionSourceSubTypeSetting) []interface{} { + if input == nil { + return []interface{}{} + } + + output := make([]interface{}, 0) + + for _, e := range *input { + var name string + if e.SourceSubTypeName != nil { + name = *e.SourceSubTypeName + } + + var enabledSeverities []interface{} + if e.SeverityFilters != nil { + enabledSeverities = flattenFusionSubTypeSeverityFiltersItems(e.SeverityFilters.Filters) + } + + var enabled bool + if e.Enabled != nil { + enabled = *e.Enabled + } + + output = append(output, map[string]interface{}{ + "name": name, + "enabled": enabled, + "severities_allowed": enabledSeverities, + }) + } + + return output +} + +func flattenFusionSubTypeSeverityFiltersItems(input *[]securityinsight.FusionSubTypeSeverityFiltersItem) []interface{} { + if input == nil { + return []interface{}{} + } + + output := make([]interface{}, 0) + + for _, e := range *input { + if e.Enabled != nil && *e.Enabled { + output = append(output, string(e.Severity)) + } + } + + return output +} diff --git a/internal/services/sentinel/sentinel_alert_rule_fusion_resource_test.go b/internal/services/sentinel/sentinel_alert_rule_fusion_resource_test.go index c975348da1cf..8f8163a90ba0 100644 --- a/internal/services/sentinel/sentinel_alert_rule_fusion_resource_test.go +++ b/internal/services/sentinel/sentinel_alert_rule_fusion_resource_test.go @@ -31,42 +31,48 @@ func TestAccSentinelAlertRuleFusion_basic(t *testing.T) { }) } -func TestAccSentinelAlertRuleFusion_complete(t *testing.T) { +func TestAccSentinelAlertRuleFusion_disable(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_sentinel_alert_rule_fusion", "test") r := SentinelAlertRuleFusionResource{} data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.complete(data), + Config: r.basic(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), }, data.ImportStep(), - }) -} - -func TestAccSentinelAlertRuleFusion_update(t *testing.T) { - data := acceptance.BuildTestData(t, "azurerm_sentinel_alert_rule_fusion", "test") - r := SentinelAlertRuleFusionResource{} - - data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.basic(data), + Config: r.disabled(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), }, data.ImportStep(), { - Config: r.complete(data), + Config: r.basic(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), }, data.ImportStep(), + }) +} + +func TestAccSentinelAlertRuleFusion_sourceSetting(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_sentinel_alert_rule_fusion", "test") + r := SentinelAlertRuleFusionResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.basic(data), + Config: r.sourceSetting(data, true), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + { + Config: r.sourceSetting(data, false), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), @@ -131,7 +137,7 @@ resource "azurerm_sentinel_alert_rule_fusion" "test" { `, r.template(data), data.RandomInteger) } -func (r SentinelAlertRuleFusionResource) complete(data acceptance.TestData) string { +func (r SentinelAlertRuleFusionResource) disabled(data acceptance.TestData) string { return fmt.Sprintf(` %s @@ -149,6 +155,81 @@ resource "azurerm_sentinel_alert_rule_fusion" "test" { `, r.template(data), data.RandomInteger) } +func (r SentinelAlertRuleFusionResource) sourceSetting(data acceptance.TestData, enabled bool) string { + return fmt.Sprintf(` +%[1]s + +data "azurerm_sentinel_alert_rule_template" "test" { + display_name = "Advanced Multistage Attack Detection" + log_analytics_workspace_id = azurerm_log_analytics_solution.test.workspace_resource_id +} + +resource "azurerm_sentinel_alert_rule_fusion" "test" { + name = "acctest-SentinelAlertRule-Fusion-%[2]d" + log_analytics_workspace_id = azurerm_log_analytics_solution.test.workspace_resource_id + alert_rule_template_guid = data.azurerm_sentinel_alert_rule_template.test.name + source { + name = "Anomalies" + enabled = %[3]t + } + source { + name = "Alert providers" + enabled = %[3]t + sub_type { + severities_allowed = ["High", "Informational", "Low", "Medium"] + name = "Azure Active Directory Identity Protection" + enabled = %[3]t + } + sub_type { + severities_allowed = ["High", "Informational", "Low", "Medium"] + name = "Microsoft 365 Defender" + enabled = %[3]t + } + sub_type { + severities_allowed = ["High", "Informational", "Low", "Medium"] + name = "Microsoft Cloud App Security" + enabled = %[3]t + } + sub_type { + severities_allowed = ["High", "Informational", "Low", "Medium"] + name = "Azure Defender" + enabled = %[3]t + } + sub_type { + severities_allowed = ["High", "Informational", "Low", "Medium"] + name = "Microsoft Defender for Endpoint" + enabled = %[3]t + } + sub_type { + severities_allowed = ["High", "Informational", "Low", "Medium"] + name = "Microsoft Defender for Identity" + enabled = %[3]t + } + sub_type { + severities_allowed = ["High", "Informational", "Low", "Medium"] + name = "Azure Defender for IoT" + enabled = %[3]t + } + sub_type { + severities_allowed = ["High", "Informational", "Low", "Medium"] + name = "Microsoft Defender for Office 365" + enabled = %[3]t + } + sub_type { + severities_allowed = ["High", "Informational", "Low", "Medium"] + name = "Azure Sentinel scheduled analytics rules" + enabled = %[3]t + } + sub_type { + severities_allowed = ["High", "Informational", "Low", "Medium"] + name = "Azure Sentinel NRT analytic rules" + enabled = %[3]t + } + } +} +`, r.template(data), data.RandomInteger, enabled) +} + func (r SentinelAlertRuleFusionResource) requiresImport(data acceptance.TestData) string { return fmt.Sprintf(` %s diff --git a/website/docs/r/sentinel_alert_rule_fusion.html.markdown b/website/docs/r/sentinel_alert_rule_fusion.html.markdown index 4ab3020a3466..50394f4a621e 100644 --- a/website/docs/r/sentinel_alert_rule_fusion.html.markdown +++ b/website/docs/r/sentinel_alert_rule_fusion.html.markdown @@ -57,6 +57,28 @@ The following arguments are supported: * `enabled` - (Optional) Should this Sentinel Fusion Alert Rule be enabled? Defaults to `true`. +* `source` (Optional) One or more `source` blocks as defined below. + +--- + +A `source` block supports the following: + +* `name` - (Required) The name of the Fusion source signal. Refer to Fusion alert rule template for supported values. + +* `enabled` - (Optional) Whether this source signal is enabled or disabled in Fusion detection? Defaults to `true`. + +* `sub_type` - (Optional) One or more `sub_type` blocks as defined below. + +--- + +A `sub_type` block supports the following: + +* `name` - (Required) The Name of the source subtype under a given source signal in Fusion detection. Refer to Fusion alert rule template for supported values. + +* `enabled` - (Optional) Whether this source subtype under source signal is enabled or disabled in Fusion detection. Defaults to `true`. + +* `severities_allowed` - (Optional) A list of severities that are enabled for this source subtype consumed in Fusion detection. Possible values for each element are `High`, `Medium`, `Low`, `Informational`. + ## Attributes Reference In addition to the Arguments listed above - the following Attributes are exported: