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..a484f953 --- /dev/null +++ b/cmd/list-group-membership.go @@ -0,0 +1,365 @@ +// 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/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 (focused on BloodHound essentials)", + Run: listGroupMembershipCmdImpl, + SilenceUsage: true, +} + +func listGroupMembershipCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := context.WithCancel(cmd.Context()) + defer stop() + + azClient := connectAndCreateClient() + + fmt.Printf("🎯 Collecting focused Azure AD data for BloodHound...\n\n") + startTime := time.Now() + + // Collect only essential data + result, err := collectAllGraphData(ctx, azClient) + if err != nil { + exit(err) + } + + duration := time.Since(startTime) + result.CollectionTime = duration + + // Display focused results + displayGraphDataResults(result) + + // Export focused BloodHound data + 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{}, + SignInActivity: []azure.SignIn{}, // Keep struct but don't populate + DeviceAccess: []azure.DeviceAccessData{}, // Keep struct but don't populate + Errors: []string{}, + } + + // 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 { + // 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 relevant groups\n", result.TotalGroups) + + // 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 { + // 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) + + // Skip device and sign-in collection for focused approach + fmt.Printf("⏭️ Skipping device and sign-in data (focused collection)\n") + + 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(" • 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") + + // 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") + } + + // 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_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) + } + 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("✅ 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)) + + return nil +} + +func convertToBloodHoundFormat(result *azure.GraphDataCollectionResult) azure.BloodHoundGraphData { + bloodhoundData := azure.BloodHoundGraphData{ + Meta: azure.BloodHoundMeta{ + Type: "azuread-focused", + Count: result.TotalGroups + result.TotalUsers, + Version: "1.0", + Methods: 2, // Groups + Users only (focused) + CollectedBy: "AzureHound-Focused", + CollectedAt: time.Now(), + }, + GroupMemberships: []azure.BloodHoundGroupMembership{}, + UserRoleAssignments: []azure.BloodHoundUserRoleAssignment{}, + DeviceOwnerships: []azure.BloodHoundDeviceOwnership{}, // Keep empty + SignInActivity: []azure.BloodHoundSignInActivity{}, // Keep empty + } + + // Convert Groups and Memberships only + for _, groupData := range result.GroupMemberships { + // 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 User Role Assignments only + for _, userData := range result.UserRoleAssignments { + for _, assignment := range userData.RoleAssignments { + roleAssignment := azure.BloodHoundUserRoleAssignment{ + UserId: userData.User.Id, + UserName: userData.User.DisplayName, + RoleId: assignment.AppRoleId.String(), + RoleName: assignment.PrincipalDisplayName, + ResourceId: assignment.ResourceId, + ResourceName: assignment.ResourceDisplayName, + AssignmentType: "DirectAssignment", + CreatedDateTime: time.Now(), + } + bloodhoundData.UserRoleAssignments = append(bloodhoundData.UserRoleAssignments, roleAssignment) + } + } + + // Skip device and sign-in conversion (focused approach) + // Set data wrapper to empty since we're only exporting relationships + bloodhoundData.Data = azure.BloodHoundGraphDataWrapper{ + Users: []azure.BloodHoundUser{}, // Empty - focus on relationships + Groups: []azure.BloodHoundGroup{}, // Empty - focus on relationships + Devices: []azure.BloodHoundDevice{}, // Empty - not collected + } + + 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 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"` +}