diff --git a/internal/services/sentinel/azuresdkhacks/threat_intelligence_indicator_models.go b/internal/services/sentinel/azuresdkhacks/threat_intelligence_indicator_models.go new file mode 100644 index 000000000000..8ea523f7b6de --- /dev/null +++ b/internal/services/sentinel/azuresdkhacks/threat_intelligence_indicator_models.go @@ -0,0 +1,400 @@ +package azuresdkhacks + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/Azure/go-autorest/autorest" + securityinsight "github.com/tombuildsstuff/kermit/sdk/securityinsights/2022-10-01-preview/securityinsights" +) + +// TODO 4.0: check if this could be removed. +// workaround for https://github.com/Azure/azure-rest-api-specs/issues/22893 + +type ThreatIntelligenceInformationModel struct { + autorest.Response `json:"-"` + Value BasicThreatIntelligenceInformation `json:"value,omitempty"` +} + +func (tiim *ThreatIntelligenceInformationModel) UnmarshalJSON(body []byte) error { + tii, err := unmarshalBasicThreatIntelligenceInformation(body) + if err != nil { + return err + } + tiim.Value = tii + + return nil +} + +func unmarshalBasicThreatIntelligenceInformation(body []byte) (BasicThreatIntelligenceInformation, error) { + var m map[string]interface{} + err := json.Unmarshal(body, &m) + if err != nil { + return nil, err + } + + switch m["kind"] { + case string(securityinsight.KindBasicThreatIntelligenceInformationKindIndicator): + var tiim ThreatIntelligenceIndicatorModel + err := json.Unmarshal(body, &tiim) + return tiim, err + default: + // it's not used in this hack. + return nil, fmt.Errorf("get unknown kind: %s", m["kind"]) + } +} + +type BasicThreatIntelligenceInformation interface { + AsThreatIntelligenceIndicatorModel() (*ThreatIntelligenceIndicatorModel, bool) + AsThreatIntelligenceInformation() (*securityinsight.ThreatIntelligenceInformation, bool) +} + +type ThreatIntelligenceIndicatorModel struct { + *ThreatIntelligenceIndicatorProperties `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.KindBasicThreatIntelligenceInformation `json:"kind,omitempty"` +} + +func (tiim ThreatIntelligenceIndicatorModel) MarshalJSON() ([]byte, error) { + tiim.Kind = securityinsight.KindBasicThreatIntelligenceInformationKindIndicator + objectMap := make(map[string]interface{}) + if tiim.ThreatIntelligenceIndicatorProperties != nil { + objectMap["properties"] = tiim.ThreatIntelligenceIndicatorProperties + } + if tiim.Kind != "" { + objectMap["kind"] = tiim.Kind + } + if tiim.Etag != nil { + objectMap["etag"] = tiim.Etag + } + return json.Marshal(objectMap) +} + +func (tiim ThreatIntelligenceIndicatorModel) AsThreatIntelligenceIndicatorModel() (*ThreatIntelligenceIndicatorModel, bool) { + return &tiim, true +} + +func (tiim ThreatIntelligenceIndicatorModel) AsThreatIntelligenceInformation() (*securityinsight.ThreatIntelligenceInformation, bool) { + return nil, false +} + +func (tiim *ThreatIntelligenceIndicatorModel) 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 "properties": + if v != nil { + var threatIntelligenceIndicatorProperties ThreatIntelligenceIndicatorProperties + err = json.Unmarshal(*v, &threatIntelligenceIndicatorProperties) + if err != nil { + return err + } + tiim.ThreatIntelligenceIndicatorProperties = &threatIntelligenceIndicatorProperties + } + case "kind": + if v != nil { + var kind securityinsight.KindBasicThreatIntelligenceInformation + err = json.Unmarshal(*v, &kind) + if err != nil { + return err + } + tiim.Kind = kind + } + case "etag": + if v != nil { + var etag string + err = json.Unmarshal(*v, &etag) + if err != nil { + return err + } + tiim.Etag = &etag + } + case "id": + if v != nil { + var ID string + err = json.Unmarshal(*v, &ID) + if err != nil { + return err + } + tiim.ID = &ID + } + case "name": + if v != nil { + var name string + err = json.Unmarshal(*v, &name) + if err != nil { + return err + } + tiim.Name = &name + } + case "type": + if v != nil { + var typeVar string + err = json.Unmarshal(*v, &typeVar) + if err != nil { + return err + } + tiim.Type = &typeVar + } + case "systemData": + if v != nil { + var systemData securityinsight.SystemData + err = json.Unmarshal(*v, &systemData) + if err != nil { + return err + } + tiim.SystemData = &systemData + } + } + } + + return nil +} + +type ThreatIntelligenceIndicatorProperties struct { + ThreatIntelligenceTags *[]string `json:"threatIntelligenceTags,omitempty"` + LastUpdatedTimeUtc *string `json:"lastUpdatedTimeUtc,omitempty"` + Source *string `json:"source,omitempty"` + DisplayName *string `json:"displayName,omitempty"` + Description *string `json:"description,omitempty"` + IndicatorTypes *[]string `json:"indicatorTypes,omitempty"` + Pattern *string `json:"pattern,omitempty"` + PatternType *string `json:"patternType,omitempty"` + PatternVersion *string `json:"patternVersion,omitempty"` + KillChainPhases *[]securityinsight.ThreatIntelligenceKillChainPhase `json:"killChainPhases,omitempty"` + ParsedPattern *[]securityinsight.ThreatIntelligenceParsedPattern `json:"parsedPattern,omitempty"` + ExternalID *string `json:"externalId,omitempty"` + CreatedByRef *string `json:"createdByRef,omitempty"` + Defanged *bool `json:"defanged,omitempty"` + ExternalLastUpdatedTimeUtc *string `json:"externalLastUpdatedTimeUtc,omitempty"` + ExternalReferences *[]securityinsight.ThreatIntelligenceExternalReference `json:"externalReferences,omitempty"` + GranularMarkings *[]ThreatIntelligenceGranularMarkingModel `json:"granularMarkings,omitempty"` + Labels *[]string `json:"labels,omitempty"` + Revoked *bool `json:"revoked,omitempty"` + Confidence *int32 `json:"confidence,omitempty"` + ObjectMarkingRefs *[]string `json:"objectMarkingRefs,omitempty"` + Language *string `json:"language,omitempty"` + ThreatTypes *[]string `json:"threatTypes,omitempty"` + ValidFrom *string `json:"validFrom,omitempty"` + ValidUntil *string `json:"validUntil,omitempty"` + Created *string `json:"created,omitempty"` + Modified *string `json:"modified,omitempty"` + Extensions map[string]interface{} `json:"extensions"` + AdditionalData map[string]interface{} `json:"additionalData"` + FriendlyName *string `json:"friendlyName,omitempty"` +} + +func (tiip ThreatIntelligenceIndicatorProperties) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]interface{}) + if tiip.ThreatIntelligenceTags != nil { + objectMap["threatIntelligenceTags"] = tiip.ThreatIntelligenceTags + } + if tiip.LastUpdatedTimeUtc != nil { + objectMap["lastUpdatedTimeUtc"] = tiip.LastUpdatedTimeUtc + } + if tiip.Source != nil { + objectMap["source"] = tiip.Source + } + if tiip.DisplayName != nil { + objectMap["displayName"] = tiip.DisplayName + } + if tiip.Description != nil { + objectMap["description"] = tiip.Description + } + if tiip.IndicatorTypes != nil { + objectMap["indicatorTypes"] = tiip.IndicatorTypes + } + if tiip.Pattern != nil { + objectMap["pattern"] = tiip.Pattern + } + if tiip.PatternType != nil { + objectMap["patternType"] = tiip.PatternType + } + if tiip.PatternVersion != nil { + objectMap["patternVersion"] = tiip.PatternVersion + } + if tiip.KillChainPhases != nil { + objectMap["killChainPhases"] = tiip.KillChainPhases + } + if tiip.ParsedPattern != nil { + objectMap["parsedPattern"] = tiip.ParsedPattern + } + if tiip.ExternalID != nil { + objectMap["externalId"] = tiip.ExternalID + } + if tiip.CreatedByRef != nil { + objectMap["createdByRef"] = tiip.CreatedByRef + } + if tiip.Defanged != nil { + objectMap["defanged"] = tiip.Defanged + } + if tiip.ExternalLastUpdatedTimeUtc != nil { + objectMap["externalLastUpdatedTimeUtc"] = tiip.ExternalLastUpdatedTimeUtc + } + if tiip.ExternalReferences != nil { + objectMap["externalReferences"] = tiip.ExternalReferences + } + if tiip.GranularMarkings != nil { + objectMap["granularMarkings"] = tiip.GranularMarkings + } + if tiip.Labels != nil { + objectMap["labels"] = tiip.Labels + } + if tiip.Revoked != nil { + objectMap["revoked"] = tiip.Revoked + } + if tiip.Confidence != nil { + objectMap["confidence"] = tiip.Confidence + } + if tiip.ObjectMarkingRefs != nil { + objectMap["objectMarkingRefs"] = tiip.ObjectMarkingRefs + } + if tiip.Language != nil { + objectMap["language"] = tiip.Language + } + if tiip.ThreatTypes != nil { + objectMap["threatTypes"] = tiip.ThreatTypes + } + if tiip.ValidFrom != nil { + objectMap["validFrom"] = tiip.ValidFrom + } + if tiip.ValidUntil != nil { + objectMap["validUntil"] = tiip.ValidUntil + } + if tiip.Created != nil { + objectMap["created"] = tiip.Created + } + if tiip.Modified != nil { + objectMap["modified"] = tiip.Modified + } + if tiip.Extensions != nil { + objectMap["extensions"] = tiip.Extensions + } + return json.Marshal(objectMap) +} + +type ThreatIntelligenceGranularMarkingModel struct { + Language *string `json:"language,omitempty"` + MarkingRef *string `json:"markingRef,omitempty"` + Selectors *[]string `json:"selectors,omitempty"` +} + +type ThreatIntelligenceInformationListPage struct { + fn func(context.Context, ThreatIntelligenceInformationList) (ThreatIntelligenceInformationList, error) + tiil ThreatIntelligenceInformationList +} + +type ThreatIntelligenceInformationList struct { + autorest.Response `json:"-"` + NextLink *string `json:"nextLink,omitempty"` + Value *[]BasicThreatIntelligenceInformation `json:"value,omitempty"` +} + +func (tiil ThreatIntelligenceInformationList) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]interface{}) + if tiil.Value != nil { + objectMap["value"] = tiil.Value + } + return json.Marshal(objectMap) +} + +func (tiil *ThreatIntelligenceInformationList) 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 + } + tiil.NextLink = &nextLink + } + case "value": + if v != nil { + value, err := unmarshalBasicThreatIntelligenceInformationArray(*v) + if err != nil { + return err + } + tiil.Value = &value + } + } + } + + return nil +} + +func unmarshalBasicThreatIntelligenceInformationArray(body []byte) ([]BasicThreatIntelligenceInformation, error) { + var rawMessages []*json.RawMessage + err := json.Unmarshal(body, &rawMessages) + if err != nil { + return nil, err + } + + tiiArray := make([]BasicThreatIntelligenceInformation, len(rawMessages)) + + for index, rawMessage := range rawMessages { + tii, err := unmarshalBasicThreatIntelligenceInformation(*rawMessage) + if err != nil { + return nil, err + } + tiiArray[index] = tii + } + return tiiArray, nil +} + +func (page *ThreatIntelligenceInformationListPage) NextWithContext(ctx context.Context) (err error) { + for { + next, err := page.fn(ctx, page.tiil) + if err != nil { + return err + } + page.tiil = next + if !next.hasNextLink() || !next.IsEmpty() { + break + } + } + return nil +} + +func (tiil ThreatIntelligenceInformationList) IsEmpty() bool { + return tiil.Value == nil || len(*tiil.Value) == 0 +} + +func (tiil ThreatIntelligenceInformationList) hasNextLink() bool { + return tiil.NextLink != nil && len(*tiil.NextLink) != 0 +} + +func (page *ThreatIntelligenceInformationListPage) Next() error { + return page.NextWithContext(context.Background()) +} + +func (page ThreatIntelligenceInformationListPage) NotDone() bool { + return !page.tiil.IsEmpty() +} + +func (page ThreatIntelligenceInformationListPage) Response() ThreatIntelligenceInformationList { + return page.tiil +} + +// Values returns the slice of values for the current page or nil if there are no values. +func (page ThreatIntelligenceInformationListPage) Values() []BasicThreatIntelligenceInformation { + if page.tiil.IsEmpty() { + return nil + } + return *page.tiil.Value +} diff --git a/internal/services/sentinel/azuresdkhacks/threatintelligenceindicator.go b/internal/services/sentinel/azuresdkhacks/threatintelligenceindicator.go new file mode 100644 index 000000000000..b75e2bfa13da --- /dev/null +++ b/internal/services/sentinel/azuresdkhacks/threatintelligenceindicator.go @@ -0,0 +1,344 @@ +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/to" + "github.com/Azure/go-autorest/autorest/validation" + securityinsight "github.com/tombuildsstuff/kermit/sdk/securityinsights/2022-10-01-preview/securityinsights" +) + +type ThreatIntelligenceIndicatorClient struct { + securityinsight.BaseClient +} + +func (client ThreatIntelligenceIndicatorClient) Get(ctx context.Context, resourceGroupName string, workspaceName string, name string) (result ThreatIntelligenceInformationModel, 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}, + {Target: "workspaceName", Name: validation.Pattern, Rule: `^[A-Za-z0-9][A-Za-z0-9-]+[A-Za-z0-9]$`, Chain: nil}}}}); err != nil { + return result, validation.NewError("securityinsight.ThreatIntelligenceIndicatorClient", "Get", err.Error()) + } + + req, err := client.GetPreparer(ctx, resourceGroupName, workspaceName, name) + if err != nil { + err = autorest.NewErrorWithError(err, "securityinsight.ThreatIntelligenceIndicatorClient", "Get", nil, "Failure preparing request") + return + } + + resp, err := client.GetSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "securityinsight.ThreatIntelligenceIndicatorClient", "Get", resp, "Failure sending request") + return + } + + result, err = client.GetResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "securityinsight.ThreatIntelligenceIndicatorClient", "Get", resp, "Failure responding to request") + return + } + + return +} + +func (client ThreatIntelligenceIndicatorClient) GetPreparer(ctx context.Context, resourceGroupName string, workspaceName string, name string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "name": autorest.Encode("path", name), + "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/threatIntelligence/main/indicators/{name}", pathParameters), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +func (client ThreatIntelligenceIndicatorClient) GetSender(req *http.Request) (*http.Response, error) { + return client.Send(req, azure.DoRetryWithRegistration(client.Client)) +} + +func (client ThreatIntelligenceIndicatorClient) GetResponder(resp *http.Response) (result ThreatIntelligenceInformationModel, err error) { + err = autorest.Respond( + resp, + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingJSON(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + return +} + +func (client ThreatIntelligenceIndicatorClient) CreateIndicator(ctx context.Context, resourceGroupName string, workspaceName string, threatIntelligenceProperties ThreatIntelligenceIndicatorModel) (result ThreatIntelligenceInformationModel, 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}, + {Target: "workspaceName", Name: validation.Pattern, Rule: `^[A-Za-z0-9][A-Za-z0-9-]+[A-Za-z0-9]$`, Chain: nil}}}}); err != nil { + return result, validation.NewError("securityinsight.ThreatIntelligenceIndicatorClient", "CreateIndicator", err.Error()) + } + + req, err := client.CreateIndicatorPreparer(ctx, resourceGroupName, workspaceName, threatIntelligenceProperties) + if err != nil { + err = autorest.NewErrorWithError(err, "securityinsight.ThreatIntelligenceIndicatorClient", "CreateIndicator", nil, "Failure preparing request") + return + } + + resp, err := client.CreateIndicatorSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "securityinsight.ThreatIntelligenceIndicatorClient", "CreateIndicator", resp, "Failure sending request") + return + } + + result, err = client.CreateIndicatorResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "securityinsight.ThreatIntelligenceIndicatorClient", "CreateIndicator", resp, "Failure responding to request") + return + } + + return +} + +// CreateIndicatorPreparer prepares the CreateIndicator request. +func (client ThreatIntelligenceIndicatorClient) CreateIndicatorPreparer(ctx context.Context, resourceGroupName string, workspaceName string, threatIntelligenceProperties ThreatIntelligenceIndicatorModel) (*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.AsContentType("application/json; charset=utf-8"), + autorest.AsPost(), + autorest.WithBaseURL(client.BaseURI), + autorest.WithPathParameters("/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}/providers/Microsoft.SecurityInsights/threatIntelligence/main/createIndicator", pathParameters), + autorest.WithJSON(threatIntelligenceProperties), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +func (client ThreatIntelligenceIndicatorClient) CreateIndicatorSender(req *http.Request) (*http.Response, error) { + return client.Send(req, azure.DoRetryWithRegistration(client.Client)) +} + +func (client ThreatIntelligenceIndicatorClient) CreateIndicatorResponder(resp *http.Response) (result ThreatIntelligenceInformationModel, err error) { + err = autorest.Respond( + resp, + azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusCreated), + autorest.ByUnmarshallingJSON(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + return +} + +func (client ThreatIntelligenceIndicatorClient) QueryIndicators(ctx context.Context, resourceGroupName string, workspaceName string, threatIntelligenceFilteringCriteria securityinsight.ThreatIntelligenceFilteringCriteria) (result ThreatIntelligenceInformationListPage, 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}, + {Target: "workspaceName", Name: validation.Pattern, Rule: `^[A-Za-z0-9][A-Za-z0-9-]+[A-Za-z0-9]$`, Chain: nil}}}}); err != nil { + return result, validation.NewError("securityinsight.ThreatIntelligenceIndicatorClient", "QueryIndicators", err.Error()) + } + + result.fn = client.queryIndicatorsNextResults + req, err := client.QueryIndicatorsPreparer(ctx, resourceGroupName, workspaceName, threatIntelligenceFilteringCriteria) + if err != nil { + err = autorest.NewErrorWithError(err, "securityinsight.ThreatIntelligenceIndicatorClient", "QueryIndicators", nil, "Failure preparing request") + return + } + + resp, err := client.QueryIndicatorsSender(req) + if err != nil { + result.tiil.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "securityinsight.ThreatIntelligenceIndicatorClient", "QueryIndicators", resp, "Failure sending request") + return + } + + result.tiil, err = client.QueryIndicatorsResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "securityinsight.ThreatIntelligenceIndicatorClient", "QueryIndicators", resp, "Failure responding to request") + return + } + if result.tiil.hasNextLink() && result.tiil.IsEmpty() { + err = result.NextWithContext(ctx) + return + } + + return +} + +func (client ThreatIntelligenceIndicatorClient) queryIndicatorsNextResults(ctx context.Context, lastResults ThreatIntelligenceInformationList) (result ThreatIntelligenceInformationList, err error) { + req, err := lastResults.threatIntelligenceInformationListPreparer(ctx) + if err != nil { + return result, autorest.NewErrorWithError(err, "securityinsight.ThreatIntelligenceIndicatorClient", "queryIndicatorsNextResults", nil, "Failure preparing next results request") + } + if req == nil { + return + } + resp, err := client.QueryIndicatorsSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + return result, autorest.NewErrorWithError(err, "securityinsight.ThreatIntelligenceIndicatorClient", "queryIndicatorsNextResults", resp, "Failure sending next results request") + } + result, err = client.QueryIndicatorsResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "securityinsight.ThreatIntelligenceIndicatorClient", "queryIndicatorsNextResults", resp, "Failure responding to next results request") + } + return +} + +func (tiil ThreatIntelligenceInformationList) threatIntelligenceInformationListPreparer(ctx context.Context) (*http.Request, error) { + if !tiil.hasNextLink() { + return nil, nil + } + return autorest.Prepare((&http.Request{}).WithContext(ctx), + autorest.AsJSON(), + autorest.AsGet(), + autorest.WithBaseURL(to.String(tiil.NextLink))) +} + +func (client ThreatIntelligenceIndicatorClient) QueryIndicatorsPreparer(ctx context.Context, resourceGroupName string, workspaceName string, threatIntelligenceFilteringCriteria securityinsight.ThreatIntelligenceFilteringCriteria) (*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.AsContentType("application/json; charset=utf-8"), + autorest.AsPost(), + autorest.WithBaseURL(client.BaseURI), + autorest.WithPathParameters("/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}/providers/Microsoft.SecurityInsights/threatIntelligence/main/queryIndicators", pathParameters), + autorest.WithJSON(threatIntelligenceFilteringCriteria), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// QueryIndicatorsSender sends the QueryIndicators request. The method will close the +// http.Response Body if it receives an error. +func (client ThreatIntelligenceIndicatorClient) QueryIndicatorsSender(req *http.Request) (*http.Response, error) { + return client.Send(req, azure.DoRetryWithRegistration(client.Client)) +} + +// QueryIndicatorsResponder handles the response to the QueryIndicators request. The method always +// closes the http.Response Body. +func (client ThreatIntelligenceIndicatorClient) QueryIndicatorsResponder(resp *http.Response) (result ThreatIntelligenceInformationList, err error) { + err = autorest.Respond( + resp, + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingJSON(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + return +} + +func (client ThreatIntelligenceIndicatorClient) Create(ctx context.Context, resourceGroupName string, workspaceName string, name string, threatIntelligenceProperties ThreatIntelligenceIndicatorModel) (result ThreatIntelligenceInformationModel, 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}, + {Target: "workspaceName", Name: validation.Pattern, Rule: `^[A-Za-z0-9][A-Za-z0-9-]+[A-Za-z0-9]$`, Chain: nil}}}}); err != nil { + return result, validation.NewError("securityinsight.ThreatIntelligenceIndicatorClient", "Create", err.Error()) + } + + req, err := client.CreatePreparer(ctx, resourceGroupName, workspaceName, name, threatIntelligenceProperties) + if err != nil { + err = autorest.NewErrorWithError(err, "securityinsight.ThreatIntelligenceIndicatorClient", "Create", nil, "Failure preparing request") + return + } + + resp, err := client.CreateSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "securityinsight.ThreatIntelligenceIndicatorClient", "Create", resp, "Failure sending request") + return + } + + result, err = client.CreateResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "securityinsight.ThreatIntelligenceIndicatorClient", "Create", resp, "Failure responding to request") + return + } + + return +} + +func (client ThreatIntelligenceIndicatorClient) CreatePreparer(ctx context.Context, resourceGroupName string, workspaceName string, name string, threatIntelligenceProperties ThreatIntelligenceIndicatorModel) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "name": autorest.Encode("path", name), + "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.AsContentType("application/json; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(client.BaseURI), + autorest.WithPathParameters("/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}/providers/Microsoft.SecurityInsights/threatIntelligence/main/indicators/{name}", pathParameters), + autorest.WithJSON(threatIntelligenceProperties), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +func (client ThreatIntelligenceIndicatorClient) CreateSender(req *http.Request) (*http.Response, error) { + return client.Send(req, azure.DoRetryWithRegistration(client.Client)) +} + +func (client ThreatIntelligenceIndicatorClient) CreateResponder(resp *http.Response) (result ThreatIntelligenceInformationModel, err error) { + err = autorest.Respond( + resp, + azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusCreated), + autorest.ByUnmarshallingJSON(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + return +} diff --git a/internal/services/sentinel/client/client.go b/internal/services/sentinel/client/client.go index 6a9bff4b560f..cf55c87ce48c 100644 --- a/internal/services/sentinel/client/client.go +++ b/internal/services/sentinel/client/client.go @@ -19,6 +19,7 @@ type Client struct { WatchlistItemsClient *securityinsight.WatchlistItemsClient OnboardingStatesClient *sentinelonboardingstates.SentinelOnboardingStatesClient AnalyticsSettingsClient *securityinsight.SecurityMLAnalyticsSettingsClient + ThreatIntelligenceClient *securityinsight.ThreatIntelligenceIndicatorClient MetadataClient *metadata.MetadataClient } @@ -47,6 +48,9 @@ func NewClient(o *common.ClientOptions) *Client { analyticsSettingsClient := securityinsight.NewSecurityMLAnalyticsSettingsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&analyticsSettingsClient.Client, o.ResourceManagerAuthorizer) + threatIntelligenceClient := securityinsight.NewThreatIntelligenceIndicatorClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) + o.ConfigureClient(&threatIntelligenceClient.Client, o.ResourceManagerAuthorizer) + metadataClient := metadata.NewMetadataClientWithBaseURI(o.ResourceManagerEndpoint) o.ConfigureClient(&metadataClient.Client, o.ResourceManagerAuthorizer) @@ -59,6 +63,7 @@ func NewClient(o *common.ClientOptions) *Client { WatchlistItemsClient: &watchListItemsClient, OnboardingStatesClient: &onboardingStatesClient, AnalyticsSettingsClient: &analyticsSettingsClient, + ThreatIntelligenceClient: &threatIntelligenceClient, MetadataClient: &metadataClient, } } diff --git a/internal/services/sentinel/parse/threat_intelligence_indicator.go b/internal/services/sentinel/parse/threat_intelligence_indicator.go new file mode 100644 index 000000000000..9984336bc70d --- /dev/null +++ b/internal/services/sentinel/parse/threat_intelligence_indicator.go @@ -0,0 +1,81 @@ +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 ThreatIntelligenceIndicatorId struct { + SubscriptionId string + ResourceGroup string + WorkspaceName string + ThreatIntelligenceName string + IndicatorName string +} + +func NewThreatIntelligenceIndicatorID(subscriptionId, resourceGroup, workspaceName, threatIntelligenceName, indicatorName string) ThreatIntelligenceIndicatorId { + return ThreatIntelligenceIndicatorId{ + SubscriptionId: subscriptionId, + ResourceGroup: resourceGroup, + WorkspaceName: workspaceName, + ThreatIntelligenceName: threatIntelligenceName, + IndicatorName: indicatorName, + } +} + +func (id ThreatIntelligenceIndicatorId) String() string { + segments := []string{ + fmt.Sprintf("Indicator Name %q", id.IndicatorName), + fmt.Sprintf("Threat Intelligence Name %q", id.ThreatIntelligenceName), + fmt.Sprintf("Workspace Name %q", id.WorkspaceName), + fmt.Sprintf("Resource Group %q", id.ResourceGroup), + } + segmentsStr := strings.Join(segments, " / ") + return fmt.Sprintf("%s: (%s)", "Threat Intelligence Indicator", segmentsStr) +} + +func (id ThreatIntelligenceIndicatorId) ID() string { + fmtString := "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.OperationalInsights/workspaces/%s/providers/Microsoft.SecurityInsights/threatIntelligence/%s/indicators/%s" + return fmt.Sprintf(fmtString, id.SubscriptionId, id.ResourceGroup, id.WorkspaceName, id.ThreatIntelligenceName, id.IndicatorName) +} + +// ThreatIntelligenceIndicatorID parses a ThreatIntelligenceIndicator ID into an ThreatIntelligenceIndicatorId struct +func ThreatIntelligenceIndicatorID(input string) (*ThreatIntelligenceIndicatorId, error) { + id, err := resourceids.ParseAzureResourceID(input) + if err != nil { + return nil, err + } + + resourceId := ThreatIntelligenceIndicatorId{ + 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.ThreatIntelligenceName, err = id.PopSegment("threatIntelligence"); err != nil { + return nil, err + } + if resourceId.IndicatorName, err = id.PopSegment("indicators"); 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/threat_intelligence_indicator_test.go b/internal/services/sentinel/parse/threat_intelligence_indicator_test.go new file mode 100644 index 000000000000..3e0282c9b48b --- /dev/null +++ b/internal/services/sentinel/parse/threat_intelligence_indicator_test.go @@ -0,0 +1,144 @@ +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 = ThreatIntelligenceIndicatorId{} + +func TestThreatIntelligenceIndicatorIDFormatter(t *testing.T) { + actual := NewThreatIntelligenceIndicatorID("12345678-1234-9876-4563-123456789012", "resGroup1", "workspace1", "main", "indicator1").ID() + expected := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/workspaces/workspace1/providers/Microsoft.SecurityInsights/threatIntelligence/main/indicators/indicator1" + if actual != expected { + t.Fatalf("Expected %q but got %q", expected, actual) + } +} + +func TestThreatIntelligenceIndicatorID(t *testing.T) { + testData := []struct { + Input string + Error bool + Expected *ThreatIntelligenceIndicatorId + }{ + + { + // 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 ThreatIntelligenceName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/workspaces/workspace1/providers/Microsoft.SecurityInsights/", + Error: true, + }, + + { + // missing value for ThreatIntelligenceName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/workspaces/workspace1/providers/Microsoft.SecurityInsights/threatIntelligence/", + Error: true, + }, + + { + // missing IndicatorName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/workspaces/workspace1/providers/Microsoft.SecurityInsights/threatIntelligence/main/", + Error: true, + }, + + { + // missing value for IndicatorName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/workspaces/workspace1/providers/Microsoft.SecurityInsights/threatIntelligence/main/indicators/", + Error: true, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/workspaces/workspace1/providers/Microsoft.SecurityInsights/threatIntelligence/main/indicators/indicator1", + Expected: &ThreatIntelligenceIndicatorId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "resGroup1", + WorkspaceName: "workspace1", + ThreatIntelligenceName: "main", + IndicatorName: "indicator1", + }, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/12345678-1234-9876-4563-123456789012/RESOURCEGROUPS/RESGROUP1/PROVIDERS/MICROSOFT.OPERATIONALINSIGHTS/WORKSPACES/WORKSPACE1/PROVIDERS/MICROSOFT.SECURITYINSIGHTS/THREATINTELLIGENCE/MAIN/INDICATORS/INDICATOR1", + Error: true, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Input) + + actual, err := ThreatIntelligenceIndicatorID(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.ThreatIntelligenceName != v.Expected.ThreatIntelligenceName { + t.Fatalf("Expected %q but got %q for ThreatIntelligenceName", v.Expected.ThreatIntelligenceName, actual.ThreatIntelligenceName) + } + if actual.IndicatorName != v.Expected.IndicatorName { + t.Fatalf("Expected %q but got %q for IndicatorName", v.Expected.IndicatorName, actual.IndicatorName) + } + } +} diff --git a/internal/services/sentinel/registration.go b/internal/services/sentinel/registration.go index 75c86541178e..369a6f5ffb96 100644 --- a/internal/services/sentinel/registration.go +++ b/internal/services/sentinel/registration.go @@ -78,5 +78,6 @@ func (r Registration) Resources() []sdk.Resource { AlertRuleAnomalyBuiltInResource{}, MetadataResource{}, AlertRuleAnomalyDuplicateResource{}, + ThreatIntelligenceIndicator{}, } } diff --git a/internal/services/sentinel/resourceids.go b/internal/services/sentinel/resourceids.go index 914f50d08820..dc69ff0b82a6 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=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 +//go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=ThreatIntelligenceIndicator -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/workspaces/workspace1/providers/Microsoft.SecurityInsights/threatIntelligence/main/indicators/indicator1 diff --git a/internal/services/sentinel/sentinel_threat_intelligence_indicator_resource.go b/internal/services/sentinel/sentinel_threat_intelligence_indicator_resource.go new file mode 100644 index 000000000000..b3a0e5cdcbdd --- /dev/null +++ b/internal/services/sentinel/sentinel_threat_intelligence_indicator_resource.go @@ -0,0 +1,1037 @@ +package sentinel + +import ( + "context" + "fmt" + "regexp" + "time" + + "github.com/hashicorp/go-azure-sdk/resource-manager/operationalinsights/2020-08-01/workspaces" + "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" + "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 IndicatorPatternType string + +const ( + PatternTypeDomainName IndicatorPatternType = "domain-name" + PatternTypeFile IndicatorPatternType = "file" + PatternTypeIpV4Addr IndicatorPatternType = "ipv4-addr" + PatternTypeIpV6Addr IndicatorPatternType = "ipv6-addr" + PatternTypeUrl IndicatorPatternType = "url" +) + +const killChainName = "Lockheed Martin - Intrusion Kill Chain" + +type IndicatorModel struct { + Name string `tfschema:"guid"` + WorkspaceId string `tfschema:"workspace_id"` + Confidence int32 `tfschema:"confidence"` + CreatedByRef string `tfschema:"created_by"` + Description string `tfschema:"description"` + DisplayName string `tfschema:"display_name"` + Extensions string `tfschema:"extension"` + ExternalRefrence []externalReferenceModel `tfschema:"external_reference"` + GranularMarkings []granularMarkingModel `tfschema:"granular_marking"` + IndicatorTypes []string `tfschema:"indicator_type"` + KillChainPhases []killChainPhaseModel `tfschema:"kill_chain_phase"` + Labels []string `tfschema:"tags"` + Language string `tfschema:"language"` + ObjectMarking []string `tfschema:"object_marking_refs"` + ParsedPattern []parsedPatternModel `tfschema:"parsed_pattern"` + Pattern string `tfschema:"pattern"` + PatternType string `tfschema:"pattern_type"` + PatternVersion string `tfschema:"pattern_version"` + Revoked bool `tfschema:"revoked"` + Source string `tfschema:"source"` + ThreatTypes []string `tfschema:"threat_types"` + ValidFrom string `tfschema:"validate_from_utc"` + ValidUntil string `tfschema:"validate_until_utc"` + CreatedOn string `tfschema:"created_on"` + ExternalId string `tfschema:"external_id"` + ExternalLastUpdatedTimeUtc string `tfschema:"external_last_updated_time_utc"` + LastUpdatedTimeUtc string `tfschema:"last_updated_time_utc"` + Defanged bool `tfschema:"defanged"` +} + +type externalReferenceModel struct { + SourceName string `tfschema:"source_name"` + Url string `tfschema:"url"` + Hashes map[string]string `tfschema:"hashes"` + Description string `tfschema:"description"` + ExternalId string `tfschema:"id"` +} + +type granularMarkingModel struct { + MarkingRef string `tfschema:"marking_ref"` + Selectors []string `tfschema:"selectors"` + Language string `tfschema:"language"` +} +type killChainPhaseModel struct { + PhaseName string `tfschema:"name"` +} +type parsedPatternModel struct { + PatternTypeValues []patternTypeValuesModel `tfschema:"pattern_type_values"` + PatternTypeKey string `tfschema:"pattern_type_key"` +} + +type patternTypeValuesModel struct { + Value string `tfschema:"value"` + ValueType string `tfschema:"value_type"` +} + +type ThreatIntelligenceIndicator struct{} + +var _ sdk.ResourceWithUpdate = ThreatIntelligenceIndicator{} + +func (r ThreatIntelligenceIndicator) ResourceType() string { + return "azurerm_sentinel_threat_intelligence_indicator" +} + +func (r ThreatIntelligenceIndicator) ModelObject() interface{} { + return &IndicatorModel{} +} + +func (r ThreatIntelligenceIndicator) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return validate.ThreatIntelligenceIndicatorID +} + +func (r ThreatIntelligenceIndicator) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "workspace_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "confidence": { + Type: pluginsdk.TypeInt, + Optional: true, + ValidateFunc: validation.IntBetween(0, 100), + Default: -1, // set the default value to -1 to split `nil` and `0`. + }, + + "created_by": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "description": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "display_name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "extension": { + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringIsJSON, + DiffSuppressFunc: pluginsdk.SuppressJsonDiff, + }, + + "external_reference": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "description": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "id": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "hashes": { + Type: pluginsdk.TypeMap, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + + "source_name": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "url": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + }, + }, + + "granular_marking": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "language": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "marking_ref": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "selectors": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + }, + }, + }, + + "kill_chain_phase": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + }, + }, + + "tags": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + + "language": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "object_marking_refs": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + + "pattern": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "pattern_type": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + string(PatternTypeDomainName), + string(PatternTypeFile), + string(PatternTypeIpV4Addr), + string(PatternTypeIpV6Addr), + string(PatternTypeUrl), + }, false), + }, + + "pattern_version": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "revoked": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + }, + + "source": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "threat_types": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + + "validate_from_utc": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.IsRFC3339Time, + }, + + "validate_until_utc": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.IsRFC3339Time, + }, + } +} + +func (r ThreatIntelligenceIndicator) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "guid": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "created_on": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "defanged": { + Type: pluginsdk.TypeBool, + Computed: true, + }, + + "external_id": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "external_last_updated_time_utc": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "last_updated_time_utc": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "parsed_pattern": { + Type: pluginsdk.TypeList, + Computed: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "pattern_type_key": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "pattern_type_values": { + Type: pluginsdk.TypeList, + Computed: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "value": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "value_type": { + Type: pluginsdk.TypeString, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + + "indicator_type": { + Type: pluginsdk.TypeList, + Computed: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + } +} + +func (r ThreatIntelligenceIndicator) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + var model IndicatorModel + if err := metadata.Decode(&model); err != nil { + return fmt.Errorf("decoding: %+v", err) + } + + client := azuresdkhacks.ThreatIntelligenceIndicatorClient{ + BaseClient: metadata.Client.Sentinel.ThreatIntelligenceClient.BaseClient, + } + workspaceId, err := workspaces.ParseWorkspaceID(model.WorkspaceId) + if err != nil { + return fmt.Errorf("parsing Workspace id %s: %+v", model.WorkspaceId, err) + } + + patternValue, err := expandIndicatorPattern(model.PatternType, model.Pattern) + if err != nil { + return err + } + + // it could not get the indicator by name before it has been created, because the name is generated by service side. + // but we can not create duplicated indicator with same values, so list the values and find the existing one. + existingIndicators, err := queryIndicatorsList(ctx, client, workspaceId) + if err != nil { + return fmt.Errorf("listing indicators: %+v", err) + } + for _, indicator := range existingIndicators { + if indicator.PatternType != nil && *indicator.PatternType == model.PatternType { + if indicator.Pattern != nil && *indicator.Pattern == patternValue { + if indicator.ID != nil && *indicator.ID != "" { + return tf.ImportAsExistsError("azurerm_sentinel_threat_intelligence_indicator", *indicator.ID) + } + return fmt.Errorf("checking existing indicator: `id` is nil") + } + } + } + + properties := azuresdkhacks.ThreatIntelligenceIndicatorModel{ + Kind: securityinsight.KindBasicThreatIntelligenceInformationKindIndicator, + ThreatIntelligenceIndicatorProperties: &azuresdkhacks.ThreatIntelligenceIndicatorProperties{ + PatternType: &model.PatternType, + Revoked: &model.Revoked, + }, + } + + props := properties.ThreatIntelligenceIndicatorProperties + + props.Pattern = &patternValue + + if model.Confidence != -1 { + props.Confidence = utils.Int32(model.Confidence) + } + + if model.CreatedByRef != "" { + props.CreatedByRef = &model.CreatedByRef + } + + if model.Description != "" { + props.Description = &model.Description + } + + if model.DisplayName != "" { + props.DisplayName = &model.DisplayName + } + + if model.Extensions != "" { + extensionsValue, err := pluginsdk.ExpandJsonFromString(model.Extensions) + if err != nil { + return err + } + props.Extensions = extensionsValue + } + + props.ExternalReferences = expandThreatIntelligenceExternalReferenceModel(model.ExternalRefrence) + + props.GranularMarkings = expandThreatIntelligenceGranularMarkingModelModel(model.GranularMarkings) + + props.KillChainPhases = expandThreatIntelligenceKillChainPhaseModel(model.KillChainPhases) + + if model.Language != "" { + props.Language = &model.Language + } + + if model.PatternVersion != "" { + props.PatternVersion = &model.PatternVersion + } + + if model.Source != "" { + props.Source = &model.Source + } + + if len(model.ObjectMarking) > 0 { + props.ObjectMarkingRefs = &model.ObjectMarking + } + + if len(model.Labels) > 0 { + props.Labels = &model.Labels + } + + if len(model.ThreatTypes) > 0 { + props.ThreatTypes = &model.ThreatTypes + } + + if model.ValidFrom != "" { + gmtLoc, _ := time.LoadLocation("GMT") + t, err := time.Parse(time.RFC3339, model.ValidFrom) + if err != nil { + return err + } + validFromValue := t.In(gmtLoc).Format(time.RFC1123Z) + props.ValidFrom = &validFromValue + } + + if model.ValidUntil != "" { + gmtLoc, _ := time.LoadLocation("GMT") + t, err := time.Parse(time.RFC3339, model.ValidUntil) + if err != nil { + return err + } + validUntilValue := t.In(gmtLoc).Format(time.RFC1123Z) + props.ValidUntil = &validUntilValue + } + + resp, err := client.CreateIndicator(ctx, workspaceId.ResourceGroupName, workspaceId.WorkspaceName, properties) + if err != nil { + return fmt.Errorf("creating threaten intelligence indicator in workspace %s: %+v", workspaceId, err) + } + + info, ok := resp.Value.AsThreatIntelligenceIndicatorModel() + if !ok { + return fmt.Errorf("creating threaten intelligence indicator in workspace %s: `model` type mismatch", workspaceId) + } + + id, err := parse.ThreatIntelligenceIndicatorID(*info.ID) + if err != nil { + return fmt.Errorf("parsing threat intelligence indicator id %s: %+v", *info.ID, err) + } + + metadata.SetID(id) + return nil + }, + } +} + +func (r ThreatIntelligenceIndicator) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := azuresdkhacks.ThreatIntelligenceIndicatorClient{ + BaseClient: metadata.Client.Sentinel.ThreatIntelligenceClient.BaseClient, + } + + id, err := parse.ThreatIntelligenceIndicatorID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + var model IndicatorModel + if err := metadata.Decode(&model); err != nil { + return fmt.Errorf("decoding: %+v", err) + } + + resp, err := client.Get(ctx, id.ResourceGroup, id.WorkspaceName, id.IndicatorName) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", *id, err) + } + + properties, ok := resp.Value.AsThreatIntelligenceIndicatorModel() + if !ok { + return fmt.Errorf("retrieving %s: type mismatch", id) + } + + if metadata.ResourceData.HasChange("confidence") { + if model.Confidence == -1 { + properties.Confidence = nil + } else { + properties.Confidence = &model.Confidence + } + } + + if metadata.ResourceData.HasChange("created_by") { + properties.CreatedByRef = &model.CreatedByRef + } + + if metadata.ResourceData.HasChange("description") { + properties.Description = &model.Description + } + + if metadata.ResourceData.HasChange("display_name") { + properties.DisplayName = &model.DisplayName + } + + if metadata.ResourceData.HasChange("extension") { + extensionsValue, err := pluginsdk.ExpandJsonFromString(model.Extensions) + if err != nil { + return err + } + properties.Extensions = extensionsValue + } + + if metadata.ResourceData.HasChange("external_reference") { + properties.ExternalReferences = expandThreatIntelligenceExternalReferenceModel(model.ExternalRefrence) + + } + + if metadata.ResourceData.HasChange("granular_marking") { + properties.GranularMarkings = expandThreatIntelligenceGranularMarkingModelModel(model.GranularMarkings) + } + + if metadata.ResourceData.HasChange("indicator_type") { + properties.IndicatorTypes = &model.IndicatorTypes + } + + if metadata.ResourceData.HasChange("kill_chain_phase") { + properties.KillChainPhases = expandThreatIntelligenceKillChainPhaseModel(model.KillChainPhases) + } + + if metadata.ResourceData.HasChange("tags") { + properties.Labels = &model.Labels + } + + if metadata.ResourceData.HasChange("language") { + properties.Language = &model.Language + } + + if metadata.ResourceData.HasChange("object_marking_refs") { + properties.ObjectMarkingRefs = &model.ObjectMarking + } + + if metadata.ResourceData.HasChange("pattern") { + patternValue, err := expandIndicatorPattern(model.PatternType, model.Pattern) + if err != nil { + return err + } + properties.Pattern = &patternValue + } + + if metadata.ResourceData.HasChange("pattern_type") { + properties.PatternType = &model.PatternType + } + + if metadata.ResourceData.HasChange("pattern_version") { + properties.PatternVersion = &model.PatternVersion + } + + if metadata.ResourceData.HasChange("revoked") { + properties.Revoked = &model.Revoked + } + + if metadata.ResourceData.HasChange("source") { + properties.Source = &model.Source + } + + if metadata.ResourceData.HasChange("threat_types") { + properties.ThreatTypes = &model.ThreatTypes + } + + if metadata.ResourceData.HasChange("validate_from_utc") { + properties.ValidFrom = &model.ValidFrom + } + + if metadata.ResourceData.HasChange("validate_until_utc") { + properties.ValidUntil = &model.ValidUntil + } + + if _, err := client.Create(ctx, id.ResourceGroup, id.WorkspaceName, id.IndicatorName, *properties); err != nil { + return fmt.Errorf("updating %s: %+v", *id, err) + } + + return nil + }, + } +} + +func (r ThreatIntelligenceIndicator) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := azuresdkhacks.ThreatIntelligenceIndicatorClient{ + BaseClient: metadata.Client.Sentinel.ThreatIntelligenceClient.BaseClient, + } + id, err := parse.ThreatIntelligenceIndicatorID(metadata.ResourceData.Id()) + if err != nil { + return err + } + workspaceId := workspaces.WorkspaceId{ + SubscriptionId: id.SubscriptionId, + ResourceGroupName: id.ResourceGroup, + WorkspaceName: id.WorkspaceName, + } + + resp, err := client.Get(ctx, id.ResourceGroup, id.WorkspaceName, id.IndicatorName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return metadata.MarkAsGone(id) + } + + return fmt.Errorf("retrieving %s: %+v", *id, err) + } + + model, ok := resp.Value.AsThreatIntelligenceIndicatorModel() + if !ok { + return fmt.Errorf("retrieving %s: type mismatch", id) + } + + state := IndicatorModel{ + Name: *model.Name, + CreatedOn: *model.Created, + WorkspaceId: workspaceId.ID(), + PatternType: *model.PatternType, + Revoked: *model.Revoked, + } + + patternValue, err := flattenIndicatorPattern(*model.Pattern) + if err != nil { + return err + } + state.Pattern = patternValue + + if model.Confidence != nil { + state.Confidence = *model.Confidence + } else { + state.Confidence = -1 + } + + if model.CreatedByRef != nil { + state.CreatedByRef = *model.CreatedByRef + } + + if model.Description != nil { + state.Description = *model.Description + } + + if model.DisplayName != nil { + state.DisplayName = *model.DisplayName + } + + if model.Extensions != nil && len(model.Extensions) > 0 { + extensionsValue, err := pluginsdk.FlattenJsonToString(model.Extensions) + if err != nil { + return err + } + state.Extensions = extensionsValue + } + + state.ExternalRefrence = flattenThreatIntelligenceExternalReferenceModel(model.ExternalReferences) + + state.GranularMarkings = flattenThreatIntelligenceGranularMarkingModelModel(model.GranularMarkings) + + state.KillChainPhases = flattenThreatIntelligenceKillChainPhaseModel(model.KillChainPhases) + + if model.IndicatorTypes != nil && len(*model.IndicatorTypes) > 0 { + state.IndicatorTypes = *model.IndicatorTypes + } + + if model.Language != nil { + state.Language = *model.Language + } + + if model.PatternVersion != nil { + state.PatternVersion = *model.PatternVersion + } + + if model.Source != nil { + state.Source = *model.Source + } + + if model.ObjectMarkingRefs != nil && len(*model.ObjectMarkingRefs) > 0 { + state.ObjectMarking = *model.ObjectMarkingRefs + } + + if model.Labels != nil && len(*model.Labels) > 0 { + state.Labels = *model.Labels + } + + if model.ThreatTypes != nil && len(*model.ThreatTypes) > 0 { + state.ThreatTypes = *model.ThreatTypes + } + + if model.ValidFrom != nil && *model.ValidFrom != "" { + t, err := time.Parse(time.RFC3339, *model.ValidFrom) + if err != nil { + return err + } + state.ValidFrom = t.Format(time.RFC3339) + } + + if model.ValidUntil != nil && *model.ValidUntil != "" { + t, err := time.Parse(time.RFC3339, *model.ValidUntil) + if err != nil { + return err + } + state.ValidUntil = t.Format(time.RFC3339) + } + + if model.Defanged != nil { + state.Defanged = *model.Defanged + } + + if model.ExternalID != nil { + state.ExternalId = *model.ExternalID + } + + if model.ExternalLastUpdatedTimeUtc != nil { + state.ExternalLastUpdatedTimeUtc = *model.ExternalLastUpdatedTimeUtc + } + + if model.LastUpdatedTimeUtc != nil { + state.LastUpdatedTimeUtc = *model.LastUpdatedTimeUtc + } + + state.ParsedPattern = flattenIndicatorParsedPattern(model.ParsedPattern) + + return metadata.Encode(&state) + }, + } +} + +func (r ThreatIntelligenceIndicator) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Sentinel.ThreatIntelligenceClient + + id, err := parse.ThreatIntelligenceIndicatorID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + if _, err := client.Delete(ctx, id.ResourceGroup, id.WorkspaceName, id.IndicatorName); err != nil { + return fmt.Errorf("deleting %s: %+v", id, err) + } + + return nil + }, + } +} + +func expandThreatIntelligenceExternalReferenceModel(inputList []externalReferenceModel) *[]securityinsight.ThreatIntelligenceExternalReference { + var outputList []securityinsight.ThreatIntelligenceExternalReference + for _, v := range inputList { + input := v + hashesValue := make(map[string]*string, 0) + for k, hash := range input.Hashes { + hashesValue[k] = &hash + } + + output := securityinsight.ThreatIntelligenceExternalReference{ + Hashes: hashesValue, + } + + if input.Description != "" { + output.Description = &input.Description + } + + if input.ExternalId != "" { + output.ExternalID = &input.ExternalId + } + + if input.SourceName != "" { + output.SourceName = &input.SourceName + } + + if input.Url != "" { + output.URL = &input.Url + } + + outputList = append(outputList, output) + } + + return &outputList +} + +func flattenThreatIntelligenceExternalReferenceModel(input *[]securityinsight.ThreatIntelligenceExternalReference) []externalReferenceModel { + output := make([]externalReferenceModel, 0) + if input == nil { + return output + } + for _, v := range *input { + o := externalReferenceModel{} + if v.ExternalID != nil { + o.ExternalId = *v.ExternalID + } + if v.URL != nil { + o.Url = *v.URL + } + if v.SourceName != nil { + o.SourceName = *v.SourceName + } + if v.Description != nil { + o.Description = *v.Description + } + if len(v.Hashes) > 0 { + o.Hashes = make(map[string]string, 0) + for k, hash := range v.Hashes { + o.Hashes[k] = *hash + } + } + output = append(output, o) + } + return output +} + +func expandThreatIntelligenceGranularMarkingModelModel(inputList []granularMarkingModel) *[]azuresdkhacks.ThreatIntelligenceGranularMarkingModel { + var outputList []azuresdkhacks.ThreatIntelligenceGranularMarkingModel + for _, v := range inputList { + input := v + output := azuresdkhacks.ThreatIntelligenceGranularMarkingModel{ + MarkingRef: &input.MarkingRef, + Selectors: &input.Selectors, + } + + if input.Language != "" { + output.Language = &input.Language + } + + outputList = append(outputList, output) + } + + return &outputList +} + +func flattenThreatIntelligenceGranularMarkingModelModel(input *[]azuresdkhacks.ThreatIntelligenceGranularMarkingModel) []granularMarkingModel { + output := make([]granularMarkingModel, 0) + if input == nil { + return output + } + for _, v := range *input { + o := granularMarkingModel{} + if v.MarkingRef != nil { + o.MarkingRef = *v.MarkingRef + } + if v.Selectors != nil { + o.Selectors = *v.Selectors + } + if v.Language != nil { + o.Language = *v.Language + } + output = append(output, o) + } + return output +} + +func expandThreatIntelligenceKillChainPhaseModel(inputList []killChainPhaseModel) *[]securityinsight.ThreatIntelligenceKillChainPhase { + var outputList []securityinsight.ThreatIntelligenceKillChainPhase + for _, v := range inputList { + input := v + output := securityinsight.ThreatIntelligenceKillChainPhase{ + KillChainName: utils.String(killChainName), + } + + if input.PhaseName != "" { + output.PhaseName = &input.PhaseName + } + + outputList = append(outputList, output) + } + + return &outputList +} + +func flattenThreatIntelligenceKillChainPhaseModel(input *[]securityinsight.ThreatIntelligenceKillChainPhase) []killChainPhaseModel { + output := make([]killChainPhaseModel, 0) + if input == nil { + return output + } + for _, v := range *input { + o := killChainPhaseModel{} + if v.PhaseName != nil { + o.PhaseName = *v.PhaseName + } + output = append(output, o) + } + return output +} + +func expandIndicatorPattern(patternType string, pattern string) (string, error) { + // possible values get from Portal + // [domain-name:value = 'example.com'] + // [file:hashes.'MD5' = '6b0770e8133eee220333733931610598' ] + // although the Portal support multiple hash, the service only accept one, so we ignore this type. + // [file:hashes.'MD5' = '6b0770e8133eee220333733931610598' OR file:hashes.'MD5' = '6b0770e8133eee220333733931610591' ] + // [ipv4-addr:value = '1.1.1.1']" + // [ipv6-addr:value = '::1'] + // [url:value = 'http://www.example.com'] + if patternType == string(PatternTypeFile) { + reg := regexp.MustCompile(`(MD5|SHA-1|SHA-256|SHA-512|MD6|RIPEMD-160|SHA-224|SHA3-224|SHA3-256|SHA3-384|SHA3-512|SHA-384|SSDEEPWHIRLPOOL):`) + hashTypes := reg.FindStringSubmatch(pattern) + if len(hashTypes) != 2 { + return "", fmt.Errorf("when `pattern_type` set to `file`, `pattern` must combine a hash type with the hash code with a colon, an example is `MD5:78ecc5c05cd8b79af480df2f8fba0b9d`") + } + hashType := hashTypes[1] + return fmt.Sprintf(`[file:hashes.'%s' = '%s']`, hashType, pattern), nil + } + return fmt.Sprintf(`[%s:value = '%s']`, patternType, pattern), nil +} + +func flattenIndicatorPattern(input string) (string, error) { + reg := regexp.MustCompile(`\s=\s\'(.+)\'`) + result := reg.FindStringSubmatch(input) + if len(result) == 2 { + return result[1], nil + } + return "", fmt.Errorf("unable to parse pattern %s", input) +} + +func flattenIndicatorParsedPattern(input *[]securityinsight.ThreatIntelligenceParsedPattern) []parsedPatternModel { + output := make([]parsedPatternModel, 0) + if input == nil { + return output + } + for _, v := range *input { + o := parsedPatternModel{} + if v.PatternTypeKey != nil { + o.PatternTypeKey = *v.PatternTypeKey + } + if v.PatternTypeValues != nil { + for _, patternTypeValue := range *v.PatternTypeValues { + valueOutput := patternTypeValuesModel{} + if patternTypeValue.Value != nil { + valueOutput.Value = *patternTypeValue.Value + } + if patternTypeValue.ValueType != nil { + valueOutput.ValueType = *patternTypeValue.ValueType + } + o.PatternTypeValues = append(o.PatternTypeValues, valueOutput) + } + } + output = append(output, o) + } + return output +} + +func queryIndicatorsList(ctx context.Context, client azuresdkhacks.ThreatIntelligenceIndicatorClient, workspaceId *workspaces.WorkspaceId) ([]*azuresdkhacks.ThreatIntelligenceIndicatorModel, error) { + output := make([]*azuresdkhacks.ThreatIntelligenceIndicatorModel, 0) + resp, err := client.QueryIndicators(ctx, workspaceId.ResourceGroupName, workspaceId.WorkspaceName, securityinsight.ThreatIntelligenceFilteringCriteria{}) + if err != nil { + return output, err + } + for resp.NotDone() { + for _, indicator := range resp.Values() { + indicator, ok := indicator.AsThreatIntelligenceIndicatorModel() + if !ok { + continue + } + output = append(output, indicator) + } + if err := resp.NextWithContext(ctx); err != nil { + return output, err + } + } + return output, nil +} diff --git a/internal/services/sentinel/sentinel_threat_intelligence_indicator_resource_test.go b/internal/services/sentinel/sentinel_threat_intelligence_indicator_resource_test.go new file mode 100644 index 000000000000..d027609518fe --- /dev/null +++ b/internal/services/sentinel/sentinel_threat_intelligence_indicator_resource_test.go @@ -0,0 +1,269 @@ +package sentinel_test + +import ( + "context" + "fmt" + "testing" + + "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/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 SecurityInsightsIndicatorResource struct{} + +func TestAccSecurityInsightsIndicator_basicDomainName(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_sentinel_threat_intelligence_indicator", "test") + r := SecurityInsightsIndicatorResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data, "domain-name", "http://test.com"), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccSecurityInsightsIndicator_basicFile(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_sentinel_threat_intelligence_indicator", "test") + r := SecurityInsightsIndicatorResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data, "file", "MD5:78ecc5c05cd8b79af480df2f8fba0b9d"), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccSecurityInsightsIndicator_basicIpV4(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_sentinel_threat_intelligence_indicator", "test") + r := SecurityInsightsIndicatorResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data, "ipv4-addr", "1.1.1.1"), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccSecurityInsightsIndicator_basicIpV6(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_sentinel_threat_intelligence_indicator", "test") + r := SecurityInsightsIndicatorResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data, "ipv6-addr", "::1"), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccSecurityInsightsIndicator_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_sentinel_threat_intelligence_indicator", "test") + r := SecurityInsightsIndicatorResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data, "domain-name", "http://test.com"), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.RequiresImportErrorStep(r.requiresImport), + }) +} + +func TestAccSecurityInsightsIndicator_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_sentinel_threat_intelligence_indicator", "test") + r := SecurityInsightsIndicatorResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.complete(data, "domain-name", "http://test.com"), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccSecurityInsightsIndicator_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_sentinel_threat_intelligence_indicator", "test") + r := SecurityInsightsIndicatorResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.complete(data, "domain-name", "http://example.com"), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.update(data, "domain-name", "http://example.com"), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func (r SecurityInsightsIndicatorResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := parse.ThreatIntelligenceIndicatorID(state.ID) + if err != nil { + return nil, err + } + + client := azuresdkhacks.ThreatIntelligenceIndicatorClient{ + BaseClient: clients.Sentinel.ThreatIntelligenceClient.BaseClient, + } + resp, err := client.Get(ctx, id.ResourceGroup, id.WorkspaceName, id.IndicatorName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return utils.Bool(false), nil + } + return nil, fmt.Errorf("retrieving %s: %+v", id, err) + } + + return utils.Bool(resp.Value != nil), nil +} + +func (r SecurityInsightsIndicatorResource) template(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctest-rg-%[1]d" + location = "%[2]s" +} + +resource "azurerm_log_analytics_workspace" "test" { + name = "acctestLAW-%[1]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku = "PerGB2018" + retention_in_days = 30 +} + +resource "azurerm_sentinel_log_analytics_workspace_onboarding" "test" { + resource_group_name = azurerm_resource_group.test.name + workspace_name = azurerm_log_analytics_workspace.test.name +} +`, data.RandomInteger, data.Locations.Primary) +} + +func (r SecurityInsightsIndicatorResource) basic(data acceptance.TestData, patternType string, pattern string) string { + return fmt.Sprintf(` + %s + +resource "azurerm_sentinel_threat_intelligence_indicator" "test" { + workspace_id = azurerm_log_analytics_workspace.test.id + pattern_type = "%s" + pattern = "%s" + source = "Microsoft Sentinel" + validate_from_utc = "2022-12-14T16:00:00Z" + display_name = "test" + + depends_on = [azurerm_sentinel_log_analytics_workspace_onboarding.test] +} +`, r.template(data), patternType, pattern) +} + +func (r SecurityInsightsIndicatorResource) requiresImport(data acceptance.TestData) string { + config := r.basic(data, "domain-name", "http://test.com") + return fmt.Sprintf(` + %s + +resource "azurerm_sentinel_threat_intelligence_indicator" "import" { + workspace_id = azurerm_sentinel_threat_intelligence_indicator.test.workspace_id + pattern_type = azurerm_sentinel_threat_intelligence_indicator.test.pattern_type + pattern = azurerm_sentinel_threat_intelligence_indicator.test.pattern + source = azurerm_sentinel_threat_intelligence_indicator.test.source + validate_from_utc = azurerm_sentinel_threat_intelligence_indicator.test.validate_from_utc + display_name = azurerm_sentinel_threat_intelligence_indicator.test.display_name +} +`, config) +} + +func (r SecurityInsightsIndicatorResource) complete(data acceptance.TestData, patternType string, pattern string) string { + return fmt.Sprintf(` + %s + +resource "azurerm_sentinel_threat_intelligence_indicator" "test" { + workspace_id = azurerm_log_analytics_workspace.test.id + pattern_type = "%s" + pattern = "%s" + confidence = 5 + created_by = "testcraeted@microsoft.com" + description = "test indicator" + display_name = "test" + language = "en" + pattern_version = 1 + revoked = true + tags = ["test-tags"] + kill_chain_phase { + name = "testtest" + } + external_reference { + description = "test-external" + source_name = "test-sourcename" + } + granular_marking { + language = "en" + } + source = "test Sentinel" + validate_from_utc = "2022-12-14T16:00:00Z" + + depends_on = [azurerm_sentinel_log_analytics_workspace_onboarding.test] +} +`, r.template(data), patternType, pattern) +} + +func (r SecurityInsightsIndicatorResource) update(data acceptance.TestData, patternType string, pattern string) string { + return fmt.Sprintf(` + %s + +resource "azurerm_sentinel_threat_intelligence_indicator" "test" { + workspace_id = azurerm_log_analytics_workspace.test.id + pattern_type = "%s" + pattern = "%s" + confidence = 5 + created_by = "testcraeted@microsoft.com" + description = "updated indicator" + display_name = "updated" + language = "en" + pattern_version = 1 + revoked = true + tags = ["updated-tags"] + kill_chain_phase { + name = "testtest" + } + external_reference { + description = "test-external" + source_name = "test-sourcename" + } + granular_marking { + language = "en" + } + source = "updated Sentinel" + validate_from_utc = "2022-12-15T16:00:00Z" + + depends_on = [azurerm_sentinel_log_analytics_workspace_onboarding.test] +} +`, r.template(data), patternType, pattern) +} diff --git a/internal/services/sentinel/validate/threat_intelligence_indicator_id.go b/internal/services/sentinel/validate/threat_intelligence_indicator_id.go new file mode 100644 index 000000000000..ff4121d733ea --- /dev/null +++ b/internal/services/sentinel/validate/threat_intelligence_indicator_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 ThreatIntelligenceIndicatorID(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.ThreatIntelligenceIndicatorID(v); err != nil { + errors = append(errors, err) + } + + return +} diff --git a/internal/services/sentinel/validate/threat_intelligence_indicator_id_test.go b/internal/services/sentinel/validate/threat_intelligence_indicator_id_test.go new file mode 100644 index 000000000000..734f6bfcfb39 --- /dev/null +++ b/internal/services/sentinel/validate/threat_intelligence_indicator_id_test.go @@ -0,0 +1,100 @@ +package validate + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import "testing" + +func TestThreatIntelligenceIndicatorID(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 ThreatIntelligenceName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/workspaces/workspace1/providers/Microsoft.SecurityInsights/", + Valid: false, + }, + + { + // missing value for ThreatIntelligenceName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/workspaces/workspace1/providers/Microsoft.SecurityInsights/threatIntelligence/", + Valid: false, + }, + + { + // missing IndicatorName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/workspaces/workspace1/providers/Microsoft.SecurityInsights/threatIntelligence/main/", + Valid: false, + }, + + { + // missing value for IndicatorName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/workspaces/workspace1/providers/Microsoft.SecurityInsights/threatIntelligence/main/indicators/", + Valid: false, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.OperationalInsights/workspaces/workspace1/providers/Microsoft.SecurityInsights/threatIntelligence/main/indicators/indicator1", + Valid: true, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/12345678-1234-9876-4563-123456789012/RESOURCEGROUPS/RESGROUP1/PROVIDERS/MICROSOFT.OPERATIONALINSIGHTS/WORKSPACES/WORKSPACE1/PROVIDERS/MICROSOFT.SECURITYINSIGHTS/THREATINTELLIGENCE/MAIN/INDICATORS/INDICATOR1", + Valid: false, + }, + } + for _, tc := range cases { + t.Logf("[DEBUG] Testing Value %s", tc.Input) + _, errors := ThreatIntelligenceIndicatorID(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/r/sentinel_threat_intelligence_indicator.html.markdown b/website/docs/r/sentinel_threat_intelligence_indicator.html.markdown new file mode 100644 index 000000000000..4adcb1c7da42 --- /dev/null +++ b/website/docs/r/sentinel_threat_intelligence_indicator.html.markdown @@ -0,0 +1,179 @@ +--- +subcategory: "Sentinel" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_sentinel_threat_intelligence_indicator" +description: |- + Manages a Sentinel Threat Intelligence Indicator. +--- + +# azurerm_sentinel_threat_intelligence_indicator + +Manages a Sentinel Threat Intelligence Indicator. + +## Example Usage + +```hcl +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "example" { + name = "example-rg" + location = "east us" +} + +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" + retention_in_days = 30 +} + +resource "azurerm_sentinel_log_analytics_workspace_onboarding" "example" { + resource_group_name = azurerm_resource_group.example.name + workspace_name = azurerm_log_analytics_workspace.example.name +} + +resource "azurerm_sentinel_threat_intelligence_indicator" "example" { + workspace_id = azurerm_log_analytics_workspace.example.id + pattern_type = "domain-name" + pattern = "http://example.com" + source = "Microsoft Sentinel" + validate_from_utc = "2022-12-14T16:00:00Z" + display_name = "example-indicator" + + depends_on = [azurerm_sentinel_log_analytics_workspace_onboarding.test] +} +``` + +## Arguments Reference + +The following arguments are supported: + +* `display_name` - (Required) The display name of the Threat Intelligence Indicator. + +* `pattern_type` - (Required) The type of pattern used by the Threat Intelligence Indicator. Possible values are `domain-name`, `file`, `ipv4-addr`, `ipv6-addr` and `url`. + +* `pattern` - (Required) The pattern used by the Threat Intelligence Indicator. When `pattern_type` set to `file`, `pattern` must be specified with `:` format, such as `MD5:78ecc5c05cd8b79af480df2f8fba0b9d`. + +* `source` - (Required) Source of the Threat Intelligence Indicator. + +* `validate_from_utc` - (Required) The start of validate date in RFC3339. + +* `workspace_id` - (Required) The ID of the Log Analytics Workspace. Changing this forces a new Sentinel Threat Intelligence Indicator to be created. + +--- + +* `confidence` - (Optional) Confidence levels of the Threat Intelligence Indicator. + +* `created_by` - (Optional) The creator of the Threat Intelligence Indicator. + +* `description` - (Optional) The description of the Threat Intelligence Indicator. + +* `extension` - (Optional) The extension config of the Threat Intelligence Indicator in JSON format. + +* `external_reference` - (Optional) One or more `external_reference` blocks as defined below. + +* `granular_marking` - (Optional) One or more `granular_marking` blocks as defined below. + +* `kill_chain_phase` - (Optional) One or more `kill_chain_phase` blocks as defined below. + +* `tags` - (Optional) Specifies a list of tags of the Threat Intelligence Indicator. + +* `language` - (Optional) The language of the Threat Intelligence Indicator. + +* `modified_by` - (Optional) The user or service principal who modified the Threat Intelligence Indicator. + +* `object_marking_refs` - (Optional) Specifies a list of Threat Intelligence marking references. + +* `pattern_version` - (Optional) The version of a Threat Intelligence entity. + +* `revoked` - (Optional) Whether the Threat Intelligence entity revoked. + +* `threat_types` - (Optional) Specifies a list of threat types of this Threat Intelligence Indicator. + +* `validate_until_utc` - (Optional) The end of validate date of the Threat Intelligence Indicator in RFC3339 format. + +--- + +A `external_reference` block supports the following: + +* `description` - (Optional) The description of the external reference of the Threat Intelligence Indicator. + +* `hashes` - (Optional) The list of hashes of the external reference of the Threat Intelligence Indicator. + +* `source_name` - (Optional) The source name of the external reference of the Threat Intelligence Indicator. + +* `url` - (Optional) The url of the external reference of the Threat Intelligence Indicator. + +--- + +A `granular_marking` block supports the following: + +* `language` - (Optional) The language of granular marking of the Threat Intelligence Indicator. + +* `marking_ref` - (Optional) The reference of the granular marking of the Threat Intelligence Indicator. + +* `selectors` - (Optional) A list of selectors of the granular marking of the Threat Intelligence Indicator. + +--- + +A `kill_chain_phase` block supports the following: + +* `name` - (Optional) The name which should be used for the Lockheed Martin cyber kill chain phase. + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the Sentinel Threat Intelligence Indicator. + +* `created_on` - The date of this Threat Intelligence Indicator created. + +* `defanged` - Whether the Threat Intelligence entity is defanged? + +* `external_id` - The external ID of the Threat Intelligence Indicator. + +* `external_last_updated_time_utc` - the External last updated time in UTC. + +* `indicator_types` - A list of indicator types of this Threat Intelligence Indicator. + +* `last_updated_time_utc` - The last updated time of the Threat Intelligence Indicator in UTC. + +* `guid` - The guid of this Sentinel Threat Intelligence Indicator. + +* `parsed_pattern` - A `parsed_pattern` block as defined below. + +--- + +A `parsed_pattern` block exports the following: + +* `pattern_type_key` - The type key of parsed pattern. + +* `pattern_type_values` - A `pattern_type_values` block as defined below. + +--- + +A `pattern_type_values` block exports the following: + +* `value` - The value of the parsed pattern type. + +* `value_type` - The type of the value of the parsed pattern type value. + +## 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 Sentinel Threat Intelligence Indicator. +* `read` - (Defaults to 5 minutes) Used when retrieving the Sentinel Threat Intelligence Indicator. +* `update` - (Defaults to 30 minutes) Used when updating the Sentinel Threat Intelligence Indicator. +* `delete` - (Defaults to 30 minutes) Used when deleting the Sentinel Threat Intelligence Indicator. + +## Import + +Sentinel Threat Intelligence Indicators can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_sentinel_threat_intelligence_indicator.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourcegroup1/providers/Microsoft.OperationalInsights/workspaces/workspace1/providers/Microsoft.SecurityInsights/threatIntelligence/main/indicators/indicator1 +``` \ No newline at end of file