diff --git a/internal/services/sentinel/registration.go b/internal/services/sentinel/registration.go index 16462b2536ca..75c86541178e 100644 --- a/internal/services/sentinel/registration.go +++ b/internal/services/sentinel/registration.go @@ -77,5 +77,6 @@ func (r Registration) Resources() []sdk.Resource { DataConnectorMicrosoftThreatIntelligenceResource{}, AlertRuleAnomalyBuiltInResource{}, MetadataResource{}, + AlertRuleAnomalyDuplicateResource{}, } } diff --git a/internal/services/sentinel/sentinel_alert_rule_anomaly_duplicate_resource.go b/internal/services/sentinel/sentinel_alert_rule_anomaly_duplicate_resource.go new file mode 100644 index 000000000000..652f1d8b391a --- /dev/null +++ b/internal/services/sentinel/sentinel_alert_rule_anomaly_duplicate_resource.go @@ -0,0 +1,786 @@ +package sentinel + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/google/uuid" + "github.com/hashicorp/go-azure-sdk/resource-manager/operationalinsights/2022-10-01/workspaces" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/sentinel/azuresdkhacks" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/sentinel/parse" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/sentinel/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" + "github.com/hashicorp/terraform-provider-azurerm/utils" + securityinsight "github.com/tombuildsstuff/kermit/sdk/securityinsights/2022-10-01-preview/securityinsights" +) + +type AlertRuleAnomalyDuplicateModel struct { + Name string `tfschema:"name"` + DisplayName string `tfschema:"display_name"` + BuiltInRuleId string `tfschema:"built_in_rule_id"` + WorkspaceId string `tfschema:"log_analytics_workspace_id"` + Enabled bool `tfschema:"enabled"` + Mode string `tfschema:"mode"` + AnomalyVersion string `tfschema:"anomaly_version"` + AnomalySettingsVersion int32 `tfschema:"anomaly_settings_version"` + Description string `tfschema:"description"` + Frequency string `tfschema:"frequency"` + IsDefaultSettings bool `tfschema:"is_default_settings"` + RequiredDataConnectors []AnomalyRuleRequiredDataConnectorModel `tfschema:"required_data_connector"` + SettingsDefinitionId string `tfschema:"settings_definition_id"` + Tactics []string `tfschema:"tactics"` + Techniques []string `tfschema:"techniques"` + ThresholdObservation []AnomalyRuleThresholdModel `tfschema:"threshold_observation"` + MultiSelectObservation []AnomalyRuleMultiSelectModel `tfschema:"multi_select_observation"` + SingleSelectObservation []AnomalyRuleSingleSelectModel `tfschema:"single_select_observation"` + PrioritizeExcludeObservation []AnomalyRulePriorityModel `tfschema:"prioritized_exclude_observation"` +} + +type AlertRuleAnomalyDuplicateResource struct{} + +var _ sdk.ResourceWithUpdate = AlertRuleAnomalyDuplicateResource{} + +func (r AlertRuleAnomalyDuplicateResource) ModelObject() interface{} { + return &AlertRuleAnomalyDuplicateModel{} +} + +func (r AlertRuleAnomalyDuplicateResource) ResourceType() string { + return "azurerm_sentinel_alert_rule_anomaly_duplicate" +} + +func (r AlertRuleAnomalyDuplicateResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return validate.MLAnalyticsSettingsID +} + +func (r AlertRuleAnomalyDuplicateResource) Arguments() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "display_name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "built_in_rule_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.MLAnalyticsSettingsID, + }, + + "log_analytics_workspace_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: workspaces.ValidateWorkspaceID, + }, + + "enabled": { + Type: pluginsdk.TypeBool, + Required: true, + }, + + "mode": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + string(securityinsight.SettingsStatusProduction), + string(securityinsight.SettingsStatusFlighting), + }, false), + }, + + "multi_select_observation": { + Type: pluginsdk.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "values": { + Type: pluginsdk.TypeList, + Required: true, + Elem: &schema.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + + "description": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "supported_values": { + Type: pluginsdk.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: pluginsdk.TypeString, + }, + }, + }, + }, + }, + + "single_select_observation": { + Type: pluginsdk.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "description": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "supported_values": { + Type: pluginsdk.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: pluginsdk.TypeString, + }, + }, + + "value": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + }, + }, + + "prioritized_exclude_observation": { + Type: pluginsdk.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + }, + + "description": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "prioritize": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "exclude": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + }, + }, + + "threshold_observation": { + Type: pluginsdk.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "description": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "max": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "min": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "value": { + Type: pluginsdk.TypeString, + Required: true, + }, + }, + }, + }, + } +} + +func (r AlertRuleAnomalyDuplicateResource) Attributes() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "name": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "anomaly_version": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "anomaly_settings_version": { + Type: pluginsdk.TypeInt, + Computed: true, + }, + + "description": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "frequency": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "is_default_settings": { + Type: pluginsdk.TypeBool, + Computed: true, + }, + + "required_data_connector": { + Type: pluginsdk.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "connector_id": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "data_types": { + Type: pluginsdk.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: pluginsdk.TypeString, + }, + }, + }, + }, + }, + + "settings_definition_id": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "tactics": { + Type: pluginsdk.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: pluginsdk.TypeString, + }, + }, + + "techniques": { + Type: pluginsdk.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: pluginsdk.TypeString, + }, + }, + } +} + +func (r AlertRuleAnomalyDuplicateResource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + var metaModel AlertRuleAnomalyDuplicateModel + if err := metadata.Decode(&metaModel); err != nil { + return fmt.Errorf("decoding: %+v", err) + } + + client := metadata.Client.Sentinel.AnalyticsSettingsClient + + workspaceId, err := workspaces.ParseWorkspaceID(metaModel.WorkspaceId) + if err != nil { + return fmt.Errorf("parsing workspace id: %+v", err) + } + + builtInAnomalyRule, err := AlertRuleAnomalyReadWithPredicate(ctx, client.BaseClient, *workspaceId, func(v *azuresdkhacks.AnomalySecurityMLAnalyticsSettings) bool { + if v.ID != nil && strings.EqualFold(AlertRuleAnomalyIdFromWorkspaceId(*workspaceId, *v.Name), metaModel.BuiltInRuleId) { + return true + } + + return false + }) + + if err != nil { + return fmt.Errorf("reading built-in anomaly rule: %+v", err) + } + if builtInAnomalyRule == nil { + return fmt.Errorf("built-in anomaly rule not found") + } + + existingDuplicateRule, err := AlertRuleAnomalyReadWithPredicate(ctx, client.BaseClient, *workspaceId, func(v *azuresdkhacks.AnomalySecurityMLAnalyticsSettings) bool { + if v.SettingsDefinitionID != nil && + builtInAnomalyRule.SettingsDefinitionID != nil && + strings.EqualFold(v.SettingsDefinitionID.String(), builtInAnomalyRule.SettingsDefinitionID.String()) && + v.Name != nil && builtInAnomalyRule.Name != nil && *v.Name != *builtInAnomalyRule.Name { + return true + } + return false + }) + if err != nil { + return fmt.Errorf("checking for presence of existing duplicate rule of built-in rule: %+v", err) + } + if existingDuplicateRule != nil { + parsedExistingId, err := parse.MLAnalyticsSettingsID(AlertRuleAnomalyIdFromWorkspaceId(*workspaceId, *existingDuplicateRule.Name)) + if err != nil { + return fmt.Errorf("parsing: %+v", err) + } + return fmt.Errorf("only one duplicate rule of the same built-in rule is allowed, there is an existing duplicate rule of %s with id %q", *builtInAnomalyRule.DisplayName, parsedExistingId.ID()) + } + + id := parse.NewMLAnalyticsSettingsID(workspaceId.SubscriptionId, workspaceId.ResourceGroupName, workspaceId.WorkspaceName, uuid.New().String()) + // no need to do another existing check, it will be checked by finding existing duplicate rule of the template. + + if builtInAnomalyRule.SettingsStatus == securityinsight.SettingsStatusProduction && metaModel.Mode == string(securityinsight.SettingsStatusProduction) { + return fmt.Errorf("built-in anomaly rule %s is in production mode, it's not allowed to create duplicate rule in production mode", *builtInAnomalyRule.DisplayName) + } + + param := securityinsight.AnomalySecurityMLAnalyticsSettings{ + Kind: securityinsight.KindBasicSecurityMLAnalyticsSettingKindAnomaly, + AnomalySecurityMLAnalyticsSettingsProperties: &securityinsight.AnomalySecurityMLAnalyticsSettingsProperties{ + Description: builtInAnomalyRule.Description, + DisplayName: utils.String(metaModel.DisplayName), + RequiredDataConnectors: builtInAnomalyRule.RequiredDataConnectors, + Tactics: builtInAnomalyRule.Tactics, + Techniques: builtInAnomalyRule.Techniques, + AnomalyVersion: builtInAnomalyRule.AnomalyVersion, + Frequency: builtInAnomalyRule.Frequency, + IsDefaultSettings: utils.Bool(false), // for duplicate one, it's not default settings. + AnomalySettingsVersion: builtInAnomalyRule.AnomalySettingsVersion, + SettingsDefinitionID: builtInAnomalyRule.SettingsDefinitionID, + Enabled: utils.Bool(metaModel.Enabled), + SettingsStatus: securityinsight.SettingsStatusFlighting, + }, + } + + customizableObservations := &azuresdkhacks.AnomalySecurityMLAnalyticsCustomizableObservations{} + customizableObservations.MultiSelectObservations, err = expandAlertRuleAnomalyMultiSelectObservations(builtInAnomalyRule.CustomizableObservations.MultiSelectObservations, metaModel.MultiSelectObservation) + if err != nil { + return fmt.Errorf("expanding `multi_select_observation`: %+v", err) + } + customizableObservations.SingleSelectObservations, err = expandAlertRuleAnomalySingleSelectObservations(builtInAnomalyRule.CustomizableObservations.SingleSelectObservations, metaModel.SingleSelectObservation) + if err != nil { + return fmt.Errorf("expanding `single_select_observation`: %+v", err) + } + customizableObservations.PrioritizeExcludeObservations, err = expandAlertRuleAnomalyPrioritizeExcludeObservations(builtInAnomalyRule.CustomizableObservations.PrioritizeExcludeObservations, metaModel.PrioritizeExcludeObservation) + if err != nil { + return fmt.Errorf("expanding `prioritize_exclude_observation`: %+v", err) + } + customizableObservations.ThresholdObservations, err = expandAlertRuleAnomalyThresholdObservations(builtInAnomalyRule.CustomizableObservations.ThresholdObservations, metaModel.ThresholdObservation) + if err != nil { + return fmt.Errorf("expanding `threshold_observation`: %+v", err) + } + + param.AnomalySecurityMLAnalyticsSettingsProperties.CustomizableObservations = customizableObservations + + _, err = client.CreateOrUpdate(ctx, id.ResourceGroup, id.WorkspaceName, id.SecurityMLAnalyticsSettingName, param) + if err != nil { + return fmt.Errorf("creating %s: %+v", id, err) + } + + metadata.SetID(id) + return nil + }, + } +} + +func (r AlertRuleAnomalyDuplicateResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Sentinel.AnalyticsSettingsClient + + id, err := parse.MLAnalyticsSettingsID(metadata.ResourceData.Id()) + if err != nil { + return fmt.Errorf("parsing %s: %+v", metadata.ResourceData.Id(), err) + } + workspaceId := workspaces.NewWorkspaceID(id.SubscriptionId, id.ResourceGroup, id.WorkspaceName) + + resp, err := AlertRuleAnomalyReadWithPredicate(ctx, client.BaseClient, workspaceId, func(v *azuresdkhacks.AnomalySecurityMLAnalyticsSettings) bool { + if v.ID != nil && strings.EqualFold(*v.ID, id.ID()) { + return true + } + return false + }) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", *id, err) + } + if resp == nil { + return metadata.MarkAsGone(id) + } + + state := AlertRuleAnomalyDuplicateModel{ + WorkspaceId: workspaceId.ID(), + Mode: string(resp.SettingsStatus), + } + + if resp.Name != nil { + state.Name = *resp.Name + } + if resp.DisplayName != nil { + state.DisplayName = *resp.DisplayName + } + if resp.AnomalyVersion != nil { + state.AnomalyVersion = *resp.AnomalyVersion + } + if resp.AnomalySettingsVersion != nil { + state.AnomalySettingsVersion = *resp.AnomalySettingsVersion + } + if resp.Description != nil { + state.Description = *resp.Description + } + if resp.Enabled != nil { + state.Enabled = *resp.Enabled + } + if resp.Frequency != nil { + state.Frequency = *resp.Frequency + } + if resp.IsDefaultSettings != nil { + state.IsDefaultSettings = *resp.IsDefaultSettings + } + state.RequiredDataConnectors = flattenSentinelAlertRuleAnomalyRequiredDataConnectors(resp.RequiredDataConnectors) + if resp.SettingsDefinitionID != nil { + state.SettingsDefinitionId = resp.SettingsDefinitionID.String() + } + state.Tactics = flattenSentinelAlertRuleAnomalyTactics(resp.Tactics) + if resp.Techniques != nil { + state.Techniques = *resp.Techniques + } + + if resp.CustomizableObservations != nil { + state.MultiSelectObservation = flattenSentinelAlertRuleAnomalyMultiSelect(resp.CustomizableObservations.MultiSelectObservations) + state.SingleSelectObservation = flattenSentinelAlertRuleAnomalySingleSelect(resp.CustomizableObservations.SingleSelectObservations) + state.PrioritizeExcludeObservation = flattenSentinelAlertRuleAnomalyPriority(resp.CustomizableObservations.PrioritizeExcludeObservations) + state.ThresholdObservation = flattenSentinelAlertRuleAnomalyThreshold(resp.CustomizableObservations.ThresholdObservations) + } + + if resp.SettingsDefinitionID != nil { + state.BuiltInRuleId = AlertRuleAnomalyIdFromWorkspaceId(workspaceId, resp.SettingsDefinitionID.String()) + } + + return metadata.Encode(&state) + }, + } +} + +func (r AlertRuleAnomalyDuplicateResource) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + var metaModel AlertRuleAnomalyDuplicateModel + if err := metadata.Decode(&metaModel); err != nil { + return fmt.Errorf("decoding: %+v", err) + } + + client := metadata.Client.Sentinel.AnalyticsSettingsClient + + id, err := parse.MLAnalyticsSettingsID(metadata.ResourceData.Id()) + if err != nil { + return fmt.Errorf("parsing %s: %+v", metadata.ResourceData.Id(), err) + } + workspaceId := workspaces.NewWorkspaceID(id.SubscriptionId, id.ResourceGroup, id.WorkspaceName) + + existing, err := AlertRuleAnomalyReadWithPredicate(ctx, client.BaseClient, workspaceId, func(v *azuresdkhacks.AnomalySecurityMLAnalyticsSettings) bool { + if v.ID != nil && strings.EqualFold(*v.ID, id.ID()) { + return true + } + + return false + }) + + if err != nil { + return fmt.Errorf("retrieving %s: %+v", *id, err) + } + if existing == nil { + return fmt.Errorf("retrieving %s: Alert Rule Anomaly not found", *id) + } + + param := securityinsight.AnomalySecurityMLAnalyticsSettings{ + Kind: securityinsight.KindBasicSecurityMLAnalyticsSettingKindAnomaly, + AnomalySecurityMLAnalyticsSettingsProperties: &securityinsight.AnomalySecurityMLAnalyticsSettingsProperties{ + Description: existing.Description, + DisplayName: existing.DisplayName, + RequiredDataConnectors: existing.RequiredDataConnectors, + Tactics: existing.Tactics, + Techniques: existing.Techniques, + AnomalyVersion: existing.AnomalyVersion, + Frequency: existing.Frequency, + IsDefaultSettings: existing.IsDefaultSettings, + AnomalySettingsVersion: existing.AnomalySettingsVersion, + SettingsDefinitionID: existing.SettingsDefinitionID, + Enabled: utils.Bool(metaModel.Enabled), + SettingsStatus: securityinsight.SettingsStatus(metaModel.Mode), + }, + } + + customizableObservations := &azuresdkhacks.AnomalySecurityMLAnalyticsCustomizableObservations{} + customizableObservations.MultiSelectObservations, err = expandAlertRuleAnomalyMultiSelectObservations(existing.CustomizableObservations.MultiSelectObservations, metaModel.MultiSelectObservation) + if err != nil { + return fmt.Errorf("expanding `multi_select_observation`: %+v", err) + } + customizableObservations.SingleSelectObservations, err = expandAlertRuleAnomalySingleSelectObservations(existing.CustomizableObservations.SingleSelectObservations, metaModel.SingleSelectObservation) + if err != nil { + return fmt.Errorf("expanding `single_select_observation`: %+v", err) + } + customizableObservations.PrioritizeExcludeObservations, err = expandAlertRuleAnomalyPrioritizeExcludeObservations(existing.CustomizableObservations.PrioritizeExcludeObservations, metaModel.PrioritizeExcludeObservation) + if err != nil { + return fmt.Errorf("expanding `prioritize_exclude_observation`: %+v", err) + } + customizableObservations.ThresholdObservations, err = expandAlertRuleAnomalyThresholdObservations(existing.CustomizableObservations.ThresholdObservations, metaModel.ThresholdObservation) + if err != nil { + return fmt.Errorf("expanding `threshold_observation`: %+v", err) + } + + param.AnomalySecurityMLAnalyticsSettingsProperties.CustomizableObservations = customizableObservations + + _, err = client.CreateOrUpdate(ctx, id.ResourceGroup, id.WorkspaceName, id.SecurityMLAnalyticsSettingName, param) + if err != nil { + return fmt.Errorf("updating %s: %+v", id, err) + } + + return nil + }, + } +} + +func (r AlertRuleAnomalyDuplicateResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Sentinel.AnalyticsSettingsClient + + id, err := parse.MLAnalyticsSettingsID(metadata.ResourceData.Id()) + if err != nil { + return fmt.Errorf("parsing %s: %+v", metadata.ResourceData.Id(), err) + } + + _, err = client.Delete(ctx, id.ResourceGroup, id.WorkspaceName, id.SecurityMLAnalyticsSettingName) + if err != nil { + return fmt.Errorf("deleting %s: %+v", *id, err) + } + + return nil + }, + } +} + +func expandAlertRuleAnomalyMultiSelectObservations(builtInRule *[]azuresdkhacks.AnomalySecurityMLAnalyticsMultiSelectObservations, input []AnomalyRuleMultiSelectModel) (*[]azuresdkhacks.AnomalySecurityMLAnalyticsMultiSelectObservations, error) { + if builtInRule != nil && len(*builtInRule) < len(input) { + return nil, fmt.Errorf("the number of `multi_select_observation` must equal or less than %d", len(*builtInRule)) + } + + if builtInRule == nil { + return nil, nil + } + + inputValueMap := make(map[string]AnomalyRuleMultiSelectModel) + for _, v := range input { + inputValueMap[strings.ToLower(v.Name)] = v + } + + output := make([]azuresdkhacks.AnomalySecurityMLAnalyticsMultiSelectObservations, 0) + for _, v := range *builtInRule { + if v.Name == nil { + return nil, fmt.Errorf("the name of built in `multi_select_observation` is nil") + } + // copy from built in rule + o := azuresdkhacks.AnomalySecurityMLAnalyticsMultiSelectObservations{ + Name: v.Name, + Description: v.Description, + Values: v.Values, + SupportValues: v.SupportValues, + SupportedValuesKql: v.SupportedValuesKql, + ValuesKql: v.ValuesKql, + SequenceNumber: v.SequenceNumber, + Rerun: v.Rerun, + } + if in, ok := inputValueMap[strings.ToLower(*v.Name)]; ok { + o.Values = &in.Values + delete(inputValueMap, strings.ToLower(*v.Name)) + } + output = append(output, o) + } + + if len(inputValueMap) != 0 { + keys := make([]string, 0) + for k := range inputValueMap { + keys = append(keys, k) + } + return nil, fmt.Errorf("the following `multi_select_observation` are not supported: %s", strings.Join(keys, ", ")) + } + + return &output, nil +} + +func expandAlertRuleAnomalySingleSelectObservations(builtInRule *[]azuresdkhacks.AnomalySecurityMLAnalyticsSingleSelectObservations, input []AnomalyRuleSingleSelectModel) (*[]azuresdkhacks.AnomalySecurityMLAnalyticsSingleSelectObservations, error) { + if builtInRule != nil && len(*builtInRule) < len(input) { + return nil, fmt.Errorf("the number of `single_select_observation` must equals or less than %d", len(*builtInRule)) + } + + if builtInRule == nil { + return nil, nil + } + + inputValueMap := make(map[string]AnomalyRuleSingleSelectModel) + for _, v := range input { + inputValueMap[strings.ToLower(v.Name)] = v + } + + output := make([]azuresdkhacks.AnomalySecurityMLAnalyticsSingleSelectObservations, 0) + for _, v := range *builtInRule { + if v.Name == nil { + return nil, fmt.Errorf("the name of built in `multi_select_observation` is nil") + } + // copy from built in rule + o := azuresdkhacks.AnomalySecurityMLAnalyticsSingleSelectObservations{ + Name: v.Name, + Description: v.Description, + Value: v.Value, + SupportValues: v.SupportValues, + SupportedValuesKql: v.SupportedValuesKql, + SequenceNumber: v.SequenceNumber, + Rerun: v.Rerun, + } + if in, ok := inputValueMap[strings.ToLower(*v.Name)]; ok { + o.Value = &in.Value + delete(inputValueMap, strings.ToLower(*v.Name)) + } + output = append(output, o) + } + + if len(inputValueMap) != 0 { + keys := make([]string, 0) + for k := range inputValueMap { + keys = append(keys, k) + } + return nil, fmt.Errorf("the following `single_select_observation` are not supported: %s", strings.Join(keys, ", ")) + } + + return &output, nil +} + +func expandAlertRuleAnomalyPrioritizeExcludeObservations(builtInRule *[]azuresdkhacks.AnomalySecurityMLAnalyticsPrioritizeExcludeObservations, input []AnomalyRulePriorityModel) (*[]azuresdkhacks.AnomalySecurityMLAnalyticsPrioritizeExcludeObservations, error) { + if builtInRule != nil && len(*builtInRule) < len(input) { + return nil, fmt.Errorf("the number of `prioritized_exclude_observation` must equals or less than %d", len(*builtInRule)) + } + + if builtInRule == nil { + return nil, nil + } + + inputValueMap := make(map[string]AnomalyRulePriorityModel) + for _, v := range input { + inputValueMap[strings.ToLower(v.Name)] = v + } + + output := make([]azuresdkhacks.AnomalySecurityMLAnalyticsPrioritizeExcludeObservations, 0) + for _, v := range *builtInRule { + if v.Name == nil { + return nil, fmt.Errorf("the name of built in `multi_select_observation` is nil") + } + // copy from built in rule + o := azuresdkhacks.AnomalySecurityMLAnalyticsPrioritizeExcludeObservations{ + Name: v.Name, + Description: v.Description, + Prioritize: v.Prioritize, + Exclude: v.Exclude, + DataType: v.DataType, + SequenceNumber: v.SequenceNumber, + Rerun: v.Rerun, + } + if in, ok := inputValueMap[strings.ToLower(*v.Name)]; ok { + o.Exclude = &in.Exclude + o.Prioritize = &in.Prioritize + delete(inputValueMap, strings.ToLower(*v.Name)) + } + output = append(output, o) + } + + if len(inputValueMap) != 0 { + keys := make([]string, 0) + for k := range inputValueMap { + keys = append(keys, k) + } + return nil, fmt.Errorf("the following `prioritized_exclude_observation` are not supported: %s", strings.Join(keys, ", ")) + } + + return &output, nil +} + +func expandAlertRuleAnomalyThresholdObservations(builtInRule *[]azuresdkhacks.AnomalySecurityMLAnalyticsThresholdObservations, input []AnomalyRuleThresholdModel) (*[]azuresdkhacks.AnomalySecurityMLAnalyticsThresholdObservations, error) { + if builtInRule != nil && len(*builtInRule) < len(input) { + return nil, fmt.Errorf("the number of `threshold_observation` must equals or less than %d", len(*builtInRule)) + } + + if builtInRule == nil { + return nil, nil + } + + inputValueMap := make(map[string]AnomalyRuleThresholdModel) + for _, v := range input { + inputValueMap[strings.ToLower(v.Name)] = v + } + + output := make([]azuresdkhacks.AnomalySecurityMLAnalyticsThresholdObservations, 0) + for _, v := range *builtInRule { + if v.Name == nil { + return nil, fmt.Errorf("the name of built in `multi_select_observation` is nil") + } + // copy from built in rule + o := azuresdkhacks.AnomalySecurityMLAnalyticsThresholdObservations{ + Name: v.Name, + Description: v.Description, + Max: v.Max, + Min: v.Min, + Value: v.Value, + SequenceNumber: v.SequenceNumber, + Rerun: v.Rerun, + } + if in, ok := inputValueMap[strings.ToLower(*v.Name)]; ok { + o.Value = &in.Value + delete(inputValueMap, strings.ToLower(*v.Name)) + } + output = append(output, o) + } + + if len(inputValueMap) != 0 { + keys := make([]string, 0) + for k := range inputValueMap { + keys = append(keys, k) + } + return nil, fmt.Errorf("the following `threshold_observation` are not supported: %s", strings.Join(keys, ", ")) + } + + return &output, nil +} diff --git a/internal/services/sentinel/sentinel_alert_rule_anomaly_duplicate_resource_test.go b/internal/services/sentinel/sentinel_alert_rule_anomaly_duplicate_resource_test.go new file mode 100644 index 000000000000..9c3dded8ac42 --- /dev/null +++ b/internal/services/sentinel/sentinel_alert_rule_anomaly_duplicate_resource_test.go @@ -0,0 +1,259 @@ +package sentinel_test + +import ( + "context" + "fmt" + "regexp" + "strings" + "testing" + + "github.com/hashicorp/go-azure-sdk/resource-manager/operationalinsights/2022-10-01/workspaces" + "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" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/sentinel/azuresdkhacks" + "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 SentinelAlertRuleAnomalyDuplicateResource struct{} + +func (r SentinelAlertRuleAnomalyDuplicateResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := parse.MLAnalyticsSettingsID(state.ID) + if err != nil { + return nil, err + } + + workspaceId := workspaces.NewWorkspaceID(id.SubscriptionId, id.ResourceGroup, id.WorkspaceName) + client := clients.Sentinel.AnalyticsSettingsClient + resp, err := sentinel.AlertRuleAnomalyReadWithPredicate(ctx, client.BaseClient, workspaceId, func(r *azuresdkhacks.AnomalySecurityMLAnalyticsSettings) bool { + if r.Name != nil && strings.EqualFold(sentinel.AlertRuleAnomalyIdFromWorkspaceId(workspaceId, *r.Name), id.ID()) { + return true + } + return false + }) + if err != nil { + return nil, fmt.Errorf("retrieving Sentinel Alert Rule Anomaly Built In %q (Workspace %q / Resource Group %q): %+v", id.SecurityMLAnalyticsSettingName, id.WorkspaceName, id.ResourceGroup, err) + } + return utils.Bool(resp != nil), nil +} + +func TestAccSentinelAlertRuleAnomalyDuplicate_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_sentinel_alert_rule_anomaly_duplicate", "test") + r := SentinelAlertRuleAnomalyDuplicateResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccSentinelAlertRuleAnomalyDuplicate_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_sentinel_alert_rule_anomaly_duplicate", "test") + r := SentinelAlertRuleAnomalyDuplicateResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + { + Config: r.requiresImport(data), + ExpectError: regexp.MustCompile("only one duplicate rule of the same built-in rule is allowed, there is an existing duplicate rule of .+"), + }, + }) +} + +func TestAccSentinelAlertRuleAnomalyDuplicate_withCustomObservation(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_sentinel_alert_rule_anomaly_duplicate", "test") + r := SentinelAlertRuleAnomalyDuplicateResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basicWithThresholdObservation(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.basicWithMultiSelectObservation(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.basicWithSingleSelectObservation(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.basicWithPrioritizeExcludeObservation(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func (SentinelAlertRuleAnomalyDuplicateResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +data "azurerm_sentinel_alert_rule_anomaly" "test" { + log_analytics_workspace_id = azurerm_log_analytics_workspace.test.id + display_name = "UEBA Anomalous Sign In" + depends_on = [azurerm_sentinel_log_analytics_workspace_onboarding.test] +} + +resource "azurerm_sentinel_alert_rule_anomaly_duplicate" "test" { + display_name = "acctest duplicate rule" + log_analytics_workspace_id = azurerm_log_analytics_workspace.test.id + built_in_rule_id = data.azurerm_sentinel_alert_rule_anomaly.test.id + enabled = true + mode = "Flighting" + depends_on = [azurerm_sentinel_log_analytics_workspace_onboarding.test] +} +`, SecurityInsightsSentinelOnboardingStateResource{}.basic(data)) +} + +func (SentinelAlertRuleAnomalyDuplicateResource) basicWithThresholdObservation(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +data "azurerm_sentinel_alert_rule_anomaly" "test" { + log_analytics_workspace_id = azurerm_log_analytics_workspace.test.id + display_name = "UEBA Anomalous Sign In" + depends_on = [azurerm_sentinel_log_analytics_workspace_onboarding.test] +} + +resource "azurerm_sentinel_alert_rule_anomaly_duplicate" "test" { + display_name = "acctest duplicate rule" + log_analytics_workspace_id = azurerm_log_analytics_workspace.test.id + built_in_rule_id = data.azurerm_sentinel_alert_rule_anomaly.test.id + enabled = true + mode = "Flighting" + + threshold_observation { + name = "Anomaly score threshold" + value = "0.6" + } + + depends_on = [azurerm_sentinel_log_analytics_workspace_onboarding.test] +} +`, SecurityInsightsSentinelOnboardingStateResource{}.basic(data)) +} + +func (SentinelAlertRuleAnomalyDuplicateResource) basicWithSingleSelectObservation(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +data "azurerm_sentinel_alert_rule_anomaly" "test" { + log_analytics_workspace_id = azurerm_log_analytics_workspace.test.id + display_name = "(Preview) Unusual web traffic detected with IP in URL path" + depends_on = [azurerm_sentinel_log_analytics_workspace_onboarding.test] +} + +resource "azurerm_sentinel_alert_rule_anomaly_duplicate" "test" { + display_name = "acctest duplicate (Preview) Unusual web traffic detected with IP in URL path" + log_analytics_workspace_id = azurerm_log_analytics_workspace.test.id + built_in_rule_id = data.azurerm_sentinel_alert_rule_anomaly.test.id + enabled = true + mode = "Flighting" + + single_select_observation { + name = "Device vendor" + value = "Zscaler" + } + + depends_on = [azurerm_sentinel_log_analytics_workspace_onboarding.test] +} +`, SecurityInsightsSentinelOnboardingStateResource{}.basic(data)) +} + +func (SentinelAlertRuleAnomalyDuplicateResource) basicWithMultiSelectObservation(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +data "azurerm_sentinel_alert_rule_anomaly" "test" { + log_analytics_workspace_id = azurerm_log_analytics_workspace.test.id + display_name = "(Preview) Anomalous scanning activity" + depends_on = [azurerm_sentinel_log_analytics_workspace_onboarding.test] +} + +resource "azurerm_sentinel_alert_rule_anomaly_duplicate" "test" { + display_name = "acctest duplicate (Preview) Anomalous scanning activity" + log_analytics_workspace_id = azurerm_log_analytics_workspace.test.id + built_in_rule_id = data.azurerm_sentinel_alert_rule_anomaly.test.id + enabled = true + mode = "Flighting" + + multi_select_observation { + name = "Device action" + values = ["accept"] + } + + depends_on = [azurerm_sentinel_log_analytics_workspace_onboarding.test] +} +`, SecurityInsightsSentinelOnboardingStateResource{}.basic(data)) +} +func (SentinelAlertRuleAnomalyDuplicateResource) basicWithPrioritizeExcludeObservation(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +data "azurerm_sentinel_alert_rule_anomaly" "test" { + log_analytics_workspace_id = azurerm_log_analytics_workspace.test.id + display_name = "(Preview) Anomalous web request activity" + depends_on = [azurerm_sentinel_log_analytics_workspace_onboarding.test] +} + +resource "azurerm_sentinel_alert_rule_anomaly_duplicate" "test" { + display_name = "acctest duplicate (Preview) Anomalous web request activity" + log_analytics_workspace_id = azurerm_log_analytics_workspace.test.id + built_in_rule_id = data.azurerm_sentinel_alert_rule_anomaly.test.id + enabled = true + mode = "Flighting" + + prioritized_exclude_observation { + name = "Prioritize script suffixes of the URI stems" + prioritize = ".asp, .aspx, .armx, .asax, .ashz" + } + + prioritized_exclude_observation { + name = "Exclude noisy URI stems" + exclude = "test.com" + } + + + depends_on = [azurerm_sentinel_log_analytics_workspace_onboarding.test] +} +`, SecurityInsightsSentinelOnboardingStateResource{}.basic(data)) +} + +func (r SentinelAlertRuleAnomalyDuplicateResource) requiresImport(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_sentinel_alert_rule_anomaly_duplicate" "import" { + display_name = azurerm_sentinel_alert_rule_anomaly_duplicate.test.display_name + log_analytics_workspace_id = azurerm_sentinel_alert_rule_anomaly_duplicate.test.log_analytics_workspace_id + built_in_rule_id = azurerm_sentinel_alert_rule_anomaly_duplicate.test.built_in_rule_id + enabled = azurerm_sentinel_alert_rule_anomaly_duplicate.test.enabled + mode = azurerm_sentinel_alert_rule_anomaly_duplicate.test.mode + depends_on = [azurerm_sentinel_log_analytics_workspace_onboarding.test] +} +`, r.basic(data)) +} diff --git a/website/docs/r/sentinel_alert_rule_anomaly_duplicate.html.markdown b/website/docs/r/sentinel_alert_rule_anomaly_duplicate.html.markdown new file mode 100644 index 000000000000..a38a8567a9ea --- /dev/null +++ b/website/docs/r/sentinel_alert_rule_anomaly_duplicate.html.markdown @@ -0,0 +1,175 @@ +--- +subcategory: "Sentinel" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_sentinel_alert_rule_anomaly_duplicate" +description: |- + Manages a Duplicated Anomaly Alert Rule. +--- +# azurerm_sentinel_alert_rule_anomaly_duplicate + +Manages a Duplicated Anomaly Alert Rule. + +## Example Usage + +```hcl +resource "azurerm_resource_group" "example" { + name = "example-resources" + location = "West Europe" +} + +resource "azurerm_log_analytics_workspace" "example" { + name = "example-law" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + sku = "PerGB2018" +} + +resource "azurerm_security_insights_sentinel_onboarding" "example" { + resource_group_name = azurerm_resource_group.example.name + workspace_name = azurerm_log_analytics_workspace.example.name + customer_managed_key_enabled = false +} + +data "azurerm_sentinel_alert_rule_anomaly" "example" { + log_analytics_workspace_id = azurerm_log_analytics_workspace.example.id + display_name = "UEBA Anomalous Sign In" + + depends_on = [azurerm_sentinel_log_analytics_workspace_onboarding.example] +} + +resource "azurerm_sentinel_alert_rule_anomaly_duplicate" "example" { + display_name = "example duplicated UEBA Anomalous Sign In" + log_analytics_workspace_id = azurerm_log_analytics_workspace.example.id + built_in_rule_id = data.azurerm_sentinel_alert_rule_anomaly.example.id + enabled = true + mode = "Flighting" + + threshold_observation { + name = "Anomaly score threshold" + value = "0.6" + } +} +``` + +## Arguments Reference + +The following arguments are supported: + +* `display_name` - (Required) The Display Name of the built-in Anomaly Alert Rule. Changing this forces a new Duplicated Anomaly Alert Rule to be created. + +* `built_in_rule_id` - (Required) The ID of the built-in Anomaly Alert Rule. Changing this forces a new Duplicated Anomaly Alert Rule to be created. + +* `log_analytics_workspace_id` - (Required) The ID of the Log Analytics Workspace. Changing this forces a new Duplicated Anomaly Alert Rule to be created. + +* `enabled` - (Required) Should the Duplicated Anomaly Alert Rule be enabled? + +* `mode` - (Required) mode of the Duplicated Anomaly Alert Rule. Possible Values are `Production` and `Flighting`. + +* `multi_select_observation` - (Optional) A list of `multi_select_observation` blocks as defined below. + +* `single_select_observation` - (Optional) A list of `single_select_observation` blocks as defined below. + +* `prioritized_exclude_observation` - (Optional) A list of `prioritized_exclude_observation` blocks as defined below. + +* `threshold_observation` - (Optional) A list of `threshold_observation` blocks as defined below. + +-> **NOTE:** un-specified `multi_select_observation`, `single_select_observation`, `prioritized_exclude_observation` and `threshold_observation` will be inherited from the built-in Anomaly Alert Rule. + +--- + +A `multi_select_observation` block supports the following: + +* `name` - (Required) The name of the multi select observation. + +* `description` - The description of the multi select observation. + +* `supported_values` - A list of supported values of the multi select observation. + +* `values` - (Required) A list of values of the multi select observation. + +--- + +A `single_select_observation` block supports the following: + +* `name` - (Required) The name of the single select observation. + +* `description` - The description of the single select observation. + +* `supported_values` - A list of supported values of the single select observation. + +* `value` - (Required) The value of the multi select observation. + +--- + +A `prioritized_exclude_observation` block exports the following: + +* `name` - (Required) The name of the prioritized exclude observation. + +* `description` - The description of the prioritized exclude observation. + +* `prioritize` - (Optional) The prioritized value per `description`. + +* `exclude` - (Optional) The excluded value per `description`. + +--- + +A `threshold_observation` block exports the following: + +* `name` - (Required) The name of the threshold observation. + +* `description` - The description of the threshold observation. + +* `max` - The max value of the threshold observation. + +* `min` - The min value of the threshold observation. + +* `value` - (Required) The value of the threshold observation. + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the Built-in Anomaly Alert Rule. + +* `anomaly_settings_version` - The version of the Anomaly Security ML Analytics Settings. + +* `anomaly_version` - The anomaly version of the Anomaly Alert Rule. + +* `description` - The description of the Anomaly Alert Rule. + +* `frequency` - The frequency the Anomaly Alert Rule will be run, such as "P1D". + +* `is_default_settings` - Whether the current settings of the Anomaly Alert Rule equals default settings. + +* `required_data_connector` - A `required_data_connector` block as defined below. + +* `settings_definition_id` - The ID of the anomaly settings definition Id. + +* `tactics` - A list of categories of attacks by which to classify the rule. + +* `techniques` - A list of techniques of attacks by which to classify the rule. + +--- + +A `required_data_connector` block exports the following: + +* `connector_id` - The ID of the required Data Connector. + +* `data_types` - A list of data types of the required Data Connector. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the Built In Anomaly Alert Rule. +* `read` - (Defaults to 5 minutes) Used when retrieving the Built In Anomaly Alert Rule. +* `update` - (Defaults to 30 minutes) Used when updating the Built In Anomaly Alert Rule. +* `delete` - (Defaults to 5 minutes) Used when deleting the Built In Anomaly Alert Rule. + +## Import + +Built In Anomaly Alert Rules can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_sentinel_alert_rule_anomaly_built_in.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.OperationalInsights/workspaces/workspace1/providers/Microsoft.SecurityInsights/securityMLAnalyticsSettings/setting1 +```