From 4a32b78ad84431a22389f63349b246fad528ba1d Mon Sep 17 00:00:00 2001 From: vishalk-metron Date: Mon, 7 Jul 2025 09:35:51 +0530 Subject: [PATCH 1/2] Membership Membership & User Rights --- client/client.go | 18 ++ client/intune_groups_direct.go | 225 +++++++++++++ cmd/list-group-membership.go | 562 +++++++++++++++++++++++++++++++++ models/azure/graph_data.go | 193 +++++++++++ 4 files changed, 998 insertions(+) create mode 100644 client/intune_groups_direct.go create mode 100644 cmd/list-group-membership.go create mode 100644 models/azure/graph_data.go diff --git a/client/client.go b/client/client.go index 69515054..19ef74e4 100644 --- a/client/client.go +++ b/client/client.go @@ -201,6 +201,24 @@ type azureClient struct { } type AzureGraphClient interface { + + // Add these method signatures to the AzureGraphClient interface in client/client.go + + // User Role Assignment Methods + ListUserAppRoleAssignments(ctx context.Context, userID string, params query.GraphParams) <-chan AzureResult[azure.AppRoleAssignment] + + // Sign-in Activity Methods + ListSignIns(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.SignIn] + + // Device Registration Methods + GetDeviceRegisteredUsers(ctx context.Context, deviceId string, params query.GraphParams) <-chan AzureResult[json.RawMessage] + GetDeviceRegisteredOwners(ctx context.Context, deviceId string, params query.GraphParams) <-chan AzureResult[json.RawMessage] + + // High-level Collection Methods + CollectGroupMembershipData(ctx context.Context) <-chan AzureResult[azure.GroupMembershipData] + CollectUserRoleAssignments(ctx context.Context) <-chan AzureResult[azure.UserRoleData] + CollectDeviceAccessData(ctx context.Context) <-chan AzureResult[azure.DeviceAccessData] + ValidateScriptDeployment(ctx context.Context) error GetAzureADOrganization(ctx context.Context, selectCols []string) (*azure.Organization, error) diff --git a/client/intune_groups_direct.go b/client/intune_groups_direct.go new file mode 100644 index 00000000..fecaa817 --- /dev/null +++ b/client/intune_groups_direct.go @@ -0,0 +1,225 @@ +// client/intune_data_collection.go +package client + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +// ListUserAppRoleAssignments - Get app role assignments for a user (User Rights) +func (s *azureClient) ListUserAppRoleAssignments(ctx context.Context, userID string, params query.GraphParams) <-chan AzureResult[azure.AppRoleAssignment] { + var ( + out = make(chan AzureResult[azure.AppRoleAssignment]) + path = fmt.Sprintf("/%s/users/%s/appRoleAssignments", constants.GraphApiVersion, userID) + ) + + if params.Top == 0 { + params.Top = 999 + } + + go getAzureObjectList[azure.AppRoleAssignment](s.msgraph, ctx, path, params, out) + return out +} + +// ListSignIns - Get sign-in activity (for active sessions context) +func (s *azureClient) ListSignIns(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.SignIn] { + var ( + out = make(chan AzureResult[azure.SignIn]) + path = fmt.Sprintf("/%s/auditLogs/signIns", constants.GraphApiVersion) + ) + + if params.Top == 0 { + params.Top = 100 // Sign-ins can be large datasets + } + + go getAzureObjectList[azure.SignIn](s.msgraph, ctx, path, params, out) + return out +} + +// GetDeviceRegisteredUsers - Get users registered to a device +func (s *azureClient) GetDeviceRegisteredUsers(ctx context.Context, deviceId string, params query.GraphParams) <-chan AzureResult[json.RawMessage] { + var ( + out = make(chan AzureResult[json.RawMessage]) + path = fmt.Sprintf("/%s/devices/%s/registeredUsers", constants.GraphApiVersion, deviceId) + ) + + go getAzureObjectList[json.RawMessage](s.msgraph, ctx, path, params, out) + return out +} + +// GetDeviceRegisteredOwners - Get owners of a device +func (s *azureClient) GetDeviceRegisteredOwners(ctx context.Context, deviceId string, params query.GraphParams) <-chan AzureResult[json.RawMessage] { + var ( + out = make(chan AzureResult[json.RawMessage]) + path = fmt.Sprintf("/%s/devices/%s/registeredOwners", constants.GraphApiVersion, deviceId) + ) + + go getAzureObjectList[json.RawMessage](s.msgraph, ctx, path, params, out) + return out +} + +// CollectGroupMembershipData - Collect all group membership data using existing methods +func (s *azureClient) CollectGroupMembershipData(ctx context.Context) <-chan AzureResult[azure.GroupMembershipData] { + out := make(chan AzureResult[azure.GroupMembershipData]) + + go func() { + defer close(out) + + // Use existing ListAzureADGroups method + groups := s.ListAzureADGroups(ctx, query.GraphParams{}) + + for groupResult := range groups { + if groupResult.Error != nil { + out <- AzureResult[azure.GroupMembershipData]{Error: groupResult.Error} + continue + } + + group := groupResult.Ok + + // Use existing ListAzureADGroupMembers method - fix field name + members := s.ListAzureADGroupMembers(ctx, group.Id, query.GraphParams{}) + + var membersList []json.RawMessage + for memberResult := range members { + if memberResult.Error != nil { + continue // Skip individual member errors + } + membersList = append(membersList, memberResult.Ok) + } + + // Use existing ListAzureADGroupOwners method - fix field name + owners := s.ListAzureADGroupOwners(ctx, group.Id, query.GraphParams{}) + + var ownersList []json.RawMessage + for ownerResult := range owners { + if ownerResult.Error != nil { + continue // Skip individual owner errors + } + ownersList = append(ownersList, ownerResult.Ok) + } + + groupData := azure.GroupMembershipData{ + Group: group, + Members: membersList, + Owners: ownersList, + } + + out <- AzureResult[azure.GroupMembershipData]{Ok: groupData} + } + }() + + return out +} + +// CollectUserRoleAssignments - Collect user rights assignments from Graph API +func (s *azureClient) CollectUserRoleAssignments(ctx context.Context) <-chan AzureResult[azure.UserRoleData] { + out := make(chan AzureResult[azure.UserRoleData]) + + go func() { + defer close(out) + + // Use existing ListAzureADUsers method + users := s.ListAzureADUsers(ctx, query.GraphParams{}) + + for userResult := range users { + if userResult.Error != nil { + out <- AzureResult[azure.UserRoleData]{Error: userResult.Error} + continue + } + + user := userResult.Ok + + // Get app role assignments for this user - fix field name + roleAssignments := s.ListUserAppRoleAssignments(ctx, user.Id, query.GraphParams{}) + + var assignments []azure.AppRoleAssignment + for assignmentResult := range roleAssignments { + if assignmentResult.Error != nil { + continue // Skip individual assignment errors + } + assignments = append(assignments, assignmentResult.Ok) + } + + userData := azure.UserRoleData{ + User: user, + RoleAssignments: assignments, + } + + out <- AzureResult[azure.UserRoleData]{Ok: userData} + } + }() + + return out +} + +// CollectDeviceAccessData - Collect device access and ownership data +func (s *azureClient) CollectDeviceAccessData(ctx context.Context) <-chan AzureResult[azure.DeviceAccessData] { + out := make(chan AzureResult[azure.DeviceAccessData]) + + go func() { + defer close(out) + + // Get all Intune devices + devices := s.ListIntuneDevices(ctx, query.GraphParams{}) + + for deviceResult := range devices { + if deviceResult.Error != nil { + out <- AzureResult[azure.DeviceAccessData]{Error: deviceResult.Error} + continue + } + + device := deviceResult.Ok + + // Try to find corresponding Azure AD device using existing method + azureDevices := s.ListAzureDevices(ctx, query.GraphParams{ + Filter: fmt.Sprintf("deviceId eq '%s'", device.AzureADDeviceID), + }) + + var azureDevice *azure.Device + for azureDeviceResult := range azureDevices { + if azureDeviceResult.Error == nil { + deviceData := azureDeviceResult.Ok + azureDevice = &deviceData + break + } + } + + var registeredUsers []json.RawMessage + var registeredOwners []json.RawMessage + + if azureDevice != nil { + // Get registered users - fix field name + users := s.GetDeviceRegisteredUsers(ctx, azureDevice.Id, query.GraphParams{}) + for userResult := range users { + if userResult.Error == nil { + registeredUsers = append(registeredUsers, userResult.Ok) + } + } + + // Get registered owners - fix field name + owners := s.GetDeviceRegisteredOwners(ctx, azureDevice.Id, query.GraphParams{}) + for ownerResult := range owners { + if ownerResult.Error == nil { + registeredOwners = append(registeredOwners, ownerResult.Ok) + } + } + } + + deviceAccessData := azure.DeviceAccessData{ + IntuneDevice: device, + AzureDevice: azureDevice, + RegisteredUsers: registeredUsers, + RegisteredOwners: registeredOwners, + } + + out <- AzureResult[azure.DeviceAccessData]{Ok: deviceAccessData} + } + }() + + return out +} diff --git a/cmd/list-group-membership.go b/cmd/list-group-membership.go new file mode 100644 index 00000000..412550b3 --- /dev/null +++ b/cmd/list-group-membership.go @@ -0,0 +1,562 @@ +// cmd/list-group-membership.go +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listGroupMembershipCmd) +} + +var listGroupMembershipCmd = &cobra.Command{ + Use: "group-membership", + Long: "Collects Azure AD group membership and user role assignment data via Graph API", + Run: listGroupMembershipCmdImpl, + SilenceUsage: true, +} + +func listGroupMembershipCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := context.WithCancel(cmd.Context()) + defer stop() + + azClient := connectAndCreateClient() + + fmt.Printf("🚀 Starting Azure AD data collection via Graph API...\n\n") + startTime := time.Now() + + // Collect all data in parallel + result, err := collectAllGraphData(ctx, azClient) + if err != nil { + exit(err) + } + + duration := time.Since(startTime) + result.CollectionTime = duration + + // Display results + displayGraphDataResults(result) + + // Export to BloodHound format + err = exportGraphDataToBloodHound(result) + if err != nil { + fmt.Printf("⚠️ Warning: Failed to export BloodHound data: %v\n", err) + } +} + +func collectAllGraphData(ctx context.Context, azClient client.AzureClient) (*azure.GraphDataCollectionResult, error) { + result := &azure.GraphDataCollectionResult{ + GroupMemberships: []azure.GroupMembershipData{}, + UserRoleAssignments: []azure.UserRoleData{}, + DeviceAccess: []azure.DeviceAccessData{}, + SignInActivity: []azure.SignIn{}, + Errors: []string{}, + } + + // Collect Group Memberships + fmt.Printf("👥 Collecting Azure AD groups and memberships...\n") + groupResults := azClient.CollectGroupMembershipData(ctx) + for groupResult := range groupResults { + if groupResult.Error != nil { + result.Errors = append(result.Errors, fmt.Sprintf("Group error: %v", groupResult.Error)) + } else { + result.GroupMemberships = append(result.GroupMemberships, groupResult.Ok) + result.TotalGroups++ + } + } + fmt.Printf(" ✅ Collected %d groups\n", result.TotalGroups) + + // Collect User Role Assignments + fmt.Printf("🔑 Collecting user role assignments...\n") + userResults := azClient.CollectUserRoleAssignments(ctx) + for userResult := range userResults { + if userResult.Error != nil { + result.Errors = append(result.Errors, fmt.Sprintf("User error: %v", userResult.Error)) + } else { + result.UserRoleAssignments = append(result.UserRoleAssignments, userResult.Ok) + result.TotalUsers++ + } + } + fmt.Printf(" ✅ Collected role assignments for %d users\n", result.TotalUsers) + + // Collect Device Access Data + fmt.Printf("💻 Collecting device access and ownership data...\n") + deviceResults := azClient.CollectDeviceAccessData(ctx) + for deviceResult := range deviceResults { + if deviceResult.Error != nil { + result.Errors = append(result.Errors, fmt.Sprintf("Device error: %v", deviceResult.Error)) + } else { + result.DeviceAccess = append(result.DeviceAccess, deviceResult.Ok) + result.TotalDevices++ + } + } + fmt.Printf(" ✅ Collected access data for %d devices\n", result.TotalDevices) + + // Collect Recent Sign-in Activity (optional - can be large dataset) + fmt.Printf("📊 Collecting recent sign-in activity (last 24 hours)...\n") + signInResults := azClient.ListSignIns(ctx, query.GraphParams{ + Filter: fmt.Sprintf("createdDateTime ge %s", time.Now().Add(-24*time.Hour).Format("2006-01-02T15:04:05Z")), + Top: 1000, // Limit to avoid overwhelming results + }) + for signInResult := range signInResults { + if signInResult.Error != nil { + result.Errors = append(result.Errors, fmt.Sprintf("Sign-in error: %v", signInResult.Error)) + } else { + result.SignInActivity = append(result.SignInActivity, signInResult.Ok) + result.TotalSignIns++ + } + } + fmt.Printf(" ✅ Collected %d sign-in events\n", result.TotalSignIns) + + return result, nil +} + +func displayGraphDataResults(result *azure.GraphDataCollectionResult) { + fmt.Printf("\n=== AZURE AD DATA COLLECTION RESULTS ===\n") + fmt.Printf("⏱️ Collection Time: %v\n", result.CollectionTime) + fmt.Printf("📊 Data Summary:\n") + fmt.Printf(" • Groups: %d\n", result.TotalGroups) + fmt.Printf(" • Users: %d\n", result.TotalUsers) + fmt.Printf(" • Devices: %d\n", result.TotalDevices) + fmt.Printf(" • Sign-ins: %d\n", result.TotalSignIns) + fmt.Printf(" • Errors: %d\n", len(result.Errors)) + fmt.Printf("\n") + + // Group Analysis + if len(result.GroupMemberships) > 0 { + fmt.Printf("👥 GROUP MEMBERSHIP ANALYSIS:\n") + + totalMembers := 0 + privilegedGroups := 0 + + for _, group := range result.GroupMemberships { + totalMembers += len(group.Members) + + // Check for privileged groups + groupName := group.Group.DisplayName + if isPrivilegedGroup(groupName) { + privilegedGroups++ + fmt.Printf(" 🔴 Privileged Group: %s (%d members)\n", groupName, len(group.Members)) + } + } + + fmt.Printf(" • Total Group Memberships: %d\n", totalMembers) + fmt.Printf(" • Privileged Groups Found: %d\n", privilegedGroups) + fmt.Printf("\n") + } + + // User Rights Analysis + if len(result.UserRoleAssignments) > 0 { + fmt.Printf("🔑 USER RIGHTS ANALYSIS:\n") + + totalAssignments := 0 + privilegedUsers := 0 + + for _, user := range result.UserRoleAssignments { + totalAssignments += len(user.RoleAssignments) + + if hasPrivilegedRoles(user.RoleAssignments) { + privilegedUsers++ + fmt.Printf(" 🔴 Privileged User: %s (%d roles)\n", + user.User.DisplayName, len(user.RoleAssignments)) + } + } + + fmt.Printf(" • Total Role Assignments: %d\n", totalAssignments) + fmt.Printf(" • Users with Privileged Roles: %d\n", privilegedUsers) + fmt.Printf("\n") + } + + // Device Access Analysis + if len(result.DeviceAccess) > 0 { + fmt.Printf("💻 DEVICE ACCESS ANALYSIS:\n") + + devicesWithOwners := 0 + totalOwners := 0 + + for _, device := range result.DeviceAccess { + if len(device.RegisteredOwners) > 0 { + devicesWithOwners++ + totalOwners += len(device.RegisteredOwners) + } + } + + fmt.Printf(" • Devices with Registered Owners: %d/%d\n", devicesWithOwners, len(result.DeviceAccess)) + fmt.Printf(" • Total Device Owners: %d\n", totalOwners) + fmt.Printf("\n") + } + + // Error Summary + if len(result.Errors) > 0 { + fmt.Printf("⚠️ ERRORS ENCOUNTERED:\n") + for i, err := range result.Errors { + if i >= 10 { // Limit error display + fmt.Printf(" ... and %d more errors\n", len(result.Errors)-10) + break + } + fmt.Printf(" • %s\n", err) + } + fmt.Printf("\n") + } +} + +func exportGraphDataToBloodHound(result *azure.GraphDataCollectionResult) error { + fmt.Printf("📤 Exporting data to BloodHound format...\n") + + bloodhoundData := convertToBloodHoundFormat(result) + + // Write to file + filename := fmt.Sprintf("bloodhound_azuread_graph_%s.json", time.Now().Format("20060102_150405")) + file, err := os.Create(filename) + if err != nil { + return fmt.Errorf("failed to create export file: %w", err) + } + defer file.Close() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + + if err := encoder.Encode(bloodhoundData); err != nil { + return fmt.Errorf("failed to write BloodHound data: %w", err) + } + + fmt.Printf("✅ BloodHound data exported to: %s\n", filename) + fmt.Printf(" • Users: %d\n", len(bloodhoundData.Data.Users)) + fmt.Printf(" • Groups: %d\n", len(bloodhoundData.Data.Groups)) + fmt.Printf(" • Devices: %d\n", len(bloodhoundData.Data.Devices)) + fmt.Printf(" • Group Memberships: %d\n", len(bloodhoundData.GroupMemberships)) + fmt.Printf(" • Role Assignments: %d\n", len(bloodhoundData.UserRoleAssignments)) + fmt.Printf(" • Device Ownerships: %d\n", len(bloodhoundData.DeviceOwnerships)) + + return nil +} + +func convertToBloodHoundFormat(result *azure.GraphDataCollectionResult) azure.BloodHoundGraphData { + bloodhoundData := azure.BloodHoundGraphData{ + Meta: azure.BloodHoundMeta{ + Type: "azuread-graph", + Count: result.TotalGroups + result.TotalUsers + result.TotalDevices, + Version: "1.0", + Methods: 4, // Groups, Users, Devices, Sign-ins + CollectedBy: "AzureHound-Graph", + CollectedAt: time.Now(), + }, + GroupMemberships: []azure.BloodHoundGroupMembership{}, + UserRoleAssignments: []azure.BloodHoundUserRoleAssignment{}, + DeviceOwnerships: []azure.BloodHoundDeviceOwnership{}, + SignInActivity: []azure.BloodHoundSignInActivity{}, + } + + var users []azure.BloodHoundUser + var groups []azure.BloodHoundGroup + var devices []azure.BloodHoundDevice + + // Convert Groups and Memberships + for _, groupData := range result.GroupMemberships { + // Convert group - fix field name from ID to Id + group := azure.BloodHoundGroup{ + ObjectIdentifier: groupData.Group.Id, + Properties: azure.BloodHoundGroupProperties{ + Name: groupData.Group.DisplayName, + Domain: extractDomainFromGroup(groupData.Group), + ObjectID: groupData.Group.Id, + Description: groupData.Group.Description, + SamAccountName: groupData.Group.DisplayName, + DistinguishedName: fmt.Sprintf("CN=%s", groupData.Group.DisplayName), + }, + } + groups = append(groups, group) + + // Convert memberships + for _, memberRaw := range groupData.Members { + var member map[string]interface{} + json.Unmarshal(memberRaw, &member) + + if memberID, ok := member["id"].(string); ok { + memberName := "" + memberType := "User" + + if displayName, ok := member["displayName"].(string); ok { + memberName = displayName + } + if odataType, ok := member["@odata.type"].(string); ok { + memberType = extractTypeFromOData(odataType) + } + + membership := azure.BloodHoundGroupMembership{ + GroupId: groupData.Group.Id, + GroupName: groupData.Group.DisplayName, + MemberId: memberID, + MemberName: memberName, + MemberType: memberType, + RelationshipType: "MemberOf", + } + bloodhoundData.GroupMemberships = append(bloodhoundData.GroupMemberships, membership) + } + } + + // Convert owners + for _, ownerRaw := range groupData.Owners { + var owner map[string]interface{} + json.Unmarshal(ownerRaw, &owner) + + if ownerID, ok := owner["id"].(string); ok { + ownerName := "" + ownerType := "User" + + if displayName, ok := owner["displayName"].(string); ok { + ownerName = displayName + } + if odataType, ok := owner["@odata.type"].(string); ok { + ownerType = extractTypeFromOData(odataType) + } + + ownership := azure.BloodHoundGroupMembership{ + GroupId: groupData.Group.Id, + GroupName: groupData.Group.DisplayName, + MemberId: ownerID, + MemberName: ownerName, + MemberType: ownerType, + RelationshipType: "OwnerOf", + } + bloodhoundData.GroupMemberships = append(bloodhoundData.GroupMemberships, ownership) + } + } + } + + // Convert Users and Role Assignments + for _, userData := range result.UserRoleAssignments { + // Convert user - fix field name from ID to Id + user := azure.BloodHoundUser{ + ObjectIdentifier: userData.User.Id, + Properties: azure.BloodHoundUserProperties{ + Name: userData.User.DisplayName, + Domain: extractDomainFromUPN(userData.User.UserPrincipalName), + ObjectID: userData.User.Id, + DisplayName: userData.User.DisplayName, + Email: userData.User.UserPrincipalName, + Enabled: userData.User.AccountEnabled, + SamAccountName: userData.User.UserPrincipalName, + DistinguishedName: fmt.Sprintf("CN=%s", userData.User.DisplayName), + UnconstrainedDelegation: false, + Sensitive: false, + }, + } + users = append(users, user) + + // Convert role assignments + for _, assignment := range userData.RoleAssignments { + roleAssignment := azure.BloodHoundUserRoleAssignment{ + UserId: userData.User.Id, + UserName: userData.User.DisplayName, + RoleId: assignment.AppRoleId.String(), // Convert UUID to string + RoleName: assignment.PrincipalDisplayName, + ResourceId: assignment.ResourceId, + ResourceName: assignment.ResourceDisplayName, + AssignmentType: "DirectAssignment", + CreatedDateTime: time.Now(), + } + bloodhoundData.UserRoleAssignments = append(bloodhoundData.UserRoleAssignments, roleAssignment) + } + } + + // Convert Devices and Ownership + for _, deviceData := range result.DeviceAccess { + // Convert device - IntuneDevice uses ID (uppercase) + device := azure.BloodHoundDevice{ + ObjectIdentifier: deviceData.IntuneDevice.ID, + Properties: azure.BloodHoundDeviceProperties{ + Name: deviceData.IntuneDevice.DeviceName, + DisplayName: deviceData.IntuneDevice.DeviceName, + ObjectID: deviceData.IntuneDevice.ID, + OperatingSystem: deviceData.IntuneDevice.OperatingSystem, + OSVersion: deviceData.IntuneDevice.OSVersion, + DeviceId: deviceData.IntuneDevice.AzureADDeviceID, + IsCompliant: deviceData.IntuneDevice.ComplianceState == "compliant", + IsManaged: true, + EnrollmentType: deviceData.IntuneDevice.EnrollmentType, + LastSyncDateTime: deviceData.IntuneDevice.LastSyncDateTime, + Enabled: true, + }, + } + + // Add registered users + for _, userRaw := range deviceData.RegisteredUsers { + var user map[string]interface{} + json.Unmarshal(userRaw, &user) + + if userID, ok := user["id"].(string); ok { + deviceUser := azure.BloodHoundDeviceUser{ + ObjectIdentifier: userID, + ObjectType: "User", + } + device.RegisteredUsers = append(device.RegisteredUsers, deviceUser) + + // Create ownership relationship + userName := "" + if displayName, ok := user["displayName"].(string); ok { + userName = displayName + } + + ownership := azure.BloodHoundDeviceOwnership{ + DeviceId: deviceData.IntuneDevice.ID, + DeviceName: deviceData.IntuneDevice.DeviceName, + UserId: userID, + UserName: userName, + OwnershipType: "RegisteredUser", + ComplianceState: deviceData.IntuneDevice.ComplianceState, + } + bloodhoundData.DeviceOwnerships = append(bloodhoundData.DeviceOwnerships, ownership) + } + } + + // Add registered owners + for _, ownerRaw := range deviceData.RegisteredOwners { + var owner map[string]interface{} + json.Unmarshal(ownerRaw, &owner) + + if ownerID, ok := owner["id"].(string); ok { + deviceOwner := azure.BloodHoundDeviceUser{ + ObjectIdentifier: ownerID, + ObjectType: "User", + } + device.RegisteredOwners = append(device.RegisteredOwners, deviceOwner) + + // Create ownership relationship + ownerName := "" + if displayName, ok := owner["displayName"].(string); ok { + ownerName = displayName + } + + ownership := azure.BloodHoundDeviceOwnership{ + DeviceId: deviceData.IntuneDevice.ID, + DeviceName: deviceData.IntuneDevice.DeviceName, + UserId: ownerID, + UserName: ownerName, + OwnershipType: "RegisteredOwner", + ComplianceState: deviceData.IntuneDevice.ComplianceState, + } + bloodhoundData.DeviceOwnerships = append(bloodhoundData.DeviceOwnerships, ownership) + } + } + + devices = append(devices, device) + } + + // Convert Sign-in Activity + for _, signIn := range result.SignInActivity { + signInActivity := azure.BloodHoundSignInActivity{ + UserId: signIn.UserId, + UserName: signIn.UserDisplayName, + DeviceId: signIn.DeviceDetail.DeviceId, + DeviceName: signIn.DeviceDetail.DisplayName, + AppId: signIn.AppId, + AppName: signIn.AppDisplayName, + SignInDateTime: signIn.CreatedDateTime, + IpAddress: signIn.IpAddress, + Location: fmt.Sprintf("%s, %s", signIn.Location.City, signIn.Location.CountryOrRegion), + RiskLevel: signIn.RiskLevelAggregated, + ConditionalAccess: signIn.ConditionalAccessStatus, + } + bloodhoundData.SignInActivity = append(bloodhoundData.SignInActivity, signInActivity) + } + + // Set data wrapper + bloodhoundData.Data = azure.BloodHoundGraphDataWrapper{ + Users: users, + Groups: groups, + Devices: devices, + } + + return bloodhoundData +} + +// Helper functions +func isPrivilegedGroup(groupName string) bool { + privilegedGroups := []string{ + "Global Administrator", + "Privileged Role Administrator", + "Security Administrator", + "User Administrator", + "Exchange Administrator", + "SharePoint Administrator", + "Application Administrator", + "Cloud Application Administrator", + "Authentication Administrator", + "Privileged Authentication Administrator", + "Domain Admins", + "Enterprise Admins", + "Schema Admins", + "Administrators", + } + + for _, privileged := range privilegedGroups { + if strings.Contains(strings.ToLower(groupName), strings.ToLower(privileged)) { + return true + } + } + return false +} + +func hasPrivilegedRoles(assignments []azure.AppRoleAssignment) bool { + privilegedRoles := []string{ + "Global Administrator", + "Privileged Role Administrator", + "Security Administrator", + "User Administrator", + "Directory.ReadWrite.All", + "RoleManagement.ReadWrite.Directory", + "Application.ReadWrite.All", + } + + for _, assignment := range assignments { + // Use available fields from AppRoleAssignment + assignmentName := assignment.PrincipalDisplayName + resourceName := assignment.ResourceDisplayName + + for _, privileged := range privilegedRoles { + if strings.Contains(strings.ToLower(assignmentName), strings.ToLower(privileged)) || + strings.Contains(strings.ToLower(resourceName), strings.ToLower(privileged)) { + return true + } + } + } + return false +} + +func extractDomainFromUPN(upn string) string { + parts := strings.Split(upn, "@") + if len(parts) == 2 { + return strings.ToUpper(parts[1]) + } + return "UNKNOWN" +} + +func extractDomainFromGroup(group azure.Group) string { + // Since OnPremisesDomainName doesn't exist, use a default + // In a real implementation, you might extract this from other group properties + return "AZUREAD" +} + +func extractTypeFromOData(odataType string) string { + if strings.Contains(odataType, "user") { + return "User" + } else if strings.Contains(odataType, "group") { + return "Group" + } else if strings.Contains(odataType, "servicePrincipal") { + return "ServicePrincipal" + } else if strings.Contains(odataType, "application") { + return "Application" + } + return "Unknown" +} diff --git a/models/azure/graph_data.go b/models/azure/graph_data.go new file mode 100644 index 00000000..27759bdf --- /dev/null +++ b/models/azure/graph_data.go @@ -0,0 +1,193 @@ +// models/azure/graph_data.go +package azure + +import ( + "encoding/json" + "time" +) + +// GroupMembershipData represents Azure AD group with its members and owners +type GroupMembershipData struct { + Group Group `json:"group"` + Members []json.RawMessage `json:"members"` + Owners []json.RawMessage `json:"owners"` +} + +// UserRoleData represents a user with their role assignments +type UserRoleData struct { + User User `json:"user"` + RoleAssignments []AppRoleAssignment `json:"roleAssignments"` +} + +// DeviceAccessData represents device access permissions and ownership +type DeviceAccessData struct { + IntuneDevice IntuneDevice `json:"intuneDevice"` + AzureDevice *Device `json:"azureDevice,omitempty"` + RegisteredUsers []json.RawMessage `json:"registeredUsers"` + RegisteredOwners []json.RawMessage `json:"registeredOwners"` +} + +// SignIn represents sign-in activity data +type SignIn struct { + ID string `json:"id"` + CreatedDateTime time.Time `json:"createdDateTime"` + UserDisplayName string `json:"userDisplayName"` + UserPrincipalName string `json:"userPrincipalName"` + UserId string `json:"userId"` + AppId string `json:"appId"` + AppDisplayName string `json:"appDisplayName"` + IpAddress string `json:"ipAddress"` + ClientAppUsed string `json:"clientAppUsed"` + DeviceDetail DeviceDetail `json:"deviceDetail"` + Location SignInLocation `json:"location"` + RiskDetail string `json:"riskDetail"` + RiskLevelAggregated string `json:"riskLevelAggregated"` + RiskLevelDuringSignIn string `json:"riskLevelDuringSignIn"` + RiskState string `json:"riskState"` + Status SignInStatus `json:"status"` + ConditionalAccessStatus string `json:"conditionalAccessStatus"` + AdditionalData map[string]interface{} `json:"additionalData,omitempty"` +} + +// DeviceDetail represents device information from sign-in +type DeviceDetail struct { + DeviceId string `json:"deviceId"` + DisplayName string `json:"displayName"` + OperatingSystem string `json:"operatingSystem"` + Browser string `json:"browser"` + IsCompliant bool `json:"isCompliant"` + IsManaged bool `json:"isManaged"` + TrustType string `json:"trustType"` +} + +// SignInLocation represents sign-in location +type SignInLocation struct { + City string `json:"city"` + State string `json:"state"` + CountryOrRegion string `json:"countryOrRegion"` + GeoCoordinates GeoCoordinates `json:"geoCoordinates"` +} + +// GeoCoordinates represents geographic coordinates +type GeoCoordinates struct { + Altitude float64 `json:"altitude"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` +} + +// SignInStatus represents sign-in status +type SignInStatus struct { + ErrorCode int `json:"errorCode"` + FailureReason string `json:"failureReason"` + AdditionalDetails string `json:"additionalDetails"` +} + +// BloodHoundGraphData represents data collected via Graph API formatted for BloodHound +type BloodHoundGraphData struct { + Meta BloodHoundMeta `json:"meta"` + Data BloodHoundGraphDataWrapper `json:"data"` + GroupMemberships []BloodHoundGroupMembership `json:"groupMemberships"` + UserRoleAssignments []BloodHoundUserRoleAssignment `json:"userRoleAssignments"` + DeviceOwnerships []BloodHoundDeviceOwnership `json:"deviceOwnerships"` + SignInActivity []BloodHoundSignInActivity `json:"signInActivity"` +} + +// BloodHoundGraphDataWrapper wraps the core data +type BloodHoundGraphDataWrapper struct { + Users []BloodHoundUser `json:"users"` + Groups []BloodHoundGroup `json:"groups"` + Devices []BloodHoundDevice `json:"devices"` +} + +// BloodHoundGroupMembership represents group membership for BloodHound +type BloodHoundGroupMembership struct { + GroupId string `json:"groupId"` + GroupName string `json:"groupName"` + MemberId string `json:"memberId"` + MemberName string `json:"memberName"` + MemberType string `json:"memberType"` + RelationshipType string `json:"relationshipType"` // "MemberOf" or "OwnerOf" +} + +// BloodHoundUserRoleAssignment represents user role assignments for BloodHound +type BloodHoundUserRoleAssignment struct { + UserId string `json:"userId"` + UserName string `json:"userName"` + RoleId string `json:"roleId"` + RoleName string `json:"roleName"` + ResourceId string `json:"resourceId"` + ResourceName string `json:"resourceName"` + AssignmentType string `json:"assignmentType"` + CreatedDateTime time.Time `json:"createdDateTime"` +} + +// BloodHoundDeviceOwnership represents device ownership for BloodHound +type BloodHoundDeviceOwnership struct { + DeviceId string `json:"deviceId"` + DeviceName string `json:"deviceName"` + UserId string `json:"userId"` + UserName string `json:"userName"` + OwnershipType string `json:"ownershipType"` // "RegisteredOwner" or "RegisteredUser" + ComplianceState string `json:"complianceState"` +} + +// BloodHoundSignInActivity represents sign-in activity for BloodHound +type BloodHoundSignInActivity struct { + UserId string `json:"userId"` + UserName string `json:"userName"` + DeviceId string `json:"deviceId"` + DeviceName string `json:"deviceName"` + AppId string `json:"appId"` + AppName string `json:"appName"` + SignInDateTime time.Time `json:"signInDateTime"` + IpAddress string `json:"ipAddress"` + Location string `json:"location"` + RiskLevel string `json:"riskLevel"` + ConditionalAccess string `json:"conditionalAccess"` +} + +// BloodHoundDevice represents a device for BloodHound +type BloodHoundDevice struct { + ObjectIdentifier string `json:"ObjectIdentifier"` + Properties BloodHoundDeviceProperties `json:"Properties"` + RegisteredUsers []BloodHoundDeviceUser `json:"RegisteredUsers"` + RegisteredOwners []BloodHoundDeviceUser `json:"RegisteredOwners"` +} + +// BloodHoundDeviceProperties represents device properties for BloodHound +type BloodHoundDeviceProperties struct { + Name string `json:"name"` + DisplayName string `json:"displayName"` + ObjectID string `json:"objectid"` + OperatingSystem string `json:"operatingsystem"` + OSVersion string `json:"osversion"` + DeviceId string `json:"deviceid"` + IsCompliant bool `json:"iscompliant"` + IsManaged bool `json:"ismanaged"` + EnrollmentType string `json:"enrollmenttype"` + JoinType string `json:"jointype"` + TrustType string `json:"trusttype"` + LastSyncDateTime time.Time `json:"lastsyncdatetime"` + CreatedDateTime time.Time `json:"createddatetime"` + Enabled bool `json:"enabled"` +} + +// BloodHoundDeviceUser represents a user associated with a device +type BloodHoundDeviceUser struct { + ObjectIdentifier string `json:"ObjectIdentifier"` + ObjectType string `json:"ObjectType"` +} + +// Collection results for Graph API data +type GraphDataCollectionResult struct { + GroupMemberships []GroupMembershipData `json:"groupMemberships"` + UserRoleAssignments []UserRoleData `json:"userRoleAssignments"` + DeviceAccess []DeviceAccessData `json:"deviceAccess"` + SignInActivity []SignIn `json:"signInActivity"` + CollectionTime time.Duration `json:"collectionTime"` + TotalGroups int `json:"totalGroups"` + TotalUsers int `json:"totalUsers"` + TotalDevices int `json:"totalDevices"` + TotalSignIns int `json:"totalSignIns"` + Errors []string `json:"errors"` +} From 75217ecfd8ea03cd94d549b6390b346630c1a16d Mon Sep 17 00:00:00 2001 From: vishalk-metron Date: Mon, 7 Jul 2025 09:46:54 +0530 Subject: [PATCH 2/2] Removed unnecessarily collected data --- cmd/list-group-membership.go | 277 +++++------------------------------ 1 file changed, 40 insertions(+), 237 deletions(-) diff --git a/cmd/list-group-membership.go b/cmd/list-group-membership.go index 412550b3..a484f953 100644 --- a/cmd/list-group-membership.go +++ b/cmd/list-group-membership.go @@ -10,7 +10,6 @@ import ( "time" "github.com/bloodhoundad/azurehound/v2/client" - "github.com/bloodhoundad/azurehound/v2/client/query" "github.com/bloodhoundad/azurehound/v2/models/azure" "github.com/spf13/cobra" ) @@ -21,7 +20,7 @@ func init() { var listGroupMembershipCmd = &cobra.Command{ Use: "group-membership", - Long: "Collects Azure AD group membership and user role assignment data via Graph API", + Long: "Collects Azure AD group membership and user role assignment data (focused on BloodHound essentials)", Run: listGroupMembershipCmdImpl, SilenceUsage: true, } @@ -32,10 +31,10 @@ func listGroupMembershipCmdImpl(cmd *cobra.Command, args []string) { azClient := connectAndCreateClient() - fmt.Printf("🚀 Starting Azure AD data collection via Graph API...\n\n") + fmt.Printf("🎯 Collecting focused Azure AD data for BloodHound...\n\n") startTime := time.Now() - // Collect all data in parallel + // Collect only essential data result, err := collectAllGraphData(ctx, azClient) if err != nil { exit(err) @@ -44,10 +43,10 @@ func listGroupMembershipCmdImpl(cmd *cobra.Command, args []string) { duration := time.Since(startTime) result.CollectionTime = duration - // Display results + // Display focused results displayGraphDataResults(result) - // Export to BloodHound format + // Export focused BloodHound data err = exportGraphDataToBloodHound(result) if err != nil { fmt.Printf("⚠️ Warning: Failed to export BloodHound data: %v\n", err) @@ -58,65 +57,45 @@ func collectAllGraphData(ctx context.Context, azClient client.AzureClient) (*azu result := &azure.GraphDataCollectionResult{ GroupMemberships: []azure.GroupMembershipData{}, UserRoleAssignments: []azure.UserRoleData{}, - DeviceAccess: []azure.DeviceAccessData{}, - SignInActivity: []azure.SignIn{}, + SignInActivity: []azure.SignIn{}, // Keep struct but don't populate + DeviceAccess: []azure.DeviceAccessData{}, // Keep struct but don't populate Errors: []string{}, } - // Collect Group Memberships + // Collect Group Memberships (focused on relevant groups) fmt.Printf("👥 Collecting Azure AD groups and memberships...\n") groupResults := azClient.CollectGroupMembershipData(ctx) for groupResult := range groupResults { if groupResult.Error != nil { result.Errors = append(result.Errors, fmt.Sprintf("Group error: %v", groupResult.Error)) } else { - result.GroupMemberships = append(result.GroupMemberships, groupResult.Ok) - result.TotalGroups++ + // Only include privileged groups or groups with members + if isPrivilegedGroup(groupResult.Ok.Group.DisplayName) || len(groupResult.Ok.Members) > 0 { + result.GroupMemberships = append(result.GroupMemberships, groupResult.Ok) + result.TotalGroups++ + } } } - fmt.Printf(" ✅ Collected %d groups\n", result.TotalGroups) + fmt.Printf(" ✅ Collected %d relevant groups\n", result.TotalGroups) - // Collect User Role Assignments + // Collect User Role Assignments (focused on users with roles) fmt.Printf("🔑 Collecting user role assignments...\n") userResults := azClient.CollectUserRoleAssignments(ctx) for userResult := range userResults { if userResult.Error != nil { result.Errors = append(result.Errors, fmt.Sprintf("User error: %v", userResult.Error)) } else { - result.UserRoleAssignments = append(result.UserRoleAssignments, userResult.Ok) - result.TotalUsers++ + // Only include users with role assignments + if len(userResult.Ok.RoleAssignments) > 0 { + result.UserRoleAssignments = append(result.UserRoleAssignments, userResult.Ok) + result.TotalUsers++ + } } } fmt.Printf(" ✅ Collected role assignments for %d users\n", result.TotalUsers) - // Collect Device Access Data - fmt.Printf("💻 Collecting device access and ownership data...\n") - deviceResults := azClient.CollectDeviceAccessData(ctx) - for deviceResult := range deviceResults { - if deviceResult.Error != nil { - result.Errors = append(result.Errors, fmt.Sprintf("Device error: %v", deviceResult.Error)) - } else { - result.DeviceAccess = append(result.DeviceAccess, deviceResult.Ok) - result.TotalDevices++ - } - } - fmt.Printf(" ✅ Collected access data for %d devices\n", result.TotalDevices) - - // Collect Recent Sign-in Activity (optional - can be large dataset) - fmt.Printf("📊 Collecting recent sign-in activity (last 24 hours)...\n") - signInResults := azClient.ListSignIns(ctx, query.GraphParams{ - Filter: fmt.Sprintf("createdDateTime ge %s", time.Now().Add(-24*time.Hour).Format("2006-01-02T15:04:05Z")), - Top: 1000, // Limit to avoid overwhelming results - }) - for signInResult := range signInResults { - if signInResult.Error != nil { - result.Errors = append(result.Errors, fmt.Sprintf("Sign-in error: %v", signInResult.Error)) - } else { - result.SignInActivity = append(result.SignInActivity, signInResult.Ok) - result.TotalSignIns++ - } - } - fmt.Printf(" ✅ Collected %d sign-in events\n", result.TotalSignIns) + // Skip device and sign-in collection for focused approach + fmt.Printf("⏭️ Skipping device and sign-in data (focused collection)\n") return result, nil } @@ -125,10 +104,8 @@ func displayGraphDataResults(result *azure.GraphDataCollectionResult) { fmt.Printf("\n=== AZURE AD DATA COLLECTION RESULTS ===\n") fmt.Printf("⏱️ Collection Time: %v\n", result.CollectionTime) fmt.Printf("📊 Data Summary:\n") - fmt.Printf(" • Groups: %d\n", result.TotalGroups) - fmt.Printf(" • Users: %d\n", result.TotalUsers) - fmt.Printf(" • Devices: %d\n", result.TotalDevices) - fmt.Printf(" • Sign-ins: %d\n", result.TotalSignIns) + fmt.Printf(" • Relevant Groups: %d\n", result.TotalGroups) + fmt.Printf(" • Users with Roles: %d\n", result.TotalUsers) fmt.Printf(" • Errors: %d\n", len(result.Errors)) fmt.Printf("\n") @@ -177,25 +154,6 @@ func displayGraphDataResults(result *azure.GraphDataCollectionResult) { fmt.Printf("\n") } - // Device Access Analysis - if len(result.DeviceAccess) > 0 { - fmt.Printf("💻 DEVICE ACCESS ANALYSIS:\n") - - devicesWithOwners := 0 - totalOwners := 0 - - for _, device := range result.DeviceAccess { - if len(device.RegisteredOwners) > 0 { - devicesWithOwners++ - totalOwners += len(device.RegisteredOwners) - } - } - - fmt.Printf(" • Devices with Registered Owners: %d/%d\n", devicesWithOwners, len(result.DeviceAccess)) - fmt.Printf(" • Total Device Owners: %d\n", totalOwners) - fmt.Printf("\n") - } - // Error Summary if len(result.Errors) > 0 { fmt.Printf("⚠️ ERRORS ENCOUNTERED:\n") @@ -216,7 +174,7 @@ func exportGraphDataToBloodHound(result *azure.GraphDataCollectionResult) error bloodhoundData := convertToBloodHoundFormat(result) // Write to file - filename := fmt.Sprintf("bloodhound_azuread_graph_%s.json", time.Now().Format("20060102_150405")) + filename := fmt.Sprintf("bloodhound_azuread_focused_%s.json", time.Now().Format("20060102_150405")) file, err := os.Create(filename) if err != nil { return fmt.Errorf("failed to create export file: %w", err) @@ -230,13 +188,9 @@ func exportGraphDataToBloodHound(result *azure.GraphDataCollectionResult) error return fmt.Errorf("failed to write BloodHound data: %w", err) } - fmt.Printf("✅ BloodHound data exported to: %s\n", filename) - fmt.Printf(" • Users: %d\n", len(bloodhoundData.Data.Users)) - fmt.Printf(" • Groups: %d\n", len(bloodhoundData.Data.Groups)) - fmt.Printf(" • Devices: %d\n", len(bloodhoundData.Data.Devices)) + fmt.Printf("✅ Focused BloodHound data exported to: %s\n", filename) fmt.Printf(" • Group Memberships: %d\n", len(bloodhoundData.GroupMemberships)) fmt.Printf(" • Role Assignments: %d\n", len(bloodhoundData.UserRoleAssignments)) - fmt.Printf(" • Device Ownerships: %d\n", len(bloodhoundData.DeviceOwnerships)) return nil } @@ -244,39 +198,21 @@ func exportGraphDataToBloodHound(result *azure.GraphDataCollectionResult) error func convertToBloodHoundFormat(result *azure.GraphDataCollectionResult) azure.BloodHoundGraphData { bloodhoundData := azure.BloodHoundGraphData{ Meta: azure.BloodHoundMeta{ - Type: "azuread-graph", - Count: result.TotalGroups + result.TotalUsers + result.TotalDevices, + Type: "azuread-focused", + Count: result.TotalGroups + result.TotalUsers, Version: "1.0", - Methods: 4, // Groups, Users, Devices, Sign-ins - CollectedBy: "AzureHound-Graph", + Methods: 2, // Groups + Users only (focused) + CollectedBy: "AzureHound-Focused", CollectedAt: time.Now(), }, GroupMemberships: []azure.BloodHoundGroupMembership{}, UserRoleAssignments: []azure.BloodHoundUserRoleAssignment{}, - DeviceOwnerships: []azure.BloodHoundDeviceOwnership{}, - SignInActivity: []azure.BloodHoundSignInActivity{}, + DeviceOwnerships: []azure.BloodHoundDeviceOwnership{}, // Keep empty + SignInActivity: []azure.BloodHoundSignInActivity{}, // Keep empty } - var users []azure.BloodHoundUser - var groups []azure.BloodHoundGroup - var devices []azure.BloodHoundDevice - - // Convert Groups and Memberships + // Convert Groups and Memberships only for _, groupData := range result.GroupMemberships { - // Convert group - fix field name from ID to Id - group := azure.BloodHoundGroup{ - ObjectIdentifier: groupData.Group.Id, - Properties: azure.BloodHoundGroupProperties{ - Name: groupData.Group.DisplayName, - Domain: extractDomainFromGroup(groupData.Group), - ObjectID: groupData.Group.Id, - Description: groupData.Group.Description, - SamAccountName: groupData.Group.DisplayName, - DistinguishedName: fmt.Sprintf("CN=%s", groupData.Group.DisplayName), - }, - } - groups = append(groups, group) - // Convert memberships for _, memberRaw := range groupData.Members { var member map[string]interface{} @@ -334,32 +270,13 @@ func convertToBloodHoundFormat(result *azure.GraphDataCollectionResult) azure.Bl } } - // Convert Users and Role Assignments + // Convert User Role Assignments only for _, userData := range result.UserRoleAssignments { - // Convert user - fix field name from ID to Id - user := azure.BloodHoundUser{ - ObjectIdentifier: userData.User.Id, - Properties: azure.BloodHoundUserProperties{ - Name: userData.User.DisplayName, - Domain: extractDomainFromUPN(userData.User.UserPrincipalName), - ObjectID: userData.User.Id, - DisplayName: userData.User.DisplayName, - Email: userData.User.UserPrincipalName, - Enabled: userData.User.AccountEnabled, - SamAccountName: userData.User.UserPrincipalName, - DistinguishedName: fmt.Sprintf("CN=%s", userData.User.DisplayName), - UnconstrainedDelegation: false, - Sensitive: false, - }, - } - users = append(users, user) - - // Convert role assignments for _, assignment := range userData.RoleAssignments { roleAssignment := azure.BloodHoundUserRoleAssignment{ UserId: userData.User.Id, UserName: userData.User.DisplayName, - RoleId: assignment.AppRoleId.String(), // Convert UUID to string + RoleId: assignment.AppRoleId.String(), RoleName: assignment.PrincipalDisplayName, ResourceId: assignment.ResourceId, ResourceName: assignment.ResourceDisplayName, @@ -370,112 +287,12 @@ func convertToBloodHoundFormat(result *azure.GraphDataCollectionResult) azure.Bl } } - // Convert Devices and Ownership - for _, deviceData := range result.DeviceAccess { - // Convert device - IntuneDevice uses ID (uppercase) - device := azure.BloodHoundDevice{ - ObjectIdentifier: deviceData.IntuneDevice.ID, - Properties: azure.BloodHoundDeviceProperties{ - Name: deviceData.IntuneDevice.DeviceName, - DisplayName: deviceData.IntuneDevice.DeviceName, - ObjectID: deviceData.IntuneDevice.ID, - OperatingSystem: deviceData.IntuneDevice.OperatingSystem, - OSVersion: deviceData.IntuneDevice.OSVersion, - DeviceId: deviceData.IntuneDevice.AzureADDeviceID, - IsCompliant: deviceData.IntuneDevice.ComplianceState == "compliant", - IsManaged: true, - EnrollmentType: deviceData.IntuneDevice.EnrollmentType, - LastSyncDateTime: deviceData.IntuneDevice.LastSyncDateTime, - Enabled: true, - }, - } - - // Add registered users - for _, userRaw := range deviceData.RegisteredUsers { - var user map[string]interface{} - json.Unmarshal(userRaw, &user) - - if userID, ok := user["id"].(string); ok { - deviceUser := azure.BloodHoundDeviceUser{ - ObjectIdentifier: userID, - ObjectType: "User", - } - device.RegisteredUsers = append(device.RegisteredUsers, deviceUser) - - // Create ownership relationship - userName := "" - if displayName, ok := user["displayName"].(string); ok { - userName = displayName - } - - ownership := azure.BloodHoundDeviceOwnership{ - DeviceId: deviceData.IntuneDevice.ID, - DeviceName: deviceData.IntuneDevice.DeviceName, - UserId: userID, - UserName: userName, - OwnershipType: "RegisteredUser", - ComplianceState: deviceData.IntuneDevice.ComplianceState, - } - bloodhoundData.DeviceOwnerships = append(bloodhoundData.DeviceOwnerships, ownership) - } - } - - // Add registered owners - for _, ownerRaw := range deviceData.RegisteredOwners { - var owner map[string]interface{} - json.Unmarshal(ownerRaw, &owner) - - if ownerID, ok := owner["id"].(string); ok { - deviceOwner := azure.BloodHoundDeviceUser{ - ObjectIdentifier: ownerID, - ObjectType: "User", - } - device.RegisteredOwners = append(device.RegisteredOwners, deviceOwner) - - // Create ownership relationship - ownerName := "" - if displayName, ok := owner["displayName"].(string); ok { - ownerName = displayName - } - - ownership := azure.BloodHoundDeviceOwnership{ - DeviceId: deviceData.IntuneDevice.ID, - DeviceName: deviceData.IntuneDevice.DeviceName, - UserId: ownerID, - UserName: ownerName, - OwnershipType: "RegisteredOwner", - ComplianceState: deviceData.IntuneDevice.ComplianceState, - } - bloodhoundData.DeviceOwnerships = append(bloodhoundData.DeviceOwnerships, ownership) - } - } - - devices = append(devices, device) - } - - // Convert Sign-in Activity - for _, signIn := range result.SignInActivity { - signInActivity := azure.BloodHoundSignInActivity{ - UserId: signIn.UserId, - UserName: signIn.UserDisplayName, - DeviceId: signIn.DeviceDetail.DeviceId, - DeviceName: signIn.DeviceDetail.DisplayName, - AppId: signIn.AppId, - AppName: signIn.AppDisplayName, - SignInDateTime: signIn.CreatedDateTime, - IpAddress: signIn.IpAddress, - Location: fmt.Sprintf("%s, %s", signIn.Location.City, signIn.Location.CountryOrRegion), - RiskLevel: signIn.RiskLevelAggregated, - ConditionalAccess: signIn.ConditionalAccessStatus, - } - bloodhoundData.SignInActivity = append(bloodhoundData.SignInActivity, signInActivity) - } - - // Set data wrapper + // Skip device and sign-in conversion (focused approach) + // Set data wrapper to empty since we're only exporting relationships bloodhoundData.Data = azure.BloodHoundGraphDataWrapper{ - Users: users, - Groups: groups, - Devices: devices, + Users: []azure.BloodHoundUser{}, // Empty - focus on relationships + Groups: []azure.BloodHoundGroup{}, // Empty - focus on relationships + Devices: []azure.BloodHoundDevice{}, // Empty - not collected } return bloodhoundData @@ -534,20 +351,6 @@ func hasPrivilegedRoles(assignments []azure.AppRoleAssignment) bool { return false } -func extractDomainFromUPN(upn string) string { - parts := strings.Split(upn, "@") - if len(parts) == 2 { - return strings.ToUpper(parts[1]) - } - return "UNKNOWN" -} - -func extractDomainFromGroup(group azure.Group) string { - // Since OnPremisesDomainName doesn't exist, use a default - // In a real implementation, you might extract this from other group properties - return "AZUREAD" -} - func extractTypeFromOData(odataType string) string { if strings.Contains(odataType, "user") { return "User"