Skip to content

Commit

Permalink
feat: add support for key vault KVContribtors
Browse files Browse the repository at this point in the history
  • Loading branch information
ddlees committed Sep 6, 2022
1 parent cc4aeb4 commit 5a9cd7b
Show file tree
Hide file tree
Showing 15 changed files with 619 additions and 302 deletions.
66 changes: 21 additions & 45 deletions cmd/list-key-vault-contributors.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
"os"
"os/signal"
"path"
"sync"
"time"

"github.com/bloodhoundad/azurehound/client"
Expand Down Expand Up @@ -58,75 +57,52 @@ func listKeyVaultContributorsCmdImpl(cmd *cobra.Command, args []string) {
log.Info("collecting azure key vault contributors...")
start := time.Now()
subscriptions := listSubscriptions(ctx, azClient)
stream := listKeyVaultContributors(ctx, azClient, listKeyVaults(ctx, azClient, subscriptions))
keyVaults := listKeyVaults(ctx, azClient, subscriptions)
kvRoleAssignments := listKeyVaultRoleAssignments(ctx, azClient, keyVaults)
stream := listKeyVaultContributors(ctx, azClient, kvRoleAssignments)
outputStream(ctx, stream)
duration := time.Since(start)
log.Info("collection completed", "duration", duration.String())
}
}

