diff --git a/cmd/list-app-owners.go b/cmd/list-app-owners.go index eba29df..59a511d 100644 --- a/cmd/list-app-owners.go +++ b/cmd/list-app-owners.go @@ -91,11 +91,11 @@ func listAppOwners(ctx context.Context, client client.AzureClient, apps <-chan i for id := range stream { var ( data = models.AppOwners{ - AppId: id.(string), + AppId: id, } count = 0 ) - for item := range client.ListAzureADAppOwners(ctx, id.(string), "", "", "", nil) { + for item := range client.ListAzureADAppOwners(ctx, id, "", "", "", nil) { if item.Error != nil { log.Error(item.Error, "unable to continue processing owners for this app", "appId", id) } else { diff --git a/cmd/list-azure-rm.go b/cmd/list-azure-rm.go index 0f9cc20..74fbb69 100644 --- a/cmd/list-azure-rm.go +++ b/cmd/list-azure-rm.go @@ -26,6 +26,7 @@ import ( "github.com/bloodhoundad/azurehound/client" "github.com/bloodhoundad/azurehound/enums" + "github.com/bloodhoundad/azurehound/models" "github.com/bloodhoundad/azurehound/pipeline" "github.com/spf13/cobra" ) @@ -70,7 +71,11 @@ func listAllRM(ctx context.Context, client client.AzureClient) <-chan interface{ keyVaults = make(chan interface{}) keyVaults2 = make(chan interface{}) keyVaults3 = make(chan interface{}) - keyVaults4 = make(chan interface{}) + + keyVaultRoleAssignments1 = make(chan azureWrapper[models.KeyVaultRoleAssignments]) + keyVaultRoleAssignments2 = make(chan azureWrapper[models.KeyVaultRoleAssignments]) + keyVaultRoleAssignments3 = make(chan azureWrapper[models.KeyVaultRoleAssignments]) + keyVaultRoleAssignments4 = make(chan azureWrapper[models.KeyVaultRoleAssignments]) mgmtGroups = make(chan interface{}) mgmtGroups2 = make(chan interface{}) @@ -102,10 +107,14 @@ func listAllRM(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) - keyVaultOwners := listKeyVaultOwners(ctx, client, keyVaults2) + pipeline.Tee(ctx.Done(), listKeyVaults(ctx, client, subscriptions2), keyVaults, keyVaults2, keyVaults3) + pipeline.Tee(ctx.Done(), listKeyVaultRoleAssignments(ctx, client, keyVaults2), keyVaultRoleAssignments1, keyVaultRoleAssignments2, keyVaultRoleAssignments3, keyVaultRoleAssignments4) keyVaultAccessPolicies := listKeyVaultAccessPolicies(ctx, client, keyVaults3, []enums.KeyVaultAccessType{enums.GetCerts, enums.GetKeys, enums.GetCerts}) - keyVaultUserAccessAdmins := listKeyVaultUserAccessAdmins(ctx, client, keyVaults4) + + keyVaultOwners := listKeyVaultOwners(ctx, keyVaultRoleAssignments1) + keyVaultUserAccessAdmins := listKeyVaultUserAccessAdmins(ctx, keyVaultRoleAssignments2) + keyVaultContributors := listKeyVaultContributors(ctx, keyVaultRoleAssignments3) + keyVaultKVContributors := listKeyVaultKVContributors(ctx, keyVaultRoleAssignments4) // Enumerate ManagementGroups, ManagementGroupOwners and ManagementGroupDescendants pipeline.Tee(ctx.Done(), listManagementGroups(ctx, client), mgmtGroups, mgmtGroups2, mgmtGroups3, mgmtGroups4) @@ -129,6 +138,8 @@ func listAllRM(ctx context.Context, client client.AzureClient) <-chan interface{ return pipeline.Mux(ctx.Done(), keyVaultAccessPolicies, + keyVaultContributors, + keyVaultKVContributors, keyVaultOwners, keyVaultUserAccessAdmins, keyVaults, diff --git a/cmd/list-device-owners.go b/cmd/list-device-owners.go index 7950668..a60ccde 100644 --- a/cmd/list-device-owners.go +++ b/cmd/list-device-owners.go @@ -90,11 +90,11 @@ func listDeviceOwners(ctx context.Context, client client.AzureClient, devices <- for id := range stream { var ( data = models.DeviceOwners{ - DeviceId: id.(string), + DeviceId: id, } count = 0 ) - for item := range client.ListAzureDeviceRegisteredOwners(ctx, id.(string), false) { + for item := range client.ListAzureDeviceRegisteredOwners(ctx, id, false) { if item.Error != nil { log.Error(item.Error, "unable to continue processing owners for this device", "deviceId", id) } else { diff --git a/cmd/list-group-members.go b/cmd/list-group-members.go index dc51f93..51e9478 100644 --- a/cmd/list-group-members.go +++ b/cmd/list-group-members.go @@ -91,11 +91,11 @@ func listGroupMembers(ctx context.Context, client client.AzureClient, groups <-c for id := range stream { var ( data = models.GroupMembers{ - GroupId: id.(string), + GroupId: id, } count = 0 ) - for item := range client.ListAzureADGroupMembers(ctx, id.(string), "", "", "", nil) { + for item := range client.ListAzureADGroupMembers(ctx, id, "", "", "", nil) { if item.Error != nil { log.Error(item.Error, "unable to continue processing members for this group", "groupId", id) } else { diff --git a/cmd/list-group-owners.go b/cmd/list-group-owners.go index 01141de..7cfb051 100644 --- a/cmd/list-group-owners.go +++ b/cmd/list-group-owners.go @@ -91,11 +91,11 @@ func listGroupOwners(ctx context.Context, client client.AzureClient, groups <-ch for id := range stream { var ( groupOwners = models.GroupOwners{ - GroupId: id.(string), + GroupId: id, } count = 0 ) - for item := range client.ListAzureADGroupOwners(ctx, id.(string), "", "", "", nil) { + for item := range client.ListAzureADGroupOwners(ctx, id, "", "", "", nil) { if item.Error != nil { log.Error(item.Error, "unable to continue processing owners for this group", "groupId", id) } else { diff --git a/cmd/list-key-vault-contributors.go b/cmd/list-key-vault-contributors.go index 77d8032..1e017e7 100644 --- a/cmd/list-key-vault-contributors.go +++ b/cmd/list-key-vault-contributors.go @@ -19,15 +19,13 @@ 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/internal" "github.com/bloodhoundad/azurehound/models" "github.com/bloodhoundad/azurehound/pipeline" "github.com/spf13/cobra" @@ -59,52 +57,30 @@ func listKeyVaultContributorsCmdImpl(cmd *cobra.Command, args []string) { subscriptions := listSubscriptions(ctx, azClient) keyVaults := listKeyVaults(ctx, azClient, subscriptions) kvRoleAssignments := listKeyVaultRoleAssignments(ctx, azClient, keyVaults) - stream := listKeyVaultContributors(ctx, azClient, kvRoleAssignments) + stream := listKeyVaultContributors(ctx, kvRoleAssignments) outputStream(ctx, stream) duration := time.Since(start) log.Info("collection completed", "duration", duration.String()) } } -func listKeyVaultContributors(ctx context.Context, client client.AzureClient, vmRoleAssignments <-chan interface{}) <-chan interface{} { - out := make(chan interface{}) +func listKeyVaultContributors( + ctx context.Context, + kvRoleAssignments <-chan azureWrapper[models.KeyVaultRoleAssignments], +) <-chan any { + return pipeline.Map(ctx.Done(), kvRoleAssignments, func(ra azureWrapper[models.KeyVaultRoleAssignments]) any { + filteredAssignments := internal.Filter(ra.Data.RoleAssignments, kvRoleAssignmentFilter(constants.ContributorRoleID)) - 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 contributors", "result", result) - return - } else { - var ( - keyVaultContributors = models.KeyVaultContributors{ - KeyVaultId: roleAssignments.KeyVaultId, - } - count = 0 - ) - for _, item := range roleAssignments.RoleAssignments { - roleDefinitionId := path.Base(item.RoleAssignment.Properties.RoleDefinitionId) - - 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.KindAZVMContributor, - Data: keyVaultContributors, - } - log.V(1).Info("finished listing key vault contributors", "keyVaultId", roleAssignments.KeyVaultId, "count", count) + contributors := internal.Map(filteredAssignments, func(ra models.KeyVaultRoleAssignment) models.KeyVaultContributor { + return models.KeyVaultContributor{ + ra.RoleAssignment, + ra.KeyVaultId, } - } - log.Info("finished listing all key vault contributors") - }() + }) - return out + return NewAzureWrapper(enums.KindAZKeyVaultContributor, models.KeyVaultContributors{ + KeyVaultId: ra.Data.KeyVaultId, + Contributors: contributors, + }) + }) } diff --git a/cmd/list-key-vault-contributors_test.go b/cmd/list-key-vault-contributors_test.go index 28efe99..064e9e7 100644 --- a/cmd/list-key-vault-contributors_test.go +++ b/cmd/list-key-vault-contributors_test.go @@ -23,6 +23,7 @@ import ( "github.com/bloodhoundad/azurehound/client/mocks" "github.com/bloodhoundad/azurehound/constants" + "github.com/bloodhoundad/azurehound/enums" "github.com/bloodhoundad/azurehound/models" "github.com/bloodhoundad/azurehound/models/azure" "github.com/golang/mock/gomock" @@ -39,7 +40,7 @@ func TestListKeyVaultContributors(t *testing.T) { mockClient := mocks.NewMockAzureClient(ctrl) - mockRoleAssignmentsChannel := make(chan interface{}) + mockRoleAssignmentsChannel := make(chan azureWrapper[models.KeyVaultRoleAssignments]) mockTenant := azure.Tenant{} mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() channel := listKeyVaultContributors(ctx, mockClient, mockRoleAssignmentsChannel) @@ -47,29 +48,23 @@ func TestListKeyVaultContributors(t *testing.T) { go func() { 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, - }, + mockRoleAssignmentsChannel <- NewAzureWrapper(enums.KindAZKeyVaultRoleAssignment, models.KeyVaultRoleAssignments{ + KeyVaultId: "foo", + RoleAssignments: []models.KeyVaultRoleAssignment{ + { + RoleAssignment: azure.RoleAssignment{ + Name: constants.ContributorRoleID, + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.ContributorRoleID, }, }, }, }, - } + }) }() - if result, ok := <-channel; !ok { + if _, 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.KeyVaultContributors); !ok { - t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.KeyVaultContributors{}) } if _, ok := <-channel; ok { diff --git a/cmd/list-key-vault-kvcontributors.go b/cmd/list-key-vault-kvcontributors.go index d14287c..a9bd4f0 100644 --- a/cmd/list-key-vault-kvcontributors.go +++ b/cmd/list-key-vault-kvcontributors.go @@ -19,15 +19,13 @@ 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/internal" "github.com/bloodhoundad/azurehound/models" "github.com/bloodhoundad/azurehound/pipeline" "github.com/spf13/cobra" @@ -59,52 +57,30 @@ func listKeyVaultKVContributorsCmdImpl(cmd *cobra.Command, args []string) { subscriptions := listSubscriptions(ctx, azClient) keyVaults := listKeyVaults(ctx, azClient, subscriptions) kvRoleAssignments := listKeyVaultRoleAssignments(ctx, azClient, keyVaults) - stream := listKeyVaultKVContributors(ctx, azClient, kvRoleAssignments) + stream := listKeyVaultKVContributors(ctx, 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{}) +func listKeyVaultKVContributors( + ctx context.Context, + kvRoleAssignments <-chan azureWrapper[models.KeyVaultRoleAssignments], +) <-chan any { + return pipeline.Map(ctx.Done(), kvRoleAssignments, func(ra azureWrapper[models.KeyVaultRoleAssignments]) any { + filteredAssignments := internal.Filter(ra.Data.RoleAssignments, kvRoleAssignmentFilter(constants.KeyVaultContributorRoleID)) - 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) + kvContributors := internal.Map(filteredAssignments, func(ra models.KeyVaultRoleAssignment) models.KeyVaultKVContributor { + return models.KeyVaultKVContributor{ + KVContributor: ra.RoleAssignment, + KeyVaultId: ra.KeyVaultId, } - } - log.Info("finished listing all key vault kvContributors") - }() + }) - return out + return NewAzureWrapper(enums.KindAZKeyVaultKVContributor, models.KeyVaultKVContributors{ + KeyVaultId: ra.Data.KeyVaultId, + KVContributors: kvContributors, + }) + }) } diff --git a/cmd/list-key-vault-kvcontributors_test.go b/cmd/list-key-vault-kvcontributors_test.go index bc3dc17..63c1193 100644 --- a/cmd/list-key-vault-kvcontributors_test.go +++ b/cmd/list-key-vault-kvcontributors_test.go @@ -23,6 +23,7 @@ import ( "github.com/bloodhoundad/azurehound/client/mocks" "github.com/bloodhoundad/azurehound/constants" + "github.com/bloodhoundad/azurehound/enums" "github.com/bloodhoundad/azurehound/models" "github.com/bloodhoundad/azurehound/models/azure" "github.com/golang/mock/gomock" @@ -39,16 +40,17 @@ func TestListKeyVaultKVContributors(t *testing.T) { mockClient := mocks.NewMockAzureClient(ctrl) - mockRoleAssignmentsChannel := make(chan interface{}) + mockRoleAssignmentsChannel := make(chan azureWrapper[models.KeyVaultRoleAssignments]) mockTenant := azure.Tenant{} mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() - channel := listKeyVaultKVContributors(ctx, mockClient, mockRoleAssignmentsChannel) + channel := listKeyVaultKVContributors(ctx, mockRoleAssignmentsChannel) go func() { defer close(mockRoleAssignmentsChannel) - mockRoleAssignmentsChannel <- AzureWrapper{ - Data: models.KeyVaultRoleAssignments{ + mockRoleAssignmentsChannel <- NewAzureWrapper( + enums.KindAZKeyVaultRoleAssignment, + models.KeyVaultRoleAssignments{ KeyVaultId: "foo", RoleAssignments: []models.KeyVaultRoleAssignment{ { @@ -61,15 +63,11 @@ func TestListKeyVaultKVContributors(t *testing.T) { }, }, }, - } + ) }() - if result, ok := <-channel; !ok { + if _, 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 { diff --git a/cmd/list-key-vault-owners.go b/cmd/list-key-vault-owners.go index ecf954d..a119c98 100644 --- a/cmd/list-key-vault-owners.go +++ b/cmd/list-key-vault-owners.go @@ -19,15 +19,13 @@ 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/internal" "github.com/bloodhoundad/azurehound/models" "github.com/bloodhoundad/azurehound/pipeline" "github.com/spf13/cobra" @@ -59,52 +57,30 @@ func listKeyVaultOwnersCmdImpl(cmd *cobra.Command, args []string) { subscriptions := listSubscriptions(ctx, azClient) keyVaults := listKeyVaults(ctx, azClient, subscriptions) kvRoleAssignments := listKeyVaultRoleAssignments(ctx, azClient, keyVaults) - stream := listKeyVaultOwners(ctx, azClient, kvRoleAssignments) + stream := listKeyVaultOwners(ctx, kvRoleAssignments) outputStream(ctx, stream) duration := time.Since(start) log.Info("collection completed", "duration", duration.String()) } } -func listKeyVaultOwners(ctx context.Context, client client.AzureClient, vmRoleAssignments <-chan interface{}) <-chan interface{} { - out := make(chan interface{}) +func listKeyVaultOwners( + ctx context.Context, + kvRoleAssignments <-chan azureWrapper[models.KeyVaultRoleAssignments], +) <-chan any { + return pipeline.Map(ctx.Done(), kvRoleAssignments, func(ra azureWrapper[models.KeyVaultRoleAssignments]) any { + filteredAssignments := internal.Filter(ra.Data.RoleAssignments, kvRoleAssignmentFilter(constants.OwnerRoleID)) - 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 owners", "result", result) - return - } else { - var ( - keyVaultOwners = models.KeyVaultOwners{ - KeyVaultId: roleAssignments.KeyVaultId, - } - count = 0 - ) - for _, item := range roleAssignments.RoleAssignments { - roleDefinitionId := path.Base(item.RoleAssignment.Properties.RoleDefinitionId) - - 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.KindAZVMOwner, - Data: keyVaultOwners, - } - log.V(1).Info("finished listing key vault owners", "keyVaultId", roleAssignments.KeyVaultId, "count", count) + kvContributors := internal.Map(filteredAssignments, func(ra models.KeyVaultRoleAssignment) models.KeyVaultOwner { + return models.KeyVaultOwner{ + Owner: ra.RoleAssignment, + KeyVaultId: ra.KeyVaultId, } - } - log.Info("finished listing all key vault owners") - }() + }) - return out + return NewAzureWrapper(enums.KindAZKeyVaultOwner, models.KeyVaultOwners{ + KeyVaultId: ra.Data.KeyVaultId, + Owners: kvContributors, + }) + }) } diff --git a/cmd/list-key-vault-owners_test.go b/cmd/list-key-vault-owners_test.go index 97a6d5f..2b5c231 100644 --- a/cmd/list-key-vault-owners_test.go +++ b/cmd/list-key-vault-owners_test.go @@ -23,6 +23,7 @@ import ( "github.com/bloodhoundad/azurehound/client/mocks" "github.com/bloodhoundad/azurehound/constants" + "github.com/bloodhoundad/azurehound/enums" "github.com/bloodhoundad/azurehound/models" "github.com/bloodhoundad/azurehound/models/azure" "github.com/golang/mock/gomock" @@ -39,16 +40,17 @@ func TestListKeyVaultOwners(t *testing.T) { mockClient := mocks.NewMockAzureClient(ctrl) - mockRoleAssignmentsChannel := make(chan interface{}) + mockRoleAssignmentsChannel := make(chan azureWrapper[models.KeyVaultRoleAssignments]) mockTenant := azure.Tenant{} mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() - channel := listKeyVaultOwners(ctx, mockClient, mockRoleAssignmentsChannel) + channel := listKeyVaultOwners(ctx, mockRoleAssignmentsChannel) go func() { defer close(mockRoleAssignmentsChannel) - mockRoleAssignmentsChannel <- AzureWrapper{ - Data: models.KeyVaultRoleAssignments{ + mockRoleAssignmentsChannel <- NewAzureWrapper( + enums.KindAZKeyVaultRoleAssignment, + models.KeyVaultRoleAssignments{ KeyVaultId: "foo", RoleAssignments: []models.KeyVaultRoleAssignment{ { @@ -61,15 +63,11 @@ func TestListKeyVaultOwners(t *testing.T) { }, }, }, - } + ) }() - if result, ok := <-channel; !ok { + if _, 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.KeyVaultOwners); !ok { - t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.KeyVaultOwners{}) } if _, ok := <-channel; ok { diff --git a/cmd/list-key-vault-role-assignments.go b/cmd/list-key-vault-role-assignments.go index 208dcc3..ef8f7da 100644 --- a/cmd/list-key-vault-role-assignments.go +++ b/cmd/list-key-vault-role-assignments.go @@ -63,9 +63,9 @@ func listKeyVaultRoleAssignmentsCmdImpl(cmd *cobra.Command, args []string) { } } -func listKeyVaultRoleAssignments(ctx context.Context, client client.AzureClient, keyVaults <-chan interface{}) <-chan interface{} { +func listKeyVaultRoleAssignments(ctx context.Context, client client.AzureClient, keyVaults <-chan interface{}) <-chan azureWrapper[models.KeyVaultRoleAssignments] { var ( - out = make(chan interface{}) + out = make(chan azureWrapper[models.KeyVaultRoleAssignments]) ids = make(chan string) streams = pipeline.Demux(ctx.Done(), ids, 25) wg sync.WaitGroup @@ -92,11 +92,11 @@ func listKeyVaultRoleAssignments(ctx context.Context, client client.AzureClient, for id := range stream { var ( keyVaultRoleAssignments = models.KeyVaultRoleAssignments{ - KeyVaultId: id.(string), + KeyVaultId: id, } count = 0 ) - for item := range client.ListRoleAssignmentsForResource(ctx, id.(string), "") { + for item := range client.ListRoleAssignmentsForResource(ctx, id, "") { if item.Error != nil { log.Error(item.Error, "unable to continue processing role assignments for this key vault", "keyVaultId", id) } else { @@ -109,10 +109,7 @@ func listKeyVaultRoleAssignments(ctx context.Context, client client.AzureClient, keyVaultRoleAssignments.RoleAssignments = append(keyVaultRoleAssignments.RoleAssignments, keyVaultRoleAssignment) } } - out <- AzureWrapper{ - Kind: enums.KindAZVMRoleAssignment, - Data: keyVaultRoleAssignments, - } + out <- NewAzureWrapper(enums.KindAZKeyVaultRoleAssignment, keyVaultRoleAssignments) log.V(1).Info("finished listing key vault role assignments", "keyVaultId", id, "count", count) } }() diff --git a/cmd/list-key-vault-role-assignments_test.go b/cmd/list-key-vault-role-assignments_test.go index 49857ce..aa7aade 100644 --- a/cmd/list-key-vault-role-assignments_test.go +++ b/cmd/list-key-vault-role-assignments_test.go @@ -93,21 +93,13 @@ func TestListKeyVaultRoleAssignments(t *testing.T) { 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) + } else if len(result.Data.RoleAssignments) != 2 { + t.Errorf("got %v, want %v", len(result.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) + } else if len(result.Data.RoleAssignments) != 1 { + t.Errorf("got %v, want %v", len(result.Data.RoleAssignments), 1) } } diff --git a/cmd/list-key-vault-user-access-admins.go b/cmd/list-key-vault-user-access-admins.go index 4f8a80d..2162c87 100644 --- a/cmd/list-key-vault-user-access-admins.go +++ b/cmd/list-key-vault-user-access-admins.go @@ -19,15 +19,13 @@ 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/internal" "github.com/bloodhoundad/azurehound/models" "github.com/bloodhoundad/azurehound/pipeline" "github.com/spf13/cobra" @@ -59,52 +57,30 @@ func listKeyVaultUserAccessAdminsCmdImpl(cmd *cobra.Command, args []string) { subscriptions := listSubscriptions(ctx, azClient) keyVaults := listKeyVaults(ctx, azClient, subscriptions) kvRoleAssignments := listKeyVaultRoleAssignments(ctx, azClient, keyVaults) - stream := listKeyVaultUserAccessAdmins(ctx, azClient, kvRoleAssignments) + stream := listKeyVaultUserAccessAdmins(ctx, kvRoleAssignments) outputStream(ctx, stream) duration := time.Since(start) log.Info("collection completed", "duration", duration.String()) } } -func listKeyVaultUserAccessAdmins(ctx context.Context, client client.AzureClient, vmRoleAssignments <-chan interface{}) <-chan interface{} { - out := make(chan interface{}) +func listKeyVaultUserAccessAdmins( + ctx context.Context, + kvRoleAssignments <-chan azureWrapper[models.KeyVaultRoleAssignments], +) <-chan any { + return pipeline.Map(ctx.Done(), kvRoleAssignments, func(ra azureWrapper[models.KeyVaultRoleAssignments]) any { + filteredAssignments := internal.Filter(ra.Data.RoleAssignments, kvRoleAssignmentFilter(constants.UserAccessAdminRoleID)) - 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 userAccessAdmins", "result", result) - return - } else { - var ( - keyVaultUserAccessAdmins = models.KeyVaultUserAccessAdmins{ - KeyVaultId: roleAssignments.KeyVaultId, - } - count = 0 - ) - for _, item := range roleAssignments.RoleAssignments { - roleDefinitionId := path.Base(item.RoleAssignment.Properties.RoleDefinitionId) - - 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.KindAZVMUserAccessAdmin, - Data: keyVaultUserAccessAdmins, - } - log.V(1).Info("finished listing key vault userAccessAdmins", "keyVaultId", roleAssignments.KeyVaultId, "count", count) + kvContributors := internal.Map(filteredAssignments, func(ra models.KeyVaultRoleAssignment) models.KeyVaultUserAccessAdmin { + return models.KeyVaultUserAccessAdmin{ + UserAccessAdmin: ra.RoleAssignment, + KeyVaultId: ra.KeyVaultId, } - } - log.Info("finished listing all key vault userAccessAdmins") - }() + }) - return out + return NewAzureWrapper(enums.KindAZKeyVaultUserAccessAdmin, models.KeyVaultUserAccessAdmins{ + KeyVaultId: ra.Data.KeyVaultId, + UserAccessAdmins: kvContributors, + }) + }) } diff --git a/cmd/list-key-vault-user-access-admins_test.go b/cmd/list-key-vault-user-access-admins_test.go index 670a151..d95449c 100644 --- a/cmd/list-key-vault-user-access-admins_test.go +++ b/cmd/list-key-vault-user-access-admins_test.go @@ -23,6 +23,7 @@ import ( "github.com/bloodhoundad/azurehound/client/mocks" "github.com/bloodhoundad/azurehound/constants" + "github.com/bloodhoundad/azurehound/enums" "github.com/bloodhoundad/azurehound/models" "github.com/bloodhoundad/azurehound/models/azure" "github.com/golang/mock/gomock" @@ -39,16 +40,17 @@ func TestListKeyVaultUserAccessAdmins(t *testing.T) { mockClient := mocks.NewMockAzureClient(ctrl) - mockRoleAssignmentsChannel := make(chan interface{}) + mockRoleAssignmentsChannel := make(chan azureWrapper[models.KeyVaultRoleAssignments]) mockTenant := azure.Tenant{} mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() - channel := listKeyVaultUserAccessAdmins(ctx, mockClient, mockRoleAssignmentsChannel) + channel := listKeyVaultUserAccessAdmins(ctx, mockRoleAssignmentsChannel) go func() { defer close(mockRoleAssignmentsChannel) - mockRoleAssignmentsChannel <- AzureWrapper{ - Data: models.KeyVaultRoleAssignments{ + mockRoleAssignmentsChannel <- NewAzureWrapper( + enums.KindAZKeyVaultRoleAssignment, + models.KeyVaultRoleAssignments{ KeyVaultId: "foo", RoleAssignments: []models.KeyVaultRoleAssignment{ { @@ -61,15 +63,11 @@ func TestListKeyVaultUserAccessAdmins(t *testing.T) { }, }, }, - } + ) }() - if result, ok := <-channel; !ok { + if _, 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.KeyVaultUserAccessAdmins); !ok { - t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.KeyVaultUserAccessAdmins{}) } if _, ok := <-channel; ok { diff --git a/cmd/list-key-vaults.go b/cmd/list-key-vaults.go index cee7720..a8d582c 100644 --- a/cmd/list-key-vaults.go +++ b/cmd/list-key-vaults.go @@ -90,7 +90,7 @@ func listKeyVaults(ctx context.Context, client client.AzureClient, subscriptions defer wg.Done() for id := range stream { count := 0 - for item := range client.ListAzureKeyVaults(ctx, id.(string), 999) { + for item := range client.ListAzureKeyVaults(ctx, id, 999) { if item.Error != nil { log.Error(item.Error, "unable to continue processing key vaults for this subscription", "subscriptionId", id) } else { diff --git a/cmd/list-management-group-descendants.go b/cmd/list-management-group-descendants.go index 31350f2..e04208e 100644 --- a/cmd/list-management-group-descendants.go +++ b/cmd/list-management-group-descendants.go @@ -90,7 +90,7 @@ func listManagementGroupDescendants(ctx context.Context, client client.AzureClie defer wg.Done() for id := range stream { count := 0 - for item := range client.ListAzureManagementGroupDescendants(ctx, id.(string)) { + for item := range client.ListAzureManagementGroupDescendants(ctx, id) { if item.Error != nil { log.Error(item.Error, "unable to continue processing descendants for this management group", "managementGroupId", id) } else { diff --git a/cmd/list-management-group-owners.go b/cmd/list-management-group-owners.go index 73f6c19..12f7fd7 100644 --- a/cmd/list-management-group-owners.go +++ b/cmd/list-management-group-owners.go @@ -93,11 +93,11 @@ func listManagementGroupOwners(ctx context.Context, client client.AzureClient, m for id := range stream { var ( managementGroupOwners = models.ManagementGroupOwners{ - ManagementGroupId: id.(string), + ManagementGroupId: id, } count = 0 ) - for item := range client.ListRoleAssignmentsForResource(ctx, id.(string), "") { + for item := range client.ListRoleAssignmentsForResource(ctx, id, "") { if item.Error != nil { log.Error(item.Error, "unable to continue processing owners for this management group", "managementGroupId", id) } else { diff --git a/cmd/list-management-group-user-access-admins.go b/cmd/list-management-group-user-access-admins.go index e990571..b207245 100644 --- a/cmd/list-management-group-user-access-admins.go +++ b/cmd/list-management-group-user-access-admins.go @@ -94,11 +94,11 @@ func listManagementGroupUserAccessAdmins(ctx context.Context, client client.Azur for id := range stream { var ( mgmtGroupUserAccessAdmins = models.ManagementGroupUserAccessAdmins{ - ManagementGroupId: id.(string), + ManagementGroupId: id, } count = 0 ) - for item := range client.ListRoleAssignmentsForResource(ctx, id.(string), "") { + for item := range client.ListRoleAssignmentsForResource(ctx, id, "") { if item.Error != nil { log.Error(item.Error, "unable to continue processing user access admins for this management group", "managementGroupId", id) } else { diff --git a/cmd/list-resource-group-owners.go b/cmd/list-resource-group-owners.go index 1a86a8d..fd6923f 100644 --- a/cmd/list-resource-group-owners.go +++ b/cmd/list-resource-group-owners.go @@ -94,11 +94,11 @@ func listResourceGroupOwners(ctx context.Context, client client.AzureClient, res for id := range stream { var ( resourceGroupOwners = models.ResourceGroupOwners{ - ResourceGroupId: id.(string), + ResourceGroupId: id, } count = 0 ) - for item := range client.ListRoleAssignmentsForResource(ctx, id.(string), "") { + for item := range client.ListRoleAssignmentsForResource(ctx, id, "") { if item.Error != nil { log.Error(item.Error, "unable to continue processing owners for this resource group", "resourceGroupId", id) } else { diff --git a/cmd/list-resource-group-user-access-admins.go b/cmd/list-resource-group-user-access-admins.go index a89f6d9..7870d64 100644 --- a/cmd/list-resource-group-user-access-admins.go +++ b/cmd/list-resource-group-user-access-admins.go @@ -94,11 +94,11 @@ func listResourceGroupUserAccessAdmins(ctx context.Context, client client.AzureC for id := range stream { var ( resourceGroupUserAccessAdmins = models.ResourceGroupUserAccessAdmins{ - ResourceGroupId: id.(string), + ResourceGroupId: id, } count = 0 ) - for item := range client.ListRoleAssignmentsForResource(ctx, id.(string), "") { + for item := range client.ListRoleAssignmentsForResource(ctx, id, "") { if item.Error != nil { log.Error(item.Error, "unable to continue processing user access admins for this resource group", "resourceGroupId", id) } else { diff --git a/cmd/list-resource-groups.go b/cmd/list-resource-groups.go index ef225eb..cbba504 100644 --- a/cmd/list-resource-groups.go +++ b/cmd/list-resource-groups.go @@ -90,7 +90,7 @@ func listResourceGroups(ctx context.Context, client client.AzureClient, subscrip defer wg.Done() for id := range stream { count := 0 - for item := range client.ListAzureResourceGroups(ctx, id.(string), "") { + for item := range client.ListAzureResourceGroups(ctx, id, "") { if item.Error != nil { log.Error(item.Error, "unable to continue processing resource groups for this subscription", "subscriptionId", id) } else { diff --git a/cmd/list-role-assignments.go b/cmd/list-role-assignments.go index c3a53c7..2a4ab98 100644 --- a/cmd/list-role-assignments.go +++ b/cmd/list-role-assignments.go @@ -92,11 +92,11 @@ func listRoleAssignments(ctx context.Context, client client.AzureClient, roles < for id := range stream { var ( roleAssignments = models.RoleAssignments{ - RoleDefinitionId: id.(string), + RoleDefinitionId: id, TenantId: client.TenantInfo().TenantId, } count = 0 - filter = fmt.Sprintf("roleDefinitionId eq '%s'", id.(string)) + filter = fmt.Sprintf("roleDefinitionId eq '%s'", id) ) for item := range client.ListAzureADRoleAssignments(ctx, filter, "", "", "", nil) { if item.Error != nil { diff --git a/cmd/list-root.go b/cmd/list-root.go index bf36080..274d4f2 100644 --- a/cmd/list-root.go +++ b/cmd/list-root.go @@ -27,6 +27,7 @@ import ( "github.com/bloodhoundad/azurehound/client" "github.com/bloodhoundad/azurehound/config" "github.com/bloodhoundad/azurehound/enums" + "github.com/bloodhoundad/azurehound/models" "github.com/bloodhoundad/azurehound/pipeline" "github.com/spf13/cobra" ) @@ -82,9 +83,11 @@ func listAll(ctx context.Context, client client.AzureClient) <-chan interface{} keyVaults = make(chan interface{}) keyVaults2 = make(chan interface{}) keyVaults3 = make(chan interface{}) - keyVaults4 = make(chan interface{}) - keyVaults5 = make(chan interface{}) - keyVaults6 = make(chan interface{}) + + keyVaultRoleAssignments1 = make(chan azureWrapper[models.KeyVaultRoleAssignments]) + keyVaultRoleAssignments2 = make(chan azureWrapper[models.KeyVaultRoleAssignments]) + keyVaultRoleAssignments3 = make(chan azureWrapper[models.KeyVaultRoleAssignments]) + keyVaultRoleAssignments4 = make(chan azureWrapper[models.KeyVaultRoleAssignments]) mgmtGroups = make(chan interface{}) mgmtGroups2 = make(chan interface{}) @@ -140,12 +143,14 @@ 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, keyVaults6) - keyVaultOwners := listKeyVaultOwners(ctx, client, keyVaults2) + pipeline.Tee(ctx.Done(), listKeyVaults(ctx, client, subscriptions2), keyVaults, keyVaults2, keyVaults3) + pipeline.Tee(ctx.Done(), listKeyVaultRoleAssignments(ctx, client, keyVaults2), keyVaultRoleAssignments1, keyVaultRoleAssignments2, keyVaultRoleAssignments3, keyVaultRoleAssignments4) 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) + + keyVaultOwners := listKeyVaultOwners(ctx, keyVaultRoleAssignments1) + keyVaultUserAccessAdmins := listKeyVaultUserAccessAdmins(ctx, keyVaultRoleAssignments2) + keyVaultContributors := listKeyVaultContributors(ctx, keyVaultRoleAssignments3) + keyVaultKVContributors := listKeyVaultKVContributors(ctx, keyVaultRoleAssignments4) // Enumerate ManagementGroups, ManagementGroupOwners and ManagementGroupDescendants pipeline.Tee(ctx.Done(), listManagementGroups(ctx, client), mgmtGroups, mgmtGroups2, mgmtGroups3, mgmtGroups4) diff --git a/cmd/list-service-principal-owners.go b/cmd/list-service-principal-owners.go index bd0bf12..e856bdf 100644 --- a/cmd/list-service-principal-owners.go +++ b/cmd/list-service-principal-owners.go @@ -91,11 +91,11 @@ func listServicePrincipalOwners(ctx context.Context, client client.AzureClient, for id := range stream { var ( servicePrincipalOwners = models.ServicePrincipalOwners{ - ServicePrincipalId: id.(string), + ServicePrincipalId: id, } count = 0 ) - for item := range client.ListAzureADServicePrincipalOwners(ctx, id.(string), "", "", "", nil) { + for item := range client.ListAzureADServicePrincipalOwners(ctx, id, "", "", "", nil) { if item.Error != nil { log.Error(item.Error, "unable to continue processing owners for this service principal", "servicePrincipalId", id) } else { diff --git a/cmd/list-subscription-owners.go b/cmd/list-subscription-owners.go index 5789bf4..0dd2baf 100644 --- a/cmd/list-subscription-owners.go +++ b/cmd/list-subscription-owners.go @@ -94,11 +94,11 @@ func listSubscriptionOwners(ctx context.Context, client client.AzureClient, subs for id := range stream { var ( subscriptionOwners = models.SubscriptionOwners{ - SubscriptionId: id.(string), + SubscriptionId: id, } count = 0 ) - for item := range client.ListRoleAssignmentsForResource(ctx, id.(string), "") { + for item := range client.ListRoleAssignmentsForResource(ctx, id, "") { if item.Error != nil { log.Error(item.Error, "unable to continue processing owners for this subscription", "subscriptionId", id) } else { diff --git a/cmd/list-subscription-user-access-admins.go b/cmd/list-subscription-user-access-admins.go index 70734a6..5077399 100644 --- a/cmd/list-subscription-user-access-admins.go +++ b/cmd/list-subscription-user-access-admins.go @@ -94,11 +94,11 @@ func listSubscriptionUserAccessAdmins(ctx context.Context, client client.AzureCl for id := range stream { var ( subscriptionUserAccessAdmins = models.SubscriptionUserAccessAdmins{ - SubscriptionId: id.(string), + SubscriptionId: id, } count = 0 ) - for item := range client.ListRoleAssignmentsForResource(ctx, id.(string), "") { + for item := range client.ListRoleAssignmentsForResource(ctx, id, "") { if item.Error != nil { log.Error(item.Error, "unable to continue processing user access admins for this subscription", "subscriptionId", id) } else { diff --git a/cmd/list-virtual-machine-role-assignments.go b/cmd/list-virtual-machine-role-assignments.go index a861865..5ae32f8 100644 --- a/cmd/list-virtual-machine-role-assignments.go +++ b/cmd/list-virtual-machine-role-assignments.go @@ -92,11 +92,11 @@ func listVirtualMachineRoleAssignments(ctx context.Context, client client.AzureC for id := range stream { var ( virtualMachineRoleAssignments = models.VirtualMachineRoleAssignments{ - VirtualMachineId: id.(string), + VirtualMachineId: id, } count = 0 ) - for item := range client.ListRoleAssignmentsForResource(ctx, id.(string), "") { + for item := range client.ListRoleAssignmentsForResource(ctx, id, "") { if item.Error != nil { log.Error(item.Error, "unable to continue processing role assignments for this virtual machine", "virtualMachineId", id) } else { diff --git a/cmd/list-virtual-machines.go b/cmd/list-virtual-machines.go index 31a147c..9483cfd 100644 --- a/cmd/list-virtual-machines.go +++ b/cmd/list-virtual-machines.go @@ -89,7 +89,7 @@ func listVirtualMachines(ctx context.Context, client client.AzureClient, subscri defer wg.Done() for id := range stream { count := 0 - for item := range client.ListAzureVirtualMachines(ctx, id.(string), false) { + for item := range client.ListAzureVirtualMachines(ctx, id, false) { if item.Error != nil { log.Error(item.Error, "unable to continue processing virtual machines for this subscription", "subscriptionId", id) } else { diff --git a/cmd/utils.go b/cmd/utils.go index 35ca337..56d044d 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -32,6 +32,7 @@ import ( "net/http" "net/url" "os" + "path" "path/filepath" "time" @@ -41,6 +42,7 @@ import ( "github.com/bloodhoundad/azurehound/config" "github.com/bloodhoundad/azurehound/enums" "github.com/bloodhoundad/azurehound/logger" + "github.com/bloodhoundad/azurehound/models" "github.com/bloodhoundad/azurehound/pipeline" "github.com/bloodhoundad/azurehound/sinks" "github.com/spf13/cobra" @@ -371,12 +373,25 @@ func setupLogger() { } } +// deprecated: use azureWrapper instead type AzureWrapper struct { Kind enums.Kind `json:"kind"` Data interface{} `json:"data"` } -func outputStream(ctx context.Context, stream <-chan interface{}) { +type azureWrapper[T any] struct { + Kind enums.Kind `json:"kind"` + Data T `json:"data"` +} + +func NewAzureWrapper[T any](kind enums.Kind, data T) azureWrapper[T] { + return azureWrapper[T]{ + Kind: kind, + Data: data, + } +} + +func outputStream[T any](ctx context.Context, stream <-chan T) { formatted := pipeline.FormatJson(ctx.Done(), stream) if path := config.OutputFile.Value().(string); path != "" { if err := sinks.WriteToFile(ctx, path, formatted); err != nil { @@ -386,3 +401,9 @@ func outputStream(ctx context.Context, stream <-chan interface{}) { sinks.WriteToConsole(ctx, formatted) } } + +func kvRoleAssignmentFilter(roleId string) func(models.KeyVaultRoleAssignment) bool { + return func(ra models.KeyVaultRoleAssignment) bool { + return path.Base(ra.RoleAssignment.Properties.RoleDefinitionId) == roleId + } +} diff --git a/enums/kind.go b/enums/kind.go index b2380ac..ad05241 100644 --- a/enums/kind.go +++ b/enums/kind.go @@ -33,6 +33,7 @@ const ( KindAZKeyVaultContributor Kind = "AZKeyVaultContributor" KindAZKeyVaultKVContributor Kind = "AZKeyVaultKVContributor" KindAZKeyVaultOwner Kind = "AZKeyVaultOwner" + KindAZKeyVaultRoleAssignment Kind = "AZKeyVaultRoleAssignment" KindAZKeyVaultUserAccessAdmin Kind = "AZKeyVaultUserAccessAdmin" KindAZManagementGroup Kind = "AZManagementGroup" KindAZManagementGroupOwner Kind = "AZManagementGroupOwner" diff --git a/internal/utils.go b/internal/utils.go new file mode 100644 index 0000000..f7ce20e --- /dev/null +++ b/internal/utils.go @@ -0,0 +1,19 @@ +package internal + +func Map[T, U any](collection []T, fn func(T) U) []U { + var out []U + for i := range collection { + out = append(out, fn(collection[i])) + } + return out +} + +func Filter[T any](collection []T, fn func(T) bool) []T { + var out []T + for i := range collection { + if fn(collection[i]) { + out = append(out, collection[i]) + } + } + return out +} diff --git a/pipeline/pipeline.go b/pipeline/pipeline.go new file mode 100644 index 0000000..f3110b0 --- /dev/null +++ b/pipeline/pipeline.go @@ -0,0 +1,229 @@ +// 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 pipeline + +import ( + "encoding/json" + "reflect" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/internal" +) + +type Result[T any] struct { + Error error + Ok T +} + +// OrDone provides an explicit cancellation mechanism to ensure the encapsulated and downstream goroutines are cleaned +// up. This frees the caller from depending on the input channel to close in order to free the goroutine, thus +// preventing possible leaks. +func OrDone[D, T any](done <-chan D, in <-chan T) <-chan T { + out := make(chan T) + + go func() { + defer close(out) + for { + select { + case <-done: + return + case val, ok := <-in: + if !ok { + return + } else { + select { + case out <- val: + case <-done: + } + } + } + } + }() + return out +} + +// Mux joins multiple channels and returns a channel as single stream of data. +func Mux[D any](done <-chan D, channels ...<-chan any) <-chan any { + var wg sync.WaitGroup + out := make(chan interface{}) + + muxer := func(channel <-chan any) { + defer wg.Done() + for item := range OrDone(done, channel) { + out <- item + } + } + + wg.Add(len(channels)) + for _, channel := range channels { + go muxer(channel) + } + + go func() { + wg.Wait() + close(out) + }() + + return out +} + +// Demux distributes the stream of data from a single channel across multiple channels to parallelize CPU use and I/O +func Demux[D, T any](done <-chan D, in <-chan T, size int) []<-chan T { + outputs := make([]chan T, size) + + for i := range outputs { + outputs[i] = make(chan T) + } + + closeOutputs := func() { + for i := range outputs { + close(outputs[i]) + } + } + + cases := internal.Map(outputs, func(out chan T) reflect.SelectCase { + return reflect.SelectCase{ + Dir: reflect.SelectSend, + Chan: reflect.ValueOf(out), + } + }) + + go func() { + defer closeOutputs() + for item := range OrDone(done, in) { + // send item to exactly one channel + for i := range cases { + cases[i].Send = reflect.ValueOf(item) + } + reflect.Select(cases) + } + }() + + return internal.Map(outputs, func(out chan T) <-chan T { return out }) +} + +func Map[D, T, U any](done <-chan D, in <-chan T, fn func(T) U) <-chan U { + out := make(chan U) + go func() { + defer close(out) + for item := range OrDone(done, in) { + out <- fn(item) + } + }() + return out +} + +func Filter[D, T any](done <-chan D, in <-chan T, fn func(T) bool) <-chan T { + out := make(chan T) + go func() { + defer close(out) + for item := range OrDone(done, in) { + if fn(item) { + out <- item + } + } + }() + return out +} + +// Tee copies the stream of data from a single channel to zero or more channels +func Tee[D, T any](done <-chan D, in <-chan T, outputs ...chan<- T) { + go func() { + // Need to close outputs when goroutine exits to ensure we avoid deadlock + defer func() { + for i := range outputs { + close(outputs[i]) + } + }() + + for item := range OrDone(done, in) { + for _, out := range outputs { + select { + case <-done: + case out <- item: + } + } + } + }() +} + +func Batch[D, T any](done <-chan D, in <-chan T, maxItems int, maxTimeout time.Duration) <-chan []T { + out := make(chan []T) + + go func() { + defer close(out) + + timeout := time.After(maxTimeout) + var batch []T + for { + select { + case <-done: + if len(batch) > 0 { + out <- batch + batch = nil + } + return + case item, ok := <-in: + if !ok { + if len(batch) > 0 { + out <- batch + batch = nil + } + return + } else { + // Add to batch + batch = append(batch, item) + + // Flush if limit is reached + if len(batch) >= maxItems { + out <- batch + batch = nil + timeout = time.After(maxTimeout) + } + } + case <-timeout: + if len(batch) > 0 { + out <- batch + batch = nil + } + timeout = time.After(maxTimeout) + } + } + }() + + return out +} + +func FormatJson[D, T any](done <-chan D, in <-chan T) <-chan string { + out := make(chan string) + + go func() { + defer close(out) + + for item := range OrDone(done, in) { + if bytes, err := json.Marshal(item); err != nil { + panic(err) + } else { + out <- string(bytes) + } + } + }() + + return out +} diff --git a/pipeline/utils_test.go b/pipeline/pipeline_test.go similarity index 100% rename from pipeline/utils_test.go rename to pipeline/pipeline_test.go diff --git a/pipeline/utils.go b/pipeline/utils.go deleted file mode 100644 index 76021bf..0000000 --- a/pipeline/utils.go +++ /dev/null @@ -1,262 +0,0 @@ -// 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 pipeline - -import ( - "encoding/json" - "fmt" - "reflect" - "sync" - "time" -) - -type Result struct { - Error error - Ok interface{} -} - -// OrDone provides an explicit cancellation mechanism to ensure the encapsulated and downstream goroutines are cleaned -// up. This frees the caller from depending on the input channel to close in order to free the goroutine, thus -// preventing possible leaks. -func OrDone(done, in interface{}) <-chan interface{} { - if !isReadable(done) || !isReadable(in) { - panic(fmt.Errorf("channels must be readable")) - } - out := make(chan interface{}) - - go func() { - defer close(out) - doneCase := reflect.SelectCase{ - Dir: reflect.SelectRecv, - Chan: reflect.ValueOf(done), - } - - outerCases := []reflect.SelectCase{ - doneCase, - { - Dir: reflect.SelectRecv, - Chan: reflect.ValueOf(in), - }, - } - - innerCases := []reflect.SelectCase{ - doneCase, - { - Dir: reflect.SelectSend, - Chan: reflect.ValueOf(out), - }, - } - for { - if chosen, item, ok := reflect.Select(outerCases); chosen == 0 || !ok { - // If received on done then return - return - } else { - if !ok { - return - } else { - innerCases[1].Send = item - if chosen, _, _ := reflect.Select(innerCases); chosen == 0 || !ok { - return - } - } - } - } - }() - return out -} - -// Mux joins multiple channels and returns a channel as single stream of data. -func Mux(done interface{}, channels ...interface{}) <-chan interface{} { - var wg sync.WaitGroup - out := make(chan interface{}) - - muxer := func(channel interface{}) { - defer wg.Done() - for item := range OrDone(done, channel) { - out <- item - } - } - - wg.Add(len(channels)) - for _, channel := range channels { - go muxer(channel) - } - - go func() { - wg.Wait() - close(out) - }() - - return out -} - -// Demux distributes the stream of data from a single channel across multiple channels to parallelize CPU use and I/O -func Demux(done interface{}, in interface{}, size int) []<-chan interface{} { - // use reflection to dynamically create select statement - outputs := make([]chan interface{}, size) - readChans := []<-chan interface{}{} - for i := range outputs { - out := make(chan interface{}) - outputs[i] = out - readChans = append(readChans, out) - } - - closeOutputs := func() { - for i := range outputs { - close(outputs[i]) - } - } - - cases := make([]reflect.SelectCase, len(outputs)) - for i := range cases { - cases[i].Dir = reflect.SelectSend - cases[i].Chan = reflect.ValueOf(outputs[i]) - } - cases = append(cases, reflect.SelectCase{ - Dir: reflect.SelectRecv, - Chan: reflect.ValueOf(done), - }) - - go func() { - defer closeOutputs() - for item := range OrDone(done, in) { - // send item to exactly once channel or cancel - for i := range cases { - if cases[i].Dir == reflect.SelectSend { - cases[i].Send = reflect.ValueOf(item) - } - } - - reflect.Select(cases) - } - }() - - return readChans -} - -// Tee copies the stream of data from a single channel to zero or more channels -func Tee(done interface{}, in interface{}, outputs ...chan<- interface{}) { - // use reflection to dynamically create select block - cases := make([]reflect.SelectCase, len(outputs)) - for i := range cases { - cases[i].Dir = reflect.SelectSend - } - - go func() { - // Need to close outputs when goroutine exits to ensure we avoid deadlock - defer func() { - for i := range outputs { - close(outputs[i]) - } - }() - - for item := range OrDone(done, in) { - // setup all possible select cases - for i := range cases { - cases[i].Chan = reflect.ValueOf(outputs[i]) - cases[i].Send = reflect.ValueOf(item) - } - - // send item to each channel no more than once or cancel - for range cases { - chosen, _, _ := reflect.Select(cases) - cases[chosen].Chan = reflect.ValueOf(nil) - } - } - }() -} - -func Batch(done interface{}, in interface{}, maxItems int, maxTimeout time.Duration) <-chan []interface{} { - if !isReadable(done) || !isReadable(in) { - panic(fmt.Errorf("channels must be readable")) - } - out := make(chan []interface{}) - - go func() { - defer close(out) - - doneCase := reflect.SelectCase{ - Dir: reflect.SelectRecv, - Chan: reflect.ValueOf(done), - } - - itemCase := reflect.SelectCase{ - Dir: reflect.SelectRecv, - Chan: reflect.ValueOf(in), - } - - timeoutCase := reflect.SelectCase{ - Dir: reflect.SelectRecv, - Chan: reflect.ValueOf(time.After(maxTimeout)), - } - - var batch []interface{} - for { - if chosen, item, ok := reflect.Select([]reflect.SelectCase{doneCase, itemCase, timeoutCase}); chosen == 0 || !ok { - // Flush and return when canceled or closed - if len(batch) > 0 { - out <- batch - batch = nil - } - return - } else if chosen == 1 { - // Add to batch - batch = append(batch, item.Interface()) - - // Flush if limit is reached - if len(batch) >= maxItems { - out <- batch - batch = nil - timeoutCase.Chan = reflect.ValueOf(time.After(maxTimeout)) - } - } else { - // Timeout triggered, flush and reset - if len(batch) > 0 { - out <- batch - batch = nil - } - timeoutCase.Chan = reflect.ValueOf(time.After(maxTimeout)) - } - } - }() - - return out -} - -func FormatJson(done interface{}, in interface{}) <-chan interface{} { - out := make(chan interface{}) - - go func() { - defer close(out) - - for item := range OrDone(done, in) { - if bytes, err := json.Marshal(item); err != nil { - panic(err) - } else { - out <- string(bytes) - } - } - }() - - return out -} - -func isReadable(channel interface{}) bool { - channelType := reflect.TypeOf(channel) - return channelType.Kind() == reflect.Chan && channelType.ChanDir() != reflect.SendDir -} diff --git a/sinks/console.go b/sinks/console.go index 8c24e1a..db68a09 100644 --- a/sinks/console.go +++ b/sinks/console.go @@ -24,7 +24,7 @@ import ( "github.com/bloodhoundad/azurehound/pipeline" ) -func WriteToConsole(ctx context.Context, stream <-chan interface{}) { +func WriteToConsole[T any](ctx context.Context, stream <-chan T) { for item := range pipeline.OrDone(ctx.Done(), stream) { fmt.Println(item) } diff --git a/sinks/file.go b/sinks/file.go index 8dcf4ec..3d8eea6 100644 --- a/sinks/file.go +++ b/sinks/file.go @@ -27,7 +27,7 @@ import ( "github.com/bloodhoundad/azurehound/pipeline" ) -func WriteToFile(ctx context.Context, filePath string, stream <-chan interface{}) error { +func WriteToFile[T any](ctx context.Context, filePath string, stream <-chan T) error { if file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY, 0666); err != nil { return err