Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ type AzureGraphClient interface {
ListAzureADGroups(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.Group]
ListAzureADGroupMembers(ctx context.Context, objectId string, params query.GraphParams) <-chan AzureResult[json.RawMessage]
ListAzureADGroupOwners(ctx context.Context, objectId string, params query.GraphParams) <-chan AzureResult[json.RawMessage]
ListAzureADGroups365(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.Group365]
ListAzureADGroup365Members(ctx context.Context, objectId string, params query.GraphParams) <-chan AzureResult[json.RawMessage]
ListAzureADGroup365Owners(ctx context.Context, objectId string, params query.GraphParams) <-chan AzureResult[json.RawMessage]
ListAzureADAppOwners(ctx context.Context, objectId string, params query.GraphParams) <-chan AzureResult[json.RawMessage]
ListAzureADApps(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.Application]
ListAzureADUsers(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.User]
Expand Down
72 changes: 72 additions & 0 deletions client/groups365.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (C) 2022 Specter Ops, Inc.
//
// This file is part of AzureHound.
//
// AzureHound is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// AzureHound is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

package client

import (
"context"
"encoding/json"
"fmt"

"github.com/bloodhoundad/azurehound/v2/client/query"
"github.com/bloodhoundad/azurehound/v2/constants"
"github.com/bloodhoundad/azurehound/v2/models/azure"
)

// ListAzureGroups Microsoft 365 https://learn.microsoft.com/en-us/graph/api/group-list?view=graph-rest-beta
func (s *azureClient) ListAzureADGroups365(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.Group365] {
var (
out = make(chan AzureResult[azure.Group365])
path = fmt.Sprintf("/%s/groups", constants.GraphApiVersion)
)

if params.Top == 0 {
params.Top = 99
}

go getAzureObjectList[azure.Group365](s.msgraph, ctx, path, params, out)

return out
}

// ListAzureADGroupOwners Microsoft 365 https://learn.microsoft.com/en-us/graph/api/group-list-owners?view=graph-rest-beta
func (s *azureClient) ListAzureADGroup365Owners(ctx context.Context, objectId string, params query.GraphParams) <-chan AzureResult[json.RawMessage] {
var (
out = make(chan AzureResult[json.RawMessage])
path = fmt.Sprintf("/%s/groups/%s/owners", constants.GraphApiBetaVersion, objectId)
)

if params.Top == 0 {
params.Top = 99
}

go getAzureObjectList[json.RawMessage](s.msgraph, ctx, path, params, out)

return out
}

// ListAzureADGroupMembers Microsoft 365 https://learn.microsoft.com/en-us/graph/api/group-list-members?view=graph-rest-beta
func (s *azureClient) ListAzureADGroup365Members(ctx context.Context, objectId string, params query.GraphParams) <-chan AzureResult[json.RawMessage] {
var (
out = make(chan AzureResult[json.RawMessage])
path = fmt.Sprintf("/%s/groups/%s/members", constants.GraphApiBetaVersion, objectId)
)

go getAzureObjectList[json.RawMessage](s.msgraph, ctx, path, params, out)

return out
}
44 changes: 43 additions & 1 deletion client/mocks/client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions cmd/list-azure-ad.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ func listAllAD(ctx context.Context, client client.AzureClient) <-chan interface{
groups2 = make(chan interface{})
groups3 = make(chan interface{})

o365groups = make(chan interface{})
o365groups2 = make(chan interface{})
o365groups3 = make(chan interface{})

roles = make(chan interface{})
roles2 = make(chan interface{})

Expand All @@ -94,6 +98,11 @@ func listAllAD(ctx context.Context, client client.AzureClient) <-chan interface{
groupOwners := listGroupOwners(ctx, client, groups2)
groupMembers := listGroupMembers(ctx, client, groups3)

// Enumerate Microsoft 365 Groups, GroupOwners and GroupMembers
pipeline.Tee(ctx.Done(), listGroups365(ctx, client), o365groups, o365groups2, o365groups3)
group365Owners := listGroup365Owners(ctx, client, o365groups2)
group365Members := listGroup365Members(ctx, client, o365groups3)

// Enumerate ServicePrincipals and ServicePrincipalOwners
pipeline.Tee(ctx.Done(), listServicePrincipals(ctx, client), servicePrincipals, servicePrincipals2, servicePrincipals3)
servicePrincipalOwners := listServicePrincipalOwners(ctx, client, servicePrincipals2)
Expand Down Expand Up @@ -126,6 +135,9 @@ func listAllAD(ctx context.Context, client client.AzureClient) <-chan interface{
groupMembers,
groupOwners,
groups,
group365Members,
group365Owners,
o365groups,
roleAssignments,
roles,
servicePrincipalOwners,
Expand Down
142 changes: 142 additions & 0 deletions cmd/list-group-o365-members.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright (C) 2022 Specter Ops, Inc.
//
// This file is part of AzureHound.
//
// AzureHound is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// AzureHound is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

package cmd

import (
"context"
"fmt"
"os"
"os/signal"
"sync"
"time"

"github.com/bloodhoundad/azurehound/v2/client"
"github.com/bloodhoundad/azurehound/v2/client/query"
"github.com/bloodhoundad/azurehound/v2/config"
"github.com/bloodhoundad/azurehound/v2/enums"
"github.com/bloodhoundad/azurehound/v2/models"
"github.com/bloodhoundad/azurehound/v2/panicrecovery"
"github.com/bloodhoundad/azurehound/v2/pipeline"
"github.com/spf13/cobra"
)

func init() {
listRootCmd.AddCommand(listGroup365MembersCmd)
listGroup365MembersCmd.Flags().StringSliceVar(&listGroup365MembersSelect, "select", []string{"id,displayName,createdDateTime"}, `Select properties to include. Use "" for Azure default properties. Azurehound default is "id,displayName,createdDateTime" if flag is not supplied.`)
}

var listGroup365MembersCmd = &cobra.Command{
Use: "group365-members",
Long: "Lists Azure AD Group Microsoft 365 Members",
Run: listGroup365MembersCmdImpl,
SilenceUsage: true,
}

var listGroup365MembersSelect []string

func listGroup365MembersCmdImpl(cmd *cobra.Command, _ []string) {
ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill)
defer gracefulShutdown(stop)

log.V(1).Info("testing connections")
azClient := connectAndCreateClient()
log.Info("collecting azure group microsoft 365 members...")
start := time.Now()
stream := listGroup365Members(ctx, azClient, listGroups365(ctx, azClient))
outputStream(ctx, stream)
duration := time.Since(start)
log.Info("collection completed", "duration", duration.String())
}

func listGroup365Members(ctx context.Context, client client.AzureClient, groups <-chan interface{}) <-chan interface{} {
var (
out = make(chan interface{})
ids = make(chan string)
streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int))
wg sync.WaitGroup
params = query.GraphParams{
Select: unique(listGroup365MembersSelect),
Filter: "",
Count: false,
Search: "",
Top: 0,
Expand: "",
}
)

go func() {
defer panicrecovery.PanicRecovery()
defer close(ids)

for result := range pipeline.OrDone(ctx.Done(), groups) {
if group, ok := result.(AzureWrapper).Data.(models.Group365); !ok {
log.Error(fmt.Errorf("failed group 365 type assertion"), "unable to continue enumerating group Microsoft 365 members", "result", result)
return
} else {
if ok := pipeline.Send(ctx.Done(), ids, group.Id); !ok {
return
}
}
}
}()

wg.Add(len(streams))
for i := range streams {
stream := streams[i]
go func() {
defer panicrecovery.PanicRecovery()
defer wg.Done()
for id := range stream {
var (
data = models.Group365Members{
GroupId: id,
}
count = 0
)
for item := range client.ListAzureADGroup365Members(ctx, id, params) {
if item.Error != nil {
log.Error(item.Error, "unable to continue processing members for this Microsoft 365 group", "groupId", id)
} else {
group365Member := models.Group365Member{
Member: item.Ok,
GroupId: id,
}
log.V(2).Info("found group Microsoft 365 member", "groupMember", group365Member)
count++
data.Members = append(data.Members, group365Member)
}
}
if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{
Kind: enums.KindAZGroup365Member,
Data: data,
}); !ok {
return
}
log.V(1).Info("finished listing group memberships", "groupId", id, "count", count)
}
}()
}

go func() {
wg.Wait()
close(out)
log.Info("finished listing members for all Microsoft 365 groups")
}()

return out
}
Loading