From 8e25e5c3b712ebf151eb24cb8e2f44658f46b0f5 Mon Sep 17 00:00:00 2001 From: Preslav Gerchev Date: Wed, 30 Oct 2024 07:56:06 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20microsoft.user.mfaEnabled=20f?= =?UTF-8?q?ield.=20(#4792)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Preslav --- providers/ms365/connection/adapter.go | 17 ++++++ providers/ms365/go.mod | 1 + providers/ms365/go.sum | 2 + providers/ms365/resources/microsoft.go | 8 +++ providers/ms365/resources/ms365.lr | 2 + providers/ms365/resources/ms365.lr.go | 14 +++++ .../ms365/resources/ms365.lr.manifest.yaml | 2 + providers/ms365/resources/users.go | 59 ++++++++++++++++++- 8 files changed, 104 insertions(+), 1 deletion(-) diff --git a/providers/ms365/connection/adapter.go b/providers/ms365/connection/adapter.go index 57ad7e1394..c6e833e640 100644 --- a/providers/ms365/connection/adapter.go +++ b/providers/ms365/connection/adapter.go @@ -8,6 +8,7 @@ import ( errors "github.com/cockroachdb/errors" "github.com/microsoft/kiota-abstractions-go/authentication" a "github.com/microsoft/kiota-authentication-azure-go" + beta "github.com/microsoftgraph/msgraph-beta-sdk-go" msgraphsdkgo "github.com/microsoftgraph/msgraph-sdk-go" ) @@ -36,6 +37,22 @@ func graphClient(token azcore.TokenCredential) (*msgraphsdkgo.GraphServiceClient return graphClient, nil } +func betaGraphClient(token azcore.TokenCredential) (*beta.GraphServiceClient, error) { + providerFunc := func() (authentication.AuthenticationProvider, error) { + return a.NewAzureIdentityAuthenticationProviderWithScopes(token, DefaultMSGraphScopes) + } + adapter, err := newGraphRequestAdapterWithFn(providerFunc) + if err != nil { + return nil, err + } + graphClient := beta.NewGraphServiceClient(adapter) + return graphClient, nil +} + func (conn *Ms365Connection) GraphClient() (*msgraphsdkgo.GraphServiceClient, error) { return graphClient(conn.Token()) } + +func (conn *Ms365Connection) BetaGraphClient() (*beta.GraphServiceClient, error) { + return betaGraphClient(conn.Token()) +} diff --git a/providers/ms365/go.mod b/providers/ms365/go.mod index eca92ef572..6347bd6a97 100644 --- a/providers/ms365/go.mod +++ b/providers/ms365/go.mod @@ -10,6 +10,7 @@ require ( github.com/google/uuid v1.6.0 github.com/microsoft/kiota-abstractions-go v1.7.0 github.com/microsoft/kiota-authentication-azure-go v1.1.0 + github.com/microsoftgraph/msgraph-beta-sdk-go v0.111.0 github.com/microsoftgraph/msgraph-sdk-go v1.51.0 github.com/microsoftgraph/msgraph-sdk-go-core v1.2.1 github.com/rs/zerolog v1.33.0 diff --git a/providers/ms365/go.sum b/providers/ms365/go.sum index 309c7a21a2..6a0fbd1fd7 100644 --- a/providers/ms365/go.sum +++ b/providers/ms365/go.sum @@ -315,6 +315,8 @@ github.com/microsoft/kiota-serialization-multipart-go v1.0.0 h1:3O5sb5Zj+moLBiJy github.com/microsoft/kiota-serialization-multipart-go v1.0.0/go.mod h1:yauLeBTpANk4L03XD985akNysG24SnRJGaveZf+p4so= github.com/microsoft/kiota-serialization-text-go v1.0.0 h1:XOaRhAXy+g8ZVpcq7x7a0jlETWnWrEum0RhmbYrTFnA= github.com/microsoft/kiota-serialization-text-go v1.0.0/go.mod h1:sM1/C6ecnQ7IquQOGUrUldaO5wj+9+v7G2W3sQ3fy6M= +github.com/microsoftgraph/msgraph-beta-sdk-go v0.111.0 h1:IhaMYOdiN76xwkYGVAb7QwkOkjSZTrBMwv+5xCpf2sY= +github.com/microsoftgraph/msgraph-beta-sdk-go v0.111.0/go.mod h1:GJhFAbcwHwtiqspxJnurVNr6XZBfVtdccVEC1+6+vjo= github.com/microsoftgraph/msgraph-sdk-go v1.51.0 h1:IfRY0uVHToT8X9k6Ri19tKdt8hwPomji2yx5YsKoaw4= github.com/microsoftgraph/msgraph-sdk-go v1.51.0/go.mod h1:MVTeFCCih3qXy9D0q+f4NdOyumFnMZ+Ppcpurgd30TY= github.com/microsoftgraph/msgraph-sdk-go-core v1.2.1 h1:P1wpmn3xxfPMFJHg+PJPcusErfRkl63h6OdAnpDbkS8= diff --git a/providers/ms365/resources/microsoft.go b/providers/ms365/resources/microsoft.go index e4d9b77134..a7a85feb77 100644 --- a/providers/ms365/resources/microsoft.go +++ b/providers/ms365/resources/microsoft.go @@ -9,10 +9,18 @@ import ( var idxUsersById = &sync.RWMutex{} +type mfaResp struct { + // holds the error if that is what the request returned + err error + mfaMap map[string]bool +} + type mqlMicrosoftInternal struct { permissionIndexer // index users by id idxUsersById map[string]*mqlMicrosoftUser + // the response when asking for the user registration details + mfaResp mfaResp } // initIndex ensures the user indexes are initialized, diff --git a/providers/ms365/resources/ms365.lr b/providers/ms365/resources/ms365.lr index 9ab7d8b923..a5c6ead4ff 100644 --- a/providers/ms365/resources/ms365.lr +++ b/providers/ms365/resources/ms365.lr @@ -122,6 +122,8 @@ private microsoft.user @defaults("id displayName userPrincipalName") { contact() dict // Authentication information authMethods() microsoft.user.authenticationMethods + // Whether MFA is enabled for the user. + mfaEnabled() bool } // Microsoft Entra authentication methods diff --git a/providers/ms365/resources/ms365.lr.go b/providers/ms365/resources/ms365.lr.go index e06474fa08..97b809e020 100644 --- a/providers/ms365/resources/ms365.lr.go +++ b/providers/ms365/resources/ms365.lr.go @@ -381,6 +381,9 @@ var getDataFields = map[string]func(r plugin.Resource) *plugin.DataRes{ "microsoft.user.authMethods": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlMicrosoftUser).GetAuthMethods()).ToDataRes(types.Resource("microsoft.user.authenticationMethods")) }, + "microsoft.user.mfaEnabled": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlMicrosoftUser).GetMfaEnabled()).ToDataRes(types.Bool) + }, "microsoft.user.authenticationMethods.count": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlMicrosoftUserAuthenticationMethods).GetCount()).ToDataRes(types.Int) }, @@ -1377,6 +1380,10 @@ var setDataFields = map[string]func(r plugin.Resource, v *llx.RawData) bool { r.(*mqlMicrosoftUser).AuthMethods, ok = plugin.RawToTValue[*mqlMicrosoftUserAuthenticationMethods](v.Value, v.Error) return }, + "microsoft.user.mfaEnabled": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMicrosoftUser).MfaEnabled, ok = plugin.RawToTValue[bool](v.Value, v.Error) + return + }, "microsoft.user.authenticationMethods.__id": func(r plugin.Resource, v *llx.RawData) (ok bool) { r.(*mqlMicrosoftUserAuthenticationMethods).__id, ok = v.Value.(string) return @@ -2978,6 +2985,7 @@ type mqlMicrosoftUser struct { Job plugin.TValue[interface{}] Contact plugin.TValue[interface{}] AuthMethods plugin.TValue[*mqlMicrosoftUserAuthenticationMethods] + MfaEnabled plugin.TValue[bool] } // createMicrosoftUser creates a new instance of this resource @@ -3130,6 +3138,12 @@ func (c *mqlMicrosoftUser) GetAuthMethods() *plugin.TValue[*mqlMicrosoftUserAuth }) } +func (c *mqlMicrosoftUser) GetMfaEnabled() *plugin.TValue[bool] { + return plugin.GetOrCompute[bool](&c.MfaEnabled, func() (bool, error) { + return c.mfaEnabled() + }) +} + // mqlMicrosoftUserAuthenticationMethods for the microsoft.user.authenticationMethods resource type mqlMicrosoftUserAuthenticationMethods struct { MqlRuntime *plugin.Runtime diff --git a/providers/ms365/resources/ms365.lr.manifest.yaml b/providers/ms365/resources/ms365.lr.manifest.yaml index 6681963da5..7e0e9d72d8 100755 --- a/providers/ms365/resources/ms365.lr.manifest.yaml +++ b/providers/ms365/resources/ms365.lr.manifest.yaml @@ -433,6 +433,8 @@ resources: min_mondoo_version: 9.0.0 jobTitle: {} mail: {} + mfaEnabled: + min_mondoo_version: 9.0.0 mobilePhone: {} officeLocation: {} otherMails: {} diff --git a/providers/ms365/resources/users.go b/providers/ms365/resources/users.go index 93d8c480df..6a164c6bdc 100644 --- a/providers/ms365/resources/users.go +++ b/providers/ms365/resources/users.go @@ -7,6 +7,11 @@ import ( "context" "errors" "fmt" + "time" + + betamodels "github.com/microsoftgraph/msgraph-beta-sdk-go/models" + "github.com/microsoftgraph/msgraph-beta-sdk-go/reports" + betausers "github.com/microsoftgraph/msgraph-beta-sdk-go/users" "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/users" "go.mondoo.com/cnquery/v11/llx" @@ -14,7 +19,6 @@ import ( "go.mondoo.com/cnquery/v11/providers-sdk/v1/util/convert" "go.mondoo.com/cnquery/v11/providers/ms365/connection" "go.mondoo.com/cnquery/v11/types" - "time" ) var userSelectFields = []string{ @@ -32,6 +36,10 @@ func (a *mqlMicrosoft) users() ([]interface{}, error) { return nil, err } + betaClient, err := conn.BetaGraphClient() + if err != nil { + return nil, err + } // fetch user data ctx := context.Background() top := int32(999) @@ -51,6 +59,33 @@ func (a *mqlMicrosoft) users() ([]interface{}, error) { return nil, transformError(err) } + detailsResp, err := betaClient.Reports().AuthenticationMethods().UserRegistrationDetails().Get( + ctx, + &reports.AuthenticationMethodsUserRegistrationDetailsRequestBuilderGetRequestConfiguration{ + QueryParameters: &reports.AuthenticationMethodsUserRegistrationDetailsRequestBuilderGetQueryParameters{ + Top: &top, + }, + }) + // we do not want to fail the user fetching here, this likely means the tenant does not have the right license + if err != nil { + a.mfaResp = mfaResp{err: err} + } else { + userRegistrationDetails, err := iterate[*betamodels.UserRegistrationDetails](ctx, detailsResp, betaClient.GetAdapter(), betausers.CreateDeltaGetResponseFromDiscriminatorValue) + // we do not want to fail the user fetching here, this likely means the tenant does not have the right license + if err != nil { + a.mfaResp = mfaResp{err: err} + } else { + mfaMap := map[string]bool{} + for _, u := range userRegistrationDetails { + if u.GetId() == nil || u.GetIsMfaRegistered() == nil { + continue + } + mfaMap[*u.GetId()] = *u.GetIsMfaRegistered() + } + a.mfaResp = mfaResp{mfaMap: mfaMap} + } + } + // construct the result res := []interface{}{} for _, u := range users { @@ -172,6 +207,27 @@ var userJobContactFields = []string{ "officeLocation", "streetAddress", "city", "state", "postalCode", "country", "businessPhones", "mobilePhone", "mail", "otherMails", "faxNumber", "mailNickname", } +func (a *mqlMicrosoftUser) mfaEnabled() (bool, error) { + mql, err := CreateResource(a.MqlRuntime, "microsoft", map[string]*llx.RawData{}) + if err != nil { + return false, err + } + + microsoft := mql.(*mqlMicrosoft) + if microsoft.mfaResp.mfaMap == nil { + microsoft.mfaResp.mfaMap = make(map[string]bool) + } + if microsoft.mfaResp.err != nil { + a.MfaEnabled.Error = microsoft.mfaResp.err + a.MfaEnabled.State = plugin.StateIsSet + return false, a.MfaEnabled.Error + } + + a.MfaEnabled.Data = microsoft.mfaResp.mfaMap[a.Id.Data] + a.MfaEnabled.State = plugin.StateIsSet + return a.MfaEnabled.Data, nil +} + func (a *mqlMicrosoftUser) populateJobContactData() error { conn := a.MqlRuntime.Connection.(*connection.Ms365Connection) graphClient, err := conn.GraphClient() @@ -348,6 +404,7 @@ func (a *mqlMicrosoftUser) authMethods() (*mqlMicrosoftUserAuthenticationMethods } ctx := context.Background() + authMethods, err := graphClient.Users().ByUserId(userID).Authentication().Methods().Get(ctx, &users.ItemAuthenticationMethodsRequestBuilderGetRequestConfiguration{}) if oErr, ok := isOdataError(err); ok { if oErr.ResponseStatusCode == 403 {