Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

x-pack/filebeat/input/entityanalytics/provider/azuread/fetcher/graph: add support for user-defined query selection #37653

Merged
merged 1 commit into from Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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"]`.
bhapas marked this conversation as resolved.
Show resolved Hide resolved

[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)
}