func listKeyVaultContributors(ctx context.Context, client client.AzureClient, KeyVaults <-chan interface{}) <-chan interface{} {
var (
out = make(chan interface{})
ids = make(chan string)
streams = pipeline.Demux(ctx.Done(), ids, 25)
wg sync.WaitGroup
)
func listKeyVaultContributors(ctx context.Context, client client.AzureClient, vmRoleAssignments <-chan interface{}) <-chan interface{} {
out := make(chan interface{})

go func() {
defer close(ids)
defer close(out)

for result := range pipeline.OrDone(ctx.Done(), KeyVaults) {
if keyVault, ok := result.(AzureWrapper).Data.(models.KeyVault); !ok {
for result := range pipeline.OrDone(ctx.Done(), vmRoleAssignments) {
if roleAssignments, ok := result.(AzureWrapper).Data.(models.KeyVaultRoleAssignments); !ok {
log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating key vault contributors", "result", result)
return
} else {
ids <- keyVault.Id
}
}
}()

wg.Add(len(streams))
for i := range streams {
stream := streams[i]
go func() {
defer wg.Done()
for id := range stream {
var (
keyVaultContributors = models.KeyVaultContributors{
KeyVaultId: id.(string),
KeyVaultId: roleAssignments.KeyVaultId,
}
count = 0
)
for item := range client.ListRoleAssignmentsForResource(ctx, id.(string), "") {
if item.Error != nil {
log.Error(item.Error, "unable to continue processing contributors for this key vault", "keyVaultId", id)
} else {
roleDefinitionId := path.Base(item.Ok.Properties.RoleDefinitionId)
for _, item := range roleAssignments.RoleAssignments {
roleDefinitionId := path.Base(item.RoleAssignment.Properties.RoleDefinitionId)

if roleDefinitionId == constants.ContributorRoleID {
keyVaultContributor := models.KeyVaultContributor{
Contributor: item.Ok,
KeyVaultId: item.ParentId,
}
log.V(2).Info("found key vault contributor", "keyVaultContributor", keyVaultContributor)
count++
keyVaultContributors.Contributors = append(keyVaultContributors.Contributors, keyVaultContributor)
if roleDefinitionId == constants.ContributorRoleID {
keyVaultContributor := models.KeyVaultContributor{
Contributor: item.RoleAssignment,
KeyVaultId: item.KeyVaultId,
}
log.V(2).Info("found key vault contributor", "keyVaultContributor", keyVaultContributor)
count++
keyVaultContributors.Contributors = append(keyVaultContributors.Contributors, keyVaultContributor)
}
}
out <- AzureWrapper{
Kind: enums.KindAZKeyVaultContributor,
Kind: enums.KindAZVMContributor,
Data: keyVaultContributors,
}
log.V(1).Info("finished listing key vault contributors", "keyVaultId", id, "count", count)
log.V(1).Info("finished listing key vault contributors", "keyVaultId", roleAssignments.KeyVaultId, "count", count)
}
}()
}

go func() {
wg.Wait()
close(out)
}
log.Info("finished listing all key vault contributors")
}()

Expand Down
73 changes: 19 additions & 54 deletions cmd/list-key-vault-contributors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package cmd

import (
"context"
"fmt"
"testing"

"github.com/bloodhoundad/azurehound/client/mocks"
Expand All @@ -40,74 +39,40 @@ func TestListKeyVaultContributors(t *testing.T) {

mockClient := mocks.NewMockAzureClient(ctrl)

mockKeyVaultsChannel := make(chan interface{})
mockKeyVaultContributorChannel := make(chan azure.RoleAssignmentResult)
mockKeyVaultContributorChannel2 := make(chan azure.RoleAssignmentResult)

mockRoleAssignmentsChannel := make(chan interface{})
mockTenant := azure.Tenant{}
mockError := fmt.Errorf("I'm an error")
mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes()
mockClient.EXPECT().ListRoleAssignmentsForResource(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockKeyVaultContributorChannel).Times(1)
mockClient.EXPECT().ListRoleAssignmentsForResource(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockKeyVaultContributorChannel2).Times(1)
channel := listKeyVaultContributors(ctx, mockClient, mockKeyVaultsChannel)
channel := listKeyVaultContributors(ctx, mockClient, mockRoleAssignmentsChannel)

go func() {
defer close(mockKeyVaultsChannel)
mockKeyVaultsChannel <- AzureWrapper{
Data: models.KeyVault{},
}
mockKeyVaultsChannel <- AzureWrapper{
Data: models.KeyVault{},
}
}()
go func() {
defer close(mockKeyVaultContributorChannel)
mockKeyVaultContributorChannel <- azure.RoleAssignmentResult{
Ok: azure.RoleAssignment{
Properties: azure.RoleAssignmentPropertiesWithScope{
RoleDefinitionId: constants.ContributorRoleID,
},
},
}
mockKeyVaultContributorChannel <- azure.RoleAssignmentResult{
Ok: azure.RoleAssignment{
Properties: azure.RoleAssignmentPropertiesWithScope{
RoleDefinitionId: constants.ContributorRoleID,
},
},
}
}()
go func() {
defer close(mockKeyVaultContributorChannel2)
mockKeyVaultContributorChannel2 <- azure.RoleAssignmentResult{
Ok: azure.RoleAssignment{
Properties: azure.RoleAssignmentPropertiesWithScope{
RoleDefinitionId: constants.ContributorRoleID,
defer close(mockRoleAssignmentsChannel)

mockRoleAssignmentsChannel <- AzureWrapper{
Data: models.KeyVaultRoleAssignments{
KeyVaultId: "foo",
RoleAssignments: []models.KeyVaultRoleAssignment{
{
RoleAssignment: azure.RoleAssignment{
Name: constants.ContributorRoleID,
Properties: azure.RoleAssignmentPropertiesWithScope{
RoleDefinitionId: constants.ContributorRoleID,
},
},
},
},
},
}
mockKeyVaultContributorChannel2 <- azure.RoleAssignmentResult{
Error: mockError,
}
}()

if result, ok := <-channel; !ok {
t.Fatalf("failed to receive from channel")
} else if wrapper, ok := result.(AzureWrapper); !ok {
t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{})
} else if data, ok := wrapper.Data.(models.KeyVaultContributors); !ok {
} else if _, ok := wrapper.Data.(models.KeyVaultContributors); !ok {
t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.KeyVaultContributors{})
} else if len(data.Contributors) != 2 {
t.Errorf("got %v, want %v", len(data.Contributors), 2)
}

if result, ok := <-channel; !ok {
t.Fatalf("failed to receive from channel")
} else if wrapper, ok := result.(AzureWrapper); !ok {
t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{})
} else if data, ok := wrapper.Data.(models.KeyVaultContributors); !ok {
t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.KeyVaultContributors{})
} else if len(data.Contributors) != 1 {
t.Errorf("got %v, want %v", len(data.Contributors), 2)
if _, ok := <-channel; ok {
t.Error("should not have recieved from channel")
}
}
110 changes: 110 additions & 0 deletions cmd/list-key-vault-kvcontributors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// 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"
"time"

"github.com/bloodhoundad/azurehound/client"
"github.com/bloodhoundad/azurehound/constants"
"github.com/bloodhoundad/azurehound/enums"
"github.com/bloodhoundad/azurehound/models"
"github.com/bloodhoundad/azurehound/pipeline"
"github.com/spf13/cobra"
)

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

var listKeyVaultKVContributorsCmd = &cobra.Command{
Use: "key-vault-kvcontributors",
Long: "Lists Azure Key Vault KVContributors",
Run: listKeyVaultKVContributorsCmdImpl,
SilenceUsage: true,
}

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

log.V(1).Info("testing connections")
if err := testConnections(); err != nil {
exit(err)
} else if azClient, err := newAzureClient(); err != nil {
exit(err)
} else {
log.Info("collecting azure key vault kvcontributors...")
start := time.Now()
subscriptions := listSubscriptions(ctx, azClient)
keyVaults := listKeyVaults(ctx, azClient, subscriptions)
kvRoleAssignments := listKeyVaultRoleAssignments(ctx, azClient, keyVaults)
stream := listKeyVaultKVContributors(ctx, azClient, kvRoleAssignments)
outputStream(ctx, stream)
duration := time.Since(start)
log.Info("collection completed", "duration", duration.String())
}
}

func listKeyVaultKVContributors(ctx context.Context, client client.AzureClient, vmRoleAssignments <-chan interface{}) <-chan interface{} {
out := make(chan interface{})

go func() {
defer close(out)

for result := range pipeline.OrDone(ctx.Done(), vmRoleAssignments) {
if roleAssignments, ok := result.(AzureWrapper).Data.(models.KeyVaultRoleAssignments); !ok {
log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating key vault kvContributors", "result", result)
return
} else {
var (
keyVaultKVContributors = models.KeyVaultKVContributors{
KeyVaultId: roleAssignments.KeyVaultId,
}
count = 0
)
for _, item := range roleAssignments.RoleAssignments {
roleDefinitionId := path.Base(item.RoleAssignment.Properties.RoleDefinitionId)

if roleDefinitionId == constants.KeyVaultContributorRoleID {
keyVaultKVContributor := models.KeyVaultKVContributor{
KVContributor: item.RoleAssignment,
KeyVaultId: item.KeyVaultId,
}
log.V(2).Info("found key vault kvContributor", "keyVaultKVContributor", keyVaultKVContributor)
count++
keyVaultKVContributors.KVContributors = append(keyVaultKVContributors.KVContributors, keyVaultKVContributor)
}
}
out <- AzureWrapper{
Kind: enums.KindAZKeyVaultContributor,
Data: keyVaultKVContributors,
}
log.V(1).Info("finished listing key vault kvContributors", "keyVaultId", roleAssignments.KeyVaultId, "count", count)
}
}
log.Info("finished listing all key vault kvContributors")
}()

return out
}
78 changes: 78 additions & 0 deletions cmd/list-key-vault-kvcontributors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// 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/client/mocks"
"github.com/bloodhoundad/azurehound/constants"
"github.com/bloodhoundad/azurehound/models"
"github.com/bloodhoundad/azurehound/models/azure"
"github.com/golang/mock/gomock"
)

func init() {
setupLogger()
}

func TestListKeyVaultKVContributors(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.Background()

mockClient := mocks.NewMockAzureClient(ctrl)

mockRoleAssignmentsChannel := make(chan interface{})
mockTenant := azure.Tenant{}
mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes()
channel := listKeyVaultKVContributors(ctx, mockClient, mockRoleAssignmentsChannel)

go func() {
defer close(mockRoleAssignmentsChannel)

mockRoleAssignmentsChannel <- AzureWrapper{
Data: models.KeyVaultRoleAssignments{
KeyVaultId: "foo",
RoleAssignments: []models.KeyVaultRoleAssignment{
{
RoleAssignment: azure.RoleAssignment{
Name: constants.KeyVaultContributorRoleID,
Properties: azure.RoleAssignmentPropertiesWithScope{
RoleDefinitionId: constants.KeyVaultContributorRoleID,
},
},
},
},
},
}
}()

if result, ok := <-channel; !ok {
t.Fatalf("failed to receive from channel")
} else if wrapper, ok := result.(AzureWrapper); !ok {
t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{})
} else if _, ok := wrapper.Data.(models.KeyVaultKVContributors); !ok {
t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.KeyVaultKVContributors{})
}

if _, ok := <-channel; ok {
t.Error("should not have recieved from channel")
}
}
Loading

0 comments on commit 5a9cd7b

Please sign in to comment.