From aabdee9d5ae0e9ba9e791d8e0d328aca4b9a56b5 Mon Sep 17 00:00:00 2001 From: ziyeqf Date: Thu, 9 Feb 2023 12:58:35 +0800 Subject: [PATCH] merge built-in rule --- .../securitymlanalyticssettings.go | 121 +++++ .../securitymlanalyticssettings_models.go | 318 ++++++++++++ internal/services/sentinel/client/client.go | 5 + .../sentinel/parse/ml_analytics_settings.go | 75 +++ .../parse/ml_analytics_settings_test.go | 128 +++++ internal/services/sentinel/registration.go | 5 +- internal/services/sentinel/resourceids.go | 1 + .../sentinel/sentinel_alert_rule_anomaly.go | 307 ++++++++++++ ...el_alert_rule_anomaly_built_in_resource.go | 461 ++++++++++++++++++ ...ert_rule_anomaly_built_in_resource_test.go | 68 +++ ...sentinel_alert_rule_anomaly_data_source.go | 280 +++++++++++ ...nel_alert_rule_anomaly_data_source_test.go | 171 +++++++ .../validate/ml_analytics_settings_id.go | 23 + .../validate/ml_analytics_settings_id_test.go | 88 ++++ .../sentinel_alert_rule_anomaly.html.markdown | 158 ++++++ ..._alert_rule_anomaly_built_in.html.markdown | 175 +++++++ 16 files changed, 2383 insertions(+), 1 deletion(-) create mode 100644 internal/services/sentinel/azuresdkhacks/securitymlanalyticssettings.go create mode 100644 internal/services/sentinel/azuresdkhacks/securitymlanalyticssettings_models.go create mode 100644 internal/services/sentinel/parse/ml_analytics_settings.go create mode 100644 internal/services/sentinel/parse/ml_analytics_settings_test.go create mode 100644 internal/services/sentinel/sentinel_alert_rule_anomaly.go create mode 100644 internal/services/sentinel/sentinel_alert_rule_anomaly_built_in_resource.go create mode 100644 internal/services/sentinel/sentinel_alert_rule_anomaly_built_in_resource_test.go create mode 100644 internal/services/sentinel/sentinel_alert_rule_anomaly_data_source.go create mode 100644 internal/services/sentinel/sentinel_alert_rule_anomaly_data_source_test.go create mode 100644 internal/services/sentinel/validate/ml_analytics_settings_id.go create mode 100644 internal/services/sentinel/validate/ml_analytics_settings_id_test.go create mode 100644 website/docs/d/sentinel_alert_rule_anomaly.html.markdown create mode 100644 website/docs/r/sentinel_alert_rule_anomaly_built_in.html.markdown diff --git a/internal/services/sentinel/azuresdkhacks/securitymlanalyticssettings.go b/internal/services/sentinel/azuresdkhacks/securitymlanalyticssettings.go new file mode 100644 index 000000000000..6b3993d56d83 --- /dev/null +++ b/internal/services/sentinel/azuresdkhacks/securitymlanalyticssettings.go @@ -0,0 +1,121 @@ +package azuresdkhacks + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + securityinsight "github.com/tombuildsstuff/kermit/sdk/securityinsights/2022-10-01-preview/securityinsights" +) + +type SecurityMLAnalyticsSettingsClient struct { + securityinsight.BaseClient +} + +func (client SecurityMLAnalyticsSettingsClient) List(ctx context.Context, resourceGroupName string, workspaceName string) (result SecurityMLAnalyticsSettingsListPage, err error) { + if err := validation.Validate([]validation.Validation{ + {TargetValue: client.SubscriptionID, + Constraints: []validation.Constraint{{Target: "client.SubscriptionID", Name: validation.MinLength, Rule: 1, Chain: nil}}}, + {TargetValue: resourceGroupName, + Constraints: []validation.Constraint{{Target: "resourceGroupName", Name: validation.MaxLength, Rule: 90, Chain: nil}, + {Target: "resourceGroupName", Name: validation.MinLength, Rule: 1, Chain: nil}}}, + {TargetValue: workspaceName, + Constraints: []validation.Constraint{{Target: "workspaceName", Name: validation.MaxLength, Rule: 90, Chain: nil}, + {Target: "workspaceName", Name: validation.MinLength, Rule: 1, Chain: nil}}}}); err != nil { + return result, validation.NewError("securityinsight.SecurityMLAnalyticsSettingsClient", "List", err.Error()) + } + + result.fn = client.listNextResults + req, err := client.ListPreparer(ctx, resourceGroupName, workspaceName) + if err != nil { + err = autorest.NewErrorWithError(err, "securityinsight.SecurityMLAnalyticsSettingsClient", "List", nil, "Failure preparing request") + return + } + + resp, err := client.ListSender(req) + if err != nil { + result.smasl.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "securityinsight.SecurityMLAnalyticsSettingsClient", "List", resp, "Failure sending request") + return + } + + result.smasl, err = client.ListResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "securityinsight.SecurityMLAnalyticsSettingsClient", "List", resp, "Failure responding to request") + return + } + if result.smasl.hasNextLink() && result.smasl.IsEmpty() { + err = result.NextWithContext(ctx) + return + } + + return +} + +// ListPreparer prepares the List request. +func (client SecurityMLAnalyticsSettingsClient) ListPreparer(ctx context.Context, resourceGroupName string, workspaceName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "resourceGroupName": autorest.Encode("path", resourceGroupName), + "subscriptionId": autorest.Encode("path", client.SubscriptionID), + "workspaceName": autorest.Encode("path", workspaceName), + } + + const APIVersion = "2022-10-01-preview" + queryParameters := map[string]interface{}{ + "api-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsGet(), + autorest.WithBaseURL(client.BaseURI), + autorest.WithPathParameters("/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}/providers/Microsoft.SecurityInsights/securityMLAnalyticsSettings", pathParameters), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// ListSender sends the List request. The method will close the +// http.Response Body if it receives an error. +func (client SecurityMLAnalyticsSettingsClient) ListSender(req *http.Request) (*http.Response, error) { + return client.Send(req, azure.DoRetryWithRegistration(client.Client)) +} + +// ListResponder handles the response to the List request. The method always +// closes the http.Response Body. +func (client SecurityMLAnalyticsSettingsClient) ListResponder(resp *http.Response) (result SecurityMLAnalyticsSettingsList, err error) { + err = autorest.Respond( + resp, + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingJSON(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + return +} + +// listNextResults retrieves the next set of results, if any. +func (client SecurityMLAnalyticsSettingsClient) listNextResults(ctx context.Context, lastResults SecurityMLAnalyticsSettingsList) (result SecurityMLAnalyticsSettingsList, err error) { + req, err := lastResults.securityMLAnalyticsSettingsListPreparer(ctx) + if err != nil { + return result, autorest.NewErrorWithError(err, "securityinsight.SecurityMLAnalyticsSettingsClient", "listNextResults", nil, "Failure preparing next results request") + } + if req == nil { + return + } + resp, err := client.ListSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + return result, autorest.NewErrorWithError(err, "securityinsight.SecurityMLAnalyticsSettingsClient", "listNextResults", resp, "Failure sending next results request") + } + result, err = client.ListResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "securityinsight.SecurityMLAnalyticsSettingsClient", "listNextResults", resp, "Failure responding to next results request") + } + return +} + +// ListComplete enumerates all values, automatically crossing page boundaries as required. +func (client SecurityMLAnalyticsSettingsClient) ListComplete(ctx context.Context, resourceGroupName string, workspaceName string) (result SecurityMLAnalyticsSettingsListIterator, err error) { + result.page, err = client.List(ctx, resourceGroupName, workspaceName) + return +} diff --git a/internal/services/sentinel/azuresdkhacks/securitymlanalyticssettings_models.go b/internal/services/sentinel/azuresdkhacks/securitymlanalyticssettings_models.go new file mode 100644 index 000000000000..c07f5fe6fe83 --- /dev/null +++ b/internal/services/sentinel/azuresdkhacks/securitymlanalyticssettings_models.go @@ -0,0 +1,318 @@ +package azuresdkhacks + +import ( + "context" + "encoding/json" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/date" + "github.com/Azure/go-autorest/autorest/to" + "github.com/gofrs/uuid" + securityinsight "github.com/tombuildsstuff/kermit/sdk/securityinsights/2022-10-01-preview/securityinsights" +) + +// TODO 4.0 check if this can be removed +// Hacking the SDK model, adding definition for customizableObservations, tracked on https://github.com/Azure/azure-rest-api-specs/issues/22503 + +type SecurityMLAnalyticsSettingsListIterator struct { + i int + page SecurityMLAnalyticsSettingsListPage +} + +func (iter *SecurityMLAnalyticsSettingsListIterator) NextWithContext(ctx context.Context) (err error) { + iter.i++ + if iter.i < len(iter.page.Values()) { + return nil + } + err = iter.page.NextWithContext(ctx) + if err != nil { + iter.i-- + return err + } + iter.i = 0 + return nil +} + +func (iter *SecurityMLAnalyticsSettingsListIterator) Next() error { + return iter.NextWithContext(context.Background()) +} + +func (iter SecurityMLAnalyticsSettingsListIterator) NotDone() bool { + return iter.page.NotDone() && iter.i < len(iter.page.Values()) +} + +func (iter SecurityMLAnalyticsSettingsListIterator) Response() SecurityMLAnalyticsSettingsList { + return iter.page.Response() +} + +func (iter SecurityMLAnalyticsSettingsListIterator) Value() BasicSecurityMLAnalyticsSetting { + if !iter.page.NotDone() { + return SecurityMLAnalyticsSetting{} + } + return iter.page.Values()[iter.i] +} + +type SecurityMLAnalyticsSettingsListPage struct { + fn func(context.Context, SecurityMLAnalyticsSettingsList) (SecurityMLAnalyticsSettingsList, error) + smasl SecurityMLAnalyticsSettingsList +} + +func (page *SecurityMLAnalyticsSettingsListPage) NextWithContext(ctx context.Context) (err error) { + for { + next, err := page.fn(ctx, page.smasl) + if err != nil { + return err + } + page.smasl = next + if !next.hasNextLink() || !next.IsEmpty() { + break + } + } + return nil +} + +func (page *SecurityMLAnalyticsSettingsListPage) Next() error { + return page.NextWithContext(context.Background()) +} + +func (page SecurityMLAnalyticsSettingsListPage) NotDone() bool { + return !page.smasl.IsEmpty() +} + +func (page SecurityMLAnalyticsSettingsListPage) Response() SecurityMLAnalyticsSettingsList { + return page.smasl +} + +func (page SecurityMLAnalyticsSettingsListPage) Values() []BasicSecurityMLAnalyticsSetting { + if page.smasl.IsEmpty() { + return nil + } + return *page.smasl.Value +} + +type SecurityMLAnalyticsSettingsList struct { + autorest.Response `json:"-"` + // NextLink - READ-ONLY; URL to fetch the next set of SecurityMLAnalyticsSettings. + NextLink *string `json:"nextLink,omitempty"` + // Value - Array of SecurityMLAnalyticsSettings + Value *[]BasicSecurityMLAnalyticsSetting `json:"value,omitempty"` +} + +func (smasl SecurityMLAnalyticsSettingsList) securityMLAnalyticsSettingsListPreparer(ctx context.Context) (*http.Request, error) { + if !smasl.hasNextLink() { + return nil, nil + } + return autorest.Prepare((&http.Request{}).WithContext(ctx), + autorest.AsJSON(), + autorest.AsGet(), + autorest.WithBaseURL(to.String(smasl.NextLink))) +} + +// IsEmpty returns true if the ListResult contains no values. +func (smasl SecurityMLAnalyticsSettingsList) IsEmpty() bool { + return smasl.Value == nil || len(*smasl.Value) == 0 +} + +// hasNextLink returns true if the NextLink is not empty. +func (smasl SecurityMLAnalyticsSettingsList) hasNextLink() bool { + return smasl.NextLink != nil && len(*smasl.NextLink) != 0 +} + +// MarshalJSON is the custom marshaler for SecurityMLAnalyticsSettingsList. +func (smasl SecurityMLAnalyticsSettingsList) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]interface{}) + if smasl.Value != nil { + objectMap["value"] = smasl.Value + } + return json.Marshal(objectMap) +} + +// UnmarshalJSON is the custom unmarshaler for SecurityMLAnalyticsSettingsList struct. +func (smasl *SecurityMLAnalyticsSettingsList) UnmarshalJSON(body []byte) error { + var m map[string]*json.RawMessage + err := json.Unmarshal(body, &m) + if err != nil { + return err + } + for k, v := range m { + switch k { + case "nextLink": + if v != nil { + var nextLink string + err = json.Unmarshal(*v, &nextLink) + if err != nil { + return err + } + smasl.NextLink = &nextLink + } + case "value": + if v != nil { + value, err := unmarshalBasicSecurityMLAnalyticsSettingArray(*v) + if err != nil { + return err + } + smasl.Value = &value + } + } + } + + return nil +} + +func unmarshalBasicSecurityMLAnalyticsSettingArray(body []byte) ([]BasicSecurityMLAnalyticsSetting, error) { + var rawMessages []*json.RawMessage + err := json.Unmarshal(body, &rawMessages) + if err != nil { + return nil, err + } + + smasArray := make([]BasicSecurityMLAnalyticsSetting, len(rawMessages)) + + for index, rawMessage := range rawMessages { + smas, err := unmarshalBasicSecurityMLAnalyticsSetting(*rawMessage) + if err != nil { + return nil, err + } + smasArray[index] = smas + } + return smasArray, nil +} + +func unmarshalBasicSecurityMLAnalyticsSetting(body []byte) (BasicSecurityMLAnalyticsSetting, error) { + var m map[string]interface{} + err := json.Unmarshal(body, &m) + if err != nil { + return nil, err + } + + switch m["kind"] { + case string(securityinsight.KindBasicSecurityMLAnalyticsSettingKindAnomaly): + var asmas AnomalySecurityMLAnalyticsSettings + err := json.Unmarshal(body, &asmas) + return asmas, err + default: + var smas SecurityMLAnalyticsSetting + err := json.Unmarshal(body, &smas) + return smas, err + } +} + +// BasicSecurityMLAnalyticsSetting security ML Analytics Setting +type BasicSecurityMLAnalyticsSetting interface { + AsAnomalySecurityMLAnalyticsSettings() (*AnomalySecurityMLAnalyticsSettings, bool) + AsSecurityMLAnalyticsSetting() (*SecurityMLAnalyticsSetting, bool) +} + +// SecurityMLAnalyticsSetting security ML Analytics Setting +type SecurityMLAnalyticsSetting struct { + autorest.Response `json:"-"` + // Kind - Possible values include: 'KindBasicSecurityMLAnalyticsSettingKindSecurityMLAnalyticsSetting', 'KindBasicSecurityMLAnalyticsSettingKindAnomaly' + Kind securityinsight.KindBasicSecurityMLAnalyticsSetting `json:"kind,omitempty"` + // Etag - Etag of the azure resource + Etag *string `json:"etag,omitempty"` + // ID - READ-ONLY; Fully qualified resource ID for the resource. Ex - /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName} + ID *string `json:"id,omitempty"` + // Name - READ-ONLY; The name of the resource + Name *string `json:"name,omitempty"` + // Type - READ-ONLY; The type of the resource. E.g. "Microsoft.Compute/virtualMachines" or "Microsoft.Storage/storageAccounts" + Type *string `json:"type,omitempty"` + // SystemData - READ-ONLY; Azure Resource Manager metadata containing createdBy and modifiedBy information. + SystemData *securityinsight.SystemData `json:"systemData,omitempty"` +} + +func (smas SecurityMLAnalyticsSetting) AsAnomalySecurityMLAnalyticsSettings() (*AnomalySecurityMLAnalyticsSettings, bool) { + return nil, false +} +func (smas SecurityMLAnalyticsSetting) AsSecurityMLAnalyticsSetting() (*SecurityMLAnalyticsSetting, bool) { + return &smas, true +} +func (smas SecurityMLAnalyticsSetting) AsBasicSecurityMLAnalyticsSetting() (BasicSecurityMLAnalyticsSetting, bool) { + return &smas, true +} + +type AnomalySecurityMLAnalyticsSettings struct { + *AnomalySecurityMLAnalyticsSettingsProperties `json:"properties,omitempty"` + Etag *string `json:"etag,omitempty"` + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + Type *string `json:"type,omitempty"` + SystemData *securityinsight.SystemData `json:"systemData,omitempty"` + Kind securityinsight.KindBasicSecurityMLAnalyticsSetting `json:"kind,omitempty"` +} + +func (asmas AnomalySecurityMLAnalyticsSettings) AsAnomalySecurityMLAnalyticsSettings() (*AnomalySecurityMLAnalyticsSettings, bool) { + return &asmas, true +} +func (asmas AnomalySecurityMLAnalyticsSettings) AsSecurityMLAnalyticsSetting() (*SecurityMLAnalyticsSetting, bool) { + return nil, false +} +func (asmas AnomalySecurityMLAnalyticsSettings) AsBasicSecurityMLAnalyticsSetting() (BasicSecurityMLAnalyticsSetting, bool) { + return &asmas, true +} + +type AnomalySecurityMLAnalyticsSettingsProperties struct { + Description *string `json:"description,omitempty"` + DisplayName *string `json:"displayName,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + LastModifiedUtc *date.Time `json:"lastModifiedUtc,omitempty"` + RequiredDataConnectors *[]securityinsight.SecurityMLAnalyticsSettingsDataSource `json:"requiredDataConnectors,omitempty"` + Tactics *[]securityinsight.AttackTactic `json:"tactics,omitempty"` + Techniques *[]string `json:"techniques,omitempty"` + AnomalyVersion *string `json:"anomalyVersion,omitempty"` + CustomizableObservations *AnomalySecurityMLAnalyticsCustomizableObservations `json:"customizableObservations,omitempty"` + Frequency *string `json:"frequency,omitempty"` + SettingsStatus securityinsight.SettingsStatus `json:"settingsStatus,omitempty"` + IsDefaultSettings *bool `json:"isDefaultSettings,omitempty"` + AnomalySettingsVersion *int32 `json:"anomalySettingsVersion,omitempty"` + SettingsDefinitionID *uuid.UUID `json:"settingsDefinitionId,omitempty"` +} + +type AnomalySecurityMLAnalyticsCustomizableObservations struct { + MultiSelectObservations *[]AnomalySecurityMLAnalyticsMultiSelectObservations `json:"multiSelectObservations,omitempty"` + SingleSelectObservations *[]AnomalySecurityMLAnalyticsSingleSelectObservations `json:"singleSelectObservations,omitempty"` + PrioritizeExcludeObservations *[]AnomalySecurityMLAnalyticsPrioritizeExcludeObservations `json:"prioritizeExcludeObservations,omitempty"` + ThresholdObservations *[]AnomalySecurityMLAnalyticsThresholdObservations `json:"thresholdObservations,omitempty"` +} + +// unused properties are defined to interface{}. +type AnomalySecurityMLAnalyticsMultiSelectObservations struct { + SupportValues *[]string `json:"supportedValues,omitempty"` + Values *[]string `json:"values,omitempty"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + SupportedValuesKql *interface{} `json:"supportedValuesKql,omitempty"` + ValuesKql *interface{} `json:"valuesKql,omitempty"` + SequenceNumber *interface{} `json:"sequenceNumber,omitempty"` + Rerun *interface{} `json:"rerun,omitempty"` +} + +type AnomalySecurityMLAnalyticsSingleSelectObservations struct { + SupportValues *[]string `json:"supportedValues,omitempty"` + Value *string `json:"value,omitempty"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + SupportedValuesKql *interface{} `json:"supportedValuesKql,omitempty"` + SequenceNumber *interface{} `json:"sequenceNumber,omitempty"` + Rerun *interface{} `json:"rerun,omitempty"` +} + +type AnomalySecurityMLAnalyticsPrioritizeExcludeObservations struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Prioritize *string `json:"prioritize,omitempty"` + Exclude *string `json:"exclude,omitempty"` + DataType *interface{} `json:"dataType,omitempty"` + SequenceNumber *interface{} `json:"sequenceNumber,omitempty"` + Rerun *interface{} `json:"rerun,omitempty"` +} + +type AnomalySecurityMLAnalyticsThresholdObservations struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Max *string `json:"maximum,omitempty"` + Min *string `json:"minimum,omitempty"` + Value *string `json:"value,omitempty"` + SequenceNumber *interface{} `json:"sequenceNumber,omitempty"` + Rerun *interface{} `json:"rerun,omitempty"` +} diff --git a/internal/services/sentinel/client/client.go b/internal/services/sentinel/client/client.go index e71307000a15..07324f89a073 100644 --- a/internal/services/sentinel/client/client.go +++ b/internal/services/sentinel/client/client.go @@ -15,6 +15,7 @@ type Client struct { WatchlistsClient *securityinsight.WatchlistsClient WatchlistItemsClient *securityinsight.WatchlistItemsClient OnboardingStatesClient *sentinelonboardingstates.SentinelOnboardingStatesClient + AnalyticsSettingsClient *securityinsight.SecurityMLAnalyticsSettingsClient } func NewClient(o *common.ClientOptions) *Client { @@ -39,6 +40,9 @@ func NewClient(o *common.ClientOptions) *Client { onboardingStatesClient := sentinelonboardingstates.NewSentinelOnboardingStatesClientWithBaseURI(o.ResourceManagerEndpoint) o.ConfigureClient(&onboardingStatesClient.Client, o.ResourceManagerAuthorizer) + analyticsSettingsClient := securityinsight.NewSecurityMLAnalyticsSettingsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) + o.ConfigureClient(&analyticsSettingsClient.Client, o.ResourceManagerAuthorizer) + return &Client{ AlertRulesClient: &alertRulesClient, AlertRuleTemplatesClient: &alertRuleTemplatesClient, @@ -47,5 +51,6 @@ func NewClient(o *common.ClientOptions) *Client { WatchlistsClient: &watchListsClient, WatchlistItemsClient: &watchListItemsClient, OnboardingStatesClient: &onboardingStatesClient, + AnalyticsSettingsClient: &analyticsSettingsClient, } } diff --git a/internal/services/sentinel/parse/ml_analytics_settings.go b/internal/services/sentinel/parse/ml_analytics_settings.go new file mode 100644 index 000000000000..ad500f2529eb --- /dev/null +++ b/internal/services/sentinel/parse/ml_analytics_settings.go @@ -0,0 +1,75 @@ +package parse + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "fmt" + "strings" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" +) + +type MLAnalyticsSettingsId struct { + SubscriptionId string + ResourceGroup string + WorkspaceName string + SecurityMLAnalyticsSettingName string +} + +func NewMLAnalyticsSettingsID(subscriptionId, resourceGroup, workspaceName, securityMLAnalyticsSettingName string) MLAnalyticsSettingsId { + return MLAnalyticsSettingsId{ + SubscriptionId: subscriptionId, + ResourceGroup: resourceGroup, + WorkspaceName: workspaceName, + SecurityMLAnalyticsSettingName: securityMLAnalyticsSettingName, + } +} + +func (id MLAnalyticsSettingsId) String() string { + segments := []string{ + fmt.Sprintf("Security M L Analytics Setting Name %q", id.SecurityMLAnalyticsSettingName), + fmt.Sprintf("Workspace Name %q", id.WorkspaceName), + fmt.Sprintf("Resource Group %q", id.ResourceGroup), + } + segmentsStr := strings.Join(segments, " / ") + return fmt.Sprintf("%s: (%s)", "M L Analytics Settings", segmentsStr) +} + +func (id MLAnalyticsSettingsId) ID() string { + fmtString := "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.OperationalInsights/workspaces/%s/providers/Microsoft.SecurityInsights/securityMLAnalyticsSettings/%s" + return fmt.Sprintf(fmtString, id.SubscriptionId, id.ResourceGroup, id.WorkspaceName, id.SecurityMLAnalyticsSettingName) +} + +// MLAnalyticsSettingsID parses a MLAnalyticsSettings ID into an MLAnalyticsSettingsId struct +func MLAnalyticsSettingsID(input string) (*MLAnalyticsSettingsId, error) { + id, err := resourceids.ParseAzureResourceID(input) + if err != nil { + return nil, err + } + + resourceId := MLAnalyticsSettingsId{ + SubscriptionId: id.SubscriptionID, + ResourceGroup: id.ResourceGroup, + } + + if resourceId.SubscriptionId == "" { + return nil, fmt.Errorf("ID was missing the 'subscriptions' element") + } + + if resourceId.ResourceGroup == "" { + return nil, fmt.Errorf("ID was missing the 'resourceGroups' element") + } + + if resourceId.WorkspaceName, err = id.PopSegment("workspaces"); err != nil { + return nil, err + } + if resourceId.SecurityMLAnalyticsSettingName, err = id.PopSegment("securityMLAnalyticsSettings"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &resourceId, nil +} diff --git a/internal/services/sentinel/parse/ml_analytics_settings_test.go b/internal/services/sentinel/parse/ml_analytics_settings_test.go new file mode 100644 index 000000000000..e0eee0876617 --- /dev/null +++ b/internal/services/sentinel/parse/ml_analytics_settings_test.go @@ -0,0 +1,128 @@ +package parse + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "testing" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" +) + +var _ resourceids.Id = MLAnalyticsSettingsId{} + +func TestMLAnalyticsSettingsIDFormatter(t *testing.T) { + actual := NewMLAnalyticsSettingsID("12345678-1234-9876-4563-123456789012", "resGroup1", "workspace1", "setting1").ID() + expected := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/workspaces/workspace1/providers/Microsoft.SecurityInsights/securityMLAnalyticsSettings/setting1" + if actual != expected { + t.Fatalf("Expected %q but got %q", expected, actual) + } +} + +func TestMLAnalyticsSettingsID(t *testing.T) { + testData := []struct { + Input string + Error bool + Expected *MLAnalyticsSettingsId + }{ + + { + // empty + Input: "", + Error: true, + }, + + { + // missing SubscriptionId + Input: "/", + Error: true, + }, + + { + // missing value for SubscriptionId + Input: "/subscriptions/", + Error: true, + }, + + { + // missing ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/", + Error: true, + }, + + { + // missing value for ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/", + Error: true, + }, + + { + // missing WorkspaceName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/", + Error: true, + }, + + { + // missing value for WorkspaceName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/workspaces/", + Error: true, + }, + + { + // missing SecurityMLAnalyticsSettingName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/workspaces/workspace1/providers/Microsoft.SecurityInsights/", + Error: true, + }, + + { + // missing value for SecurityMLAnalyticsSettingName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/workspaces/workspace1/providers/Microsoft.SecurityInsights/securityMLAnalyticsSettings/", + Error: true, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/workspaces/workspace1/providers/Microsoft.SecurityInsights/securityMLAnalyticsSettings/setting1", + Expected: &MLAnalyticsSettingsId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "resGroup1", + WorkspaceName: "workspace1", + SecurityMLAnalyticsSettingName: "setting1", + }, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/12345678-1234-9876-4563-123456789012/RESOURCEGROUPS/RESGROUP1/PROVIDERS/MICROSOFT.OPERATIONALINSIGHTS/WORKSPACES/WORKSPACE1/PROVIDERS/MICROSOFT.SECURITYINSIGHTS/SECURITYMLANALYTICSSETTINGS/SETTING1", + Error: true, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Input) + + actual, err := MLAnalyticsSettingsID(v.Input) + if err != nil { + if v.Error { + continue + } + + t.Fatalf("Expect a value but got an error: %s", err) + } + if v.Error { + t.Fatal("Expect an error but didn't get one") + } + + if actual.SubscriptionId != v.Expected.SubscriptionId { + t.Fatalf("Expected %q but got %q for SubscriptionId", v.Expected.SubscriptionId, actual.SubscriptionId) + } + if actual.ResourceGroup != v.Expected.ResourceGroup { + t.Fatalf("Expected %q but got %q for ResourceGroup", v.Expected.ResourceGroup, actual.ResourceGroup) + } + if actual.WorkspaceName != v.Expected.WorkspaceName { + t.Fatalf("Expected %q but got %q for WorkspaceName", v.Expected.WorkspaceName, actual.WorkspaceName) + } + if actual.SecurityMLAnalyticsSettingName != v.Expected.SecurityMLAnalyticsSettingName { + t.Fatalf("Expected %q but got %q for SecurityMLAnalyticsSettingName", v.Expected.SecurityMLAnalyticsSettingName, actual.SecurityMLAnalyticsSettingName) + } + } +} diff --git a/internal/services/sentinel/registration.go b/internal/services/sentinel/registration.go index 2a4093bf7160..9c8c56861ee0 100644 --- a/internal/services/sentinel/registration.go +++ b/internal/services/sentinel/registration.go @@ -55,7 +55,9 @@ func (r Registration) SupportedResources() map[string]*pluginsdk.Resource { } func (r Registration) DataSources() []sdk.DataSource { - return []sdk.DataSource{} + return []sdk.DataSource{ + AlertRuleAnomalyDataSource{}, + } } func (r Registration) Resources() []sdk.Resource { @@ -72,5 +74,6 @@ func (r Registration) Resources() []sdk.Resource { LogAnalyticsWorkspaceOnboardResource{}, DataConnectorThreatIntelligenceTAXIIResource{}, DataConnectorMicrosoftThreatIntelligenceResource{}, + AlertRuleAnomalyBuiltInResource{}, } } diff --git a/internal/services/sentinel/resourceids.go b/internal/services/sentinel/resourceids.go index dc5677e2a9eb..5dcd3a553e0d 100644 --- a/internal/services/sentinel/resourceids.go +++ b/internal/services/sentinel/resourceids.go @@ -6,3 +6,4 @@ package sentinel //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=AutomationRule -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/workspaces/workspace1/providers/Microsoft.SecurityInsights/automationRules/rule1 -rewrite=true //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=Watchlist -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/workspaces/workspace1/providers/Microsoft.SecurityInsights/watchlists/list1 //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=WatchlistItem -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/workspaces/workspace1/providers/Microsoft.SecurityInsights/watchlists/list1/watchlistItems/item1 +//go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=MLAnalyticsSettings -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/workspaces/workspace1/providers/Microsoft.SecurityInsights/securityMLAnalyticsSettings/setting1 diff --git a/internal/services/sentinel/sentinel_alert_rule_anomaly.go b/internal/services/sentinel/sentinel_alert_rule_anomaly.go new file mode 100644 index 000000000000..7ec3b67bf2d4 --- /dev/null +++ b/internal/services/sentinel/sentinel_alert_rule_anomaly.go @@ -0,0 +1,307 @@ +package sentinel + +import ( + "context" + "fmt" + + "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/services/sentinel/azuresdkhacks" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/sentinel/parse" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + securityinsight "github.com/tombuildsstuff/kermit/sdk/securityinsights/2022-10-01-preview/securityinsights" +) + +type AnomalyRuleRequiredDataConnectorModel struct { + ConnectorId string `tfschema:"connector_id"` + DataTypes []string `tfschema:"data_types"` +} + +type AnomalyRuleMultiSelectModel struct { + SupportValues []string `tfschema:"supported_values"` + Values []string `tfschema:"values"` + Name string `tfschema:"name"` + Description string `tfschema:"description"` +} + +func AnomalyRuleMultiSelectSchema() *pluginsdk.Schema { + return &pluginsdk.Schema{ + Type: pluginsdk.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "description": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "supported_values": { + Type: pluginsdk.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: pluginsdk.TypeString, + }, + }, + "values": { + Type: pluginsdk.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: pluginsdk.TypeString, + }, + }, + }, + }, + } +} + +func flattenSentinelAlertRuleAnomalyMultiSelect(input *[]azuresdkhacks.AnomalySecurityMLAnalyticsMultiSelectObservations) []AnomalyRuleMultiSelectModel { + if input == nil { + return []AnomalyRuleMultiSelectModel{} + } + + output := make([]AnomalyRuleMultiSelectModel, 0) + for _, item := range *input { + o := AnomalyRuleMultiSelectModel{} + if item.Values != nil { + values := make([]string, 0) + values = append(values, *item.Values...) + o.Values = values + } + if item.SupportValues != nil { + supportValues := make([]string, 0) + supportValues = append(supportValues, *item.SupportValues...) + o.SupportValues = supportValues + } + if item.Name != nil { + o.Name = *item.Name + } + if item.Description != nil { + o.Description = *item.Description + } + output = append(output, o) + } + return output +} + +type AnomalyRuleSingleSelectModel struct { + Name string `tfschema:"name"` + Description string `tfschema:"description"` + SupportValues []string `tfschema:"supported_values"` + Value string `tfschema:"value"` +} + +func AnomalyRuleSingleSelectSchema() *pluginsdk.Schema { + return &pluginsdk.Schema{ + Type: pluginsdk.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "description": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "supported_values": { + Type: pluginsdk.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: pluginsdk.TypeString, + }, + }, + "value": { + Type: pluginsdk.TypeString, + Computed: true, + }, + }, + }, + } +} + +func flattenSentinelAlertRuleAnomalySingleSelect(input *[]azuresdkhacks.AnomalySecurityMLAnalyticsSingleSelectObservations) []AnomalyRuleSingleSelectModel { + if input == nil { + return []AnomalyRuleSingleSelectModel{} + } + + output := make([]AnomalyRuleSingleSelectModel, 0) + for _, item := range *input { + o := AnomalyRuleSingleSelectModel{} + if item.Value != nil { + o.Value = *item.Value + } + if item.SupportValues != nil { + supportValues := make([]string, 0) + supportValues = append(supportValues, *item.SupportValues...) + o.SupportValues = supportValues + } + if item.Name != nil { + o.Name = *item.Name + } + if item.Description != nil { + o.Description = *item.Description + } + output = append(output, o) + } + return output +} + +type AnomalyRulePriorityModel struct { + Name string `tfschema:"name"` + Description string `tfschema:"description"` + Prioritize string `tfschema:"prioritize"` + Exclude string `tfschema:"exclude"` +} + +func AnomalyRulePrioritySchema() *pluginsdk.Schema { + return &pluginsdk.Schema{ + Type: pluginsdk.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "description": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "prioritize": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "exclude": { + Type: pluginsdk.TypeString, + Computed: true, + }, + }, + }, + } +} + +func flattenSentinelAlertRuleAnomalyPriority(input *[]azuresdkhacks.AnomalySecurityMLAnalyticsPrioritizeExcludeObservations) []AnomalyRulePriorityModel { + if input == nil { + return []AnomalyRulePriorityModel{} + } + + output := make([]AnomalyRulePriorityModel, 0) + for _, item := range *input { + o := AnomalyRulePriorityModel{} + if item.Prioritize != nil { + o.Prioritize = *item.Prioritize + } + if item.Exclude != nil { + o.Exclude = *item.Exclude + } + if item.Name != nil { + o.Name = *item.Name + } + if item.Description != nil { + o.Description = *item.Description + } + output = append(output, o) + } + return output +} + +type AnomalyRuleThresholdModel struct { + Name string `tfschema:"name"` + Description string `tfschema:"description"` + Max string `tfschema:"max"` + Min string `tfschema:"min"` + Value string `tfschema:"value"` +} + +func AnomalyRuleThresholdSchema() *pluginsdk.Schema { + return &pluginsdk.Schema{ + Type: pluginsdk.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "description": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "max": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "min": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "value": { + Type: pluginsdk.TypeString, + Computed: true, + }, + }, + }, + } +} + +func flattenSentinelAlertRuleAnomalyThreshold(input *[]azuresdkhacks.AnomalySecurityMLAnalyticsThresholdObservations) []AnomalyRuleThresholdModel { + if input == nil { + return []AnomalyRuleThresholdModel{} + } + + output := make([]AnomalyRuleThresholdModel, 0) + for _, item := range *input { + o := AnomalyRuleThresholdModel{} + if item.Max != nil { + o.Max = *item.Max + } + if item.Min != nil { + o.Min = *item.Min + } + if item.Value != nil { + o.Value = *item.Value + } + if item.Name != nil { + o.Name = *item.Name + } + if item.Description != nil { + o.Description = *item.Description + } + output = append(output, o) + } + return output +} + +// the service always return a fixed one no matter what id we pass, tracked on https://github.com/Azure/azure-rest-api-specs/issues/22485 +func AlertRuleAnomalyReadWithPredicate(ctx context.Context, baseClient securityinsight.BaseClient, workspaceId workspaces.WorkspaceId, predicateFunc func(v *azuresdkhacks.AnomalySecurityMLAnalyticsSettings) bool) (*azuresdkhacks.AnomalySecurityMLAnalyticsSettings, error) { + client := azuresdkhacks.SecurityMLAnalyticsSettingsClient{BaseClient: baseClient} + resp, err := client.ListComplete(ctx, workspaceId.ResourceGroupName, workspaceId.WorkspaceName) + if err != nil { + return nil, fmt.Errorf("retrieving: %+v", err) + } + + for resp.NotDone() { + item := resp.Value() + if v, ok := item.AsAnomalySecurityMLAnalyticsSettings(); ok { + if predicateFunc(v) { + return v, nil + } + + } + if err := resp.NextWithContext(ctx); err != nil { + return nil, fmt.Errorf("listing next: %+v", err) + } + } + return nil, nil +} + +// when the id of workspace is too long, the service return without workspace name: +// "/subscriptions/{sub_id}/resourceGroups/{rg_name}/providers/Microsoft.OperationalInsights/workspaces//providers/Microsoft.SecurityInsights/securityMLAnalyticsSettings/5020e404-9768-4364-98f6-679940c21362", +// tracked on https://github.com/Azure/azure-rest-api-specs/issues/22500 +func AlertRuleAnomalyIdFromWorkspaceId(workspaceId workspaces.WorkspaceId, name string) string { + return parse.NewMLAnalyticsSettingsID(workspaceId.SubscriptionId, workspaceId.ResourceGroupName, workspaceId.WorkspaceName, name).ID() +} diff --git a/internal/services/sentinel/sentinel_alert_rule_anomaly_built_in_resource.go b/internal/services/sentinel/sentinel_alert_rule_anomaly_built_in_resource.go new file mode 100644 index 000000000000..4f17f46f1ceb --- /dev/null +++ b/internal/services/sentinel/sentinel_alert_rule_anomaly_built_in_resource.go @@ -0,0 +1,461 @@ +package sentinel + +import ( + "context" + "fmt" + "strings" + "time" + + "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 AlertRuleAnomalyBuiltInModel struct { + Name string `tfschema:"name"` + DisplayName string `tfschema:"display_name"` + WorkspaceId string `tfschema:"log_analytics_workspace_id"` + CustomizableObservations string `tfschema:"customizable_observations"` + 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 AlertRuleAnomalyBuiltInResource struct{} + +var _ sdk.ResourceWithUpdate = AlertRuleAnomalyBuiltInResource{} + +func (r AlertRuleAnomalyBuiltInResource) ModelObject() interface{} { + return &AlertRuleAnomalyBuiltInModel{} +} + +func (r AlertRuleAnomalyBuiltInResource) ResourceType() string { + return "azurerm_sentinel_alert_rule_anomaly_built_in" +} + +func (r AlertRuleAnomalyBuiltInResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return validate.MLAnalyticsSettingsID +} + +func (r AlertRuleAnomalyBuiltInResource) Arguments() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "name": { + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringIsNotEmpty, + ExactlyOneOf: []string{"name", "display_name"}, + }, + "display_name": { + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringIsNotEmpty, + ExactlyOneOf: []string{"name", "display_name"}, + }, + "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), + }, + } +} + +func (r AlertRuleAnomalyBuiltInResource) Attributes() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "anomaly_version": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "anomaly_settings_version": { + Type: pluginsdk.TypeInt, + Computed: true, + }, + "customizable_observations": { + Type: pluginsdk.TypeString, + 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, + }, + }, + "multi_select_observation": AnomalyRuleMultiSelectSchema(), + "single_select_observation": AnomalyRuleSingleSelectSchema(), + "prioritized_exclude_observation": AnomalyRulePrioritySchema(), + "threshold_observation": AnomalyRuleThresholdSchema(), + } +} + +func (r AlertRuleAnomalyBuiltInResource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + var metaModel AlertRuleAnomalyBuiltInModel + 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) + } + + builtinRule, err := AlertRuleAnomalyReadWithPredicate(ctx, client.BaseClient, *workspaceId, func(v *azuresdkhacks.AnomalySecurityMLAnalyticsSettings) bool { + if v.Name != nil && strings.EqualFold(*v.Name, metaModel.Name) { + return true + } + + if v.DisplayName != nil && strings.EqualFold(*v.DisplayName, metaModel.DisplayName) { + return true + } + + return false + }) + + if err != nil { + return fmt.Errorf("reading: %+v", err) + } + if builtinRule == nil { + if metaModel.DisplayName != "" { + return fmt.Errorf("built in rule (Display Name %q) was not found", metaModel.DisplayName) + } + return fmt.Errorf("built in rule (Display Name %q) was not found", metaModel.Name) + } + + id, err := parse.MLAnalyticsSettingsID(AlertRuleAnomalyIdFromWorkspaceId(*workspaceId, *builtinRule.Name)) + if err != nil { + return fmt.Errorf("parsing: %+v", err) + } + + param := securityinsight.AnomalySecurityMLAnalyticsSettings{ + Kind: securityinsight.KindBasicSecurityMLAnalyticsSettingKindAnomaly, + AnomalySecurityMLAnalyticsSettingsProperties: &securityinsight.AnomalySecurityMLAnalyticsSettingsProperties{ + Description: builtinRule.Description, + DisplayName: builtinRule.DisplayName, + RequiredDataConnectors: builtinRule.RequiredDataConnectors, + Tactics: builtinRule.Tactics, + Techniques: builtinRule.Techniques, + AnomalyVersion: builtinRule.AnomalyVersion, + Frequency: builtinRule.Frequency, + IsDefaultSettings: builtinRule.IsDefaultSettings, + AnomalySettingsVersion: builtinRule.AnomalySettingsVersion, + SettingsDefinitionID: builtinRule.SettingsDefinitionID, + Enabled: utils.Bool(metaModel.Enabled), + SettingsStatus: securityinsight.SettingsStatus(metaModel.Mode), + CustomizableObservations: builtinRule.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 AlertRuleAnomalyBuiltInResource) 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 := AlertRuleAnomalyBuiltInModel{ + 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) + } + + return metadata.Encode(&state) + }, + } +} + +func (r AlertRuleAnomalyBuiltInResource) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + var metaModel AlertRuleAnomalyBuiltInModel + 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: %+v", err) + } + + workspaceId, err := workspaces.ParseWorkspaceID(metaModel.WorkspaceId) + if err != nil { + return fmt.Errorf("parsing workspace id: %+v", err) + } + + 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: 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: existing.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 AlertRuleAnomalyBuiltInResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + // it's not able to delete built-in rules. + var metaModel AlertRuleAnomalyBuiltInModel + 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: %+v", err) + } + + workspaceId, err := workspaces.ParseWorkspaceID(metaModel.WorkspaceId) + if err != nil { + return fmt.Errorf("parsing workspace id: %+v", err) + } + + 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: 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(false), + SettingsStatus: securityinsight.SettingsStatus(metaModel.Mode), + }, + } + + if metaModel.CustomizableObservations != "" { + v, err := pluginsdk.ExpandJsonFromString(metaModel.CustomizableObservations) + if err != nil { + return fmt.Errorf("expanding `customizable_observations`: %+v", err) + } + param.AnomalySecurityMLAnalyticsSettingsProperties.CustomizableObservations = v + } else { + param.AnomalySecurityMLAnalyticsSettingsProperties.CustomizableObservations = existing.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 + }, + } +} diff --git a/internal/services/sentinel/sentinel_alert_rule_anomaly_built_in_resource_test.go b/internal/services/sentinel/sentinel_alert_rule_anomaly_built_in_resource_test.go new file mode 100644 index 000000000000..8f6cb330dda9 --- /dev/null +++ b/internal/services/sentinel/sentinel_alert_rule_anomaly_built_in_resource_test.go @@ -0,0 +1,68 @@ +package sentinel_test + +import ( + "context" + "fmt" + "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 SentinelAlertRuleAnomalyBuiltInResource struct{} + +func (r SentinelAlertRuleAnomalyBuiltInResource) 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 && resp.Enabled != nil && *resp.Enabled == true), nil +} + +func TestAccSentinelAlertRuleAnomalyBuiltIn_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_sentinel_alert_rule_anomaly_built_in", "test") + r := SentinelAlertRuleAnomalyBuiltInResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func (SentinelAlertRuleAnomalyBuiltInResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +%s +resource "azurerm_sentinel_alert_rule_anomaly_built_in" "test" { + display_name = "UEBA Anomalous Sign In" + log_analytics_workspace_id = azurerm_log_analytics_workspace.test.id + enabled = true + mode = "Production" + depends_on = [azurerm_sentinel_log_analytics_workspace_onboarding.test] +} +`, SecurityInsightsSentinelOnboardingStateResource{}.basic(data)) +} diff --git a/internal/services/sentinel/sentinel_alert_rule_anomaly_data_source.go b/internal/services/sentinel/sentinel_alert_rule_anomaly_data_source.go new file mode 100644 index 000000000000..fb74fddef660 --- /dev/null +++ b/internal/services/sentinel/sentinel_alert_rule_anomaly_data_source.go @@ -0,0 +1,280 @@ +package sentinel + +import ( + "context" + "fmt" + "strings" + "time" + + "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/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" + securityinsight "github.com/tombuildsstuff/kermit/sdk/securityinsights/2022-10-01-preview/securityinsights" +) + +type AlertRuleAnomalyDataSourceModel struct { + Name string `tfschema:"name"` + DisplayName string `tfschema:"display_name"` + WorkspaceId string `tfschema:"log_analytics_workspace_id"` + AnomalyVersion string `tfschema:"anomaly_version"` + AnomalySettingsVersion int32 `tfschema:"anomaly_settings_version"` + Description string `tfschema:"description"` + Enabled bool `tfschema:"enabled"` + Frequency string `tfschema:"frequency"` + IsDefaultSettings bool `tfschema:"is_default_settings"` + RequiredDataConnectors []AnomalyRuleRequiredDataConnectorModel `tfschema:"required_data_connector"` + SettingsDefinitionId string `tfschema:"settings_definition_id"` + Mode string `tfschema:"mode"` + 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 AlertRuleAnomalyDataSource struct{} + +var _ sdk.DataSource = AlertRuleAnomalyDataSource{} + +func (a AlertRuleAnomalyDataSource) Arguments() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "name": { + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringIsNotEmpty, + ExactlyOneOf: []string{"name", "display_name"}, + }, + + "display_name": { + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringIsNotEmpty, + ExactlyOneOf: []string{"name", "display_name"}, + }, + + "log_analytics_workspace_id": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: workspaces.ValidateWorkspaceID, + }, + } +} + +func (a AlertRuleAnomalyDataSource) Attributes() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "anomaly_version": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "anomaly_settings_version": { + Type: pluginsdk.TypeInt, + Computed: true, + }, + "customizable_observations": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "description": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "enabled": { + Type: pluginsdk.TypeBool, + 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, + }, + }, + }, + }, + }, + "mode": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "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, + }, + }, + "multi_select_observation": AnomalyRuleMultiSelectSchema(), + "single_select_observation": AnomalyRuleSingleSelectSchema(), + "prioritized_exclude_observation": AnomalyRulePrioritySchema(), + "threshold_observation": AnomalyRuleThresholdSchema(), + } +} + +func (a AlertRuleAnomalyDataSource) ModelObject() interface{} { + return &AlertRuleAnomalyDataSourceModel{} +} + +func (a AlertRuleAnomalyDataSource) ResourceType() string { + return "azurerm_sentinel_alert_rule_anomaly" +} + +func (a AlertRuleAnomalyDataSource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + var metaModel AlertRuleAnomalyDataSourceModel + 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) + } + + setting, err := AlertRuleAnomalyReadWithPredicate(ctx, client.BaseClient, *workspaceId, func(v *azuresdkhacks.AnomalySecurityMLAnalyticsSettings) bool { + if v.Name != nil && strings.EqualFold(*v.Name, metaModel.Name) { + return true + } + + if v.DisplayName != nil && strings.EqualFold(*v.DisplayName, metaModel.DisplayName) { + return true + } + + return false + }) + + if err != nil { + return fmt.Errorf("retrieving: %+v", err) + } + if setting == nil { + if metaModel.DisplayName != "" { + return fmt.Errorf("reading Sentinel Anomaly Rule (Display Name %q) was not found", metaModel.DisplayName) + } + return fmt.Errorf("reading Sentinel Anomaly Rule (Name %q) was not found", metaModel.Name) + } + + id, err := parse.MLAnalyticsSettingsID(AlertRuleAnomalyIdFromWorkspaceId(*workspaceId, *setting.Name)) + if err != nil { + return fmt.Errorf("parsing: %+v", err) + } + + state := AlertRuleAnomalyDataSourceModel{ + WorkspaceId: workspaceId.ID(), + Mode: string(setting.SettingsStatus), + } + + if setting.Name != nil { + state.Name = *setting.Name + } + if setting.DisplayName != nil { + state.DisplayName = *setting.DisplayName + } + if setting.AnomalyVersion != nil { + state.AnomalyVersion = *setting.AnomalyVersion + } + if setting.AnomalySettingsVersion != nil { + state.AnomalySettingsVersion = *setting.AnomalySettingsVersion + } + if setting.Description != nil { + state.Description = *setting.Description + } + if setting.Enabled != nil { + state.Enabled = *setting.Enabled + } + if setting.Frequency != nil { + state.Frequency = *setting.Frequency + } + if setting.IsDefaultSettings != nil { + state.IsDefaultSettings = *setting.IsDefaultSettings + } + state.RequiredDataConnectors = flattenSentinelAlertRuleAnomalyRequiredDataConnectors(setting.RequiredDataConnectors) + if setting.SettingsDefinitionID != nil { + state.SettingsDefinitionId = setting.SettingsDefinitionID.String() + } + state.Tactics = flattenSentinelAlertRuleAnomalyTactics(setting.Tactics) + if setting.Techniques != nil { + state.Techniques = *setting.Techniques + } + + if setting.CustomizableObservations != nil { + state.MultiSelectObservation = flattenSentinelAlertRuleAnomalyMultiSelect(setting.CustomizableObservations.MultiSelectObservations) + state.SingleSelectObservation = flattenSentinelAlertRuleAnomalySingleSelect(setting.CustomizableObservations.SingleSelectObservations) + state.PrioritizeExcludeObservation = flattenSentinelAlertRuleAnomalyPriority(setting.CustomizableObservations.PrioritizeExcludeObservations) + state.ThresholdObservation = flattenSentinelAlertRuleAnomalyThreshold(setting.CustomizableObservations.ThresholdObservations) + } + + metadata.SetID(id) + return metadata.Encode(&state) + }, + } +} + +func flattenSentinelAlertRuleAnomalyRequiredDataConnectors(input *[]securityinsight.SecurityMLAnalyticsSettingsDataSource) []AnomalyRuleRequiredDataConnectorModel { + if input == nil { + return []AnomalyRuleRequiredDataConnectorModel{} + } + + output := make([]AnomalyRuleRequiredDataConnectorModel, 0) + for _, v := range *input { + if v.ConnectorID == nil || v.DataTypes == nil { + continue + } + + output = append(output, AnomalyRuleRequiredDataConnectorModel{ + ConnectorId: *v.ConnectorID, + DataTypes: *v.DataTypes, + }) + } + + return output +} + +func flattenSentinelAlertRuleAnomalyTactics(input *[]securityinsight.AttackTactic) []string { + if input == nil { + return []string{} + } + + output := make([]string, 0) + for _, v := range *input { + output = append(output, string(v)) + } + + return output +} diff --git a/internal/services/sentinel/sentinel_alert_rule_anomaly_data_source_test.go b/internal/services/sentinel/sentinel_alert_rule_anomaly_data_source_test.go new file mode 100644 index 000000000000..9b2f982a2ec6 --- /dev/null +++ b/internal/services/sentinel/sentinel_alert_rule_anomaly_data_source_test.go @@ -0,0 +1,171 @@ +package sentinel_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" +) + +type SentinelAlertRuleAnomalyDataSource struct{} + +func TestAccSentinelAlertRuleAnomalyDataSource_basicWithThreshold(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azurerm_sentinel_alert_rule_anomaly", "test") + r := SentinelAlertRuleAnomalyDataSource{} + + data.DataSourceTest(t, []acceptance.TestStep{ + { + Config: r.basic_withThreshold(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).Key("id").Exists(), + check.That(data.ResourceName).Key("name").Exists(), + check.That(data.ResourceName).Key("display_name").Exists(), + check.That(data.ResourceName).Key("anomaly_version").Exists(), + check.That(data.ResourceName).Key("anomaly_settings_version").Exists(), + check.That(data.ResourceName).Key("description").Exists(), + check.That(data.ResourceName).Key("enabled").Exists(), + check.That(data.ResourceName).Key("frequency").Exists(), + check.That(data.ResourceName).Key("is_default_settings").Exists(), + check.That(data.ResourceName).Key("required_data_connector.#").HasValue("2"), + check.That(data.ResourceName).Key("mode").Exists(), + check.That(data.ResourceName).Key("settings_definition_id").Exists(), + check.That(data.ResourceName).Key("tactics.#").HasValue("1"), + check.That(data.ResourceName).Key("techniques.#").HasValue("1"), + check.That(data.ResourceName).Key("threshold_observation.#").HasValue("1"), + ), + }, + }) +} + +func TestAccSentinelAlertRuleAnomalyDataSource_basicWithSingleSelect(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azurerm_sentinel_alert_rule_anomaly", "test") + r := SentinelAlertRuleAnomalyDataSource{} + + data.DataSourceTest(t, []acceptance.TestStep{ + { + Config: r.basic_withSingleSelect(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).Key("id").Exists(), + check.That(data.ResourceName).Key("name").Exists(), + check.That(data.ResourceName).Key("display_name").Exists(), + check.That(data.ResourceName).Key("anomaly_version").Exists(), + check.That(data.ResourceName).Key("anomaly_settings_version").Exists(), + check.That(data.ResourceName).Key("description").Exists(), + check.That(data.ResourceName).Key("enabled").Exists(), + check.That(data.ResourceName).Key("frequency").Exists(), + check.That(data.ResourceName).Key("is_default_settings").Exists(), + check.That(data.ResourceName).Key("required_data_connector.#").HasValue("1"), + check.That(data.ResourceName).Key("mode").Exists(), + check.That(data.ResourceName).Key("settings_definition_id").Exists(), + check.That(data.ResourceName).Key("tactics.#").HasValue("1"), + check.That(data.ResourceName).Key("techniques.#").HasValue("2"), + check.That(data.ResourceName).Key("single_select_observation.#").HasValue("2"), + ), + }, + }) +} + +func TestAccSentinelAlertRuleAnomalyDataSource_basicWithMultiSelect(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azurerm_sentinel_alert_rule_anomaly", "test") + r := SentinelAlertRuleAnomalyDataSource{} + + data.DataSourceTest(t, []acceptance.TestStep{ + { + Config: r.basic_withMultiSelect(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).Key("id").Exists(), + check.That(data.ResourceName).Key("name").Exists(), + check.That(data.ResourceName).Key("display_name").Exists(), + check.That(data.ResourceName).Key("anomaly_version").Exists(), + check.That(data.ResourceName).Key("anomaly_settings_version").Exists(), + check.That(data.ResourceName).Key("description").Exists(), + check.That(data.ResourceName).Key("enabled").Exists(), + check.That(data.ResourceName).Key("frequency").Exists(), + check.That(data.ResourceName).Key("is_default_settings").Exists(), + check.That(data.ResourceName).Key("required_data_connector.#").HasValue("1"), + check.That(data.ResourceName).Key("mode").Exists(), + check.That(data.ResourceName).Key("settings_definition_id").Exists(), + check.That(data.ResourceName).Key("tactics.#").HasValue("1"), + check.That(data.ResourceName).Key("techniques.#").HasValue("1"), + check.That(data.ResourceName).Key("multi_select_observation.#").HasValue("1"), + ), + }, + }) +} + +func TestAccSentinelAlertRuleAnomalyDataSource_basicWithPrioritized(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azurerm_sentinel_alert_rule_anomaly", "test") + r := SentinelAlertRuleAnomalyDataSource{} + + data.DataSourceTest(t, []acceptance.TestStep{ + { + Config: r.basic_withPrioritizeExclude(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).Key("id").Exists(), + check.That(data.ResourceName).Key("name").Exists(), + check.That(data.ResourceName).Key("display_name").Exists(), + check.That(data.ResourceName).Key("anomaly_version").Exists(), + check.That(data.ResourceName).Key("anomaly_settings_version").Exists(), + check.That(data.ResourceName).Key("description").Exists(), + check.That(data.ResourceName).Key("enabled").Exists(), + check.That(data.ResourceName).Key("frequency").Exists(), + check.That(data.ResourceName).Key("is_default_settings").Exists(), + check.That(data.ResourceName).Key("required_data_connector.#").HasValue("1"), + check.That(data.ResourceName).Key("mode").Exists(), + check.That(data.ResourceName).Key("settings_definition_id").Exists(), + check.That(data.ResourceName).Key("tactics.#").HasValue("2"), + check.That(data.ResourceName).Key("techniques.#").HasValue("2"), + check.That(data.ResourceName).Key("prioritized_exclude_observation.#").HasValue("2"), + ), + }, + }) +} + +func (SentinelAlertRuleAnomalyDataSource) basic_withThreshold(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] +} +`, SecurityInsightsSentinelOnboardingStateResource{}.basic(data)) +} + +func (SentinelAlertRuleAnomalyDataSource) basic_withSingleSelect(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) Suspicious geography change in Palo Alto GlobalProtect account logins" + depends_on = [azurerm_sentinel_log_analytics_workspace_onboarding.test] +} +`, SecurityInsightsSentinelOnboardingStateResource{}.basic(data)) +} + +func (SentinelAlertRuleAnomalyDataSource) basic_withMultiSelect(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) Attempted user account bruteforce per logon type" + depends_on = [azurerm_sentinel_log_analytics_workspace_onboarding.test] +} +`, SecurityInsightsSentinelOnboardingStateResource{}.basic(data)) +} + +func (SentinelAlertRuleAnomalyDataSource) basic_withPrioritizeExclude(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] +} +`, SecurityInsightsSentinelOnboardingStateResource{}.basic(data)) +} diff --git a/internal/services/sentinel/validate/ml_analytics_settings_id.go b/internal/services/sentinel/validate/ml_analytics_settings_id.go new file mode 100644 index 000000000000..17e3d54d0bdd --- /dev/null +++ b/internal/services/sentinel/validate/ml_analytics_settings_id.go @@ -0,0 +1,23 @@ +package validate + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "fmt" + + "github.com/hashicorp/terraform-provider-azurerm/internal/services/sentinel/parse" +) + +func MLAnalyticsSettingsID(input interface{}, key string) (warnings []string, errors []error) { + v, ok := input.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected %q to be a string", key)) + return + } + + if _, err := parse.MLAnalyticsSettingsID(v); err != nil { + errors = append(errors, err) + } + + return +} diff --git a/internal/services/sentinel/validate/ml_analytics_settings_id_test.go b/internal/services/sentinel/validate/ml_analytics_settings_id_test.go new file mode 100644 index 000000000000..9d87ab2cfdfb --- /dev/null +++ b/internal/services/sentinel/validate/ml_analytics_settings_id_test.go @@ -0,0 +1,88 @@ +package validate + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import "testing" + +func TestMLAnalyticsSettingsID(t *testing.T) { + cases := []struct { + Input string + Valid bool + }{ + + { + // empty + Input: "", + Valid: false, + }, + + { + // missing SubscriptionId + Input: "/", + Valid: false, + }, + + { + // missing value for SubscriptionId + Input: "/subscriptions/", + Valid: false, + }, + + { + // missing ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/", + Valid: false, + }, + + { + // missing value for ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/", + Valid: false, + }, + + { + // missing WorkspaceName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/", + Valid: false, + }, + + { + // missing value for WorkspaceName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/workspaces/", + Valid: false, + }, + + { + // missing SecurityMLAnalyticsSettingName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/workspaces/workspace1/providers/Microsoft.SecurityInsights/", + Valid: false, + }, + + { + // missing value for SecurityMLAnalyticsSettingName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/workspaces/workspace1/providers/Microsoft.SecurityInsights/securityMLAnalyticsSettings/", + Valid: false, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/workspaces/workspace1/providers/Microsoft.SecurityInsights/securityMLAnalyticsSettings/setting1", + Valid: true, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/12345678-1234-9876-4563-123456789012/RESOURCEGROUPS/RESGROUP1/PROVIDERS/MICROSOFT.OPERATIONALINSIGHTS/WORKSPACES/WORKSPACE1/PROVIDERS/MICROSOFT.SECURITYINSIGHTS/SECURITYMLANALYTICSSETTINGS/SETTING1", + Valid: false, + }, + } + for _, tc := range cases { + t.Logf("[DEBUG] Testing Value %s", tc.Input) + _, errors := MLAnalyticsSettingsID(tc.Input, "test") + valid := len(errors) == 0 + + if tc.Valid != valid { + t.Fatalf("Expected %t but got %t", tc.Valid, valid) + } + } +} diff --git a/website/docs/d/sentinel_alert_rule_anomaly.html.markdown b/website/docs/d/sentinel_alert_rule_anomaly.html.markdown new file mode 100644 index 000000000000..07fe94e90104 --- /dev/null +++ b/website/docs/d/sentinel_alert_rule_anomaly.html.markdown @@ -0,0 +1,158 @@ +--- +subcategory: "Sentinel" +layout: "azurerm" +page_title: "Azure Resource Manager: Data Source: azurerm_sentinel_alert_rule_anomaly" +description: |- + Gets information about an existing Anomaly Alert Rule. +--- + +# Data Source: azurerm_sentinel_alert_rule_anomaly + +Use this data source to access information about an existing 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] +} + +output "id" { + value = data.azurerm_sentinel_alert_rule_anomaly.example.id +} +``` + +## Arguments Reference + +The following arguments are supported: + +* `log_analytics_workspace_id` - (Required) The ID of the Log Analytics Workspace. + +* `name` - (Optional) The guid of this Sentinel Alert Rule Template. Either `display_name` or `name` have to be specified. + +* `display_name` - (Optional) The display name of this Sentinel Alert Rule Template. Either `display_name` or `name` have to be specified. + +~> **NOTE** One of `name` or `display_name` must be specified. + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the Anomaly Alert Rule. + +* `anomaly_settings_version` - The anomaly settings version of the Anomaly security ml analytics settings that dictates whether job version gets updated or not. + +* `anomaly_version` - The anomaly version of the Anomaly Alert Rule. + +* `customizable_observations` - The customizable observations of the Anomaly Alert Rule. + +* `description` - The description of the Anomaly Alert Rule. + +* `enabled` - Is the Anomaly Alert Rule enabled? + +* `frequency` - The frequency the Anomaly Alert Rule will be run. + +* `is_default_settings` - Whether this Anomaly Alert Rule is a default settings. + +* `required_data_connector` - A `required_data_connector` block as defined below. + +* `settings_definition_id` - The ID of the anomaly settings definition Id. + +* `Mode` - The Mode of the Anomaly Alert Rule. + +* `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. + +* `multi_select_observation` - A list of `multi_select_observation` blocks as defined below. + +* `single_select_observation` - A list of `single_select_observation` blocks as defined below. + +* `prioritized_exclude_observation` - A list of `prioritized_exclude_observation` blocks as defined below. + +* `threshold_observation` - A list of `threshold_observation` blocks as defined below. + +--- + +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. + +--- + +A `multi_select_observation` block exports the following: + +* `name` - 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` - A list of values of the single select observation. + +--- + +A `single_select_observation` block exports the following: + +* `name` - 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` - The value of the multi select observation. + +--- + +A `prioritized_exclude_observation` block exports the following: + +* `name` - The name of the prioritized exclude observation. + +* `description` - The description of the prioritized exclude observation. + +* `prioritize` - The prioritized value per `description`. + +* `exclude` - The excluded value per `description`. + +--- + +A `threshold_observation` block exports the following: + +* `name` - 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` - The value of the threshold observation. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: + +* `read` - (Defaults to 5 minutes) Used when retrieving the Anomaly Alert Rule. diff --git a/website/docs/r/sentinel_alert_rule_anomaly_built_in.html.markdown b/website/docs/r/sentinel_alert_rule_anomaly_built_in.html.markdown new file mode 100644 index 000000000000..df437cd9f911 --- /dev/null +++ b/website/docs/r/sentinel_alert_rule_anomaly_built_in.html.markdown @@ -0,0 +1,175 @@ +--- +subcategory: "Sentinel" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_sentinel_alert_rule_anomaly_built_in" +description: |- + Manages a Built-in Anomaly Alert Rule. +--- +## Disclaimers + +~> **Note:** A Built-in Anomaly Alert Rule could not be deleted. delete a Terraform managed Built-in Anomaly Alert Rule will cause the Built-in Anomaly Alert Rule to be disabled. + +# azurerm_sentinel_alert_rule_anomaly_built_in + +Manages a Built-in 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_built_in" "example" { + display_name = "UEBA Anomalous Sign In" + log_analytics_workspace_id = azurerm_log_analytics_workspace.example.id + mode = "Production" + enabled = false +} +``` + +## Arguments Reference + +The following arguments are supported: + +* `name` - (Optional) The Name of the built-in Anomaly Alert Rule. Changing this forces a new Built-in Anomaly Alert Rule to be created. + +* `display_name` - (Optional) The Display Name of the built-in Anomaly Alert Rule. Changing this forces a new Built-in Anomaly Alert Rule to be created. + +~> **Note:** One of `name` or `display_name` block must be specified. + +* `log_analytics_workspace_id` - (Required) The ID of the Log Analytics Workspace. Changing this forces a new Built-in Anomaly Alert Rule to be created. + +* `enabled` - (Required) Should the Built-in Anomaly Alert Rule be enabled? + +* `mode` - (Required) mode of the Built-in Anomaly Alert Rule. Possible Values are `Production` and `Flighting`. + +## 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 anomaly settings version of the Anomaly security ml analytics settings that dictates whether job version gets updated or not. + +* `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. + +* `is_default_settings` - Whether this Anomaly Alert Rule is a 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. + +* `customizable_observations` - The customizable observations of the Built-in Anomaly Alert Rule. + +* `multi_select_observation` - A list of `multi_select_observation` blocks as defined below. + +* `single_select_observation` - A list of `single_select_observation` blocks as defined below. + +* `prioritized_exclude_observation` - A list of `prioritized_exclude_observation` blocks as defined below. + +* `threshold_observation` - A list of `threshold_observation` blocks as defined below. + +--- + +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. + + +--- + +A `multi_select_observation` block exports the following: + +* `name` - 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` - A list of values of the single select observation. + +--- + +A `single_select_observation` block exports the following: + +* `name` - 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` - The value of the multi select observation. + +--- + +A `prioritized_exclude_observation` block exports the following: + +* `name` - The name of the prioritized exclude observation. + +* `description` - The description of the prioritized exclude observation. + +* `prioritize` - The prioritized value per `description`. + +* `exclude` - The excluded value per `description`. + +--- + +A `threshold_observation` block exports the following: + +* `name` - 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` - The value of the threshold observation. + +## 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 +```