Skip to content

Commit

Permalink
x-pack/filebeat/input/entityanalytics/provider/azuread/fetcher/graph:…
Browse files Browse the repository at this point in the history
… add support for user-defined query selection

The $select optional queries were previously hardcoded. Make these accessible via
a configuration group at the root level.
  • Loading branch information
efd6 committed Jan 17, 2024
1 parent 3e05c2a commit cfcdf67
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 27 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.next.asciidoc
Expand Up @@ -207,6 +207,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*

Expand Down
21 changes: 21 additions & 0 deletions x-pack/filebeat/docs/inputs/input-entity-analytics.asciidoc
Expand Up @@ -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`)
Expand Down
Expand Up @@ -15,6 +15,7 @@ import (
"io"
"net/http"
"net/url"
"strings"

"github.com/google/uuid"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Expand Up @@ -12,6 +12,7 @@ import (
"net/http/httptest"
"path"
"reflect"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -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)
}

0 comments on commit cfcdf67

Please sign in to comment.