Skip to content

Commit

Permalink
Add feature to collect eligible groups
Browse files Browse the repository at this point in the history
  • Loading branch information
youduda committed Aug 29, 2023
1 parent 9f37352 commit 5dd99c3
Show file tree
Hide file tree
Showing 8 changed files with 438 additions and 63 deletions.
3 changes: 3 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ type AzureClient interface {
GetAzureADApps(ctx context.Context, filter, search, orderBy, expand string, selectCols []string, top int32, count bool) (azure.ApplicationList, error)
GetAzureADDirectoryObject(ctx context.Context, objectId string) (json.RawMessage, error)
GetAzureADGroup(ctx context.Context, objectId string, selectCols []string) (*azure.Group, error)
GetAzureADGroupEligibilityScheduleInstance(ctx context.Context, objectId string, selectCols []string) (*azure.PrivilegedAccessGroupEligibilityScheduleInstance, error)
GetAzureADGroupEligibilityScheduleInstances(ctx context.Context, filter, search, orderBy, expand string, selectCols []string, top int32, count bool) (azure.PrivilegedAccessGroupEligibilityScheduleInstanceList, error)
GetAzureADGroupOwners(ctx context.Context, objectId string, filter string, search string, orderBy string, selectCols []string, top int32, count bool) (azure.DirectoryObjectList, error)
GetAzureADGroups(ctx context.Context, filter, search, orderBy, expand string, selectCols []string, top int32, count bool) (azure.GroupList, error)
GetAzureADOrganization(ctx context.Context, selectCols []string) (*azure.Organization, error)
Expand Down Expand Up @@ -140,6 +142,7 @@ type AzureClient interface {
ListAzureADGroupMembers(ctx context.Context, objectId string, filter, search, orderBy string, selectCols []string) <-chan azure.MemberObjectResult
ListAzureADGroupOwners(ctx context.Context, objectId string, filter, search, orderBy string, selectCols []string) <-chan azure.GroupOwnerResult
ListAzureADGroups(ctx context.Context, filter, search, orderBy, expand string, selectCols []string) <-chan azure.GroupResult
ListAzureADGroupEligibilityScheduleInstances(ctx context.Context, filter, search, orderBy, expand string, selectCols []string) <-chan azure.PrivilegedAccessGroupEligibilityScheduleInstanceResult
ListAzureADRoleAssignments(ctx context.Context, filter, search, orderBy, expand string, selectCols []string) <-chan azure.UnifiedRoleAssignmentResult
ListAzureADRoleEligibilityScheduleInstances(ctx context.Context, filter, search, orderBy, expand string, selectCols []string) <-chan azure.UnifiedRoleEligibilityScheduleInstanceResult
ListAzureADRoles(ctx context.Context, filter, expand string) <-chan azure.RoleResult
Expand Down
116 changes: 116 additions & 0 deletions client/group_eligibility_schedule_instances.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// 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"
"fmt"
"net/url"
"strings"

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

func (s *azureClient) GetAzureADGroupEligibilityScheduleInstance(ctx context.Context, objectId string, selectCols []string) (*azure.PrivilegedAccessGroupEligibilityScheduleInstance, error) {
var (
path = fmt.Sprintf("/%s/identityGovernance/privilegedAccess/group/eligibilityScheduleInstances/%s", constants.GraphApiBetaVersion, objectId)
params = query.Params{Select: selectCols}.AsMap()
response azure.PrivilegedAccessGroupEligibilityScheduleInstance
)
if res, err := s.msgraph.Get(ctx, path, params, nil); err != nil {
return nil, err
} else if err := rest.Decode(res.Body, &response); err != nil {
return nil, err
} else {
return &response, nil
}
}

func (s *azureClient) GetAzureADGroupEligibilityScheduleInstances(ctx context.Context, filter, search, orderBy, expand string, selectCols []string, top int32, count bool) (azure.PrivilegedAccessGroupEligibilityScheduleInstanceList, error) {
var (
path = fmt.Sprintf("/%s/identityGovernance/privilegedAccess/group/eligibilityScheduleInstances", constants.GraphApiBetaVersion)
params = query.Params{Filter: filter, Search: search, OrderBy: orderBy, Select: selectCols, Top: top, Count: count, Expand: expand}
headers map[string]string
response azure.PrivilegedAccessGroupEligibilityScheduleInstanceList
)
count = count || search != "" || (filter != "" && orderBy != "") || strings.Contains(filter, "endsWith")
if count {
headers = make(map[string]string)
headers["ConsistencyLevel"] = "eventual"
}
if res, err := s.msgraph.Get(ctx, path, params.AsMap(), headers); err != nil {
return response, err
} else if err := rest.Decode(res.Body, &response); err != nil {
return response, err
} else {
return response, nil
}
}

func (s *azureClient) ListAzureADGroupEligibilityScheduleInstances(ctx context.Context, filter, search, orderBy, expand string, selectCols []string) <-chan azure.PrivilegedAccessGroupEligibilityScheduleInstanceResult {
out := make(chan azure.PrivilegedAccessGroupEligibilityScheduleInstanceResult)

go func() {
defer close(out)

var (
errResult = azure.PrivilegedAccessGroupEligibilityScheduleInstanceResult{}
nextLink string
)

if list, err := s.GetAzureADGroupEligibilityScheduleInstances(ctx, filter, search, orderBy, expand, selectCols, 999, false); err != nil {
errResult.Error = err
out <- errResult
} else {
for _, u := range list.Value {
out <- azure.PrivilegedAccessGroupEligibilityScheduleInstanceResult{Ok: u}
}

nextLink = list.NextLink
for nextLink != "" {
var list azure.PrivilegedAccessGroupEligibilityScheduleInstanceList
if url, err := url.Parse(nextLink); err != nil {
errResult.Error = err
out <- errResult
nextLink = ""
} else if req, err := rest.NewRequest(ctx, "GET", url, nil, nil, nil); err != nil {
errResult.Error = err
out <- errResult
nextLink = ""
} else if res, err := s.msgraph.Send(req); err != nil {
errResult.Error = err
out <- errResult
nextLink = ""
} else if err := rest.Decode(res.Body, &list); err != nil {
errResult.Error = err
out <- errResult
nextLink = ""
} else {
for _, u := range list.Value {
out <- azure.PrivilegedAccessGroupEligibilityScheduleInstanceResult{Ok: u}
}
nextLink = list.NextLink
}
}
}
}()
return out
}
44 changes: 44 additions & 0 deletions client/mocks/client.go

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

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

roles = make(chan interface{})
roles2 = make(chan interface{})
Expand All @@ -89,10 +90,13 @@ func listAllAD(ctx context.Context, client client.AzureClient) <-chan interface{
deviceOwners := listDeviceOwners(ctx, client, devices2)

// Enumerate Groups, GroupOwners and GroupMembers
pipeline.Tee(ctx.Done(), listGroups(ctx, client), groups, groups2, groups3)
pipeline.Tee(ctx.Done(), listGroups(ctx, client), groups, groups2, groups3, groups4)
groupOwners := listGroupOwners(ctx, client, groups2)
groupMembers := listGroupMembers(ctx, client, groups3)

// Enumerate Groups Eligibility Schedule Instances
groupEligibilityScheduleInstances := listGroupEligibilityScheduleInstances(ctx, client, groups4)

// Enumerate ServicePrincipals and ServicePrincipalOwners
pipeline.Tee(ctx.Done(), listServicePrincipals(ctx, client), servicePrincipals, servicePrincipals2, servicePrincipals3)
servicePrincipalOwners := listServicePrincipalOwners(ctx, client, servicePrincipals2)
Expand All @@ -107,7 +111,7 @@ func listAllAD(ctx context.Context, client client.AzureClient) <-chan interface{
pipeline.Tee(ctx.Done(), listRoles(ctx, client), roles, roles2, roles3)
roleAssignments := listRoleAssignments(ctx, client, roles2)

// Enumerate Roles Eligibility Schedule Requests
// Enumerate Roles Eligibility Schedule Instances
roleEligibilityScheduleInstances := listRoleEligibilityScheduleInstances(ctx, client, roles3)

// Enumerate AppRoleAssignments
Expand All @@ -119,6 +123,7 @@ func listAllAD(ctx context.Context, client client.AzureClient) <-chan interface{
apps,
deviceOwners,
devices,
groupEligibilityScheduleInstances,
groupMembers,
groupOwners,
groups,
Expand Down
121 changes: 121 additions & 0 deletions cmd/list-group-eligibility-schedule-instances.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// 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/enums"
"github.com/bloodhoundad/azurehound/v2/models"
"github.com/bloodhoundad/azurehound/v2/pipeline"
"github.com/spf13/cobra"
)

func init() {
listRootCmd.AddCommand(listGroupEligibilityScheduleInstancesCmd)
}

var listGroupEligibilityScheduleInstancesCmd = &cobra.Command{
Use: "group-eligibility-schedule-instances",
Long: "Lists Azure Active Directory Group Eligibility Instances",
Run: listGroupEligibilityScheduleInstancesCmdImpl,
SilenceUsage: true,
}

func listGroupEligibilityScheduleInstancesCmdImpl(cmd *cobra.Command, args []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 active directory group eligibility instances...")
start := time.Now()
groups := listGroups(ctx, azClient)
stream := listGroupEligibilityScheduleInstances(ctx, azClient, groups)
outputStream(ctx, stream)
duration := time.Since(start)
log.Info("collection completed", "duration", duration.String())
}

func listGroupEligibilityScheduleInstances(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, 25)
wg sync.WaitGroup
)

go func() {
defer close(ids)

for result := range pipeline.OrDone(ctx.Done(), groups) {
if group, ok := result.(AzureWrapper).Data.(models.Group); !ok {
log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating group eligibility schedule instances", "result", result)
return
} else {
ids <- group.Id
}
}
}()

wg.Add(len(streams))
for i := range streams {
stream := streams[i]
go func() {
defer wg.Done()
for id := range stream {
var (
groupEligibilityScheduleInstances = models.GroupEligibilityScheduleInstances{
GroupId: id,
TenantId: client.TenantInfo().TenantId,
}
count = 0
filter = fmt.Sprintf("groupId eq '%s'", id)
)
for item := range client.ListAzureADGroupEligibilityScheduleInstances(ctx, filter, "", "", "", nil) {
if item.Error != nil {
log.Error(item.Error, "unable to continue processing group eligibility schedule instances for this group", "groupId", id)
} else {
log.V(2).Info("found group eligibility schedule instance", "groupEligibilityScheduleInstance", item)
count++
groupEligibilityScheduleInstances.GroupEligibilityScheduleInstances = append(groupEligibilityScheduleInstances.GroupEligibilityScheduleInstances, item.Ok)
}
}
out <- AzureWrapper{
Kind: enums.KindAZGroupEligibilityScheduleInstance,
Data: groupEligibilityScheduleInstances,
}
log.V(1).Info("finished listing group eligibility schedule instances", "groupId", id, "count", count)
}
}()
}

go func() {
wg.Wait()
close(out)
log.Info("finished listing all group eligibility schedule instances")
}()

return out
}
Loading

0 comments on commit 5dd99c3

Please sign in to comment.