diff --git a/CHANGELOG.next.asciidoc b/CHANGELOG.next.asciidoc index 862231e015e..c6b4b4e6e4f 100644 --- a/CHANGELOG.next.asciidoc +++ b/CHANGELOG.next.asciidoc @@ -166,6 +166,7 @@ Setting environmental variable ELASTIC_NETINFO:false in Elastic Agent pod will d - Prevent CEL input from re-entering the eval loop when an evaluation failed. {pull}37161[37161] - Update CEL extensions library to v1.7.0. {pull}37172[37172] - Add support for complete URL replacement in HTTPJSON chain steps. {pull}37486[37486] +- Add support for user-defined query selection in EntraID entity analytics provider. {pull}37653[37653] *Auditbeat* diff --git a/x-pack/filebeat/docs/inputs/input-entity-analytics.asciidoc b/x-pack/filebeat/docs/inputs/input-entity-analytics.asciidoc index bb86de8ebcc..86143f727bc 100644 --- a/x-pack/filebeat/docs/inputs/input-entity-analytics.asciidoc +++ b/x-pack/filebeat/docs/inputs/input-entity-analytics.asciidoc @@ -314,6 +314,27 @@ so. Altering this value will also require a change to `login_scopes`. Override the default authentication scopes. Only change if directed to do so. +[float] +===== `select.users` + +Override the default https://learn.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0&tabs=http#optional-query-parameters[user query selections]. +This is a list of optional query parameters. The default is `["accountEnabled", "userPrincipalName", +"mail", "displayName", "givenName", "surname", "jobTitle", "officeLocation", "mobilePhone", +"businessPhones"]`. + +[float] +===== `select.groups` + +Override the default https://learn.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0&tabs=http#optional-query-parameters[group query selections]. +This is a list of optional query parameters. The default is `["displayName", "members"]`. + +[float] +===== `select.devices` + +Override the default https://learn.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0&tabs=http#optional-query-parameters[device query selections]. +This is a list of optional query parameters. The default is `["accountEnabled", "deviceId", +"displayName", "operatingSystem", "operatingSystemVersion", "physicalIds", "extensionAttributes", +"alternativeSecurityIds"]`. [id="provider-okta"] ==== Okta User Identities (`okta`) diff --git a/x-pack/filebeat/input/entityanalytics/provider/azuread/fetcher/graph/graph.go b/x-pack/filebeat/input/entityanalytics/provider/azuread/fetcher/graph/graph.go index 44754e10fa6..6cabdf887e8 100644 --- a/x-pack/filebeat/input/entityanalytics/provider/azuread/fetcher/graph/graph.go +++ b/x-pack/filebeat/input/entityanalytics/provider/azuread/fetcher/graph/graph.go @@ -15,6 +15,7 @@ import ( "io" "net/http" "net/url" + "strings" "github.com/google/uuid" @@ -98,11 +99,18 @@ type removed struct { // conf contains parameters needed to configure the fetcher. type graphConf struct { - APIEndpoint string `config:"api_endpoint"` + APIEndpoint string `config:"api_endpoint"` + Select selection `config:"select"` Transport httpcommon.HTTPTransportSettings `config:",inline"` } +type selection struct { + UserQuery []string `config:"users"` + GroupQuery []string `config:"groups"` + DeviceQuery []string `config:"devices"` +} + // graph implements the fetcher.Fetcher interface. type graph struct { conf graphConf @@ -345,21 +353,21 @@ func New(cfg *config.C, logger *logp.Logger, auth authenticator.Authenticator) ( if err != nil { return nil, fmt.Errorf("invalid groups URL endpoint: %w", err) } - groupsURL.RawQuery = url.QueryEscape(defaultGroupsQuery) + groupsURL.RawQuery = url.QueryEscape(formatQuery(c.Select.GroupQuery, defaultGroupsQuery)) f.groupsURL = groupsURL.String() usersURL, err := url.Parse(f.conf.APIEndpoint + "/users/delta") if err != nil { return nil, fmt.Errorf("invalid users URL endpoint: %w", err) } - usersURL.RawQuery = url.QueryEscape(defaultUsersQuery) + usersURL.RawQuery = url.QueryEscape(formatQuery(c.Select.UserQuery, defaultUsersQuery)) f.usersURL = usersURL.String() devicesURL, err := url.Parse(f.conf.APIEndpoint + "/devices/delta") if err != nil { return nil, fmt.Errorf("invalid devices URL endpoint: %w", err) } - devicesURL.RawQuery = url.QueryEscape(defaultDevicesQuery) + devicesURL.RawQuery = url.QueryEscape(formatQuery(c.Select.DeviceQuery, defaultDevicesQuery)) f.devicesURL = devicesURL.String() // The API takes a departure from the query approach here, so we @@ -374,6 +382,13 @@ func New(cfg *config.C, logger *logp.Logger, auth authenticator.Authenticator) ( return &f, nil } +func formatQuery(query []string, dflt string) string { + if len(query) == 0 { + return dflt + } + return "$select=" + strings.Join(query, ",") +} + // newUserFromAPI translates an API-representation of a user to a fetcher.User. func newUserFromAPI(u userAPI) (*fetcher.User, error) { var newUser fetcher.User diff --git a/x-pack/filebeat/input/entityanalytics/provider/azuread/fetcher/graph/graph_test.go b/x-pack/filebeat/input/entityanalytics/provider/azuread/fetcher/graph/graph_test.go index e54a05a2bd5..f439cc91679 100644 --- a/x-pack/filebeat/input/entityanalytics/provider/azuread/fetcher/graph/graph_test.go +++ b/x-pack/filebeat/input/entityanalytics/provider/azuread/fetcher/graph/graph_test.go @@ -12,6 +12,7 @@ import ( "net/http/httptest" "path" "reflect" + "strings" "testing" "time" @@ -457,28 +458,46 @@ func TestGraph_Devices(t *testing.T) { }, } - rawConf := graphConf{ - APIEndpoint: "http://" + testSrv.addr, - } - c, err := config.NewConfigFrom(&rawConf) - require.NoError(t, err) - auth := mock.New(mock.DefaultTokenValue) - - f, err := New(c, logp.L(), auth) - require.NoError(t, err) - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - gotDevices, gotDeltaLink, gotErr := f.Devices(ctx, "") - - require.NoError(t, gotErr) - // Using go-cmp because testify is too weak for this comparison. - // reflect.DeepEqual works, but won't show a reasonable diff. - exporter := cmp.Exporter(func(t reflect.Type) bool { - return t == reflect.TypeOf(collections.UUIDSet{}) - }) - if !cmp.Equal(wantDevices, gotDevices, exporter) { - t.Errorf("unexpected result:\n--- got\n--- want\n%s", cmp.Diff(wantDevices, gotDevices, exporter)) + for _, test := range []struct { + name string + selection selection + }{ + {name: "default_selection"}, + { + name: "user_selection", + selection: selection{ + UserQuery: strings.Split(strings.TrimPrefix(defaultUsersQuery, "$select="), ","), + GroupQuery: strings.Split(strings.TrimPrefix(defaultGroupsQuery, "$select="), ","), + DeviceQuery: strings.Split(strings.TrimPrefix(defaultDevicesQuery, "$select="), ","), + }, + }, + } { + t.Run(test.name, func(t *testing.T) { + rawConf := graphConf{ + APIEndpoint: "http://" + testSrv.addr, + Select: test.selection, + } + c, err := config.NewConfigFrom(&rawConf) + require.NoError(t, err) + auth := mock.New(mock.DefaultTokenValue) + + f, err := New(c, logp.L(), auth) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + gotDevices, gotDeltaLink, gotErr := f.Devices(ctx, "") + + require.NoError(t, gotErr) + // Using go-cmp because testify is too weak for this comparison. + // reflect.DeepEqual works, but won't show a reasonable diff. + exporter := cmp.Exporter(func(t reflect.Type) bool { + return t == reflect.TypeOf(collections.UUIDSet{}) + }) + if !cmp.Equal(wantDevices, gotDevices, exporter) { + t.Errorf("unexpected result:\n--- got\n--- want\n%s", cmp.Diff(wantDevices, gotDevices, exporter)) + } + require.Equal(t, wantDeltaLink, gotDeltaLink) + }) } - require.Equal(t, wantDeltaLink, gotDeltaLink) }