Skip to content

Commit 8cc57b4

Browse files
mergify[bot]efd6
andauthored
[8.19](backport #44761) x-pack/filebeat/input/entityanalytic/provider/azuread/fetcher/graph: add support for expand (#45016)
* x-pack/filebeat/input/entityanalytic/provider/azuread/fetcher/graph: add support for expand (#44761) (cherry picked from commit 3ca2970) # Conflicts: # docs/reference/filebeat/filebeat-input-entity-analytics.md * remove incorrectly back-ported markdown * add asciidoc --------- Co-authored-by: Dan Kortschak <dan.kortschak@elastic.co>
1 parent ccea0c7 commit 8cc57b4

File tree

4 files changed

+147
-5
lines changed

4 files changed

+147
-5
lines changed

CHANGELOG.next.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,7 @@ otherwise no tag is added. {issue}42208[42208] {pull}42403[42403]
416416
- Filestream now logs at level warn the number of files that are too small to be ingested {pull}44751[44751]
417417
- Add Fleet status updating to HTTP JSON input. {issue}44282[44282] {pull}44365[44365]
418418
- Add proxy support to GCP Pub/Sub input. {pull}44892[44892]
419+
- Add support for relationship expansion to EntraID entity analytics provider. {issue}43324[43324] {pull}44761[44761]
419420

420421
*Auditbeat*
421422

x-pack/filebeat/docs/inputs/input-entity-analytics.asciidoc

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,13 @@ Example configuration:
455455
client_id: "CLIENT_ID"
456456
tenant_id: "TENANT_ID"
457457
secret: "SECRET"
458+
expand:
459+
users:
460+
manager:
461+
- displayName
462+
- id
463+
directReports:
464+
- id
458465
----
459466

460467
The `azure-ad` provider supports the following configuration:
@@ -529,6 +536,27 @@ This is a list of optional query parameters. The default is `["accountEnabled",
529536
"displayName", "operatingSystem", "operatingSystemVersion", "physicalIds", "extensionAttributes",
530537
"alternativeSecurityIds"]`.
531538

539+
[float]
540+
===== `expand.users`
541+
542+
Add https://learn.microsoft.com/en-us/graph/api/resources/user?view=graph-rest-1.0#relationships[user query relationship expansions].
543+
This is a map of relationship names to attribute lists. By default this is not set. If an empty
544+
relationship list is given, the relationship expansion is the same as the users query.
545+
546+
[float]
547+
===== `expand.groups`
548+
549+
Add https://learn.microsoft.com/en-us/graph/api/resources/group?view=graph-rest-1.0#relationships[group query relationship expansions].
550+
This is a map of relationship names to attribute lists. By default this is not set. If an empty
551+
relationship list is given, the relationship expansion is the same as the groups query.
552+
553+
[float]
554+
===== `expand.devices`
555+
556+
Add https://learn.microsoft.com/en-us/graph/api/resources/device?view=graph-rest-1.0#relationships[device query relationship expansions].
557+
This is a map of relationship names to attribute lists. By default this is not set. If an empty
558+
relationship list is given, the relationship expansion is the same as the devices query.
559+
532560
[float]
533561
==== `tracer.enabled`
534562

x-pack/filebeat/input/entityanalytics/provider/azuread/fetcher/graph/graph.go

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"net/url"
1919
"os"
2020
"path/filepath"
21+
"sort"
2122
"strings"
2223

2324
"github.com/gofrs/uuid/v5"
@@ -43,6 +44,7 @@ const (
4344
defaultGroupsQuery = "displayName,members"
4445
defaultUsersQuery = "accountEnabled,userPrincipalName,mail,displayName,givenName,surname,jobTitle,officeLocation,mobilePhone,businessPhones"
4546
defaultDevicesQuery = "accountEnabled,deviceId,displayName,operatingSystem,operatingSystemVersion,physicalIds,extensionAttributes,alternativeSecurityIds"
47+
expandName = "$expand"
4648

4749
apiGroupType = "#microsoft.graph.group"
4850
apiUserType = "#microsoft.graph.user"
@@ -110,6 +112,7 @@ type removed struct {
110112
type graphConf struct {
111113
APIEndpoint string `config:"api_endpoint"`
112114
Select selection `config:"select"`
115+
Expand expansion `config:"expand"`
113116

114117
Transport httpcommon.HTTPTransportSettings `config:",inline"`
115118

@@ -132,6 +135,12 @@ type selection struct {
132135
DeviceQuery []string `config:"devices"`
133136
}
134137

138+
type expansion struct {
139+
UserExpansion map[string][]string `config:"users"`
140+
GroupExpansion map[string][]string `config:"groups"`
141+
DeviceExpansion map[string][]string `config:"devices"`
142+
}
143+
135144
// graph implements the fetcher.Fetcher interface.
136145
type graph struct {
137146
conf graphConf
@@ -380,21 +389,30 @@ func New(ctx context.Context, id string, cfg *config.C, logger *logp.Logger, aut
380389
if err != nil {
381390
return nil, fmt.Errorf("invalid groups URL endpoint: %w", err)
382391
}
383-
groupsURL.RawQuery = formatQuery(queryName, c.Select.GroupQuery, defaultGroupsQuery)
392+
groupsURL.RawQuery, err = formatQuery(queryName, c.Select.GroupQuery, defaultGroupsQuery, c.Expand.GroupExpansion)
393+
if err != nil {
394+
return nil, fmt.Errorf("failed to format group query: %w", err)
395+
}
384396
f.groupsURL = groupsURL.String()
385397

386398
usersURL, err := url.Parse(f.conf.APIEndpoint + "/users/delta")
387399
if err != nil {
388400
return nil, fmt.Errorf("invalid users URL endpoint: %w", err)
389401
}
390-
usersURL.RawQuery = formatQuery(queryName, c.Select.UserQuery, defaultUsersQuery)
402+
usersURL.RawQuery, err = formatQuery(queryName, c.Select.UserQuery, defaultUsersQuery, c.Expand.UserExpansion)
403+
if err != nil {
404+
return nil, fmt.Errorf("failed to format user query: %w", err)
405+
}
391406
f.usersURL = usersURL.String()
392407

393408
devicesURL, err := url.Parse(f.conf.APIEndpoint + "/devices/delta")
394409
if err != nil {
395410
return nil, fmt.Errorf("invalid devices URL endpoint: %w", err)
396411
}
397-
devicesURL.RawQuery = formatQuery(queryName, c.Select.DeviceQuery, defaultDevicesQuery)
412+
devicesURL.RawQuery, err = formatQuery(queryName, c.Select.DeviceQuery, defaultDevicesQuery, c.Expand.DeviceExpansion)
413+
if err != nil {
414+
return nil, fmt.Errorf("failed to format device query: %w", err)
415+
}
398416
f.devicesURL = devicesURL.String()
399417

400418
// The API takes a departure from the query approach here, so we
@@ -470,12 +488,28 @@ func sanitizeFileName(name string) string {
470488
return strings.ReplaceAll(name, string(filepath.Separator), "_")
471489
}
472490

473-
func formatQuery(name string, query []string, dflt string) string {
491+
func formatQuery(name string, query []string, dflt string, expand map[string][]string) (string, error) {
474492
q := dflt
475493
if len(query) != 0 {
476494
q = strings.Join(query, ",")
477495
}
478-
return url.Values{name: []string{q}}.Encode()
496+
vals := url.Values{name: []string{q}}
497+
if len(expand) != 0 {
498+
exp := make([]string, 0, len(expand))
499+
for k := range expand {
500+
exp = append(exp, k)
501+
}
502+
sort.Strings(exp)
503+
for i, k := range exp {
504+
v, err := formatQuery(name, expand[k], q, nil)
505+
if err != nil {
506+
return "", err
507+
}
508+
exp[i] = fmt.Sprintf("%s(%s)", k, v)
509+
}
510+
vals.Add(expandName, strings.Join(exp, ","))
511+
}
512+
return url.QueryUnescape(vals.Encode())
479513
}
480514

481515
// newUserFromAPI translates an API-representation of a user to a fetcher.User.

x-pack/filebeat/input/entityanalytics/provider/azuread/fetcher/graph/graph_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,3 +521,82 @@ func TestGraph_Devices(t *testing.T) {
521521
})
522522
}
523523
}
524+
525+
var formatQueryTests = []struct {
526+
name string
527+
query []string
528+
deflt string
529+
expand map[string][]string
530+
want string
531+
}{
532+
{
533+
name: "default",
534+
query: nil,
535+
deflt: defaultUsersQuery,
536+
expand: nil,
537+
want: "$select=accountEnabled,userPrincipalName,mail,displayName,givenName,surname,jobTitle,officeLocation,mobilePhone,businessPhones",
538+
},
539+
{
540+
name: "defined_query_no_expand",
541+
query: []string{"id", "displayName"},
542+
deflt: defaultUsersQuery,
543+
expand: nil,
544+
want: "$select=id,displayName",
545+
},
546+
{
547+
name: "defined_query_empty_expand",
548+
query: []string{"id", "displayName"},
549+
deflt: defaultUsersQuery,
550+
expand: map[string][]string{},
551+
want: "$select=id,displayName",
552+
},
553+
{
554+
name: "default_manager_default",
555+
query: nil,
556+
deflt: defaultUsersQuery,
557+
expand: map[string][]string{"manager": {}},
558+
want: "$expand=manager($select=accountEnabled,userPrincipalName,mail,displayName,givenName,surname,jobTitle,officeLocation,mobilePhone,businessPhones)&$select=accountEnabled,userPrincipalName,mail,displayName,givenName,surname,jobTitle,officeLocation,mobilePhone,businessPhones",
559+
},
560+
{
561+
name: "default_manager_id",
562+
query: nil,
563+
deflt: defaultUsersQuery,
564+
expand: map[string][]string{"manager": {"id"}},
565+
want: "$expand=manager($select=id)&$select=accountEnabled,userPrincipalName,mail,displayName,givenName,surname,jobTitle,officeLocation,mobilePhone,businessPhones",
566+
},
567+
{
568+
name: "defined_manager_id",
569+
query: []string{"id", "displayName"},
570+
deflt: defaultUsersQuery,
571+
expand: map[string][]string{"manager": {"id"}},
572+
want: "$expand=manager($select=id)&$select=id,displayName",
573+
},
574+
{
575+
name: "defined_manager_default",
576+
query: []string{"id", "displayName"},
577+
deflt: defaultUsersQuery,
578+
expand: map[string][]string{"manager": {}},
579+
want: "$expand=manager($select=id,displayName)&$select=id,displayName",
580+
},
581+
{
582+
name: "expand_two",
583+
query: []string{"id", "displayName"},
584+
deflt: defaultUsersQuery,
585+
expand: map[string][]string{"manager": {}, "directReports": {}},
586+
want: "$expand=directReports($select=id,displayName),manager($select=id,displayName)&$select=id,displayName",
587+
},
588+
}
589+
590+
func TestFormatQuery(t *testing.T) {
591+
for _, test := range formatQueryTests {
592+
t.Run(test.name, func(t *testing.T) {
593+
got, err := formatQuery(queryName, test.query, test.deflt, test.expand)
594+
if err != nil {
595+
t.Fatalf("unexpected error from formatQuery: %v", err)
596+
}
597+
if got != test.want {
598+
t.Errorf("unexpected query string: got=%q want=%q", got, test.want)
599+
}
600+
})
601+
}
602+
}

0 commit comments

Comments
 (0)