From f6dc25c53168c09825ce52418d96962da66ea689 Mon Sep 17 00:00:00 2001 From: Salim Afiune Maya Date: Wed, 5 Feb 2025 12:02:47 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20filtered=20resources:=20`m?= =?UTF-8?q?icrosoft.users`=20&=20`microsoft.roles`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When writing policies that require fetching huge amount of data only to search or filter for specific resources, we fetch all resources and then we apply the filters using the builtin functions `where()` `any()` and more. An example of a policy check that uses these patterns is: ``` // search for emergency accounts microsoft.users.any(displayName == /emergency/) // emrgIds holds the ids which match the above criteria emrgID = microsoft.users.where(displayName == /emergency/).map(id) // check if at least one of the accounts identified as such is attached to the "Global Administrator" role microsoft.rolemanagement.roleDefinitions.where(displayName == "Global Administrator").all(assignments.any(principalId == emrgID)) ``` To improve these resources, I am proposing a new pattern, similar to the one used at https://github.com/mondoohq/cnquery/pull/5156, but with the difference that it doesn't override builtin functions, instead it leverages list resources which are natively supported in MQL with additional query parameters `filter` and `search`. These query parameters will be used directly when executing API requests against Microsoft Graph API. These query parameters are documented at: https://learn.microsoft.com/en-us/graph/filter-query-parameter?tabs=http#filter-using-lambda-operators The above example can be rewritten using these two new filtered resources like: ``` // search for emergency accounts microsoft.users(search: "displayName:emergency").any() // emrgIds holds the ids which match the above criteria emrgID = microsoft.users(search: "displayName:emergency").map(id) // check if at least one of the accounts identified as such is attached to the "Global Administrator" role microsoft.roles(filter: "displayName eq 'Global Administrator'").all(assignments.any(principalId == emrgID)) ``` Additionally, since these query parameters are directly passed to Microsoft API's, we can write very complex filters for these two new resources. A couple examples are: ``` microsoft.roles(filter: "isBuiltIn eq true and startswith(displayName, 'Global')") microsoft.users(filter: "accountEnabled eq true AND userType eq 'Member'", search: "officeLocation:berlin") ``` Closes https://github.com/mondoohq/cnquery/issues/5110 Signed-off-by: Salim Afiune Maya --- .../ms365/resources/api_query_parameters.go | 76 ++++ providers/ms365/resources/applications.go | 6 +- providers/ms365/resources/ms365.lr | 29 +- providers/ms365/resources/ms365.lr.go | 226 ++++++++++- .../ms365/resources/ms365.lr.manifest.yaml | 360 ++++++------------ providers/ms365/resources/ms365_exchange.go | 2 +- providers/ms365/resources/rolemanagement.go | 88 ++++- providers/ms365/resources/users.go | 114 ++++-- 8 files changed, 603 insertions(+), 298 deletions(-) create mode 100644 providers/ms365/resources/api_query_parameters.go diff --git a/providers/ms365/resources/api_query_parameters.go b/providers/ms365/resources/api_query_parameters.go new file mode 100644 index 0000000000..dc87e01cd5 --- /dev/null +++ b/providers/ms365/resources/api_query_parameters.go @@ -0,0 +1,76 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package resources + +import ( + "errors" + "fmt" + "strings" + + "go.mondoo.com/cnquery/v11/llx" +) + +// newListResourceIdFromArguments generates a new __id for a list resource that has query +// parameters `filter` and/or `search`. We need to use these parameters as the resource id +// so that different query parameters, or no parameter, create different resources. +// +// If none is set, the default id returned is `all` +func newListResourceIdFromArguments(resourceName string, args map[string]*llx.RawData) *llx.RawData { + filter, filterExist := args["filter"] + search, searchExist := args["search"] + + if filterExist || searchExist { + id := resourceName + if filterExist { + id += fmt.Sprintf("/filter-%s", filter.Value.(string)) + } + if searchExist { + id += fmt.Sprintf("/search-%s", search.Value.(string)) + } + return llx.StringData(id) + } + + return llx.StringData(resourceName + "/all") +} + +// parseSearch tries to help the user understand the format of Microsoft API searches. +// By default, if the user runs a simple search of one field, we scape it for them, +// though if more complicated searches with ANDs/ORs are provided, we let the user know +// that they need to scape the query on their own. +// +// Simple search: +// +// resource(search: "property:my value goes here") +// +// Multiple fields search: +// +// resource(search: '"property1:one value" and "property2:something else and complex"') +func parseSearch(search string) (string, error) { + if !strings.Contains(search, ":") { + return "", errors.New("search is not of right format: \"property:value\"") + } + + if strings.Contains(search, "\"") { + // the search filter is already scaped + return search, nil + } + + if len(strings.Split(search, ":")) > 2 { + // special case for multi field search like `displayName:foo or mail:bar` + // witout scaping the filters on their own + return "", errors.New("search with multiple fields is not of right format: " + + "'\"property:value\" [AND | OR] \"property:value\"'") + } + + // scape simple search filter like: `displayName:my name` + return fmt.Sprintf("\"%s\"", search), nil +} + +// We do not have a parseFilter function since those query parameters can be passed as is, +// and the APIs return helpful information to the user. +// +// https://learn.microsoft.com/en-us/graph/filter-query-parameter?tabs=http#filter-using-lambda-operators +// func parseFilter(search string) (string, error) { +// return "", nil +// } diff --git a/providers/ms365/resources/applications.go b/providers/ms365/resources/applications.go index 3f52e7dce0..644f8ef1d9 100644 --- a/providers/ms365/resources/applications.go +++ b/providers/ms365/resources/applications.go @@ -30,12 +30,14 @@ func (a *mqlMicrosoft) applications() ([]interface{}, error) { return nil, err } ctx := context.Background() + top := int32(500) - resp, err := graphClient.Applications().Get(ctx, &applications.ApplicationsRequestBuilderGetRequestConfiguration{ + opts := &applications.ApplicationsRequestBuilderGetRequestConfiguration{ QueryParameters: &applications.ApplicationsRequestBuilderGetQueryParameters{ Top: &top, }, - }) + } + resp, err := graphClient.Applications().Get(ctx, opts) if err != nil { return nil, transformError(err) } diff --git a/providers/ms365/resources/ms365.lr b/providers/ms365/resources/ms365.lr index 5430e2ebef..4f123707f7 100644 --- a/providers/ms365/resources/ms365.lr +++ b/providers/ms365/resources/ms365.lr @@ -11,7 +11,7 @@ microsoft { // Deprecated: use `microsoft.tenant` instead organizations() []microsoft.tenant // List of users - users() []microsoft.user + users() microsoft.users // List of groups groups() []microsoft.group // List of domains @@ -23,7 +23,7 @@ microsoft { // List of enterprise applications enterpriseApplications() []microsoft.serviceprincipal // List of roles - roles() []microsoft.rolemanagement.roledefinition + roles() microsoft.roles // Microsoft 365 settings settings() dict // The connected tenant's default domain name @@ -56,6 +56,17 @@ microsoft.tenant @defaults("name") { subscriptions() []dict } +// List of Microsoft Entra users with optional filters +microsoft.users { + []microsoft.user + + init(filter? string, search? string) + // Filter users by property values + filter string + // Search users by search phrases + search string +} + // Microsoft Conditional Access Policies microsoft.conditionalAccess { // Named locations container @@ -571,11 +582,21 @@ microsoft.policies { consentPolicySettings() dict } +// List of Microsoft Entra role definitions with optional filters +microsoft.roles { + []microsoft.rolemanagement.roledefinition + + init(filter? string, search? string) + // Filter roles by property values + filter string + // Search roles by search phrases + search string +} // Deprecated: use `microsoft.roles` instead microsoft.rolemanagement { // Deprecated: use `microsoft.roles` instead - roleDefinitions() []microsoft.rolemanagement.roledefinition + roleDefinitions() microsoft.roles } // Microsoft role definition @@ -830,4 +851,4 @@ private ms365.teams.teamsMeetingPolicyConfig { private ms365.teams.teamsMessagingPolicyConfig { // Whether users can report security concerns allowSecurityEndUserReporting bool -} \ No newline at end of file +} diff --git a/providers/ms365/resources/ms365.lr.go b/providers/ms365/resources/ms365.lr.go index df3e18049b..ca5bce0e1f 100644 --- a/providers/ms365/resources/ms365.lr.go +++ b/providers/ms365/resources/ms365.lr.go @@ -26,6 +26,10 @@ func init() { Init: initMicrosoftTenant, Create: createMicrosoftTenant, }, + "microsoft.users": { + Init: initMicrosoftUsers, + Create: createMicrosoftUsers, + }, "microsoft.conditionalAccess": { // to override args, implement: initMicrosoftConditionalAccess(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) Create: createMicrosoftConditionalAccess, @@ -118,6 +122,10 @@ func init() { // to override args, implement: initMicrosoftPolicies(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) Create: createMicrosoftPolicies, }, + "microsoft.roles": { + Init: initMicrosoftRoles, + Create: createMicrosoftRoles, + }, "microsoft.rolemanagement": { // to override args, implement: initMicrosoftRolemanagement(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) Create: createMicrosoftRolemanagement, @@ -258,7 +266,7 @@ var getDataFields = map[string]func(r plugin.Resource) *plugin.DataRes{ return (r.(*mqlMicrosoft).GetOrganizations()).ToDataRes(types.Array(types.Resource("microsoft.tenant"))) }, "microsoft.users": func(r plugin.Resource) *plugin.DataRes { - return (r.(*mqlMicrosoft).GetUsers()).ToDataRes(types.Array(types.Resource("microsoft.user"))) + return (r.(*mqlMicrosoft).GetUsers()).ToDataRes(types.Resource("microsoft.users")) }, "microsoft.groups": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlMicrosoft).GetGroups()).ToDataRes(types.Array(types.Resource("microsoft.group"))) @@ -276,7 +284,7 @@ var getDataFields = map[string]func(r plugin.Resource) *plugin.DataRes{ return (r.(*mqlMicrosoft).GetEnterpriseApplications()).ToDataRes(types.Array(types.Resource("microsoft.serviceprincipal"))) }, "microsoft.roles": func(r plugin.Resource) *plugin.DataRes { - return (r.(*mqlMicrosoft).GetRoles()).ToDataRes(types.Array(types.Resource("microsoft.rolemanagement.roledefinition"))) + return (r.(*mqlMicrosoft).GetRoles()).ToDataRes(types.Resource("microsoft.roles")) }, "microsoft.settings": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlMicrosoft).GetSettings()).ToDataRes(types.Dict) @@ -317,6 +325,15 @@ var getDataFields = map[string]func(r plugin.Resource) *plugin.DataRes{ "microsoft.tenant.subscriptions": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlMicrosoftTenant).GetSubscriptions()).ToDataRes(types.Array(types.Dict)) }, + "microsoft.users.filter": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlMicrosoftUsers).GetFilter()).ToDataRes(types.String) + }, + "microsoft.users.search": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlMicrosoftUsers).GetSearch()).ToDataRes(types.String) + }, + "microsoft.users.list": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlMicrosoftUsers).GetList()).ToDataRes(types.Array(types.Resource("microsoft.user"))) + }, "microsoft.conditionalAccess.namedLocations": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlMicrosoftConditionalAccess).GetNamedLocations()).ToDataRes(types.Resource("microsoft.conditionalAccess.namedLocations")) }, @@ -950,8 +967,17 @@ var getDataFields = map[string]func(r plugin.Resource) *plugin.DataRes{ "microsoft.policies.consentPolicySettings": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlMicrosoftPolicies).GetConsentPolicySettings()).ToDataRes(types.Dict) }, + "microsoft.roles.filter": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlMicrosoftRoles).GetFilter()).ToDataRes(types.String) + }, + "microsoft.roles.search": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlMicrosoftRoles).GetSearch()).ToDataRes(types.String) + }, + "microsoft.roles.list": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlMicrosoftRoles).GetList()).ToDataRes(types.Array(types.Resource("microsoft.rolemanagement.roledefinition"))) + }, "microsoft.rolemanagement.roleDefinitions": func(r plugin.Resource) *plugin.DataRes { - return (r.(*mqlMicrosoftRolemanagement).GetRoleDefinitions()).ToDataRes(types.Array(types.Resource("microsoft.rolemanagement.roledefinition"))) + return (r.(*mqlMicrosoftRolemanagement).GetRoleDefinitions()).ToDataRes(types.Resource("microsoft.roles")) }, "microsoft.rolemanagement.roledefinition.id": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlMicrosoftRolemanagementRoledefinition).GetId()).ToDataRes(types.String) @@ -1259,7 +1285,7 @@ var setDataFields = map[string]func(r plugin.Resource, v *llx.RawData) bool { return }, "microsoft.users": func(r plugin.Resource, v *llx.RawData) (ok bool) { - r.(*mqlMicrosoft).Users, ok = plugin.RawToTValue[[]interface{}](v.Value, v.Error) + r.(*mqlMicrosoft).Users, ok = plugin.RawToTValue[*mqlMicrosoftUsers](v.Value, v.Error) return }, "microsoft.groups": func(r plugin.Resource, v *llx.RawData) (ok bool) { @@ -1283,7 +1309,7 @@ var setDataFields = map[string]func(r plugin.Resource, v *llx.RawData) bool { return }, "microsoft.roles": func(r plugin.Resource, v *llx.RawData) (ok bool) { - r.(*mqlMicrosoft).Roles, ok = plugin.RawToTValue[[]interface{}](v.Value, v.Error) + r.(*mqlMicrosoft).Roles, ok = plugin.RawToTValue[*mqlMicrosoftRoles](v.Value, v.Error) return }, "microsoft.settings": func(r plugin.Resource, v *llx.RawData) (ok bool) { @@ -1342,6 +1368,22 @@ var setDataFields = map[string]func(r plugin.Resource, v *llx.RawData) bool { r.(*mqlMicrosoftTenant).Subscriptions, ok = plugin.RawToTValue[[]interface{}](v.Value, v.Error) return }, + "microsoft.users.__id": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMicrosoftUsers).__id, ok = v.Value.(string) + return + }, + "microsoft.users.filter": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMicrosoftUsers).Filter, ok = plugin.RawToTValue[string](v.Value, v.Error) + return + }, + "microsoft.users.search": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMicrosoftUsers).Search, ok = plugin.RawToTValue[string](v.Value, v.Error) + return + }, + "microsoft.users.list": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMicrosoftUsers).List, ok = plugin.RawToTValue[[]interface{}](v.Value, v.Error) + return + }, "microsoft.conditionalAccess.__id": func(r plugin.Resource, v *llx.RawData) (ok bool) { r.(*mqlMicrosoftConditionalAccess).__id, ok = v.Value.(string) return @@ -2278,12 +2320,28 @@ var setDataFields = map[string]func(r plugin.Resource, v *llx.RawData) bool { r.(*mqlMicrosoftPolicies).ConsentPolicySettings, ok = plugin.RawToTValue[interface{}](v.Value, v.Error) return }, + "microsoft.roles.__id": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMicrosoftRoles).__id, ok = v.Value.(string) + return + }, + "microsoft.roles.filter": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMicrosoftRoles).Filter, ok = plugin.RawToTValue[string](v.Value, v.Error) + return + }, + "microsoft.roles.search": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMicrosoftRoles).Search, ok = plugin.RawToTValue[string](v.Value, v.Error) + return + }, + "microsoft.roles.list": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMicrosoftRoles).List, ok = plugin.RawToTValue[[]interface{}](v.Value, v.Error) + return + }, "microsoft.rolemanagement.__id": func(r plugin.Resource, v *llx.RawData) (ok bool) { r.(*mqlMicrosoftRolemanagement).__id, ok = v.Value.(string) return }, "microsoft.rolemanagement.roleDefinitions": func(r plugin.Resource, v *llx.RawData) (ok bool) { - r.(*mqlMicrosoftRolemanagement).RoleDefinitions, ok = plugin.RawToTValue[[]interface{}](v.Value, v.Error) + r.(*mqlMicrosoftRolemanagement).RoleDefinitions, ok = plugin.RawToTValue[*mqlMicrosoftRoles](v.Value, v.Error) return }, "microsoft.rolemanagement.roledefinition.__id": func(r plugin.Resource, v *llx.RawData) (ok bool) { @@ -2760,13 +2818,13 @@ type mqlMicrosoft struct { __id string mqlMicrosoftInternal Organizations plugin.TValue[[]interface{}] - Users plugin.TValue[[]interface{}] + Users plugin.TValue[*mqlMicrosoftUsers] Groups plugin.TValue[[]interface{}] Domains plugin.TValue[[]interface{}] Applications plugin.TValue[[]interface{}] Serviceprincipals plugin.TValue[[]interface{}] EnterpriseApplications plugin.TValue[[]interface{}] - Roles plugin.TValue[[]interface{}] + Roles plugin.TValue[*mqlMicrosoftRoles] Settings plugin.TValue[interface{}] TenantDomainName plugin.TValue[string] } @@ -2819,15 +2877,15 @@ func (c *mqlMicrosoft) GetOrganizations() *plugin.TValue[[]interface{}] { }) } -func (c *mqlMicrosoft) GetUsers() *plugin.TValue[[]interface{}] { - return plugin.GetOrCompute[[]interface{}](&c.Users, func() ([]interface{}, error) { +func (c *mqlMicrosoft) GetUsers() *plugin.TValue[*mqlMicrosoftUsers] { + return plugin.GetOrCompute[*mqlMicrosoftUsers](&c.Users, func() (*mqlMicrosoftUsers, error) { if c.MqlRuntime.HasRecording { d, err := c.MqlRuntime.FieldResourceFromRecording("microsoft", c.__id, "users") if err != nil { return nil, err } if d != nil { - return d.Value.([]interface{}), nil + return d.Value.(*mqlMicrosoftUsers), nil } } @@ -2915,15 +2973,15 @@ func (c *mqlMicrosoft) GetEnterpriseApplications() *plugin.TValue[[]interface{}] }) } -func (c *mqlMicrosoft) GetRoles() *plugin.TValue[[]interface{}] { - return plugin.GetOrCompute[[]interface{}](&c.Roles, func() ([]interface{}, error) { +func (c *mqlMicrosoft) GetRoles() *plugin.TValue[*mqlMicrosoftRoles] { + return plugin.GetOrCompute[*mqlMicrosoftRoles](&c.Roles, func() (*mqlMicrosoftRoles, error) { if c.MqlRuntime.HasRecording { d, err := c.MqlRuntime.FieldResourceFromRecording("microsoft", c.__id, "roles") if err != nil { return nil, err } if d != nil { - return d.Value.([]interface{}), nil + return d.Value.(*mqlMicrosoftRoles), nil } } @@ -3044,6 +3102,72 @@ func (c *mqlMicrosoftTenant) GetSubscriptions() *plugin.TValue[[]interface{}] { }) } +// mqlMicrosoftUsers for the microsoft.users resource +type mqlMicrosoftUsers struct { + MqlRuntime *plugin.Runtime + __id string + // optional: if you define mqlMicrosoftUsersInternal it will be used here + Filter plugin.TValue[string] + Search plugin.TValue[string] + List plugin.TValue[[]interface{}] +} + +// createMicrosoftUsers creates a new instance of this resource +func createMicrosoftUsers(runtime *plugin.Runtime, args map[string]*llx.RawData) (plugin.Resource, error) { + res := &mqlMicrosoftUsers{ + MqlRuntime: runtime, + } + + err := SetAllData(res, args) + if err != nil { + return res, err + } + + // to override __id implement: id() (string, error) + + if runtime.HasRecording { + args, err = runtime.ResourceFromRecording("microsoft.users", res.__id) + if err != nil || args == nil { + return res, err + } + return res, SetAllData(res, args) + } + + return res, nil +} + +func (c *mqlMicrosoftUsers) MqlName() string { + return "microsoft.users" +} + +func (c *mqlMicrosoftUsers) MqlID() string { + return c.__id +} + +func (c *mqlMicrosoftUsers) GetFilter() *plugin.TValue[string] { + return &c.Filter +} + +func (c *mqlMicrosoftUsers) GetSearch() *plugin.TValue[string] { + return &c.Search +} + +func (c *mqlMicrosoftUsers) GetList() *plugin.TValue[[]interface{}] { + return plugin.GetOrCompute[[]interface{}](&c.List, func() ([]interface{}, error) { + if c.MqlRuntime.HasRecording { + d, err := c.MqlRuntime.FieldResourceFromRecording("microsoft.users", c.__id, "list") + if err != nil { + return nil, err + } + if d != nil { + return d.Value.([]interface{}), nil + } + } + + return c.list() + }) +} + // mqlMicrosoftConditionalAccess for the microsoft.conditionalAccess resource type mqlMicrosoftConditionalAccess struct { MqlRuntime *plugin.Runtime @@ -5250,12 +5374,78 @@ func (c *mqlMicrosoftPolicies) GetConsentPolicySettings() *plugin.TValue[interfa }) } +// mqlMicrosoftRoles for the microsoft.roles resource +type mqlMicrosoftRoles struct { + MqlRuntime *plugin.Runtime + __id string + // optional: if you define mqlMicrosoftRolesInternal it will be used here + Filter plugin.TValue[string] + Search plugin.TValue[string] + List plugin.TValue[[]interface{}] +} + +// createMicrosoftRoles creates a new instance of this resource +func createMicrosoftRoles(runtime *plugin.Runtime, args map[string]*llx.RawData) (plugin.Resource, error) { + res := &mqlMicrosoftRoles{ + MqlRuntime: runtime, + } + + err := SetAllData(res, args) + if err != nil { + return res, err + } + + // to override __id implement: id() (string, error) + + if runtime.HasRecording { + args, err = runtime.ResourceFromRecording("microsoft.roles", res.__id) + if err != nil || args == nil { + return res, err + } + return res, SetAllData(res, args) + } + + return res, nil +} + +func (c *mqlMicrosoftRoles) MqlName() string { + return "microsoft.roles" +} + +func (c *mqlMicrosoftRoles) MqlID() string { + return c.__id +} + +func (c *mqlMicrosoftRoles) GetFilter() *plugin.TValue[string] { + return &c.Filter +} + +func (c *mqlMicrosoftRoles) GetSearch() *plugin.TValue[string] { + return &c.Search +} + +func (c *mqlMicrosoftRoles) GetList() *plugin.TValue[[]interface{}] { + return plugin.GetOrCompute[[]interface{}](&c.List, func() ([]interface{}, error) { + if c.MqlRuntime.HasRecording { + d, err := c.MqlRuntime.FieldResourceFromRecording("microsoft.roles", c.__id, "list") + if err != nil { + return nil, err + } + if d != nil { + return d.Value.([]interface{}), nil + } + } + + return c.list() + }) +} + // mqlMicrosoftRolemanagement for the microsoft.rolemanagement resource type mqlMicrosoftRolemanagement struct { MqlRuntime *plugin.Runtime __id string // optional: if you define mqlMicrosoftRolemanagementInternal it will be used here - RoleDefinitions plugin.TValue[[]interface{}] + RoleDefinitions plugin.TValue[*mqlMicrosoftRoles] } // createMicrosoftRolemanagement creates a new instance of this resource @@ -5290,15 +5480,15 @@ func (c *mqlMicrosoftRolemanagement) MqlID() string { return c.__id } -func (c *mqlMicrosoftRolemanagement) GetRoleDefinitions() *plugin.TValue[[]interface{}] { - return plugin.GetOrCompute[[]interface{}](&c.RoleDefinitions, func() ([]interface{}, error) { +func (c *mqlMicrosoftRolemanagement) GetRoleDefinitions() *plugin.TValue[*mqlMicrosoftRoles] { + return plugin.GetOrCompute[*mqlMicrosoftRoles](&c.RoleDefinitions, func() (*mqlMicrosoftRoles, error) { if c.MqlRuntime.HasRecording { d, err := c.MqlRuntime.FieldResourceFromRecording("microsoft.rolemanagement", c.__id, "roleDefinitions") if err != nil { return nil, err } if d != nil { - return d.Value.([]interface{}), nil + return d.Value.(*mqlMicrosoftRoles), nil } } diff --git a/providers/ms365/resources/ms365.lr.manifest.yaml b/providers/ms365/resources/ms365.lr.manifest.yaml index 819d4209ca..a7775e9b77 100755 --- a/providers/ms365/resources/ms365.lr.manifest.yaml +++ b/providers/ms365/resources/ms365.lr.manifest.yaml @@ -5,102 +5,59 @@ resources: microsoft: fields: applications: {} - conditionalAccess: - min_mondoo_version: 9.0.0 domains: {} - enterpriseApplications: - min_mondoo_version: latest + enterpriseApplications: {} groups: {} organizations: {} - roles: - min_mondoo_version: 9.0.0 + roles: {} serviceprincipals: {} settings: {} - tenantDomainName: - min_mondoo_version: latest + tenantDomainName: {} users: {} - min_mondoo_version: 5.15.0 + min_mondoo_version: 9.0.0 microsoft.application: fields: - api: - min_mondoo_version: 9.0.0 + api: {} appId: {} - appRoles: - min_mondoo_version: 9.0.0 - applicationTemplateId: - min_mondoo_version: 9.0.0 - certificates: - min_mondoo_version: 9.0.0 - certification: - min_mondoo_version: 9.0.0 - createdAt: - min_mondoo_version: 9.0.0 + appRoles: {} + applicationTemplateId: {} + certificates: {} + certification: {} + createdAt: {} createdDateTime: {} - defaultRedirectUri: - min_mondoo_version: 9.0.0 - description: - min_mondoo_version: 9.0.0 - dict: - min_mondoo_version: 9.0.0 - disabledByMicrosoftStatus: - min_mondoo_version: 9.0.0 + defaultRedirectUri: {} + description: {} + disabledByMicrosoftStatus: {} displayName: {} - groupMembershipClaims: - min_mondoo_version: 9.0.0 - hasExpiredCredentials: - min_mondoo_version: 9.0.0 + groupMembershipClaims: {} + hasExpiredCredentials: {} id: {} identifierUris: {} - info: - min_mondoo_version: 9.0.0 - isDeviceOnlyAuthSupported: - min_mondoo_version: 9.0.0 - isFallbackPublicClient: - min_mondoo_version: 9.0.0 - name: - min_mondoo_version: 9.0.0 - nativeAuthenticationApisEnabled: - min_mondoo_version: 9.0.0 - notes: - min_mondoo_version: 9.0.0 - optionalClaims: - min_mondoo_version: 9.0.0 - owners: - min_mondoo_version: 9.0.0 - parentalControlSettings: - min_mondoo_version: 9.0.0 - publicClient: - min_mondoo_version: 9.0.0 + info: {} + isDeviceOnlyAuthSupported: {} + isFallbackPublicClient: {} + name: {} + nativeAuthenticationApisEnabled: {} + notes: {} + optionalClaims: {} + owners: {} + parentalControlSettings: {} + publicClient: {} publisherDomain: {} - requestSignatureVerification: - min_mondoo_version: 9.0.0 - requiredResourceAccess: - min_mondoo_version: 9.0.0 - samlMetadataUrl: - min_mondoo_version: 9.0.0 - secrets: - min_mondoo_version: 9.0.0 - serviceManagementReference: - min_mondoo_version: 9.0.0 - servicePrincipal: - min_mondoo_version: 9.0.0 - servicePrincipalLockConfiguration: - min_mondoo_version: 9.0.0 + requestSignatureVerification: {} + samlMetadataUrl: {} + secrets: {} + serviceManagementReference: {} + servicePrincipal: {} + servicePrincipalLockConfiguration: {} signInAudience: {} - spa: - min_mondoo_version: 9.0.0 - string: - min_mondoo_version: 9.0.0 - tags: - min_mondoo_version: 9.0.0 - tokenEncryptionKeyId: - min_mondoo_version: 9.0.0 - web: - min_mondoo_version: 9.0.0 - min_mondoo_version: 5.15.0 + spa: {} + tags: {} + tokenEncryptionKeyId: {} + web: {} + min_mondoo_version: 9.0.0 microsoft.application.permission: fields: - adminConsent: {} appId: {} appName: {} description: {} @@ -143,7 +100,7 @@ resources: fields: deviceCompliancePolicies: {} deviceConfigurations: {} - min_mondoo_version: 5.15.0 + min_mondoo_version: 9.0.0 microsoft.devicemanagement.devicecompliancepolicy: fields: assignments: {} @@ -155,7 +112,7 @@ resources: properties: {} version: {} is_private: true - min_mondoo_version: 5.15.0 + min_mondoo_version: 9.0.0 microsoft.devicemanagement.deviceconfiguration: fields: createdDateTime: {} @@ -166,7 +123,7 @@ resources: properties: {} version: {} is_private: true - min_mondoo_version: 5.15.0 + min_mondoo_version: 9.0.0 microsoft.domain: fields: authenticationType: {} @@ -182,7 +139,7 @@ resources: serviceConfigurationRecords: {} supportedServices: {} is_private: true - min_mondoo_version: 5.15.0 + min_mondoo_version: 9.0.0 microsoft.domaindnsrecord: fields: id: {} @@ -193,60 +150,36 @@ resources: supportedService: {} ttl: {} is_private: true - min_mondoo_version: 5.15.0 - microsoft.enterpriseApplication: - fields: - id: {} - name: {} - tags: {} - type: {} - min_mondoo_version: latest + min_mondoo_version: 9.0.0 microsoft.group: fields: displayName: {} - groupTypes: - min_mondoo_version: 9.0.0 + groupTypes: {} id: {} mail: {} mailEnabled: {} mailNickname: {} members: {} - membershipRule: - min_mondoo_version: 9.0.0 - membershipRuleProcessingState: - min_mondoo_version: 9.0.0 + membershipRule: {} + membershipRuleProcessingState: {} securityEnabled: {} - visibility: - min_mondoo_version: 9.0.0 + visibility: {} is_private: true - min_mondoo_version: 5.15.0 + min_mondoo_version: 9.0.0 microsoft.keyCredential: fields: description: {} - displayName: {} expired: {} expires: {} - hint: {} keyId: {} thumbprint: {} type: {} usage: {} is_private: true min_mondoo_version: 9.0.0 - microsoft.organization: - fields: - assignedPlans: {} - createdDateTime: {} - displayName: {} - id: {} - onPremisesSyncEnabled: - min_mondoo_version: 9.0.0 - verifiedDomains: {} - min_mondoo_version: 5.15.0 microsoft.passwordCredential: fields: description: {} - displayName: {} expired: {} expires: {} hint: {} @@ -255,19 +188,16 @@ resources: min_mondoo_version: 9.0.0 microsoft.policies: fields: - ConsentPolicySettings: - min_mondoo_version: 9.0.0 adminConsentRequestPolicy: {} authorizationPolicy: {} - consentPolicySettings: - min_mondoo_version: 9.0.0 + consentPolicySettings: {} identitySecurityDefaultsEnforcementPolicy: {} permissionGrantPolicies: {} - min_mondoo_version: 5.15.0 + min_mondoo_version: 9.0.0 microsoft.rolemanagement: fields: roleDefinitions: {} - min_mondoo_version: 5.15.0 + min_mondoo_version: 9.0.0 microsoft.rolemanagement.roleassignment: fields: id: {} @@ -275,7 +205,7 @@ resources: principalId: {} roleDefinitionId: {} is_private: true - min_mondoo_version: 5.15.0 + min_mondoo_version: 9.0.0 microsoft.rolemanagement.roledefinition: fields: assignments: {} @@ -288,14 +218,24 @@ resources: templateId: {} version: {} is_private: true - min_mondoo_version: 5.15.0 + min_mondoo_version: 9.0.0 + microsoft.rolemanagement.roledefinitions: + fields: + filter: {} + list: {} + min_mondoo_version: 9.0.0 + microsoft.roles: + fields: + filter: {} + list: {} + search: {} + min_mondoo_version: 9.0.0 microsoft.security: fields: latestSecureScores: {} - riskyUsers: - min_mondoo_version: 9.0.0 + riskyUsers: {} secureScores: {} - min_mondoo_version: 5.15.0 + min_mondoo_version: 9.0.0 microsoft.security.riskyUser: fields: id: {} @@ -321,86 +261,45 @@ resources: maxScore: {} vendorInformation: {} is_private: true - min_mondoo_version: 5.15.0 + min_mondoo_version: 9.0.0 microsoft.serviceprincipal: fields: - accountEnabled: - min_mondoo_version: 9.0.0 - appId: - min_mondoo_version: 9.0.0 - appOwnerOrganizationId: - min_mondoo_version: 9.0.0 - appRoleAssignmentRequired: - min_mondoo_version: 9.0.0 - appRoleAssignments: - min_mondoo_version: latest - appRoles: - min_mondoo_version: 9.0.0 - applicationTemplateId: - min_mondoo_version: 9.0.0 - assignmentRequired: - min_mondoo_version: latest - assignments: - min_mondoo_version: latest - description: - min_mondoo_version: 9.0.0 - enabled: - min_mondoo_version: latest - homepageUrl: - min_mondoo_version: latest - id: {} - isFirstParty: - min_mondoo_version: 9.0.0 - loginUrl: - min_mondoo_version: 9.0.0 - logoutUrl: - min_mondoo_version: 9.0.0 - name: - min_mondoo_version: latest - notes: - min_mondoo_version: latest - notificationEmailAddresses: - min_mondoo_version: 9.0.0 - permissions: - min_mondoo_version: 9.0.0 - preferredSingleSignOnMode: - min_mondoo_version: 9.0.0 - properties: - min_mondoo_version: latest - replyUrls: - min_mondoo_version: latest - servicePrincipalNames: - min_mondoo_version: 9.0.0 - signInAudience: - min_mondoo_version: 9.0.0 - tags: - min_mondoo_version: latest - termsOfServiceUrl: - min_mondoo_version: latest - type: - min_mondoo_version: latest - userAccessUrl: - min_mondoo_version: latest - verifiedPublisher: - min_mondoo_version: 9.0.0 - visibleToUsers: - min_mondoo_version: latest - min_mondoo_version: 5.15.0 - microsoft.serviceprincipal.appRoleAssignment: - fields: - displayName: {} + accountEnabled: {} + appId: {} + appOwnerOrganizationId: {} + appRoleAssignmentRequired: {} + appRoles: {} + applicationTemplateId: {} + assignmentRequired: {} + assignments: {} + description: {} + enabled: {} + homepageUrl: {} id: {} + isFirstParty: {} + loginUrl: {} + logoutUrl: {} + name: {} + notes: {} + notificationEmailAddresses: {} + permissions: {} + preferredSingleSignOnMode: {} + replyUrls: {} + servicePrincipalNames: {} + signInAudience: {} + tags: {} + termsOfServiceUrl: {} type: {} - min_mondoo_version: latest + verifiedPublisher: {} + visibleToUsers: {} + min_mondoo_version: 9.0.0 microsoft.serviceprincipal.assignment: fields: - appId: {} displayName: {} id: {} - resourceName: {} type: {} is_private: true - min_mondoo_version: latest + min_mondoo_version: 9.0.0 microsoft.tenant: fields: assignedPlans: {} @@ -411,7 +310,6 @@ resources: name: {} onPremisesSyncEnabled: {} provisionedPlans: {} - subscription: {} subscriptions: {} type: {} verifiedDomains: {} @@ -419,45 +317,36 @@ resources: microsoft.user: fields: accountEnabled: {} - auditlog: - min_mondoo_version: 9.0.0 - authMethods: - min_mondoo_version: 9.0.0 + auditlog: {} + authMethods: {} city: {} companyName: {} - contact: - min_mondoo_version: 9.0.0 + contact: {} country: {} createdDateTime: {} - creationType: - min_mondoo_version: 9.0.0 + creationType: {} department: {} displayName: {} employeeId: {} givenName: {} id: {} - identities: - min_mondoo_version: 9.0.0 - job: - min_mondoo_version: 9.0.0 + identities: {} + job: {} jobTitle: {} mail: {} - mfaEnabled: - min_mondoo_version: 9.0.0 + mfaEnabled: {} mobilePhone: {} officeLocation: {} otherMails: {} postalCode: {} settings: {} - signins: - min_mondoo_version: 9.0.0 state: {} streetAddress: {} surname: {} userPrincipalName: {} userType: {} is_private: true - min_mondoo_version: 5.15.0 + min_mondoo_version: 9.0.0 microsoft.user.auditlog: fields: lastInteractiveSignIn: {} @@ -493,12 +382,18 @@ resources: createdDateTime: {} id: {} interactive: {} - requestId: {} resourceDisplayName: {} userDisplayName: {} userId: {} is_private: true min_mondoo_version: 9.0.0 + microsoft.users: + fields: + filter: {} + list: {} + search: {} + userPrincipalName: {} + min_mondoo_version: 9.0.0 ms365.exchangeonline: fields: adminAuditLogConfig: {} @@ -514,20 +409,15 @@ resources: owaMailboxPolicy: {} phishFilterPolicy: {} remoteDomain: {} - reportSubmissionPolicies: - min_mondoo_version: 9.0.0 + reportSubmissionPolicies: {} roleAssignmentPolicy: {} safeAttachmentPolicy: {} safeLinksPolicy: {} - sharedMailboxes: - min_mondoo_version: 9.2.13 + sharedMailboxes: {} sharingPolicy: {} - teamsProtectionPolicies: - min_mondoo_version: latest - teamsProtectionPolicy: - min_mondoo_version: latest + teamsProtectionPolicies: {} transportRule: {} - min_mondoo_version: 5.15.0 + min_mondoo_version: 9.0.0 platform: name: - microsoft365 @@ -537,7 +427,7 @@ resources: identity: {} user: {} is_private: true - min_mondoo_version: 9.2.13 + min_mondoo_version: 9.0.0 platform: name: - microsoft365 @@ -547,7 +437,7 @@ resources: enabled: {} identity: {} is_private: true - min_mondoo_version: latest + min_mondoo_version: 9.0.0 platform: name: - microsoft365 @@ -562,7 +452,7 @@ resources: reportPhishAddresses: {} reportPhishToCustomizedAddress: {} is_private: true - min_mondoo_version: latest + min_mondoo_version: 9.0.0 platform: name: - microsoft365 @@ -577,11 +467,10 @@ resources: - microsoft365 ms365.sharepointonline: fields: - spoSites: - min_mondoo_version: 9.0.0 + spoSites: {} spoTenant: {} spoTenantSyncClientRestriction: {} - min_mondoo_version: 5.15.0 + min_mondoo_version: 9.0.0 platform: name: - microsoft365 @@ -597,13 +486,10 @@ resources: ms365.teams: fields: csTeamsClientConfiguration: {} - csTeamsMeetingPolicy: - min_mondoo_version: 9.0.0 - csTeamsMessagingPolicy: - min_mondoo_version: 9.0.0 - csTenantFederationConfiguration: - min_mondoo_version: 9.0.0 - min_mondoo_version: 5.15.0 + csTeamsMeetingPolicy: {} + csTeamsMessagingPolicy: {} + csTenantFederationConfiguration: {} + min_mondoo_version: 9.0.0 platform: name: - microsoft365 @@ -619,7 +505,7 @@ resources: designatedPresenterRoleMode: {} meetingChatEnabledType: {} is_private: true - min_mondoo_version: latest + min_mondoo_version: 9.0.0 platform: name: - microsoft365 @@ -637,11 +523,9 @@ resources: allowPublicUsers: {} allowTeamsConsumer: {} allowTeamsConsumerInbound: {} - allowedDomains: {} blockedDomains: {} identity: {} restrictTeamsConsumerToExternalUserProfiles: {} - sestrictTeamsConsumerToExternalUserProfiles: {} sharedSipAddressSpace: {} treatDiscoveredPartnersAsUnverified: {} is_private: true diff --git a/providers/ms365/resources/ms365_exchange.go b/providers/ms365/resources/ms365_exchange.go index d8c85e3afc..7b4502b09a 100644 --- a/providers/ms365/resources/ms365_exchange.go +++ b/providers/ms365/resources/ms365_exchange.go @@ -503,7 +503,7 @@ func (m *mqlMs365ExchangeonlineExoMailbox) user() (*mqlMicrosoftUser, error) { if users.Error != nil { return nil, users.Error } - for _, u := range users.Data { + for _, u := range users.Data.List.Data { mqlUser := u.(*mqlMicrosoftUser) if mqlUser.Id.Data == externalId { return mqlUser, nil diff --git a/providers/ms365/resources/rolemanagement.go b/providers/ms365/resources/rolemanagement.go index cb99dee8fa..6bb6c2abd3 100644 --- a/providers/ms365/resources/rolemanagement.go +++ b/providers/ms365/resources/rolemanagement.go @@ -5,28 +5,74 @@ package resources import ( "context" + "go.mondoo.com/cnquery/v11/providers-sdk/v1/plugin" + abstractions "github.com/microsoft/kiota-abstractions-go" "github.com/microsoftgraph/msgraph-sdk-go/rolemanagement" + "github.com/rs/zerolog/log" "go.mondoo.com/cnquery/v11/llx" "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 fetchRoles(runtime *plugin.Runtime) ([]interface{}, error) { - conn := runtime.Connection.(*connection.Ms365Connection) +var roledefinitionsSelectFields = []string{ + "id", + "description", + "displayName", + "isBuiltIn", + "isEnabled", + "rolePermissions", + "templateId", + "version", +} + +func (a *mqlMicrosoftRoles) list() ([]interface{}, error) { + conn := a.MqlRuntime.Connection.(*connection.Ms365Connection) graphClient, err := conn.GraphClient() if err != nil { return nil, err } - ctx := context.Background() - resp, err := graphClient.RoleManagement().Directory().RoleDefinitions().Get(ctx, &rolemanagement.DirectoryRoleDefinitionsRequestBuilderGetRequestConfiguration{ + ctx := context.Background() + opts := &rolemanagement.DirectoryRoleDefinitionsRequestBuilderGetRequestConfiguration{ QueryParameters: &rolemanagement.DirectoryRoleDefinitionsRequestBuilderGetQueryParameters{ - Select: []string{"id", "description", "displayName", "isBuiltIn", "isEnabled", "rolePermissions", "templateId", "version"}, + Select: roledefinitionsSelectFields, }, - }) + } + + if a.Search.State == plugin.StateIsSet || a.Filter.State == plugin.StateIsSet { + // search and filter requires this header + headers := abstractions.NewRequestHeaders() + headers.Add("ConsistencyLevel", "eventual") + opts.Headers = headers + + if a.Search.State == plugin.StateIsSet { + log.Debug(). + Str("search", a.Search.Data). + Msg("microsoft.roles.list.search set") + search, err := parseSearch(a.Search.Data) + if err != nil { + return nil, err + } + opts.QueryParameters.Search = &search + } + if a.Filter.State == plugin.StateIsSet { + log.Debug(). + Str("filter", a.Filter.Data). + Msg("microsoft.roles.list.filter set") + opts.QueryParameters.Filter = &a.Filter.Data + count := true + opts.QueryParameters.Count = &count + } + } + + resp, err := graphClient. + RoleManagement(). + Directory(). + RoleDefinitions(). + Get(ctx, opts) if err != nil { return nil, transformError(err) } @@ -38,7 +84,7 @@ func fetchRoles(runtime *plugin.Runtime) ([]interface{}, error) { if err != nil { return nil, err } - mqlResource, err := CreateResource(runtime, "microsoft.rolemanagement.roledefinition", + mqlResource, err := CreateResource(a.MqlRuntime, "microsoft.rolemanagement.roledefinition", map[string]*llx.RawData{ "id": llx.StringDataPtr(role.GetId()), "description": llx.StringDataPtr(role.GetDescription()), @@ -58,8 +104,23 @@ func fetchRoles(runtime *plugin.Runtime) ([]interface{}, error) { return res, nil } -func (a *mqlMicrosoft) roles() ([]interface{}, error) { - return fetchRoles(a.MqlRuntime) +func (a *mqlMicrosoft) roles() (*mqlMicrosoftRoles, error) { + resource, err := a.MqlRuntime.CreateResource(a.MqlRuntime, "microsoft.roles", map[string]*llx.RawData{}) + if err != nil { + return nil, err + } + + return resource.(*mqlMicrosoftRoles), nil +} + +func initMicrosoftRoles(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) { + args["__id"] = newListResourceIdFromArguments("microsoft.roles", args) + resource, err := runtime.CreateResource(runtime, "microsoft.roles", args) + if err != nil { + return args, nil, err + } + + return args, resource.(*mqlMicrosoftRoles), nil } func (m *mqlMicrosoftRolemanagementRoledefinition) id() (string, error) { @@ -72,8 +133,13 @@ func (m *mqlMicrosoftRolemanagementRoleassignment) id() (string, error) { } // Deprecated: use mqlMicrosoft roles() instead -func (a *mqlMicrosoftRolemanagement) roleDefinitions() ([]interface{}, error) { - return fetchRoles(a.MqlRuntime) +func (a *mqlMicrosoftRolemanagement) roleDefinitions() (*mqlMicrosoftRoles, error) { + resource, err := a.MqlRuntime.CreateResource(a.MqlRuntime, "microsoft.roles", map[string]*llx.RawData{}) + if err != nil { + return nil, err + } + + return resource.(*mqlMicrosoftRoles), nil } func (a *mqlMicrosoftRolemanagementRoledefinition) assignments() ([]interface{}, error) { diff --git a/providers/ms365/resources/users.go b/providers/ms365/resources/users.go index d9acf9396a..04a0b3f5cd 100644 --- a/providers/ms365/resources/users.go +++ b/providers/ms365/resources/users.go @@ -9,11 +9,13 @@ import ( "fmt" "time" + abstractions "github.com/microsoft/kiota-abstractions-go" "github.com/microsoftgraph/msgraph-beta-sdk-go/auditlogs" betamodels "github.com/microsoftgraph/msgraph-beta-sdk-go/models" "github.com/microsoftgraph/msgraph-beta-sdk-go/reports" "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/users" + "github.com/rs/zerolog/log" "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" @@ -22,15 +24,37 @@ import ( ) var userSelectFields = []string{ - "id", "accountEnabled", "city", "companyName", "country", "createdDateTime", "department", "displayName", "employeeId", "givenName", - "jobTitle", "mail", "mobilePhone", "otherMails", "officeLocation", "postalCode", "state", "streetAddress", "surname", "userPrincipalName", "userType", - "creationType", "identities", + "id", "accountEnabled", "city", "companyName", "country", "createdDateTime", + "department", "displayName", "employeeId", "givenName", "jobTitle", "mail", + "mobilePhone", "otherMails", "officeLocation", "postalCode", "state", "identities", + "streetAddress", "surname", "userPrincipalName", "userType", "creationType", } -// users reads all users from Entra ID +func (a *mqlMicrosoft) users() (*mqlMicrosoftUsers, error) { + resource, err := a.MqlRuntime.CreateResource(a.MqlRuntime, "microsoft.users", map[string]*llx.RawData{}) + if err != nil { + return nil, err + } + + return resource.(*mqlMicrosoftUsers), nil +} + +func initMicrosoftUsers(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) { + args["__id"] = newListResourceIdFromArguments("microsoft.users", args) + resource, err := runtime.CreateResource(runtime, "microsoft.users", args) + if err != nil { + return args, nil, err + } + + return args, resource.(*mqlMicrosoftUsers), nil +} + +// list fetches users from Entra ID and allows the user provide a filter to retrieve +// a subset of users +// // Permissions: User.Read.All, Directory.Read.All // see https://learn.microsoft.com/en-us/graph/api/user-list?view=graph-rest-1.0&tabs=http -func (a *mqlMicrosoft) users() ([]interface{}, error) { +func (a *mqlMicrosoftUsers) list() ([]interface{}, error) { conn := a.MqlRuntime.Connection.(*connection.Ms365Connection) graphClient, err := conn.GraphClient() if err != nil { @@ -41,40 +65,82 @@ func (a *mqlMicrosoft) users() ([]interface{}, error) { if err != nil { return nil, err } + + // Index of users are stored inside the top level resource `microsoft`, just like + // MFA response. Here we create or get the resource to access those internals + mainResource, err := CreateResource(a.MqlRuntime, "microsoft", map[string]*llx.RawData{}) + if err != nil { + return nil, err + } + microsoft := mainResource.(*mqlMicrosoft) + // fetch user data ctx := context.Background() top := int32(999) - resp, err := graphClient.Users().Get( - ctx, &users.UsersRequestBuilderGetRequestConfiguration{ - QueryParameters: &users.UsersRequestBuilderGetQueryParameters{ - Select: userSelectFields, - Top: &top, - }, + opts := &users.UsersRequestBuilderGetRequestConfiguration{ + QueryParameters: &users.UsersRequestBuilderGetQueryParameters{ + Select: userSelectFields, + Top: &top, }, - ) + } + + if a.Search.State == plugin.StateIsSet || a.Filter.State == plugin.StateIsSet { + // search and filter requires this header + headers := abstractions.NewRequestHeaders() + headers.Add("ConsistencyLevel", "eventual") + opts.Headers = headers + + if a.Search.State == plugin.StateIsSet { + log.Debug(). + Str("search", a.Search.Data). + Msg("microsoft.users.list.search set") + search, err := parseSearch(a.Search.Data) + if err != nil { + return nil, err + } + opts.QueryParameters.Search = &search + } + if a.Filter.State == plugin.StateIsSet { + log.Debug(). + Str("filter", a.Filter.Data). + Msg("microsoft.users.list.filter set") + opts.QueryParameters.Filter = &a.Filter.Data + count := true + opts.QueryParameters.Count = &count + } + } + + resp, err := graphClient.Users().Get(ctx, opts) if err != nil { return nil, transformError(err) } - users, err := iterate[*models.User](ctx, resp, graphClient.GetAdapter(), users.CreateDeltaGetResponseFromDiscriminatorValue) + users, err := iterate[*models.User](ctx, + resp, + graphClient.GetAdapter(), + users.CreateDeltaGetResponseFromDiscriminatorValue, + ) if err != nil { return nil, transformError(err) } - detailsResp, err := betaClient.Reports().AuthenticationMethods().UserRegistrationDetails().Get( - ctx, - &reports.AuthenticationMethodsUserRegistrationDetailsRequestBuilderGetRequestConfiguration{ - QueryParameters: &reports.AuthenticationMethodsUserRegistrationDetailsRequestBuilderGetQueryParameters{ - Top: &top, - }, - }) + 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} + microsoft.mfaResp = mfaResp{err: err} } else { userRegistrationDetails, err := iterate[*betamodels.UserRegistrationDetails](ctx, detailsResp, betaClient.GetAdapter(), betamodels.CreateUserRegistrationDetailsCollectionResponseFromDiscriminatorValue) // 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} + microsoft.mfaResp = mfaResp{err: err} } else { mfaMap := map[string]bool{} for _, u := range userRegistrationDetails { @@ -83,7 +149,7 @@ func (a *mqlMicrosoft) users() ([]interface{}, error) { } mfaMap[*u.GetId()] = *u.GetIsMfaRegistered() } - a.mfaResp = mfaResp{mfaMap: mfaMap} + microsoft.mfaResp = mfaResp{mfaMap: mfaMap} } } @@ -95,7 +161,7 @@ func (a *mqlMicrosoft) users() ([]interface{}, error) { return nil, err } // index users by id and principal name - a.index(graphUser) + microsoft.index(graphUser) res = append(res, graphUser) }