|
| 1 | +// Copyright (c) Mondoo, Inc. |
| 2 | +// SPDX-License-Identifier: BUSL-1.1 |
| 3 | + |
| 4 | +package resources |
| 5 | + |
| 6 | +import ( |
| 7 | + "context" |
| 8 | + "errors" |
| 9 | + "fmt" |
| 10 | + |
| 11 | + abstractions "github.com/microsoft/kiota-abstractions-go" |
| 12 | + betamodels "github.com/microsoftgraph/msgraph-beta-sdk-go/models" |
| 13 | + "github.com/microsoftgraph/msgraph-beta-sdk-go/reports" |
| 14 | + "github.com/microsoftgraph/msgraph-sdk-go/devices" |
| 15 | + "github.com/microsoftgraph/msgraph-sdk-go/models" |
| 16 | + "github.com/rs/zerolog/log" |
| 17 | + "go.mondoo.com/cnquery/v11/llx" |
| 18 | + "go.mondoo.com/cnquery/v11/providers-sdk/v1/plugin" |
| 19 | + "go.mondoo.com/cnquery/v11/providers-sdk/v1/util/convert" |
| 20 | + "go.mondoo.com/cnquery/v11/providers/ms365/connection" |
| 21 | + "go.mondoo.com/cnquery/v11/types" |
| 22 | +) |
| 23 | + |
| 24 | +// see https://learn.microsoft.com/en-us/graph/api/resources/device?view=graph-rest-1.0 |
| 25 | +var deviceSelectFields = []string{ |
| 26 | + "id", "displayName", "deviceId", "deviceCategory", "enrollmentProfileName", "enrollmentType", |
| 27 | + "isCompliant", "isManaged", "manufacturer", "isRooted", "mdmAppId", "model", "operatingSystem", |
| 28 | + "operatingSystemVersion", "physicalIds", "registrationDateTime", "systemLabels", "trustType", |
| 29 | +} |
| 30 | + |
| 31 | +func initMicrosoftDevices(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) { |
| 32 | + args["__id"] = newListResourceIdFromArguments("microsoft.devices", args) |
| 33 | + resource, err := runtime.CreateResource(runtime, "microsoft.devices", args) |
| 34 | + if err != nil { |
| 35 | + return args, nil, err |
| 36 | + } |
| 37 | + |
| 38 | + return args, resource.(*mqlMicrosoftDevices), nil |
| 39 | +} |
| 40 | + |
| 41 | +// list fetches devices from Entra ID and allows the user provide a filter to retrieve |
| 42 | +// a subset of devices |
| 43 | +// |
| 44 | +// Permissions: Device.Read.All |
| 45 | +// see https://learn.microsoft.com/en-us/graph/api/device-list?view=graph-rest-1.0&tabs=http |
| 46 | +func (a *mqlMicrosoftDevices) list() ([]interface{}, error) { |
| 47 | + conn := a.MqlRuntime.Connection.(*connection.Ms365Connection) |
| 48 | + graphClient, err := conn.GraphClient() |
| 49 | + if err != nil { |
| 50 | + return nil, err |
| 51 | + } |
| 52 | + |
| 53 | + betaClient, err := conn.BetaGraphClient() |
| 54 | + if err != nil { |
| 55 | + return nil, err |
| 56 | + } |
| 57 | + |
| 58 | + // Index of devices are stored inside the top level resource `microsoft`, just like |
| 59 | + // MFA response. Here we create or get the resource to access those internals |
| 60 | + mainResource, err := CreateResource(a.MqlRuntime, "microsoft", map[string]*llx.RawData{}) |
| 61 | + if err != nil { |
| 62 | + return nil, err |
| 63 | + } |
| 64 | + microsoft := mainResource.(*mqlMicrosoft) |
| 65 | + |
| 66 | + // fetch device data |
| 67 | + ctx := context.Background() |
| 68 | + top := int32(999) |
| 69 | + opts := &devices.DevicesRequestBuilderGetRequestConfiguration{ |
| 70 | + QueryParameters: &devices.DevicesRequestBuilderGetQueryParameters{ |
| 71 | + Select: deviceSelectFields, |
| 72 | + Top: &top, |
| 73 | + }, |
| 74 | + } |
| 75 | + |
| 76 | + if a.Search.State == plugin.StateIsSet || a.Filter.State == plugin.StateIsSet { |
| 77 | + // search and filter requires this header |
| 78 | + headers := abstractions.NewRequestHeaders() |
| 79 | + headers.Add("ConsistencyLevel", "eventual") |
| 80 | + opts.Headers = headers |
| 81 | + |
| 82 | + if a.Search.State == plugin.StateIsSet { |
| 83 | + log.Debug(). |
| 84 | + Str("search", a.Search.Data). |
| 85 | + Msg("microsoft.devices.list.search set") |
| 86 | + search, err := parseSearch(a.Search.Data) |
| 87 | + if err != nil { |
| 88 | + return nil, err |
| 89 | + } |
| 90 | + opts.QueryParameters.Search = &search |
| 91 | + } |
| 92 | + if a.Filter.State == plugin.StateIsSet { |
| 93 | + log.Debug(). |
| 94 | + Str("filter", a.Filter.Data). |
| 95 | + Msg("microsoft.devices.list.filter set") |
| 96 | + opts.QueryParameters.Filter = &a.Filter.Data |
| 97 | + count := true |
| 98 | + opts.QueryParameters.Count = &count |
| 99 | + } |
| 100 | + } |
| 101 | + |
| 102 | + resp, err := graphClient.Devices().Get(ctx, opts) |
| 103 | + if err != nil { |
| 104 | + return nil, transformError(err) |
| 105 | + } |
| 106 | + devices, err := iterate[*models.Device](ctx, |
| 107 | + resp, |
| 108 | + graphClient.GetAdapter(), |
| 109 | + devices.CreateDeltaGetResponseFromDiscriminatorValue, |
| 110 | + ) |
| 111 | + if err != nil { |
| 112 | + return nil, transformError(err) |
| 113 | + } |
| 114 | + |
| 115 | + detailsResp, err := betaClient. |
| 116 | + Reports(). |
| 117 | + AuthenticationMethods(). |
| 118 | + UserRegistrationDetails(). |
| 119 | + Get(ctx, |
| 120 | + &reports.AuthenticationMethodsUserRegistrationDetailsRequestBuilderGetRequestConfiguration{ |
| 121 | + QueryParameters: &reports.AuthenticationMethodsUserRegistrationDetailsRequestBuilderGetQueryParameters{ |
| 122 | + Top: &top, |
| 123 | + }, |
| 124 | + }) |
| 125 | + // we do not want to fail the device fetching here, this likely means the tenant does not have the right license |
| 126 | + if err != nil { |
| 127 | + microsoft.mfaResp = mfaResp{err: err} |
| 128 | + } else { |
| 129 | + userRegistrationDetails, err := iterate[*betamodels.UserRegistrationDetails](ctx, detailsResp, betaClient.GetAdapter(), betamodels.CreateUserRegistrationDetailsCollectionResponseFromDiscriminatorValue) |
| 130 | + // we do not want to fail the device fetching here, this likely means the tenant does not have the right license |
| 131 | + if err != nil { |
| 132 | + microsoft.mfaResp = mfaResp{err: err} |
| 133 | + } else { |
| 134 | + mfaMap := map[string]bool{} |
| 135 | + for _, u := range userRegistrationDetails { |
| 136 | + if u.GetId() == nil || u.GetIsMfaRegistered() == nil { |
| 137 | + continue |
| 138 | + } |
| 139 | + mfaMap[*u.GetId()] = *u.GetIsMfaRegistered() |
| 140 | + } |
| 141 | + microsoft.mfaResp = mfaResp{mfaMap: mfaMap} |
| 142 | + } |
| 143 | + } |
| 144 | + |
| 145 | + // construct the result |
| 146 | + res := []interface{}{} |
| 147 | + for _, u := range devices { |
| 148 | + graphDevice, err := newMqlMicrosoftDevice(a.MqlRuntime, u) |
| 149 | + if err != nil { |
| 150 | + return nil, err |
| 151 | + } |
| 152 | + // indexUser devices by id |
| 153 | + microsoft.indexDevice(graphDevice) |
| 154 | + res = append(res, graphDevice) |
| 155 | + } |
| 156 | + |
| 157 | + return res, nil |
| 158 | +} |
| 159 | + |
| 160 | +func initMicrosoftDevice(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) { |
| 161 | + // we only look up the user if we have been supplied by id, displayName or userPrincipalName |
| 162 | + if len(args) > 1 { |
| 163 | + return args, nil, nil |
| 164 | + } |
| 165 | + |
| 166 | + rawId, okId := args["id"] |
| 167 | + rawDisplayName, okDisplayName := args["displayName"] |
| 168 | + |
| 169 | + if !okId && !okDisplayName { |
| 170 | + // required parameters are not set, we just pass-through the initialization arguments |
| 171 | + return args, nil, nil |
| 172 | + } |
| 173 | + |
| 174 | + var filter *string |
| 175 | + if okId { |
| 176 | + idFilter := fmt.Sprintf("id eq '%s'", rawId.Value.(string)) |
| 177 | + filter = &idFilter |
| 178 | + } else if okDisplayName { |
| 179 | + displayNameFilter := fmt.Sprintf("displayName eq '%s'", rawDisplayName.Value.(string)) |
| 180 | + filter = &displayNameFilter |
| 181 | + } |
| 182 | + if filter == nil { |
| 183 | + return nil, nil, errors.New("no filter found") |
| 184 | + } |
| 185 | + |
| 186 | + conn := runtime.Connection.(*connection.Ms365Connection) |
| 187 | + graphClient, err := conn.GraphClient() |
| 188 | + if err != nil { |
| 189 | + return nil, nil, err |
| 190 | + } |
| 191 | + |
| 192 | + ctx := context.Background() |
| 193 | + resp, err := graphClient.Devices().Get(ctx, &devices.DevicesRequestBuilderGetRequestConfiguration{ |
| 194 | + QueryParameters: &devices.DevicesRequestBuilderGetQueryParameters{ |
| 195 | + Filter: filter, |
| 196 | + }, |
| 197 | + }) |
| 198 | + if err != nil { |
| 199 | + return nil, nil, transformError(err) |
| 200 | + } |
| 201 | + |
| 202 | + val := resp.GetValue() |
| 203 | + if len(val) == 0 { |
| 204 | + return nil, nil, errors.New("device not found") |
| 205 | + } |
| 206 | + |
| 207 | + deviceId := val[0].GetId() |
| 208 | + if deviceId == nil { |
| 209 | + return nil, nil, errors.New("device id not found") |
| 210 | + } |
| 211 | + |
| 212 | + // fetch devices by id |
| 213 | + device, err := graphClient.Devices().ByDeviceId(*deviceId).Get(ctx, &devices.DeviceItemRequestBuilderGetRequestConfiguration{}) |
| 214 | + if err != nil { |
| 215 | + return nil, nil, transformError(err) |
| 216 | + } |
| 217 | + mqlMsApp, err := newMqlMicrosoftDevice(runtime, device) |
| 218 | + if err != nil { |
| 219 | + return nil, nil, err |
| 220 | + } |
| 221 | + |
| 222 | + return nil, mqlMsApp, nil |
| 223 | +} |
| 224 | + |
| 225 | +func newMqlMicrosoftDevice(runtime *plugin.Runtime, u models.Deviceable) (*mqlMicrosoftDevice, error) { |
| 226 | + graphDevice, err := CreateResource(runtime, "microsoft.device", |
| 227 | + map[string]*llx.RawData{ |
| 228 | + "__id": llx.StringDataPtr(u.GetId()), |
| 229 | + "id": llx.StringDataPtr(u.GetId()), |
| 230 | + "displayName": llx.StringDataPtr(u.GetDisplayName()), |
| 231 | + "deviceId": llx.StringDataPtr(u.GetDeviceId()), |
| 232 | + "deviceCategory": llx.StringDataPtr(u.GetDeviceCategory()), |
| 233 | + "enrollmentProfileName": llx.StringDataPtr(u.GetEnrollmentProfileName()), |
| 234 | + "enrollmentType": llx.StringDataPtr(u.GetEnrollmentType()), |
| 235 | + "isCompliant": llx.BoolDataPtr(u.GetIsCompliant()), |
| 236 | + "isManaged": llx.BoolDataPtr(u.GetIsManaged()), |
| 237 | + "manufacturer": llx.StringDataPtr(u.GetManufacturer()), |
| 238 | + "isRooted": llx.BoolDataPtr(u.GetIsRooted()), |
| 239 | + "mdmAppId": llx.StringDataPtr(u.GetMdmAppId()), |
| 240 | + "model": llx.StringDataPtr(u.GetModel()), |
| 241 | + "operatingSystem": llx.StringDataPtr(u.GetOperatingSystem()), |
| 242 | + "operatingSystemVersion": llx.StringDataPtr(u.GetOperatingSystemVersion()), |
| 243 | + "physicalIds": llx.ArrayData(convert.SliceAnyToInterface(u.GetPhysicalIds()), types.String), |
| 244 | + "registrationDateTime": llx.TimeDataPtr(u.GetRegistrationDateTime()), |
| 245 | + "systemLabels": llx.ArrayData(convert.SliceAnyToInterface(u.GetSystemLabels()), types.String), |
| 246 | + "trustType": llx.StringDataPtr(u.GetTrustType()), |
| 247 | + }) |
| 248 | + if err != nil { |
| 249 | + return nil, err |
| 250 | + } |
| 251 | + return graphDevice.(*mqlMicrosoftDevice), nil |
| 252 | +} |
0 commit comments