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
1 change: 1 addition & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ type AzureResourceManagerClient interface {
ListAzureAutomationAccounts(ctx context.Context, subscriptionId string) <-chan AzureResult[azure.AutomationAccount]
ListAzureLogicApps(ctx context.Context, subscriptionId string, filter string, top int32) <-chan AzureResult[azure.LogicApp]
ListAzureFunctionApps(ctx context.Context, subscriptionId string) <-chan AzureResult[azure.FunctionApp]
ListAzureDomainServices(ctx context.Context, subscriptionId string) <-chan AzureResult[azure.DomainService]
}

type AzureClient interface {
Expand Down
38 changes: 38 additions & 0 deletions client/domain_services.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// 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"

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

func (s *azureClient) ListAzureDomainServices(ctx context.Context, subscriptionID string) <-chan AzureResult[azure.DomainService] {
var (
out = make(chan AzureResult[azure.DomainService])
path = fmt.Sprintf("/subscriptions/%s/providers/Microsoft.AAD/domainServices", subscriptionID)
params = query.RMParams{ApiVersion: "2025-06-01"}
)

go getAzureObjectList[azure.DomainService](s.resourceManager, ctx, path, params, out)

return out
}
14 changes: 14 additions & 0 deletions client/mocks/client.go

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

11 changes: 11 additions & 0 deletions cmd/list-azure-rm.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ func listAllRM(ctx context.Context, client client.AzureClient) <-chan interface{
functionApps = make(chan interface{})
functionApps2 = make(chan interface{})

domainServices = make(chan interface{})
domainServices2 = make(chan interface{})

webApps = make(chan interface{})
webApps2 = make(chan interface{})

Expand Down Expand Up @@ -119,6 +122,7 @@ func listAllRM(ctx context.Context, client client.AzureClient) <-chan interface{
subscriptions10 = make(chan interface{})
subscriptions11 = make(chan interface{})
subscriptions12 = make(chan interface{})
subscriptions13 = make(chan interface{})
subscriptionRoleAssignments1 = make(chan interface{})
subscriptionRoleAssignments2 = make(chan interface{})
subscriptionRoleAssignments3 = make(chan interface{})
Expand Down Expand Up @@ -147,6 +151,7 @@ func listAllRM(ctx context.Context, client client.AzureClient) <-chan interface{
subscriptions10,
subscriptions11,
subscriptions12,
subscriptions13,
)
pipeline.Tee(ctx.Done(), listResourceGroups(ctx, client, subscriptions2), resourceGroups, resourceGroups2)
pipeline.Tee(ctx.Done(), listKeyVaults(ctx, client, subscriptions3), keyVaults, keyVaults2, keyVaults3)
Expand All @@ -158,6 +163,7 @@ func listAllRM(ctx context.Context, client client.AzureClient) <-chan interface{
pipeline.Tee(ctx.Done(), listLogicApps(ctx, client, subscriptions10), logicApps, logicApps2)
pipeline.Tee(ctx.Done(), listManagedClusters(ctx, client, subscriptions11), managedClusters, managedClusters2)
pipeline.Tee(ctx.Done(), listVMScaleSets(ctx, client, subscriptions12), vmScaleSets, vmScaleSets2)
pipeline.Tee(ctx.Done(), listDomainServices(ctx, client, subscriptions13), domainServices, domainServices2)

// Enumerate Relationships
// ManagementGroups: Descendants, Owners, Contributors and UserAccessAdmins
Expand Down Expand Up @@ -198,6 +204,9 @@ func listAllRM(ctx context.Context, client client.AzureClient) <-chan interface{
// Enumerate Function App Role Assignments
functionAppRoleAssignments := listFunctionAppRoleAssignments(ctx, client, functionApps2)

// Enumerate Microsoft Entra Domain Services Role Assignments
domainServiceRoleAssignments := listDomainServiceRoleAssignments(ctx, client, domainServices2)

// Enumerate Web App Role Assignments
webAppRoleAssignments := listWebAppRoleAssignments(ctx, client, webApps2)

Expand All @@ -221,6 +230,8 @@ func listAllRM(ctx context.Context, client client.AzureClient) <-chan interface{
automationAccountRoleAssignments,
containerRegistries,
containerRegistryRoleAssignments,
domainServices,
domainServiceRoleAssignments,
functionApps,
functionAppRoleAssignments,
keyVaultAccessPolicies,
Expand Down
135 changes: 135 additions & 0 deletions cmd/list-domain-service-role-assignments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// 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"
"path"
"strings"
"sync"
"time"

"github.com/bloodhoundad/azurehound/v2/client"
"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(listDomainServiceRoleAssignmentsCmd)
}

var listDomainServiceRoleAssignmentsCmd = &cobra.Command{
Use: "domain-service-role-assignments",
Long: "Lists Microsoft Entra Domain Services role assignments",
Run: listDomainServiceRoleAssignmentsCmdImpl,
SilenceUsage: true,
}

func listDomainServiceRoleAssignmentsCmdImpl(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 Microsoft Entra Domain Services role assignments...")
start := time.Now()
subscriptions := listSubscriptions(ctx, azClient)
stream := listDomainServiceRoleAssignments(ctx, azClient, listDomainServices(ctx, azClient, subscriptions))
panicrecovery.HandleBubbledPanic(ctx, stop, log)
outputStream(ctx, stream)
duration := time.Since(start)
log.Info("collection completed", "duration", duration.String())
}

func listDomainServiceRoleAssignments(ctx context.Context, azureClient client.AzureClient, domainServices <-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
)

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

for result := range pipeline.OrDone(ctx.Done(), domainServices) {
if domainService, ok := result.(AzureWrapper).Data.(models.DomainService); !ok {
log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating domain service role assignments", "result", result)
return
} else if ok := pipeline.Send(ctx.Done(), ids, domainService.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 (
roleAssignments = models.AzureRoleAssignments{ObjectId: id}
count = 0
)
for item := range azureClient.ListRoleAssignmentsForResource(ctx, id, "atScope()", "") {
if item.Error != nil {
log.Error(item.Error, "unable to continue processing role assignments for this domain service", "domainServiceId", id)
continue
}
if !strings.EqualFold(item.Ok.Properties.Scope, id) {
continue
}

roleDefinitionID := path.Base(item.Ok.Properties.RoleDefinitionId)
roleAssignments.RoleAssignments = append(roleAssignments.RoleAssignments, models.AzureRoleAssignment{
Assignee: item.Ok,
ObjectId: id,
RoleDefinitionId: roleDefinitionID,
})
log.V(2).Info("found domain service role assignment", "roleDefinitionId", roleDefinitionID)
count++
}
if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{
Kind: enums.KindAZEntraDSRoleAssignment,
Data: roleAssignments,
}); !ok {
return
}
log.V(1).Info("finished listing domain service role assignments", "domainServiceId", id, "count", count)
}
}()
}

go func() {
wg.Wait()
close(out)
log.Info("finished listing all domain service role assignments")
}()

return out
}
89 changes: 89 additions & 0 deletions cmd/list-domain-service-role-assignments_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// 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"
"testing"

"github.com/bloodhoundad/azurehound/v2/client"
"github.com/bloodhoundad/azurehound/v2/client/mocks"
"github.com/bloodhoundad/azurehound/v2/constants"
"github.com/bloodhoundad/azurehound/v2/enums"
"github.com/bloodhoundad/azurehound/v2/models"
"github.com/bloodhoundad/azurehound/v2/models/azure"
"go.uber.org/mock/gomock"
)

func TestListDomainServiceRoleAssignments(t *testing.T) {
var (
ctx = context.Background()
controller = gomock.NewController(t)
mockClient = mocks.NewMockAzureClient(controller)
domainServices = make(chan interface{})
roleAssignmentResults = make(chan client.AzureResult[azure.RoleAssignment])
resourceID = "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.AAD/domainServices/example.com"
)

mockClient.EXPECT().ListRoleAssignmentsForResource(gomock.Any(), resourceID, "atScope()", "").Return(roleAssignmentResults)
results := listDomainServiceRoleAssignments(ctx, mockClient, domainServices)

go func() {
defer close(domainServices)
domainServices <- AzureWrapper{Data: models.DomainService{DomainService: azure.DomainService{Entity: azure.Entity{Id: resourceID}}}}
}()
go func() {
defer close(roleAssignmentResults)
roleAssignmentResults <- client.AzureResult[azure.RoleAssignment]{Ok: azure.RoleAssignment{
Properties: azure.RoleAssignmentPropertiesWithScope{
PrincipalId: "inherited-principal",
RoleDefinitionId: "/providers/Microsoft.Authorization/roleDefinitions/" + constants.OwnerRoleID,
Scope: "/subscriptions/sub/resourceGroups/rg",
},
}}
roleAssignmentResults <- client.AzureResult[azure.RoleAssignment]{Ok: azure.RoleAssignment{
Properties: azure.RoleAssignmentPropertiesWithScope{
PrincipalId: "principal",
RoleDefinitionId: "/providers/Microsoft.Authorization/roleDefinitions/" + constants.DomainServicesContributorRoleID,
Scope: resourceID,
},
}}
}()

result, ok := <-results
if !ok {
t.Fatal("failed to receive role assignments")
}
wrapper, ok := result.(AzureWrapper)
if !ok {
t.Fatalf("failed type assertion: got %T, want %T", result, AzureWrapper{})
}
if wrapper.Kind != enums.KindAZEntraDSRoleAssignment {
t.Errorf("unexpected kind: %s", wrapper.Kind)
}
roleAssignments, ok := wrapper.Data.(models.AzureRoleAssignments)
if !ok {
t.Fatalf("failed type assertion: got %T, want %T", wrapper.Data, models.AzureRoleAssignments{})
}
if len(roleAssignments.RoleAssignments) != 1 {
t.Fatalf("expected one role assignment, got %d", len(roleAssignments.RoleAssignments))
}
if roleAssignments.RoleAssignments[0].RoleDefinitionId != constants.DomainServicesContributorRoleID {
t.Errorf("unexpected role definition id: %s", roleAssignments.RoleAssignments[0].RoleDefinitionId)
}
}
Loading
Loading