diff --git a/internal/services/sentinel/registration.go b/internal/services/sentinel/registration.go index e8399e8e4695..4bd36cacaf0e 100644 --- a/internal/services/sentinel/registration.go +++ b/internal/services/sentinel/registration.go @@ -40,6 +40,7 @@ func (r Registration) SupportedResources() map[string]*pluginsdk.Resource { "azurerm_sentinel_alert_rule_machine_learning_behavior_analytics": resourceSentinelAlertRuleMLBehaviorAnalytics(), "azurerm_sentinel_alert_rule_ms_security_incident": resourceSentinelAlertRuleMsSecurityIncident(), "azurerm_sentinel_alert_rule_scheduled": resourceSentinelAlertRuleScheduled(), + "azurerm_sentinel_alert_rule_nrt": resourceSentinelAlertRuleNrt(), "azurerm_sentinel_data_connector_aws_cloud_trail": resourceSentinelDataConnectorAwsCloudTrail(), "azurerm_sentinel_data_connector_azure_active_directory": resourceSentinelDataConnectorAzureActiveDirectory(), "azurerm_sentinel_data_connector_azure_advanced_threat_protection": resourceSentinelDataConnectorAzureAdvancedThreatProtection(), diff --git a/internal/services/sentinel/sentinel_alert_rule.go b/internal/services/sentinel/sentinel_alert_rule.go index 0a899c3b856a..f53b28a255c1 100644 --- a/internal/services/sentinel/sentinel_alert_rule.go +++ b/internal/services/sentinel/sentinel_alert_rule.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/terraform-provider-azurerm/internal/clients" "github.com/hashicorp/terraform-provider-azurerm/internal/services/sentinel/parse" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/utils" ) func alertRuleID(rule securityinsight.BasicAlertRule) *string { @@ -23,6 +24,8 @@ func alertRuleID(rule securityinsight.BasicAlertRule) *string { return rule.ID case securityinsight.MLBehaviorAnalyticsAlertRule: return rule.ID + case securityinsight.NrtAlertRule: + return rule.ID default: return nil } @@ -59,9 +62,317 @@ func assertAlertRuleKind(rule securityinsight.BasicAlertRule, expectKind securit kind = securityinsight.AlertRuleKindMicrosoftSecurityIncidentCreation case securityinsight.ScheduledAlertRule: kind = securityinsight.AlertRuleKindScheduled + case securityinsight.NrtAlertRule: + kind = securityinsight.AlertRuleKindNRT } if expectKind != kind { return fmt.Errorf("Sentinel Alert Rule has mismatched kind, expected: %q, got %q", expectKind, kind) } return nil } + +func expandAlertRuleTactics(input []interface{}) *[]securityinsight.AttackTactic { + result := make([]securityinsight.AttackTactic, 0) + + for _, e := range input { + result = append(result, securityinsight.AttackTactic(e.(string))) + } + + return &result +} + +func flattenAlertRuleTactics(input *[]securityinsight.AttackTactic) []interface{} { + if input == nil { + return []interface{}{} + } + + output := make([]interface{}, 0) + + for _, e := range *input { + output = append(output, string(e)) + } + + return output +} + +func expandAlertRuleIncidentConfiguration(input []interface{}, createIncidentKey string, withGroupByPrefix bool) *securityinsight.IncidentConfiguration { + if len(input) == 0 || input[0] == nil { + return nil + } + + raw := input[0].(map[string]interface{}) + + output := &securityinsight.IncidentConfiguration{ + CreateIncident: utils.Bool(raw[createIncidentKey].(bool)), + GroupingConfiguration: expandAlertRuleGrouping(raw["grouping"].([]interface{}), withGroupByPrefix), + } + + return output +} + +func flattenAlertRuleIncidentConfiguration(input *securityinsight.IncidentConfiguration, createIncidentKey string, withGroupByPrefix bool) []interface{} { + if input == nil { + return []interface{}{} + } + + createIncident := false + if input.CreateIncident != nil { + createIncident = *input.CreateIncident + } + + return []interface{}{ + map[string]interface{}{ + createIncidentKey: createIncident, + "grouping": flattenAlertRuleGrouping(input.GroupingConfiguration, withGroupByPrefix), + }, + } +} + +func expandAlertRuleGrouping(input []interface{}, withGroupPrefix bool) *securityinsight.GroupingConfiguration { + if len(input) == 0 || input[0] == nil { + return nil + } + + raw := input[0].(map[string]interface{}) + + output := &securityinsight.GroupingConfiguration{ + Enabled: utils.Bool(raw["enabled"].(bool)), + ReopenClosedIncident: utils.Bool(raw["reopen_closed_incidents"].(bool)), + LookbackDuration: utils.String(raw["lookback_duration"].(string)), + MatchingMethod: securityinsight.MatchingMethod(raw["entity_matching_method"].(string)), + } + + key := "by_entities" + if withGroupPrefix { + key = "group_" + key + } + groupByEntitiesList := raw[key].([]interface{}) + groupByEntities := make([]securityinsight.EntityMappingType, len(groupByEntitiesList)) + for idx, t := range groupByEntitiesList { + groupByEntities[idx] = securityinsight.EntityMappingType(t.(string)) + } + output.GroupByEntities = &groupByEntities + + key = "by_alert_details" + if withGroupPrefix { + key = "group_" + key + } + groupByAlertDetailsList := raw[key].([]interface{}) + groupByAlertDetails := make([]securityinsight.AlertDetail, len(groupByAlertDetailsList)) + for idx, t := range groupByAlertDetailsList { + groupByAlertDetails[idx] = securityinsight.AlertDetail(t.(string)) + } + output.GroupByAlertDetails = &groupByAlertDetails + + key = "by_custom_details" + if withGroupPrefix { + key = "group_" + key + } + output.GroupByCustomDetails = utils.ExpandStringSlice(raw[key].([]interface{})) + + return output +} + +func flattenAlertRuleGrouping(input *securityinsight.GroupingConfiguration, withGroupPrefix bool) []interface{} { + if input == nil { + return []interface{}{} + } + + enabled := false + if input.Enabled != nil { + enabled = *input.Enabled + } + + lookbackDuration := "" + if input.LookbackDuration != nil { + lookbackDuration = *input.LookbackDuration + } + + reopenClosedIncidents := false + if input.ReopenClosedIncident != nil { + reopenClosedIncidents = *input.ReopenClosedIncident + } + + var groupByEntities []interface{} + if input.GroupByEntities != nil { + for _, entity := range *input.GroupByEntities { + groupByEntities = append(groupByEntities, string(entity)) + } + } + + var groupByAlertDetails []interface{} + if input.GroupByAlertDetails != nil { + for _, detail := range *input.GroupByAlertDetails { + groupByAlertDetails = append(groupByAlertDetails, string(detail)) + } + } + + var groupByCustomDetails []interface{} + if input.GroupByCustomDetails != nil { + for _, detail := range *input.GroupByCustomDetails { + groupByCustomDetails = append(groupByCustomDetails, detail) + } + } + + var ( + k1 = "by_entities" + k2 = "by_alert_details" + k3 = "by_custom_details" + ) + + if withGroupPrefix { + k1 = "group_" + k1 + k2 = "group_" + k2 + k3 = "group_" + k3 + } + return []interface{}{ + map[string]interface{}{ + "enabled": enabled, + "lookback_duration": lookbackDuration, + "reopen_closed_incidents": reopenClosedIncidents, + "entity_matching_method": string(input.MatchingMethod), + k1: groupByEntities, + k2: groupByAlertDetails, + k3: groupByCustomDetails, + }, + } +} + +func expandAlertRuleAlertDetailsOverride(input []interface{}) *securityinsight.AlertDetailsOverride { + if len(input) == 0 || input[0] == nil { + return nil + } + + b := input[0].(map[string]interface{}) + output := &securityinsight.AlertDetailsOverride{} + + if v := b["description_format"]; v != "" { + output.AlertDescriptionFormat = utils.String(v.(string)) + } + if v := b["display_name_format"]; v != "" { + output.AlertDisplayNameFormat = utils.String(v.(string)) + } + if v := b["severity_column_name"]; v != "" { + output.AlertSeverityColumnName = utils.String(v.(string)) + } + if v := b["tactics_column_name"]; v != "" { + output.AlertTacticsColumnName = utils.String(v.(string)) + } + + return output +} + +func flattenAlertRuleAlertDetailsOverride(input *securityinsight.AlertDetailsOverride) []interface{} { + if input == nil { + return []interface{}{} + } + + var descriptionFormat string + if input.AlertDescriptionFormat != nil { + descriptionFormat = *input.AlertDescriptionFormat + } + + var displayNameFormat string + if input.AlertDisplayNameFormat != nil { + displayNameFormat = *input.AlertDisplayNameFormat + } + + var severityColumnName string + if input.AlertSeverityColumnName != nil { + severityColumnName = *input.AlertSeverityColumnName + } + + var tacticsColumnName string + if input.AlertTacticsColumnName != nil { + tacticsColumnName = *input.AlertTacticsColumnName + } + + return []interface{}{ + map[string]interface{}{ + "description_format": descriptionFormat, + "display_name_format": displayNameFormat, + "severity_column_name": severityColumnName, + "tactics_column_name": tacticsColumnName, + }, + } +} + +func expandAlertRuleEntityMapping(input []interface{}) *[]securityinsight.EntityMapping { + if len(input) == 0 { + return nil + } + + result := make([]securityinsight.EntityMapping, 0) + + for _, e := range input { + b := e.(map[string]interface{}) + result = append(result, securityinsight.EntityMapping{ + EntityType: securityinsight.EntityMappingType(b["entity_type"].(string)), + FieldMappings: expandAlertRuleFieldMapping(b["field_mapping"].([]interface{})), + }) + } + + return &result +} + +func flattenAlertRuleEntityMapping(input *[]securityinsight.EntityMapping) []interface{} { + if input == nil { + return []interface{}{} + } + + output := make([]interface{}, 0) + + for _, e := range *input { + output = append(output, map[string]interface{}{ + "entity_type": string(e.EntityType), + "field_mapping": flattenAlertRuleFieldMapping(e.FieldMappings), + }) + } + + return output +} + +func expandAlertRuleFieldMapping(input []interface{}) *[]securityinsight.FieldMapping { + if len(input) == 0 { + return nil + } + + result := make([]securityinsight.FieldMapping, 0) + + for _, e := range input { + b := e.(map[string]interface{}) + result = append(result, securityinsight.FieldMapping{ + Identifier: utils.String(b["identifier"].(string)), + ColumnName: utils.String(b["column_name"].(string)), + }) + } + + return &result +} + +func flattenAlertRuleFieldMapping(input *[]securityinsight.FieldMapping) []interface{} { + if input == nil { + return []interface{}{} + } + + output := make([]interface{}, 0) + + for _, e := range *input { + var identifier string + if e.Identifier != nil { + identifier = *e.Identifier + } + + var columnName string + if e.ColumnName != nil { + columnName = *e.ColumnName + } + + output = append(output, map[string]interface{}{ + "identifier": identifier, + "column_name": columnName, + }) + } + + return output +} diff --git a/internal/services/sentinel/sentinel_alert_rule_nrt_resource.go b/internal/services/sentinel/sentinel_alert_rule_nrt_resource.go new file mode 100644 index 000000000000..202a80d2a781 --- /dev/null +++ b/internal/services/sentinel/sentinel_alert_rule_nrt_resource.go @@ -0,0 +1,471 @@ +package sentinel + +import ( + "fmt" + "log" + "time" + + "github.com/Azure/azure-sdk-for-go/services/preview/securityinsight/mgmt/2021-09-01-preview/securityinsight" + "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" + "github.com/hashicorp/terraform-provider-azurerm/helpers/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + loganalyticsParse "github.com/hashicorp/terraform-provider-azurerm/internal/services/loganalytics/parse" + loganalyticsValidate "github.com/hashicorp/terraform-provider-azurerm/internal/services/loganalytics/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/sentinel/parse" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" + "github.com/hashicorp/terraform-provider-azurerm/internal/timeouts" + "github.com/hashicorp/terraform-provider-azurerm/utils" +) + +func resourceSentinelAlertRuleNrt() *pluginsdk.Resource { + var entityMappingTypes = []string{ + string(securityinsight.EntityMappingTypeAccount), + string(securityinsight.EntityMappingTypeAzureResource), + string(securityinsight.EntityMappingTypeCloudApplication), + string(securityinsight.EntityMappingTypeDNS), + string(securityinsight.EntityMappingTypeFile), + string(securityinsight.EntityMappingTypeFileHash), + string(securityinsight.EntityMappingTypeHost), + string(securityinsight.EntityMappingTypeIP), + string(securityinsight.EntityMappingTypeMailbox), + string(securityinsight.EntityMappingTypeMailCluster), + string(securityinsight.EntityMappingTypeMailMessage), + string(securityinsight.EntityMappingTypeMalware), + string(securityinsight.EntityMappingTypeProcess), + string(securityinsight.EntityMappingTypeRegistryKey), + string(securityinsight.EntityMappingTypeRegistryValue), + string(securityinsight.EntityMappingTypeSecurityGroup), + string(securityinsight.EntityMappingTypeSubmissionMail), + string(securityinsight.EntityMappingTypeURL), + } + return &pluginsdk.Resource{ + Create: resourceSentinelAlertRuleNrtCreateUpdate, + Read: resourceSentinelAlertRuleNrtRead, + Update: resourceSentinelAlertRuleNrtCreateUpdate, + Delete: resourceSentinelAlertRuleNrtDelete, + + Importer: pluginsdk.ImporterValidatingResourceIdThen(func(id string) error { + _, err := parse.AlertRuleID(id) + return err + }, importSentinelAlertRule(securityinsight.AlertRuleKindNRT)), + + Timeouts: &pluginsdk.ResourceTimeout{ + Create: pluginsdk.DefaultTimeout(30 * time.Minute), + Read: pluginsdk.DefaultTimeout(5 * time.Minute), + Update: pluginsdk.DefaultTimeout(30 * time.Minute), + Delete: pluginsdk.DefaultTimeout(30 * time.Minute), + }, + + Schema: map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "log_analytics_workspace_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: loganalyticsValidate.LogAnalyticsWorkspaceID, + }, + + "display_name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "alert_rule_template_guid": { + Type: pluginsdk.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.IsUUID, + }, + + "alert_rule_template_version": { + Type: pluginsdk.TypeString, + Optional: true, + ForceNew: true, + }, + + "description": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "tactics": { + Type: pluginsdk.TypeSet, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringInSlice([]string{ + string(securityinsight.AttackTacticCollection), + string(securityinsight.AttackTacticCommandAndControl), + string(securityinsight.AttackTacticCredentialAccess), + string(securityinsight.AttackTacticDefenseEvasion), + string(securityinsight.AttackTacticDiscovery), + string(securityinsight.AttackTacticExecution), + string(securityinsight.AttackTacticExfiltration), + string(securityinsight.AttackTacticImpact), + string(securityinsight.AttackTacticInitialAccess), + string(securityinsight.AttackTacticLateralMovement), + string(securityinsight.AttackTacticPersistence), + string(securityinsight.AttackTacticPrivilegeEscalation), + string(securityinsight.AttackTacticPreAttack), + }, false), + }, + }, + + "incident": { + Type: pluginsdk.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + MinItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "create_incident_enabled": { + Required: true, + Type: pluginsdk.TypeBool, + }, + "grouping": { + Type: pluginsdk.TypeList, + Required: true, + MaxItems: 1, + MinItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, + }, + "lookback_duration": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validate.ISO8601Duration, + Default: "PT5M", + }, + "reopen_closed_incidents": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + }, + "entity_matching_method": { + Type: pluginsdk.TypeString, + Optional: true, + Default: securityinsight.MatchingMethodAnyAlert, + ValidateFunc: validation.StringInSlice([]string{ + string(securityinsight.MatchingMethodAnyAlert), + string(securityinsight.MatchingMethodSelected), + string(securityinsight.MatchingMethodAllEntities), + }, false), + }, + "by_entities": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringInSlice(entityMappingTypes, false), + }, + }, + "by_alert_details": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringInSlice([]string{ + string(securityinsight.AlertDetailDisplayName), + string(securityinsight.AlertDetailSeverity), + }, + false), + }, + }, + "by_custom_details": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + }, + }, + }, + }, + }, + }, + + "severity": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + string(securityinsight.AlertSeverityHigh), + string(securityinsight.AlertSeverityMedium), + string(securityinsight.AlertSeverityLow), + string(securityinsight.AlertSeverityInformational), + }, false), + }, + + "enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, + }, + + "query": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "suppression_enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + }, + "suppression_duration": { + Type: pluginsdk.TypeString, + Optional: true, + Default: "PT5H", + ValidateFunc: validate.ISO8601DurationBetween("PT5M", "PT24H"), + }, + "alert_details_override": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "description_format": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + "display_name_format": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + "severity_column_name": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + "tactics_column_name": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + }, + }, + "custom_details": { + Type: pluginsdk.TypeMap, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + "entity_mapping": { + Type: pluginsdk.TypeList, + Optional: true, + MaxItems: 5, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "entity_type": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(entityMappingTypes, false), + }, + "field_mapping": { + Type: pluginsdk.TypeList, + MaxItems: 3, + Required: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "identifier": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + "column_name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func resourceSentinelAlertRuleNrtCreateUpdate(d *pluginsdk.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Sentinel.AlertRulesClient + ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d) + defer cancel() + + name := d.Get("name").(string) + workspaceID, err := loganalyticsParse.LogAnalyticsWorkspaceID(d.Get("log_analytics_workspace_id").(string)) + if err != nil { + return err + } + id := parse.NewAlertRuleID(workspaceID.SubscriptionId, workspaceID.ResourceGroup, workspaceID.WorkspaceName, name) + + if d.IsNewResource() { + resp, err := client.Get(ctx, workspaceID.ResourceGroup, workspaceID.WorkspaceName, name) + if err != nil { + if !utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("checking for existing %q: %+v", id, err) + } + } + + id := alertRuleID(resp.Value) + if id != nil && *id != "" { + return tf.ImportAsExistsError("azurerm_sentinel_alert_rule_nrt", *id) + } + } + + // query frequency must <= suppression duration: otherwise suppression has no effect. + suppressionDuration := d.Get("suppression_duration").(string) + suppressionEnabled := d.Get("suppression_enabled").(bool) + + param := securityinsight.NrtAlertRule{ + Kind: securityinsight.KindBasicAlertRuleKindNRT, + NrtAlertRuleProperties: &securityinsight.NrtAlertRuleProperties{ + Description: utils.String(d.Get("description").(string)), + DisplayName: utils.String(d.Get("display_name").(string)), + Tactics: expandAlertRuleTactics(d.Get("tactics").(*pluginsdk.Set).List()), + IncidentConfiguration: expandAlertRuleIncidentConfiguration(d.Get("incident").([]interface{}), "create_incident_enabled", false), + Severity: securityinsight.AlertSeverity(d.Get("severity").(string)), + Enabled: utils.Bool(d.Get("enabled").(bool)), + Query: utils.String(d.Get("query").(string)), + SuppressionEnabled: &suppressionEnabled, + SuppressionDuration: &suppressionDuration, + }, + } + + if v, ok := d.GetOk("alert_rule_template_guid"); ok { + param.NrtAlertRuleProperties.AlertRuleTemplateName = utils.String(v.(string)) + } + if v, ok := d.GetOk("alert_rule_template_version"); ok { + param.NrtAlertRuleProperties.TemplateVersion = utils.String(v.(string)) + } + if v, ok := d.GetOk("alert_details_override"); ok { + param.NrtAlertRuleProperties.AlertDetailsOverride = expandAlertRuleAlertDetailsOverride(v.([]interface{})) + } + if v, ok := d.GetOk("custom_details"); ok { + param.NrtAlertRuleProperties.CustomDetails = utils.ExpandMapStringPtrString(v.(map[string]interface{})) + } + if v, ok := d.GetOk("entity_mapping"); ok { + param.NrtAlertRuleProperties.EntityMappings = expandAlertRuleEntityMapping(v.([]interface{})) + } + + // Service avoid concurrent update of this resource via checking the "etag" to guarantee it is the same value as last Read. + if !d.IsNewResource() { + resp, err := client.Get(ctx, workspaceID.ResourceGroup, workspaceID.WorkspaceName, name) + if err != nil { + return fmt.Errorf("retrieving %q: %+v", id, err) + } + + if err := assertAlertRuleKind(resp.Value, securityinsight.AlertRuleKindNRT); err != nil { + return fmt.Errorf("asserting %q: %+v", id, err) + } + param.Etag = resp.Value.(securityinsight.NrtAlertRule).Etag + } + + if _, err := client.CreateOrUpdate(ctx, workspaceID.ResourceGroup, workspaceID.WorkspaceName, name, param); err != nil { + return fmt.Errorf("creating %q: %+v", id, err) + } + + d.SetId(id.ID()) + + return resourceSentinelAlertRuleNrtRead(d, meta) +} + +func resourceSentinelAlertRuleNrtRead(d *pluginsdk.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Sentinel.AlertRulesClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.AlertRuleID(d.Id()) + if err != nil { + return err + } + + resp, err := client.Get(ctx, id.ResourceGroup, id.WorkspaceName, id.Name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[DEBUG] %q was not found - removing from state!", id) + d.SetId("") + return nil + } + + return fmt.Errorf("retrieving %q: %+v", id, err) + } + + if err := assertAlertRuleKind(resp.Value, securityinsight.AlertRuleKindNRT); err != nil { + return fmt.Errorf("asserting %q: %+v", id, err) + } + rule := resp.Value.(securityinsight.NrtAlertRule) + + d.Set("name", id.Name) + + workspaceId := loganalyticsParse.NewLogAnalyticsWorkspaceID(id.SubscriptionId, id.ResourceGroup, id.WorkspaceName) + d.Set("log_analytics_workspace_id", workspaceId.ID()) + + if prop := rule.NrtAlertRuleProperties; prop != nil { + d.Set("description", prop.Description) + d.Set("display_name", prop.DisplayName) + if err := d.Set("tactics", flattenAlertRuleTactics(prop.Tactics)); err != nil { + return fmt.Errorf("setting `tactics`: %+v", err) + } + if err := d.Set("incident", flattenAlertRuleIncidentConfiguration(prop.IncidentConfiguration, "create_incident_enabled", false)); err != nil { + return fmt.Errorf("setting `incident`: %+v", err) + } + d.Set("severity", string(prop.Severity)) + d.Set("enabled", prop.Enabled) + d.Set("query", prop.Query) + + d.Set("suppression_enabled", prop.SuppressionEnabled) + d.Set("suppression_duration", prop.SuppressionDuration) + d.Set("alert_rule_template_guid", prop.AlertRuleTemplateName) + d.Set("alert_rule_template_version", prop.TemplateVersion) + + if err := d.Set("alert_details_override", flattenAlertRuleAlertDetailsOverride(prop.AlertDetailsOverride)); err != nil { + return fmt.Errorf("setting `alert_details_override`: %+v", err) + } + if err := d.Set("custom_details", utils.FlattenMapStringPtrString(prop.CustomDetails)); err != nil { + return fmt.Errorf("setting `custom_details`: %+v", err) + } + if err := d.Set("entity_mapping", flattenAlertRuleEntityMapping(prop.EntityMappings)); err != nil { + return fmt.Errorf("setting `entity_mapping`: %+v", err) + } + } + + return nil +} + +func resourceSentinelAlertRuleNrtDelete(d *pluginsdk.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Sentinel.AlertRulesClient + ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.AlertRuleID(d.Id()) + if err != nil { + return err + } + + if _, err := client.Delete(ctx, id.ResourceGroup, id.WorkspaceName, id.Name); err != nil { + return fmt.Errorf("deleting Sentinel Alert Rule Nrt %q: %+v", id, err) + } + + return nil +} diff --git a/internal/services/sentinel/sentinel_alert_rule_nrt_resource_test.go b/internal/services/sentinel/sentinel_alert_rule_nrt_resource_test.go new file mode 100644 index 000000000000..a40bacb3b1d0 --- /dev/null +++ b/internal/services/sentinel/sentinel_alert_rule_nrt_resource_test.go @@ -0,0 +1,284 @@ +package sentinel_test + +import ( + "context" + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/services/preview/securityinsight/mgmt/2021-09-01-preview/securityinsight" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" + "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/sentinel/parse" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/utils" +) + +type SentinelAlertRuleNrtResource struct{} + +func TestAccSentinelAlertRuleNrt_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_sentinel_alert_rule_nrt", "test") + r := SentinelAlertRuleNrtResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccSentinelAlertRuleNrt_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_sentinel_alert_rule_nrt", "test") + r := SentinelAlertRuleNrtResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccSentinelAlertRuleNrt_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_sentinel_alert_rule_nrt", "test") + r := SentinelAlertRuleNrtResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.completeUpdate(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccSentinelAlertRuleNrt_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_sentinel_alert_rule_nrt", "test") + r := SentinelAlertRuleNrtResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.RequiresImportErrorStep(r.requiresImport), + }) +} + +func TestAccSentinelAlertRuleNrt_withAlertRuleTemplateGuid(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_sentinel_alert_rule_nrt", "test") + r := SentinelAlertRuleNrtResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.alertRuleTemplateGuid(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func (t SentinelAlertRuleNrtResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := parse.AlertRuleID(state.ID) + if err != nil { + return nil, err + } + + resp, err := clients.Sentinel.AlertRulesClient.Get(ctx, id.ResourceGroup, id.WorkspaceName, id.Name) + if err != nil { + return nil, fmt.Errorf("reading %q: %v", id, err) + } + + rule, ok := resp.Value.(securityinsight.NrtAlertRule) + if !ok { + return nil, fmt.Errorf("the Alert Rule %q is not a NRT Alert Rule", id) + } + + return utils.Bool(rule.ID != nil), nil +} + +func (r SentinelAlertRuleNrtResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_sentinel_alert_rule_nrt" "test" { + name = "acctest-SentinelAlertRule-NRT-%d" + log_analytics_workspace_id = azurerm_log_analytics_solution.test.workspace_resource_id + display_name = "Some Rule" + severity = "High" + query = <