diff --git a/cmd/list-key-vault-contributors.go b/cmd/list-key-vault-contributors.go index 6293ba1..77d8032 100644 --- a/cmd/list-key-vault-contributors.go +++ b/cmd/list-key-vault-contributors.go @@ -23,7 +23,6 @@ import ( "os" "os/signal" "path" - "sync" "time" "github.com/bloodhoundad/azurehound/client" @@ -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") }() diff --git a/cmd/list-key-vault-contributors_test.go b/cmd/list-key-vault-contributors_test.go index e93be82..28efe99 100644 --- a/cmd/list-key-vault-contributors_test.go +++ b/cmd/list-key-vault-contributors_test.go @@ -19,7 +19,6 @@ package cmd import ( "context" - "fmt" "testing" "github.com/bloodhoundad/azurehound/client/mocks" @@ -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") } } diff --git a/cmd/list-key-vault-kvcontributors.go b/cmd/list-key-vault-kvcontributors.go new file mode 100644 index 0000000..d14287c --- /dev/null +++ b/cmd/list-key-vault-kvcontributors.go @@ -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 . + +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 +} diff --git a/cmd/list-key-vault-kvcontributors_test.go b/cmd/list-key-vault-kvcontributors_test.go new file mode 100644 index 0000000..bc3dc17 --- /dev/null +++ b/cmd/list-key-vault-kvcontributors_test.go @@ -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 . + +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") + } +} diff --git a/cmd/list-key-vault-owners.go b/cmd/list-key-vault-owners.go index 0ae2f32..ecf954d 100644 --- a/cmd/list-key-vault-owners.go +++ b/cmd/list-key-vault-owners.go @@ -23,7 +23,6 @@ import ( "os" "os/signal" "path" - "sync" "time" "github.com/bloodhoundad/azurehound/client" @@ -58,75 +57,52 @@ func listKeyVaultOwnersCmdImpl(cmd *cobra.Command, args []string) { log.Info("collecting azure key vault owners...") start := time.Now() subscriptions := listSubscriptions(ctx, azClient) - stream := listKeyVaultOwners(ctx, azClient, listKeyVaults(ctx, azClient, subscriptions)) + keyVaults := listKeyVaults(ctx, azClient, subscriptions) + kvRoleAssignments := listKeyVaultRoleAssignments(ctx, azClient, keyVaults) + stream := listKeyVaultOwners(ctx, azClient, kvRoleAssignments) outputStream(ctx, stream) duration := time.Since(start) log.Info("collection completed", "duration", duration.String()) } } -func listKeyVaultOwners(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 listKeyVaultOwners(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 { - log.Error(fmt.Errorf("failed KeyVault type assertion"), "unable to continue enumerating key vault owners", "result", result) + 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 owners", "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 ( keyVaultOwners = models.KeyVaultOwners{ - 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 owners 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.OwnerRoleID { - keyVaultOwner := models.KeyVaultOwner{ - Owner: item.Ok, - KeyVaultId: item.ParentId, - } - log.V(2).Info("found key vault owner", "keyVaultOwner", keyVaultOwner) - count++ - keyVaultOwners.Owners = append(keyVaultOwners.Owners, keyVaultOwner) + if roleDefinitionId == constants.OwnerRoleID { + keyVaultOwner := models.KeyVaultOwner{ + Owner: item.RoleAssignment, + KeyVaultId: item.KeyVaultId, } + log.V(2).Info("found key vault owner", "keyVaultOwner", keyVaultOwner) + count++ + keyVaultOwners.Owners = append(keyVaultOwners.Owners, keyVaultOwner) } } out <- AzureWrapper{ - Kind: enums.KindAZKeyVaultOwner, + Kind: enums.KindAZVMOwner, Data: keyVaultOwners, } - log.V(1).Info("finished listing key vault owners", "keyVaultId", id, "count", count) + log.V(1).Info("finished listing key vault owners", "keyVaultId", roleAssignments.KeyVaultId, "count", count) } - }() - } - - go func() { - wg.Wait() - close(out) + } log.Info("finished listing all key vault owners") }() diff --git a/cmd/list-key-vault-owners_test.go b/cmd/list-key-vault-owners_test.go index ec6129f..97a6d5f 100644 --- a/cmd/list-key-vault-owners_test.go +++ b/cmd/list-key-vault-owners_test.go @@ -19,7 +19,6 @@ package cmd import ( "context" - "fmt" "testing" "github.com/bloodhoundad/azurehound/client/mocks" @@ -40,74 +39,40 @@ func TestListKeyVaultOwners(t *testing.T) { mockClient := mocks.NewMockAzureClient(ctrl) - mockKeyVaultsChannel := make(chan interface{}) - mockKeyVaultOwnerChannel := make(chan azure.RoleAssignmentResult) - mockKeyVaultOwnerChannel2 := 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(mockKeyVaultOwnerChannel).Times(1) - mockClient.EXPECT().ListRoleAssignmentsForResource(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockKeyVaultOwnerChannel2).Times(1) - channel := listKeyVaultOwners(ctx, mockClient, mockKeyVaultsChannel) + channel := listKeyVaultOwners(ctx, mockClient, mockRoleAssignmentsChannel) go func() { - defer close(mockKeyVaultsChannel) - mockKeyVaultsChannel <- AzureWrapper{ - Data: models.KeyVault{}, - } - mockKeyVaultsChannel <- AzureWrapper{ - Data: models.KeyVault{}, - } - }() - go func() { - defer close(mockKeyVaultOwnerChannel) - mockKeyVaultOwnerChannel <- azure.RoleAssignmentResult{ - Ok: azure.RoleAssignment{ - Properties: azure.RoleAssignmentPropertiesWithScope{ - RoleDefinitionId: constants.OwnerRoleID, - }, - }, - } - mockKeyVaultOwnerChannel <- azure.RoleAssignmentResult{ - Ok: azure.RoleAssignment{ - Properties: azure.RoleAssignmentPropertiesWithScope{ - RoleDefinitionId: constants.OwnerRoleID, - }, - }, - } - }() - go func() { - defer close(mockKeyVaultOwnerChannel2) - mockKeyVaultOwnerChannel2 <- azure.RoleAssignmentResult{ - Ok: azure.RoleAssignment{ - Properties: azure.RoleAssignmentPropertiesWithScope{ - RoleDefinitionId: constants.OwnerRoleID, + defer close(mockRoleAssignmentsChannel) + + mockRoleAssignmentsChannel <- AzureWrapper{ + Data: models.KeyVaultRoleAssignments{ + KeyVaultId: "foo", + RoleAssignments: []models.KeyVaultRoleAssignment{ + { + RoleAssignment: azure.RoleAssignment{ + Name: constants.OwnerRoleID, + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.OwnerRoleID, + }, + }, + }, }, }, } - mockKeyVaultOwnerChannel2 <- 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.KeyVaultOwners); !ok { + } else if _, ok := wrapper.Data.(models.KeyVaultOwners); !ok { t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.KeyVaultOwners{}) - } else if len(data.Owners) != 2 { - t.Errorf("got %v, want %v", len(data.Owners), 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.KeyVaultOwners); !ok { - t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.KeyVaultOwners{}) - } else if len(data.Owners) != 1 { - t.Errorf("got %v, want %v", len(data.Owners), 2) + if _, ok := <-channel; ok { + t.Error("should not have recieved from channel") } } diff --git a/cmd/list-key-vault-role-assignments.go b/cmd/list-key-vault-role-assignments.go new file mode 100644 index 0000000..208dcc3 --- /dev/null +++ b/cmd/list-key-vault-role-assignments.go @@ -0,0 +1,128 @@ +// 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 . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/client" + "github.com/bloodhoundad/azurehound/enums" + "github.com/bloodhoundad/azurehound/models" + "github.com/bloodhoundad/azurehound/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listKeyVaultRoleAssignmentsCmd) +} + +var listKeyVaultRoleAssignmentsCmd = &cobra.Command{ + Use: "key-vault-role-assignments", + Long: "Lists Key Vault Role Assignments", + Run: listKeyVaultRoleAssignmentsCmdImpl, + SilenceUsage: true, +} + +func listKeyVaultRoleAssignmentsCmdImpl(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 role assignments...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + stream := listKeyVaultRoleAssignments(ctx, azClient, listKeyVaults(ctx, azClient, subscriptions)) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) + } +} + +func listKeyVaultRoleAssignments(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 + ) + + go func() { + defer close(ids) + + for result := range pipeline.OrDone(ctx.Done(), keyVaults) { + if keyVault, ok := result.(AzureWrapper).Data.(models.KeyVault); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating key vault role assignments", "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 ( + keyVaultRoleAssignments = models.KeyVaultRoleAssignments{ + KeyVaultId: id.(string), + } + count = 0 + ) + for item := range client.ListRoleAssignmentsForResource(ctx, id.(string), "") { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing role assignments for this key vault", "keyVaultId", id) + } else { + keyVaultRoleAssignment := models.KeyVaultRoleAssignment{ + KeyVaultId: item.ParentId, + RoleAssignment: item.Ok, + } + log.V(2).Info("found key vault role assignment", "keyVaultRoleAssignment", keyVaultRoleAssignment) + count++ + keyVaultRoleAssignments.RoleAssignments = append(keyVaultRoleAssignments.RoleAssignments, keyVaultRoleAssignment) + } + } + out <- AzureWrapper{ + Kind: enums.KindAZVMRoleAssignment, + Data: keyVaultRoleAssignments, + } + log.V(1).Info("finished listing key vault role assignments", "keyVaultId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all key vault role assignments") + }() + + return out +} diff --git a/cmd/list-key-vault-role-assignments_test.go b/cmd/list-key-vault-role-assignments_test.go new file mode 100644 index 0000000..49857ce --- /dev/null +++ b/cmd/list-key-vault-role-assignments_test.go @@ -0,0 +1,113 @@ +// 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 . + +package cmd + +import ( + "context" + "fmt" + "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 TestListKeyVaultRoleAssignments(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockKeyVaultsChannel := make(chan interface{}) + mockKeyVaultRoleAssignmentChannel := make(chan azure.RoleAssignmentResult) + mockKeyVaultRoleAssignmentChannel2 := make(chan azure.RoleAssignmentResult) + + 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(mockKeyVaultRoleAssignmentChannel).Times(1) + mockClient.EXPECT().ListRoleAssignmentsForResource(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockKeyVaultRoleAssignmentChannel2).Times(1) + channel := listKeyVaultRoleAssignments(ctx, mockClient, mockKeyVaultsChannel) + + go func() { + defer close(mockKeyVaultsChannel) + mockKeyVaultsChannel <- AzureWrapper{ + Data: models.KeyVault{}, + } + mockKeyVaultsChannel <- AzureWrapper{ + Data: models.KeyVault{}, + } + }() + go func() { + defer close(mockKeyVaultRoleAssignmentChannel) + mockKeyVaultRoleAssignmentChannel <- azure.RoleAssignmentResult{ + Ok: azure.RoleAssignment{ + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.KeyVaultContributorRoleID, + }, + }, + } + mockKeyVaultRoleAssignmentChannel <- azure.RoleAssignmentResult{ + Ok: azure.RoleAssignment{ + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.ContributorRoleID, + }, + }, + } + }() + go func() { + defer close(mockKeyVaultRoleAssignmentChannel2) + mockKeyVaultRoleAssignmentChannel2 <- azure.RoleAssignmentResult{ + Ok: azure.RoleAssignment{ + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.KeyVaultAdministratorRoleID, + }, + }, + } + mockKeyVaultRoleAssignmentChannel2 <- 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.KeyVaultRoleAssignments); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.KeyVaultRoleAssignments{}) + } else if len(data.RoleAssignments) != 2 { + t.Errorf("got %v, want %v", len(data.RoleAssignments), 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.KeyVaultRoleAssignments); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.KeyVaultRoleAssignments{}) + } else if len(data.RoleAssignments) != 1 { + t.Errorf("got %v, want %v", len(data.RoleAssignments), 2) + } +} diff --git a/cmd/list-key-vault-user-access-admins.go b/cmd/list-key-vault-user-access-admins.go index 6219a15..4f8a80d 100644 --- a/cmd/list-key-vault-user-access-admins.go +++ b/cmd/list-key-vault-user-access-admins.go @@ -23,7 +23,6 @@ import ( "os" "os/signal" "path" - "sync" "time" "github.com/bloodhoundad/azurehound/client" @@ -58,76 +57,54 @@ func listKeyVaultUserAccessAdminsCmdImpl(cmd *cobra.Command, args []string) { log.Info("collecting azure key vault user access admins...") start := time.Now() subscriptions := listSubscriptions(ctx, azClient) - stream := listKeyVaultUserAccessAdmins(ctx, azClient, listKeyVaults(ctx, azClient, subscriptions)) + keyVaults := listKeyVaults(ctx, azClient, subscriptions) + kvRoleAssignments := listKeyVaultRoleAssignments(ctx, azClient, keyVaults) + stream := listKeyVaultUserAccessAdmins(ctx, azClient, kvRoleAssignments) outputStream(ctx, stream) duration := time.Since(start) log.Info("collection completed", "duration", duration.String()) } } -func listKeyVaultUserAccessAdmins(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 listKeyVaultUserAccessAdmins(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 { - log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating key vault user access admins", "result", result) + 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 userAccessAdmins", "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 ( keyVaultUserAccessAdmins = models.KeyVaultUserAccessAdmins{ - 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 user access admins 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.UserAccessAdminRoleID { - keyVaultUserAccessAdmin := models.KeyVaultUserAccessAdmin{ - UserAccessAdmin: item.Ok, - KeyVaultId: item.ParentId, - } - log.V(2).Info("found key vault user access admin", "keyVaultUserAccessAdmin", keyVaultUserAccessAdmin) - count++ - keyVaultUserAccessAdmins.UserAccessAdmins = append(keyVaultUserAccessAdmins.UserAccessAdmins, keyVaultUserAccessAdmin) + if roleDefinitionId == constants.UserAccessAdminRoleID { + keyVaultUserAccessAdmin := models.KeyVaultUserAccessAdmin{ + UserAccessAdmin: item.RoleAssignment, + KeyVaultId: item.KeyVaultId, } + log.V(2).Info("found key vault userAccessAdmin", "keyVaultUserAccessAdmin", keyVaultUserAccessAdmin) + count++ + keyVaultUserAccessAdmins.UserAccessAdmins = append(keyVaultUserAccessAdmins.UserAccessAdmins, keyVaultUserAccessAdmin) } } out <- AzureWrapper{ - Kind: enums.KindAZKeyVaultUserAccessAdmin, + Kind: enums.KindAZVMUserAccessAdmin, Data: keyVaultUserAccessAdmins, } - log.V(1).Info("finished listing key vault user access admins", "keyVaultId", id, "count", count) + log.V(1).Info("finished listing key vault userAccessAdmins", "keyVaultId", roleAssignments.KeyVaultId, "count", count) } - }() - } - - go func() { - wg.Wait() - close(out) - log.Info("finished listing all key vault user access admins") + } + log.Info("finished listing all key vault userAccessAdmins") }() + return out } diff --git a/cmd/list-key-vault-user-access-admins_test.go b/cmd/list-key-vault-user-access-admins_test.go index b3f22fe..670a151 100644 --- a/cmd/list-key-vault-user-access-admins_test.go +++ b/cmd/list-key-vault-user-access-admins_test.go @@ -19,7 +19,6 @@ package cmd import ( "context" - "fmt" "testing" "github.com/bloodhoundad/azurehound/client/mocks" @@ -40,74 +39,40 @@ func TestListKeyVaultUserAccessAdmins(t *testing.T) { mockClient := mocks.NewMockAzureClient(ctrl) - mockKeyVaultsChannel := make(chan interface{}) - mockKeyVaultUserAccessAdminChannel := make(chan azure.RoleAssignmentResult) - mockKeyVaultUserAccessAdminChannel2 := 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(mockKeyVaultUserAccessAdminChannel).Times(1) - mockClient.EXPECT().ListRoleAssignmentsForResource(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockKeyVaultUserAccessAdminChannel2).Times(1) - channel := listKeyVaultUserAccessAdmins(ctx, mockClient, mockKeyVaultsChannel) + channel := listKeyVaultUserAccessAdmins(ctx, mockClient, mockRoleAssignmentsChannel) go func() { - defer close(mockKeyVaultsChannel) - mockKeyVaultsChannel <- AzureWrapper{ - Data: models.KeyVault{}, - } - mockKeyVaultsChannel <- AzureWrapper{ - Data: models.KeyVault{}, - } - }() - go func() { - defer close(mockKeyVaultUserAccessAdminChannel) - mockKeyVaultUserAccessAdminChannel <- azure.RoleAssignmentResult{ - Ok: azure.RoleAssignment{ - Properties: azure.RoleAssignmentPropertiesWithScope{ - RoleDefinitionId: constants.UserAccessAdminRoleID, - }, - }, - } - mockKeyVaultUserAccessAdminChannel <- azure.RoleAssignmentResult{ - Ok: azure.RoleAssignment{ - Properties: azure.RoleAssignmentPropertiesWithScope{ - RoleDefinitionId: constants.UserAccessAdminRoleID, - }, - }, - } - }() - go func() { - defer close(mockKeyVaultUserAccessAdminChannel2) - mockKeyVaultUserAccessAdminChannel2 <- azure.RoleAssignmentResult{ - Ok: azure.RoleAssignment{ - Properties: azure.RoleAssignmentPropertiesWithScope{ - RoleDefinitionId: constants.UserAccessAdminRoleID, + defer close(mockRoleAssignmentsChannel) + + mockRoleAssignmentsChannel <- AzureWrapper{ + Data: models.KeyVaultRoleAssignments{ + KeyVaultId: "foo", + RoleAssignments: []models.KeyVaultRoleAssignment{ + { + RoleAssignment: azure.RoleAssignment{ + Name: constants.UserAccessAdminRoleID, + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.UserAccessAdminRoleID, + }, + }, + }, }, }, } - mockKeyVaultUserAccessAdminChannel2 <- 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.KeyVaultUserAccessAdmins); !ok { + } else if _, ok := wrapper.Data.(models.KeyVaultUserAccessAdmins); !ok { t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.KeyVaultUserAccessAdmins{}) - } else if len(data.UserAccessAdmins) != 2 { - t.Errorf("got %v, want %v", len(data.UserAccessAdmins), 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.KeyVaultUserAccessAdmins); !ok { - t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.KeyVaultUserAccessAdmins{}) - } else if len(data.UserAccessAdmins) != 1 { - t.Errorf("got %v, want %v", len(data.UserAccessAdmins), 2) + if _, ok := <-channel; ok { + t.Error("should not have recieved from channel") } } diff --git a/cmd/list-root.go b/cmd/list-root.go index c357187..bf36080 100644 --- a/cmd/list-root.go +++ b/cmd/list-root.go @@ -84,6 +84,7 @@ func listAll(ctx context.Context, client client.AzureClient) <-chan interface{} keyVaults3 = make(chan interface{}) keyVaults4 = make(chan interface{}) keyVaults5 = make(chan interface{}) + keyVaults6 = make(chan interface{}) mgmtGroups = make(chan interface{}) mgmtGroups2 = make(chan interface{}) @@ -139,11 +140,12 @@ func listAll(ctx context.Context, client client.AzureClient) <-chan interface{} subscriptionUserAccessAdmins := listSubscriptionUserAccessAdmins(ctx, client, subscriptions6) // Enumerate KeyVaults, KeyVaultOwners, KeyVaultAccessPolicies and KeyVaultUserAccessAdmins - pipeline.Tee(ctx.Done(), listKeyVaults(ctx, client, subscriptions2), keyVaults, keyVaults2, keyVaults3, keyVaults4, keyVaults5) + pipeline.Tee(ctx.Done(), listKeyVaults(ctx, client, subscriptions2), keyVaults, keyVaults2, keyVaults3, keyVaults4, keyVaults5, keyVaults6) keyVaultOwners := listKeyVaultOwners(ctx, client, keyVaults2) keyVaultAccessPolicies := listKeyVaultAccessPolicies(ctx, client, keyVaults3, []enums.KeyVaultAccessType{enums.GetCerts, enums.GetKeys, enums.GetCerts}) keyVaultUserAccessAdmins := listKeyVaultUserAccessAdmins(ctx, client, keyVaults4) keyVaultContributors := listKeyVaultContributors(ctx, client, keyVaults5) + keyVaultKVContributors := listKeyVaultKVContributors(ctx, client, keyVaults6) // Enumerate ManagementGroups, ManagementGroupOwners and ManagementGroupDescendants pipeline.Tee(ctx.Done(), listManagementGroups(ctx, client), mgmtGroups, mgmtGroups2, mgmtGroups3, mgmtGroups4) @@ -191,6 +193,7 @@ func listAll(ctx context.Context, client client.AzureClient) <-chan interface{} groups, keyVaultAccessPolicies, keyVaultContributors, + keyVaultKVContributors, keyVaultOwners, keyVaultUserAccessAdmins, keyVaults, diff --git a/cmd/list-virtual-machine-role-assignments.go b/cmd/list-virtual-machine-role-assignments.go index 931e1df..a861865 100644 --- a/cmd/list-virtual-machine-role-assignments.go +++ b/cmd/list-virtual-machine-role-assignments.go @@ -33,7 +33,7 @@ import ( ) func init() { - listRootCmd.AddCommand(listVirtualMachineVMContributorsCmd) + listRootCmd.AddCommand(listVirtualMachineRoleAssignmentsCmd) } var listVirtualMachineRoleAssignmentsCmd = &cobra.Command{ diff --git a/enums/kind.go b/enums/kind.go index 48ccb2b..b2380ac 100644 --- a/enums/kind.go +++ b/enums/kind.go @@ -31,6 +31,7 @@ const ( KindAZKeyVault Kind = "AZKeyVault" KindAZKeyVaultAccessPolicy Kind = "AZKeyVaultAccessPolicy" KindAZKeyVaultContributor Kind = "AZKeyVaultContributor" + KindAZKeyVaultKVContributor Kind = "AZKeyVaultKVContributor" KindAZKeyVaultOwner Kind = "AZKeyVaultOwner" KindAZKeyVaultUserAccessAdmin Kind = "AZKeyVaultUserAccessAdmin" KindAZManagementGroup Kind = "AZManagementGroup" diff --git a/models/key-vault-kvcontributor.go b/models/key-vault-kvcontributor.go new file mode 100644 index 0000000..a2fdc72 --- /dev/null +++ b/models/key-vault-kvcontributor.go @@ -0,0 +1,30 @@ +// 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 . + +package models + +import "github.com/bloodhoundad/azurehound/models/azure" + +type KeyVaultKVContributor struct { + KVContributor azure.RoleAssignment `json:"kvContributor"` + KeyVaultId string `json:"keyVaultId"` +} + +type KeyVaultKVContributors struct { + KVContributors []KeyVaultKVContributor `json:"kvContributors"` + KeyVaultId string `json:"keyVaultId"` +} diff --git a/models/key-vault-role-assignment.go b/models/key-vault-role-assignment.go new file mode 100644 index 0000000..b3846d1 --- /dev/null +++ b/models/key-vault-role-assignment.go @@ -0,0 +1,30 @@ +// 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 . + +package models + +import "github.com/bloodhoundad/azurehound/models/azure" + +type KeyVaultRoleAssignment struct { + RoleAssignment azure.RoleAssignment `json:"roleAssignment"` + KeyVaultId string `json:"virtualMachineId"` +} + +type KeyVaultRoleAssignments struct { + RoleAssignments []KeyVaultRoleAssignment `json:"roleAssignments"` + KeyVaultId string `json:"virtualMachineId"` +}