From 0f3ffcb40347c3e9d4be83d2644a11f692b14f22 Mon Sep 17 00:00:00 2001 From: Christoph Hartmann Date: Mon, 12 Aug 2024 09:27:57 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=B9=20improve=20user=20resource=20hand?= =?UTF-8?q?ling=20in=20Microsoft=20365=20(#4518)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🧹 improve user handling in ms 365 - new init for `microsoft.user` - `micosoft.group.members` returns human users - `microsoft.application.owner` returns all application owner * Update providers/ms365/resources/ms365.lr.manifest.yaml Co-authored-by: Tim Smith * Apply suggestions from code review Co-authored-by: Preslav Gerchev * 🧹 apply changes from code review --------- Co-authored-by: Tim Smith Co-authored-by: Preslav Gerchev --- providers/ms365/resources/applications.go | 60 ++++++- providers/ms365/resources/groups.go | 57 ++++++- providers/ms365/resources/microsoft.go | 29 ++++ providers/ms365/resources/ms365.lr | 6 +- providers/ms365/resources/ms365.lr.go | 35 +++- .../ms365/resources/ms365.lr.manifest.yaml | 2 + providers/ms365/resources/users.go | 155 ++++++++++++++---- 7 files changed, 293 insertions(+), 51 deletions(-) diff --git a/providers/ms365/resources/applications.go b/providers/ms365/resources/applications.go index 9eabbae145..59049e6543 100644 --- a/providers/ms365/resources/applications.go +++ b/providers/ms365/resources/applications.go @@ -27,7 +27,12 @@ func (a *mqlMicrosoft) applications() ([]interface{}, error) { return nil, err } ctx := context.Background() - resp, err := graphClient.Applications().Get(ctx, &applications.ApplicationsRequestBuilderGetRequestConfiguration{}) + top := int32(999) + resp, err := graphClient.Applications().Get(ctx, &applications.ApplicationsRequestBuilderGetRequestConfiguration{ + QueryParameters: &applications.ApplicationsRequestBuilderGetQueryParameters{ + Top: &top, + }, + }) if err != nil { return nil, transformError(err) } @@ -46,7 +51,7 @@ func (a *mqlMicrosoft) applications() ([]interface{}, error) { } func initMicrosoftApplication(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) { - // we only look up the package, if we have been supplied by its name and nothing else + // we only look up the application if we have been supplied by its name and nothing else raw, ok := args["name"] if !ok || len(args) != 1 { return args, nil, nil @@ -114,6 +119,57 @@ func (a *mqlMicrosoftApplication) hasExpiredCredentials() (bool, error) { return false, nil } +func (a *mqlMicrosoftApplication) owners() ([]interface{}, error) { + conn := a.MqlRuntime.Connection.(*connection.Ms365Connection) + + msResource, err := a.MqlRuntime.CreateResource(a.MqlRuntime, "microsoft", map[string]*llx.RawData{}) + if err != nil { + return nil, err + } + mqlMicrsoftResource := msResource.(*mqlMicrosoft) + + graphClient, err := conn.GraphClient() + if err != nil { + return nil, err + } + + ctx := context.Background() + resp, err := graphClient.Applications().ByApplicationId(a.GetId().Data).Owners().Get(ctx, &applications.ItemOwnersRequestBuilderGetRequestConfiguration{ + QueryParameters: &applications.ItemOwnersRequestBuilderGetQueryParameters{ + Select: []string{"id"}, + }, + }) + if err != nil { + return nil, transformError(err) + } + + res := []interface{}{} + for i := range resp.GetValue() { + ownerId := resp.GetValue()[i].GetId() + if ownerId == nil { + continue + } + + // if the user is already indexed, we can reuse it + userResource, ok := mqlMicrsoftResource.userById(*ownerId) + if ok { + res = append(res, userResource) + continue + } + + // otherwise we create a new user resource + newUserResource, err := a.MqlRuntime.NewResource(a.MqlRuntime, "microsoft.user", map[string]*llx.RawData{ + "id": llx.StringDataPtr(ownerId), + }) + if err != nil { + return nil, err + } + mqlMicrsoftResource.index(newUserResource.(*mqlMicrosoftUser)) + res = append(res, newUserResource) + } + return res, nil +} + // newMqlMicrosoftApplication creates a new mqlMicrosoftApplication resource func newMqlMicrosoftApplication(runtime *plugin.Runtime, app models.Applicationable) (*mqlMicrosoftApplication, error) { info, _ := convert.JsonToDictSlice(app.GetInfo()) diff --git a/providers/ms365/resources/groups.go b/providers/ms365/resources/groups.go index b218b5d348..2b8c9db8f6 100644 --- a/providers/ms365/resources/groups.go +++ b/providers/ms365/resources/groups.go @@ -5,11 +5,8 @@ package resources import ( "context" - "errors" - "github.com/microsoftgraph/msgraph-sdk-go/groups" "github.com/microsoftgraph/msgraph-sdk-go/models" - "go.mondoo.com/cnquery/v11/llx" "go.mondoo.com/cnquery/v11/providers/ms365/connection" "go.mondoo.com/cnquery/v11/types" @@ -20,7 +17,59 @@ func (m *mqlMicrosoftGroup) id() (string, error) { } func (a *mqlMicrosoftGroup) members() ([]interface{}, error) { - return nil, errors.New("not implemented") + msResource, err := a.MqlRuntime.CreateResource(a.MqlRuntime, "microsoft", map[string]*llx.RawData{}) + if err != nil { + return nil, err + } + mqlMicrosoftResource := msResource.(*mqlMicrosoft) + + groupId := a.Id.Data + conn := a.MqlRuntime.Connection.(*connection.Ms365Connection) + graphClient, err := conn.GraphClient() + if err != nil { + return nil, err + } + top := int32(200) + + queryParams := &groups.ItemMembersRequestBuilderGetQueryParameters{ + Top: &top, + } + ctx := context.Background() + resp, err := graphClient.Groups().ByGroupId(groupId).Members().Get(ctx, &groups.ItemMembersRequestBuilderGetRequestConfiguration{ + QueryParameters: queryParams, + }) + if err != nil { + return nil, transformError(err) + } + + res := []interface{}{} + for _, member := range resp.GetValue() { + memberId := member.GetId() + if memberId == nil { + continue + } + + if member.GetOdataType() != nil && *member.GetOdataType() != "#microsoft.graph.user" { + continue + } + + // if the user is already indexed, we can reuse it + userResource, ok := mqlMicrosoftResource.userById(*memberId) + if ok { + res = append(res, userResource) + continue + } + + newUserResource, err := a.MqlRuntime.NewResource(a.MqlRuntime, "microsoft.user", map[string]*llx.RawData{ + "id": llx.StringDataPtr(memberId), + }) + if err != nil { + return nil, err + } + mqlMicrosoftResource.index(newUserResource.(*mqlMicrosoftUser)) + res = append(res, newUserResource) + } + return res, nil } func (a *mqlMicrosoft) groups() ([]interface{}, error) { diff --git a/providers/ms365/resources/microsoft.go b/providers/ms365/resources/microsoft.go index 32699ae402..c54b19fb7f 100644 --- a/providers/ms365/resources/microsoft.go +++ b/providers/ms365/resources/microsoft.go @@ -10,6 +10,35 @@ import ( "go.mondoo.com/cnquery/v11/providers/ms365/connection" ) +type mqlMicrosoftInternal struct { + // index users by id + idxUsersById map[string]*mqlMicrosoftUser +} + +// initIndex ensures the user indexes are initialized, +// can be called multiple times without side effects +func (a *mqlMicrosoft) initIndex() { + if a.idxUsersById == nil { + a.idxUsersById = make(map[string]*mqlMicrosoftUser) + } +} + +// index adds a user to the internal indexes +func (a *mqlMicrosoft) index(user *mqlMicrosoftUser) { + a.initIndex() + a.idxUsersById[user.Id.Data] = user +} + +// userById returns a user by id if it exists in the index +func (a *mqlMicrosoft) userById(id string) (*mqlMicrosoftUser, bool) { + if a.idxUsersById == nil { + return nil, false + } + + res, ok := a.idxUsersById[id] + return res, ok +} + func (a *mqlMicrosoft) tenantDomainName() (string, error) { conn := a.MqlRuntime.Connection.(*connection.Ms365Connection) graphClient, err := conn.GraphClient() diff --git a/providers/ms365/resources/ms365.lr b/providers/ms365/resources/ms365.lr index 54a8088e1c..6665b4f1bc 100644 --- a/providers/ms365/resources/ms365.lr +++ b/providers/ms365/resources/ms365.lr @@ -42,8 +42,8 @@ private microsoft.organization @defaults("displayName") { onPremisesSyncEnabled bool } -// Microsoft user -private microsoft.user @defaults("id displayName mail") { +// Microsoft Entra ID user +private microsoft.user @defaults("id displayName userPrincipalName") { // User ID id string // Whether the user account is enabled @@ -197,6 +197,8 @@ microsoft.application @defaults("id displayName hasExpiredCredentials") { certificates []microsoft.keyCredential // Whether the credentials have expired hasExpiredCredentials() bool + // Application owner + owners() []microsoft.user } // Microsoft Entra AD Application certificate diff --git a/providers/ms365/resources/ms365.lr.go b/providers/ms365/resources/ms365.lr.go index 903be6c1c5..d7a901a6fa 100644 --- a/providers/ms365/resources/ms365.lr.go +++ b/providers/ms365/resources/ms365.lr.go @@ -27,7 +27,7 @@ func init() { Create: createMicrosoftOrganization, }, "microsoft.user": { - // to override args, implement: initMicrosoftUser(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) + Init: initMicrosoftUser, Create: createMicrosoftUser, }, "microsoft.group": { @@ -459,6 +459,9 @@ var getDataFields = map[string]func(r plugin.Resource) *plugin.DataRes{ "microsoft.application.hasExpiredCredentials": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlMicrosoftApplication).GetHasExpiredCredentials()).ToDataRes(types.Bool) }, + "microsoft.application.owners": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlMicrosoftApplication).GetOwners()).ToDataRes(types.Array(types.Resource("microsoft.user"))) + }, "microsoft.keyCredential.keyId": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlMicrosoftKeyCredential).GetKeyId()).ToDataRes(types.String) }, @@ -1251,6 +1254,10 @@ var setDataFields = map[string]func(r plugin.Resource, v *llx.RawData) bool { r.(*mqlMicrosoftApplication).HasExpiredCredentials, ok = plugin.RawToTValue[bool](v.Value, v.Error) return }, + "microsoft.application.owners": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMicrosoftApplication).Owners, ok = plugin.RawToTValue[[]interface{}](v.Value, v.Error) + return + }, "microsoft.keyCredential.__id": func(r plugin.Resource, v *llx.RawData) (ok bool) { r.(*mqlMicrosoftKeyCredential).__id, ok = v.Value.(string) return @@ -1935,7 +1942,7 @@ func SetAllData(resource plugin.Resource, args map[string]*llx.RawData) error { type mqlMicrosoft struct { MqlRuntime *plugin.Runtime __id string - // optional: if you define mqlMicrosoftInternal it will be used here + mqlMicrosoftInternal Organizations plugin.TValue[[]interface{}] Users plugin.TValue[[]interface{}] Groups plugin.TValue[[]interface{}] @@ -2217,12 +2224,7 @@ func createMicrosoftUser(runtime *plugin.Runtime, args map[string]*llx.RawData) return res, err } - if res.__id == "" { - res.__id, err = res.id() - if err != nil { - return nil, err - } - } + // to override __id implement: id() (string, error) if runtime.HasRecording { args, err = runtime.ResourceFromRecording("microsoft.user", res.__id) @@ -2660,6 +2662,7 @@ type mqlMicrosoftApplication struct { Secrets plugin.TValue[[]interface{}] Certificates plugin.TValue[[]interface{}] HasExpiredCredentials plugin.TValue[bool] + Owners plugin.TValue[[]interface{}] } // createMicrosoftApplication creates a new instance of this resource @@ -2760,6 +2763,22 @@ func (c *mqlMicrosoftApplication) GetHasExpiredCredentials() *plugin.TValue[bool }) } +func (c *mqlMicrosoftApplication) GetOwners() *plugin.TValue[[]interface{}] { + return plugin.GetOrCompute[[]interface{}](&c.Owners, func() ([]interface{}, error) { + if c.MqlRuntime.HasRecording { + d, err := c.MqlRuntime.FieldResourceFromRecording("microsoft.application", c.__id, "owners") + if err != nil { + return nil, err + } + if d != nil { + return d.Value.([]interface{}), nil + } + } + + return c.owners() + }) +} + // mqlMicrosoftKeyCredential for the microsoft.keyCredential resource type mqlMicrosoftKeyCredential struct { MqlRuntime *plugin.Runtime diff --git a/providers/ms365/resources/ms365.lr.manifest.yaml b/providers/ms365/resources/ms365.lr.manifest.yaml index 555c7444c3..10d9e6d395 100755 --- a/providers/ms365/resources/ms365.lr.manifest.yaml +++ b/providers/ms365/resources/ms365.lr.manifest.yaml @@ -37,6 +37,8 @@ resources: min_mondoo_version: 9.0.0 notes: min_mondoo_version: 9.0.0 + owners: + min_mondoo_version: 9.0.0 publisherDomain: {} secrets: min_mondoo_version: 9.0.0 diff --git a/providers/ms365/resources/users.go b/providers/ms365/resources/users.go index f2b8daf172..ad20bb0489 100644 --- a/providers/ms365/resources/users.go +++ b/providers/ms365/resources/users.go @@ -5,17 +5,20 @@ package resources import ( "context" - + "errors" + "fmt" "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/users" "go.mondoo.com/cnquery/v11/llx" + "go.mondoo.com/cnquery/v11/providers-sdk/v1/plugin" "go.mondoo.com/cnquery/v11/providers-sdk/v1/util/convert" "go.mondoo.com/cnquery/v11/providers/ms365/connection" "go.mondoo.com/cnquery/v11/types" ) -func (m *mqlMicrosoftUser) id() (string, error) { - return m.Id.Data, nil +var userSelectFields = []string{ + "id", "accountEnabled", "city", "companyName", "country", "createdDateTime", "department", "displayName", "employeeId", "givenName", + "jobTitle", "mail", "mobilePhone", "otherMails", "officeLocation", "postalCode", "state", "streetAddress", "surname", "userPrincipalName", "userType", } func (a *mqlMicrosoft) users() ([]interface{}, error) { @@ -25,16 +28,17 @@ func (a *mqlMicrosoft) users() ([]interface{}, error) { return nil, err } - selectFields := []string{ - "id", "accountEnabled", "city", "companyName", "country", "createdDateTime", "department", "displayName", "employeeId", "givenName", - "jobTitle", "mail", "mobilePhone", "otherMails", "officeLocation", "postalCode", "state", "streetAddress", "surname", "userPrincipalName", "userType", - } + // fetch user data ctx := context.Background() top := int32(999) - resp, err := graphClient.Users().Get(ctx, &users.UsersRequestBuilderGetRequestConfiguration{QueryParameters: &users.UsersRequestBuilderGetQueryParameters{ - Select: selectFields, - Top: &top, - }}) + resp, err := graphClient.Users().Get( + ctx, &users.UsersRequestBuilderGetRequestConfiguration{ + QueryParameters: &users.UsersRequestBuilderGetQueryParameters{ + Select: userSelectFields, + Top: &top, + }, + }, + ) if err != nil { return nil, transformError(err) } @@ -42,41 +46,122 @@ func (a *mqlMicrosoft) users() ([]interface{}, error) { if err != nil { return nil, transformError(err) } + + // construct the result res := []interface{}{} for _, u := range users { - graphUser, err := CreateResource(a.MqlRuntime, "microsoft.user", - map[string]*llx.RawData{ - "id": llx.StringDataPtr(u.GetId()), - "accountEnabled": llx.BoolDataPtr(u.GetAccountEnabled()), - "city": llx.StringDataPtr(u.GetCity()), - "companyName": llx.StringDataPtr(u.GetCompanyName()), - "country": llx.StringDataPtr(u.GetCountry()), - "createdDateTime": llx.TimeDataPtr(u.GetCreatedDateTime()), - "department": llx.StringDataPtr(u.GetDepartment()), - "displayName": llx.StringDataPtr(u.GetDisplayName()), - "employeeId": llx.StringDataPtr(u.GetEmployeeId()), - "givenName": llx.StringDataPtr(u.GetGivenName()), - "jobTitle": llx.StringDataPtr(u.GetJobTitle()), - "mail": llx.StringDataPtr(u.GetMail()), - "mobilePhone": llx.StringDataPtr(u.GetMobilePhone()), - "otherMails": llx.ArrayData(llx.TArr2Raw(u.GetOtherMails()), types.String), - "officeLocation": llx.StringDataPtr(u.GetOfficeLocation()), - "postalCode": llx.StringDataPtr(u.GetPostalCode()), - "state": llx.StringDataPtr(u.GetState()), - "streetAddress": llx.StringDataPtr(u.GetStreetAddress()), - "surname": llx.StringDataPtr(u.GetSurname()), - "userPrincipalName": llx.StringDataPtr(u.GetUserPrincipalName()), - "userType": llx.StringDataPtr(u.GetUserType()), - }) + graphUser, err := newMqlMicrosoftUser(a.MqlRuntime, u) if err != nil { return nil, err } + // index users by id and principal name + a.index(graphUser) res = append(res, graphUser) } return res, nil } +func initMicrosoftUser(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) { + // we only look up the user if we have been supplied by id, displayName or userPrincipalName + if len(args) > 1 { + return args, nil, nil + } + + rawId, okId := args["id"] + rawDisplayName, okDisplayName := args["displayName"] + rawPrincipalName, okPrincipalName := args["userPrincipalName"] + + if !okId && !okDisplayName && !okPrincipalName { + return args, nil, nil + } + + var filter *string + if okId { + idFilter := fmt.Sprintf("id eq '%s'", rawId.Value.(string)) + filter = &idFilter + } else if okPrincipalName { + principalNameFilter := fmt.Sprintf("userPrincipalName eq '%s'", rawPrincipalName.Value.(string)) + filter = &principalNameFilter + } else if okDisplayName { + displayNameFilter := fmt.Sprintf("displayName eq '%s'", rawDisplayName.Value.(string)) + filter = &displayNameFilter + } + if filter == nil { + return nil, nil, errors.New("no filter found") + } + + conn := runtime.Connection.(*connection.Ms365Connection) + graphClient, err := conn.GraphClient() + if err != nil { + return nil, nil, err + } + + ctx := context.Background() + resp, err := graphClient.Users().Get(ctx, &users.UsersRequestBuilderGetRequestConfiguration{ + QueryParameters: &users.UsersRequestBuilderGetQueryParameters{ + Filter: filter, + }, + }) + if err != nil { + return nil, nil, transformError(err) + } + + val := resp.GetValue() + if len(val) == 0 { + return nil, nil, errors.New("user not found") + } + + userId := val[0].GetId() + if userId == nil { + return nil, nil, errors.New("user id not found") + } + + // fetch user by id + user, err := graphClient.Users().ByUserId(*userId).Get(ctx, &users.UserItemRequestBuilderGetRequestConfiguration{}) + if err != nil { + return nil, nil, transformError(err) + } + mqlMsApp, err := newMqlMicrosoftUser(runtime, user) + if err != nil { + return nil, nil, err + } + + return nil, mqlMsApp, nil +} + +func newMqlMicrosoftUser(runtime *plugin.Runtime, u models.Userable) (*mqlMicrosoftUser, error) { + graphUser, err := CreateResource(runtime, "microsoft.user", + map[string]*llx.RawData{ + "__id": llx.StringDataPtr(u.GetId()), + "id": llx.StringDataPtr(u.GetId()), + "accountEnabled": llx.BoolDataPtr(u.GetAccountEnabled()), + "city": llx.StringDataPtr(u.GetCity()), + "companyName": llx.StringDataPtr(u.GetCompanyName()), + "country": llx.StringDataPtr(u.GetCountry()), + "createdDateTime": llx.TimeDataPtr(u.GetCreatedDateTime()), + "department": llx.StringDataPtr(u.GetDepartment()), + "displayName": llx.StringDataPtr(u.GetDisplayName()), + "employeeId": llx.StringDataPtr(u.GetEmployeeId()), + "givenName": llx.StringDataPtr(u.GetGivenName()), + "jobTitle": llx.StringDataPtr(u.GetJobTitle()), + "mail": llx.StringDataPtr(u.GetMail()), + "mobilePhone": llx.StringDataPtr(u.GetMobilePhone()), + "otherMails": llx.ArrayData(llx.TArr2Raw(u.GetOtherMails()), types.String), + "officeLocation": llx.StringDataPtr(u.GetOfficeLocation()), + "postalCode": llx.StringDataPtr(u.GetPostalCode()), + "state": llx.StringDataPtr(u.GetState()), + "streetAddress": llx.StringDataPtr(u.GetStreetAddress()), + "surname": llx.StringDataPtr(u.GetSurname()), + "userPrincipalName": llx.StringDataPtr(u.GetUserPrincipalName()), + "userType": llx.StringDataPtr(u.GetUserType()), + }) + if err != nil { + return nil, err + } + return graphUser.(*mqlMicrosoftUser), nil +} + func (a *mqlMicrosoftUser) settings() (interface{}, error) { conn := a.MqlRuntime.Connection.(*connection.Ms365Connection) graphClient, err := conn.GraphClient()