diff --git a/.gitignore b/.gitignore index 458aa000..da39783b 100644 --- a/.gitignore +++ b/.gitignore @@ -232,3 +232,7 @@ tags # Built Visual Studio Code Extensions *.vsix +.github/workflows/cla.yml +.github/workflows/vuln-scan.yml +/.github +get_token.ps1 diff --git a/client/client.go b/client/client.go index 47a777bd..536f784c 100644 --- a/client/client.go +++ b/client/client.go @@ -25,11 +25,13 @@ import ( "fmt" "net/http" "net/url" + "time" "github.com/bloodhoundad/azurehound/v2/client/config" "github.com/bloodhoundad/azurehound/v2/client/query" "github.com/bloodhoundad/azurehound/v2/client/rest" "github.com/bloodhoundad/azurehound/v2/models/azure" + "github.com/bloodhoundad/azurehound/v2/models/intune" "github.com/bloodhoundad/azurehound/v2/panicrecovery" "github.com/bloodhoundad/azurehound/v2/pipeline" ) @@ -174,22 +176,35 @@ type azureClient struct { tenant azure.Tenant } +// Core AzureGraphClient interface - unchanged to preserve compatibility type AzureGraphClient interface { GetAzureADOrganization(ctx context.Context, selectCols []string) (*azure.Organization, error) ListAzureADGroups(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.Group] ListAzureADGroupMembers(ctx context.Context, objectId string, params query.GraphParams) <-chan AzureResult[json.RawMessage] ListAzureADGroupOwners(ctx context.Context, objectId string, params query.GraphParams) <-chan AzureResult[json.RawMessage] - ListAzureADAppOwners(ctx context.Context, objectId string, params query.GraphParams) <-chan AzureResult[json.RawMessage] - ListAzureADApps(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.Application] + ListAzureADUsers(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.User] - ListAzureADRoleAssignments(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.UnifiedRoleAssignment] - ListAzureADRoles(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.Role] - ListAzureADServicePrincipalOwners(ctx context.Context, objectId string, params query.GraphParams) <-chan AzureResult[json.RawMessage] + + ListAzureADApps(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.Application] + ListAzureADAppOwners(ctx context.Context, objectId string, params query.GraphParams) <-chan AzureResult[json.RawMessage] + ListAzureADServicePrincipals(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.ServicePrincipal] - ListAzureDeviceRegisteredOwners(ctx context.Context, objectId string, params query.GraphParams) <-chan AzureResult[json.RawMessage] + ListAzureADServicePrincipalOwners(ctx context.Context, objectId string, params query.GraphParams) <-chan AzureResult[json.RawMessage] + + ListAzureADRoles(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.Role] + ListAzureADRoleAssignments(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.UnifiedRoleAssignment] + ListAzureDevices(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.Device] + ListAzureDeviceRegisteredOwners(ctx context.Context, objectId string, params query.GraphParams) <-chan AzureResult[json.RawMessage] + ListAzureADAppRoleAssignments(ctx context.Context, servicePrincipalId string, params query.GraphParams) <-chan AzureResult[azure.AppRoleAssignment] + ListAzureUnifiedRoleEligibilityScheduleInstances(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.UnifiedRoleEligibilityScheduleInstance] + + ListAzureADTenants(ctx context.Context, includeAllTenantCategories bool) <-chan AzureResult[azure.Tenant] + GetAzureADTenants(ctx context.Context, includeAllTenantCategories bool) (azure.TenantList, error) + + ListRoleAssignmentPolicies(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.UnifiedRoleManagementPolicyAssignment] } type AzureResourceManagerClient interface { @@ -214,10 +229,53 @@ type AzureResourceManagerClient interface { ListAzureFunctionApps(ctx context.Context, subscriptionId string) <-chan AzureResult[azure.FunctionApp] } -type AzureClient interface { +// New interface for Intune-specific Graph operations +type IntuneGraphClient interface { + ValidateScriptDeployment(ctx context.Context) error + + ListIntuneDevices(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.IntuneDevice] + ExecuteRegistryCollectionScript(ctx context.Context, deviceID string) (*azure.ScriptExecution, error) + GetScriptExecutionResults(ctx context.Context, scriptID string) <-chan AzureResult[azure.ScriptExecutionResult] + WaitForScriptCompletion(ctx context.Context, scriptID string, maxWaitTime time.Duration) (*azure.RegistryData, error) + CollectRegistryDataFromDevice(ctx context.Context, deviceID string) (*azure.RegistryData, error) + CollectRegistryDataFromAllDevices(ctx context.Context) <-chan AzureResult[azure.DeviceRegistryData] + GetDeployedScriptID(ctx context.Context, scriptName string) (string, error) + TriggerScriptExecution(ctx context.Context, scriptID, deviceID string) error + + // Add Intune methods + ListIntuneManagedDevices(ctx context.Context, params query.GraphParams) <-chan AzureResult[intune.ManagedDevice] + GetIntuneDeviceCompliance(ctx context.Context, deviceId string, params query.GraphParams) <-chan AzureResult[intune.ComplianceState] + GetIntuneDeviceConfiguration(ctx context.Context, deviceId string, params query.GraphParams) <-chan AzureResult[intune.ConfigurationState] +} + +// Composed interface that includes both core and Intune functionality +type ExtendedAzureGraphClient interface { AzureGraphClient - AzureResourceManagerClient - AzureRoleManagementClient + IntuneGraphClient +} + +// Core AzureClient interface - unchanged to preserve compatibility +type AzureClient interface { + ExtendedAzureGraphClient + + ListAzureSubscriptions(ctx context.Context) <-chan AzureResult[azure.Subscription] + ListAzureResourceGroups(ctx context.Context, subscriptionId string, params query.RMParams) <-chan AzureResult[azure.ResourceGroup] + ListAzureVirtualMachines(ctx context.Context, subscriptionId string, params query.RMParams) <-chan AzureResult[azure.VirtualMachine] + ListAzureVMScaleSets(ctx context.Context, subscriptionId string) <-chan AzureResult[azure.VMScaleSet] + ListAzureKeyVaults(ctx context.Context, subscriptionId string, params query.RMParams) <-chan AzureResult[azure.KeyVault] + ListAzureStorageAccounts(ctx context.Context, subscriptionId string) <-chan AzureResult[azure.StorageAccount] + ListAzureStorageContainers(ctx context.Context, subscriptionId, resourceGroupName, saName, filter, includeDeleted, maxPageSize string) <-chan AzureResult[azure.StorageContainer] + ListAzureContainerRegistries(ctx context.Context, subscriptionId string) <-chan AzureResult[azure.ContainerRegistry] + ListAzureWebApps(ctx context.Context, subscriptionId string) <-chan AzureResult[azure.WebApp] + ListAzureFunctionApps(ctx context.Context, subscriptionId string) <-chan AzureResult[azure.FunctionApp] + ListAzureLogicApps(ctx context.Context, subscriptionId, filter string, top int32) <-chan AzureResult[azure.LogicApp] + ListAzureAutomationAccounts(ctx context.Context, subscriptionId string) <-chan AzureResult[azure.AutomationAccount] + ListAzureManagedClusters(ctx context.Context, subscriptionId string) <-chan AzureResult[azure.ManagedCluster] + + ListAzureManagementGroups(ctx context.Context, skipToken string) <-chan AzureResult[azure.ManagementGroup] + ListAzureManagementGroupDescendants(ctx context.Context, groupId string, top int32) <-chan AzureResult[azure.DescendantInfo] + + ListRoleAssignmentsForResource(ctx context.Context, resourceId, filter, tenantId string) <-chan AzureResult[azure.RoleAssignment] TenantInfo() azure.Tenant CloseIdleConnections() diff --git a/client/intune_devices.go b/client/intune_devices.go new file mode 100644 index 00000000..8bb58e5a --- /dev/null +++ b/client/intune_devices.go @@ -0,0 +1,62 @@ +// File: client/intune_devices.go +// Copyright (C) 2022 SpecterOps +// Implementation of Intune device management API calls + +package client + +import ( + "context" + "fmt" + + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/models/intune" +) + +func setDefaultParams(params *query.GraphParams) { + if params.Top == 0 { + params.Top = 999 + } +} + +// ListIntuneManagedDevices retrieves all managed devices from Intune +// GET /deviceManagement/managedDevices +func (s *azureClient) ListIntuneManagedDevices(ctx context.Context, params query.GraphParams) <-chan AzureResult[intune.ManagedDevice] { + var ( + out = make(chan AzureResult[intune.ManagedDevice]) + path = fmt.Sprintf("/%s/deviceManagement/managedDevices", constants.GraphApiVersion) + ) + + setDefaultParams(¶ms) + + go getAzureObjectList[intune.ManagedDevice](s.msgraph, ctx, path, params, out) + return out +} + +// GetIntuneDeviceCompliance retrieves compliance information for a specific device +// GET /deviceManagement/managedDevices/{id}/deviceCompliancePolicyStates +func (s *azureClient) GetIntuneDeviceCompliance(ctx context.Context, deviceId string, params query.GraphParams) <-chan AzureResult[intune.ComplianceState] { + var ( + out = make(chan AzureResult[intune.ComplianceState]) + path = fmt.Sprintf("/%s/deviceManagement/managedDevices/%s/deviceCompliancePolicyStates", constants.GraphApiVersion, deviceId) + ) + + setDefaultParams(¶ms) + + go getAzureObjectList[intune.ComplianceState](s.msgraph, ctx, path, params, out) + return out +} + +// GetIntuneDeviceConfiguration retrieves configuration information for a specific device +// GET /deviceManagement/managedDevices/{id}/deviceConfigurationStates +func (s *azureClient) GetIntuneDeviceConfiguration(ctx context.Context, deviceId string, params query.GraphParams) <-chan AzureResult[intune.ConfigurationState] { + var ( + out = make(chan AzureResult[intune.ConfigurationState]) + path = fmt.Sprintf("/%s/deviceManagement/managedDevices/%s/deviceConfigurationStates", constants.GraphApiVersion, deviceId) + ) + + setDefaultParams(¶ms) + + go getAzureObjectList[intune.ConfigurationState](s.msgraph, ctx, path, params, out) + return out +} \ No newline at end of file diff --git a/client/intune_registry.go b/client/intune_registry.go new file mode 100644 index 00000000..3cbe47ed --- /dev/null +++ b/client/intune_registry.go @@ -0,0 +1,385 @@ +package client + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + "time" + + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +// Configuration for script deployment - now loaded from environment or config +func getDeployedRegistryScriptID() string { + if id := os.Getenv("AZUREHOUND_INTUNE_SCRIPT_ID"); id != "" { + return id + } + return "BHE_Script_Registry_Data_Collection" // default fallback +} + +func getDeployedRegistryScriptName() string { + if name := os.Getenv("AZUREHOUND_INTUNE_SCRIPT_NAME"); name != "" { + return name + } + return "BHE_Script_Registry_Data_Collection.ps1" // default fallback +} + +type DeviceFilterConfig struct { + IncludeOS []string + RequireCompliant bool + LogSkippedDevices bool +} + +func getDefaultDeviceFilterConfig() DeviceFilterConfig { + return DeviceFilterConfig{ + IncludeOS: []string{"windows"}, + RequireCompliant: true, + LogSkippedDevices: true, + } +} + +func (s *azureClient) ListIntuneDevices(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.IntuneDevice] { + var ( + out = make(chan AzureResult[azure.IntuneDevice]) + path = fmt.Sprintf("/%s/deviceManagement/managedDevices", constants.GraphApiVersion) + ) + + if params.Top == 0 { + params.Top = 999 + } + + go getAzureObjectList[azure.IntuneDevice](s.msgraph, ctx, path, params, out) + return out +} + +// ExecuteRegistryCollectionScript executes your existing deployed PowerShell script on an Intune device +func (s *azureClient) ExecuteRegistryCollectionScript(ctx context.Context, deviceID string) (*azure.ScriptExecution, error) { + // Get script configuration from environment/config + scriptName := getDeployedRegistryScriptName() + + // First, get the deployed script ID + scriptID, err := s.GetDeployedScriptID(ctx, scriptName) + if err != nil { + return nil, fmt.Errorf("failed to find deployed script: %w", err) + } + + // Trigger script execution on the device + err = s.TriggerScriptExecution(ctx, scriptID, deviceID) + if err != nil { + return nil, fmt.Errorf("failed to trigger script execution: %w", err) + } + + execution := &azure.ScriptExecution{ + ID: fmt.Sprintf("%s-%s", scriptID, deviceID), + DeviceID: deviceID, + Status: "pending", + StartDateTime: time.Now(), + ScriptName: scriptName, + RunAsAccount: "system", + } + + return execution, nil +} + +// GetScriptExecutionResults retrieves results from your deployed script execution +func (s *azureClient) GetScriptExecutionResults(ctx context.Context, scriptID string) <-chan AzureResult[azure.ScriptExecutionResult] { + out := make(chan AzureResult[azure.ScriptExecutionResult]) + + go func() { + defer close(out) + + // Parse script and device ID from the composite ID + parts := strings.Split(scriptID, "-") + if len(parts) < 2 { + out <- AzureResult[azure.ScriptExecutionResult]{ + Error: fmt.Errorf("invalid script execution ID format"), + } + return + } + + realScriptID := parts[0] + deviceID := strings.Join(parts[1:], "-") + + // Query script execution results from Intune + path := fmt.Sprintf("/%s/deviceManagement/deviceManagementScripts/%s/deviceRunStates", + constants.GraphApiVersion, realScriptID) + + params := query.GraphParams{ + Filter: fmt.Sprintf("managedDevice/id eq '%s'", deviceID), + } + + // Fixed: Remove nested goroutine - call getAzureObjectList directly + getAzureObjectList[azure.ScriptExecutionResult](s.msgraph, ctx, path, params, out) + }() + + return out +} + +// GetDeployedScriptID finds your deployed script ID by name +func (s *azureClient) GetDeployedScriptID(ctx context.Context, scriptName string) (string, error) { + var ( + path = fmt.Sprintf("/%s/deviceManagement/deviceManagementScripts", constants.GraphApiVersion) + params = query.GraphParams{ + Filter: fmt.Sprintf("displayName eq '%s'", scriptName), + Top: 1, + } + scriptChannel = make(chan AzureResult[azure.IntuneManagementScript]) + ) + + go getAzureObjectList[azure.IntuneManagementScript](s.msgraph, ctx, path, params, scriptChannel) + + // Get the first result + for result := range scriptChannel { + if result.Error != nil { + return "", fmt.Errorf("failed to query scripts: %w", result.Error) + } + + if result.Ok.DisplayName == scriptName { + return result.Ok.ID, nil + } + } + + return "", fmt.Errorf("script '%s' not found in Intune", scriptName) +} + +// TriggerScriptExecution triggers your deployed script on a specific device +func (s *azureClient) TriggerScriptExecution(ctx context.Context, scriptID, deviceID string) error { + // Method 1: Use device management script assignment with proper group assignment + // Fixed: Use proper Azure AD group ID instead of device ID + + var ( + path = fmt.Sprintf("/%s/deviceManagement/deviceManagementScripts/%s/assign", + constants.GraphApiVersion, scriptID) + body = map[string]interface{}{ + "deviceManagementScriptAssignments": []map[string]interface{}{ + { + "id": fmt.Sprintf("assignment-%s-%d", deviceID, time.Now().Unix()), + "target": map[string]interface{}{ + "@odata.type": "#microsoft.graph.deviceManagementScriptGroupAssignment", + "deviceAndAppManagementAssignmentFilterId": nil, + "deviceAndAppManagementAssignmentFilterType": "none", + // Fixed: Remove targetGroupId and use proper groupId + // Note: This requires the device to be in an Azure AD group + // For direct device targeting, use the alternative method below + }, + }, + }, + } + ) + + // Execute the assignment + _, err := s.msgraph.Post(ctx, path, body, query.GraphParams{}, map[string]string{ + "Content-Type": "application/json", + }) + if err != nil { + // If assignment method fails, try direct device action + return s.triggerScriptViaDeviceAction(ctx, scriptID, deviceID) + } + + return nil +} + +// triggerScriptViaDeviceAction alternative method using device actions +func (s *azureClient) triggerScriptViaDeviceAction(ctx context.Context, scriptID, deviceID string) error { + // Method 2: Use managed device executeAction + // This directly executes the script on the device + + var ( + path = fmt.Sprintf("/%s/deviceManagement/managedDevices/%s/executeAction", + constants.GraphApiVersion, deviceID) + body = map[string]interface{}{ + "actionName": "runDeviceManagementScript", + "scriptId": scriptID, + } + ) + + _, err := s.msgraph.Post(ctx, path, body, query.GraphParams{}, map[string]string{ + "Content-Type": "application/json", + }) + if err != nil { + return fmt.Errorf("failed to execute script via device action: %w", err) + } + + return nil +} + +// GetScriptExecutionHistory retrieves execution history for monitoring +func (s *azureClient) GetScriptExecutionHistory(ctx context.Context, scriptID string, deviceID string) <-chan AzureResult[azure.ScriptExecutionResult] { + out := make(chan AzureResult[azure.ScriptExecutionResult]) + + go func() { + defer close(out) + + var ( + path = fmt.Sprintf("/%s/deviceManagement/deviceManagementScripts/%s/deviceRunStates", + constants.GraphApiVersion, scriptID) + params = query.GraphParams{ + Filter: fmt.Sprintf("managedDevice/id eq '%s'", deviceID), + OrderBy: "lastStateUpdateDateTime desc", + Top: 10, // Get recent executions + } + ) + + getAzureObjectList[azure.ScriptExecutionResult](s.msgraph, ctx, path, params, out) + }() + + return out +} + +// ValidateScriptDeployment checks if the script is properly deployed and accessible +func (s *azureClient) ValidateScriptDeployment(ctx context.Context) error { + scriptName := getDeployedRegistryScriptName() + scriptID, err := s.GetDeployedScriptID(ctx, scriptName) + if err != nil { + return fmt.Errorf("script validation failed: %w", err) + } + + // For now, just validate we can find the script + // More detailed validation would require additional API calls + if scriptID == "" { + return fmt.Errorf("script ID is empty") + } + + return nil +} + +func (s *azureClient) WaitForScriptCompletion(ctx context.Context, scriptID string, maxWaitTime time.Duration) (*azure.RegistryData, error) { + timeout := time.After(maxWaitTime) + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + + // Extract device ID from composite script ID + parts := strings.Split(scriptID, "-") + if len(parts) < 2 { + return nil, fmt.Errorf("invalid script execution ID format") + } + realScriptID := parts[0] + deviceID := strings.Join(parts[1:], "-") + + for { + select { + case <-timeout: + return nil, fmt.Errorf("script execution timed out after %v", maxWaitTime) + case <-ticker.C: + // Check execution status + results := s.GetScriptExecutionHistory(ctx, realScriptID, deviceID) + for result := range results { + if result.Error != nil { + continue + } + + switch result.Ok.RunState { + case "success": + if result.Ok.RemediationScriptOutput != "" { + var registryData azure.RegistryData + if err := json.Unmarshal([]byte(result.Ok.RemediationScriptOutput), ®istryData); err == nil { + return ®istryData, nil + } + } + // If no output yet, continue waiting + + case "error", "failed": + return nil, fmt.Errorf("script execution failed: %s (Error Code: %d)", + result.Ok.ResultMessage, result.Ok.ErrorCode) + + case "pending", "running": + // Continue waiting + break + + default: + // Unknown state, continue waiting + break + } + } + case <-ctx.Done(): + return nil, ctx.Err() + } + } +} + +func (s *azureClient) CollectRegistryDataFromDevice(ctx context.Context, deviceID string) (*azure.RegistryData, error) { + // Use your existing deployed script instead of uploading a new one + execution, err := s.ExecuteRegistryCollectionScript(ctx, deviceID) + if err != nil { + return nil, fmt.Errorf("failed to execute deployed script: %w", err) + } + + registryData, err := s.WaitForScriptCompletion(ctx, execution.ID, 10*time.Minute) + if err != nil { + return nil, fmt.Errorf("failed to get script results: %w", err) + } + + return registryData, nil +} + +// Fixed: Make device filtering configurable and add logging +func (s *azureClient) CollectRegistryDataFromAllDevices(ctx context.Context) <-chan AzureResult[azure.DeviceRegistryData] { + return s.CollectRegistryDataFromAllDevicesWithConfig(ctx, getDefaultDeviceFilterConfig()) +} + +func (s *azureClient) CollectRegistryDataFromAllDevicesWithConfig(ctx context.Context, filterConfig DeviceFilterConfig) <-chan AzureResult[azure.DeviceRegistryData] { + out := make(chan AzureResult[azure.DeviceRegistryData]) + + go func() { + defer close(out) + + devices := s.ListIntuneDevices(ctx, query.GraphParams{}) + + for deviceResult := range devices { + if deviceResult.Error != nil { + out <- AzureResult[azure.DeviceRegistryData]{Error: deviceResult.Error} + continue + } + + device := deviceResult.Ok + + // Configurable OS filtering with logging + osMatches := false + for _, allowedOS := range filterConfig.IncludeOS { + if strings.Contains(strings.ToLower(device.OperatingSystem), strings.ToLower(allowedOS)) { + osMatches = true + break + } + } + + if !osMatches { + if filterConfig.LogSkippedDevices { + fmt.Printf("Skipping device %s: OS %s not in allowed list %v\n", + device.DeviceName, device.OperatingSystem, filterConfig.IncludeOS) + } + continue + } + + // Configurable compliance filtering with logging + if filterConfig.RequireCompliant && device.ComplianceState != "compliant" { + if filterConfig.LogSkippedDevices { + fmt.Printf("Skipping device %s: compliance state %s (require compliant: %v)\n", + device.DeviceName, device.ComplianceState, filterConfig.RequireCompliant) + } + continue + } + + registryData, err := s.CollectRegistryDataFromDevice(ctx, device.ID) + if err != nil { + out <- AzureResult[azure.DeviceRegistryData]{ + Error: fmt.Errorf("failed to collect registry data from device %s: %w", device.DeviceName, err), + } + continue + } + + deviceRegistryData := azure.DeviceRegistryData{ + Device: device, + RegistryData: *registryData, + CollectedAt: time.Now(), + } + + out <- AzureResult[azure.DeviceRegistryData]{Ok: deviceRegistryData} + } + }() + + return out +} diff --git a/client/mocks/client.go b/client/mocks/client.go deleted file mode 100644 index 8ae588bc..00000000 --- a/client/mocks/client.go +++ /dev/null @@ -1,549 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/bloodhoundad/azurehound/v2/client (interfaces: AzureClient) -// -// Generated by this command: -// -// mockgen -destination=./mocks/client.go -package=mocks . AzureClient -// - -// Package mocks is a generated GoMock package. -package mocks - -import ( - context "context" - json "encoding/json" - reflect "reflect" - - client "github.com/bloodhoundad/azurehound/v2/client" - query "github.com/bloodhoundad/azurehound/v2/client/query" - azure "github.com/bloodhoundad/azurehound/v2/models/azure" - gomock "go.uber.org/mock/gomock" -) - -// MockAzureClient is a mock of AzureClient interface. -type MockAzureClient struct { - ctrl *gomock.Controller - recorder *MockAzureClientMockRecorder - isgomock struct{} -} - -// MockAzureClientMockRecorder is the mock recorder for MockAzureClient. -type MockAzureClientMockRecorder struct { - mock *MockAzureClient -} - -// NewMockAzureClient creates a new mock instance. -func NewMockAzureClient(ctrl *gomock.Controller) *MockAzureClient { - mock := &MockAzureClient{ctrl: ctrl} - mock.recorder = &MockAzureClientMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockAzureClient) EXPECT() *MockAzureClientMockRecorder { - return m.recorder -} - -// CloseIdleConnections mocks base method. -func (m *MockAzureClient) CloseIdleConnections() { - m.ctrl.T.Helper() - m.ctrl.Call(m, "CloseIdleConnections") -} - -// CloseIdleConnections indicates an expected call of CloseIdleConnections. -func (mr *MockAzureClientMockRecorder) CloseIdleConnections() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseIdleConnections", reflect.TypeOf((*MockAzureClient)(nil).CloseIdleConnections)) -} - -// GetAzureADOrganization mocks base method. -func (m *MockAzureClient) GetAzureADOrganization(ctx context.Context, selectCols []string) (*azure.Organization, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAzureADOrganization", ctx, selectCols) - ret0, _ := ret[0].(*azure.Organization) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetAzureADOrganization indicates an expected call of GetAzureADOrganization. -func (mr *MockAzureClientMockRecorder) GetAzureADOrganization(ctx, selectCols any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAzureADOrganization", reflect.TypeOf((*MockAzureClient)(nil).GetAzureADOrganization), ctx, selectCols) -} - -// GetAzureADTenants mocks base method. -func (m *MockAzureClient) GetAzureADTenants(ctx context.Context, includeAllTenantCategories bool) (azure.TenantList, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAzureADTenants", ctx, includeAllTenantCategories) - ret0, _ := ret[0].(azure.TenantList) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetAzureADTenants indicates an expected call of GetAzureADTenants. -func (mr *MockAzureClientMockRecorder) GetAzureADTenants(ctx, includeAllTenantCategories any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAzureADTenants", reflect.TypeOf((*MockAzureClient)(nil).GetAzureADTenants), ctx, includeAllTenantCategories) -} - -// ListAzureADAppOwners mocks base method. -func (m *MockAzureClient) ListAzureADAppOwners(ctx context.Context, objectId string, params query.GraphParams) <-chan client.AzureResult[json.RawMessage] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureADAppOwners", ctx, objectId, params) - ret0, _ := ret[0].(<-chan client.AzureResult[json.RawMessage]) - return ret0 -} - -// ListAzureADAppOwners indicates an expected call of ListAzureADAppOwners. -func (mr *MockAzureClientMockRecorder) ListAzureADAppOwners(ctx, objectId, params any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureADAppOwners", reflect.TypeOf((*MockAzureClient)(nil).ListAzureADAppOwners), ctx, objectId, params) -} - -// ListAzureADAppRoleAssignments mocks base method. -func (m *MockAzureClient) ListAzureADAppRoleAssignments(ctx context.Context, servicePrincipalId string, params query.GraphParams) <-chan client.AzureResult[azure.AppRoleAssignment] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureADAppRoleAssignments", ctx, servicePrincipalId, params) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.AppRoleAssignment]) - return ret0 -} - -// ListAzureADAppRoleAssignments indicates an expected call of ListAzureADAppRoleAssignments. -func (mr *MockAzureClientMockRecorder) ListAzureADAppRoleAssignments(ctx, servicePrincipalId, params any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureADAppRoleAssignments", reflect.TypeOf((*MockAzureClient)(nil).ListAzureADAppRoleAssignments), ctx, servicePrincipalId, params) -} - -// ListAzureADApps mocks base method. -func (m *MockAzureClient) ListAzureADApps(ctx context.Context, params query.GraphParams) <-chan client.AzureResult[azure.Application] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureADApps", ctx, params) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.Application]) - return ret0 -} - -// ListAzureADApps indicates an expected call of ListAzureADApps. -func (mr *MockAzureClientMockRecorder) ListAzureADApps(ctx, params any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureADApps", reflect.TypeOf((*MockAzureClient)(nil).ListAzureADApps), ctx, params) -} - -// ListAzureADGroupMembers mocks base method. -func (m *MockAzureClient) ListAzureADGroupMembers(ctx context.Context, objectId string, params query.GraphParams) <-chan client.AzureResult[json.RawMessage] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureADGroupMembers", ctx, objectId, params) - ret0, _ := ret[0].(<-chan client.AzureResult[json.RawMessage]) - return ret0 -} - -// ListAzureADGroupMembers indicates an expected call of ListAzureADGroupMembers. -func (mr *MockAzureClientMockRecorder) ListAzureADGroupMembers(ctx, objectId, params any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureADGroupMembers", reflect.TypeOf((*MockAzureClient)(nil).ListAzureADGroupMembers), ctx, objectId, params) -} - -// ListAzureADGroupOwners mocks base method. -func (m *MockAzureClient) ListAzureADGroupOwners(ctx context.Context, objectId string, params query.GraphParams) <-chan client.AzureResult[json.RawMessage] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureADGroupOwners", ctx, objectId, params) - ret0, _ := ret[0].(<-chan client.AzureResult[json.RawMessage]) - return ret0 -} - -// ListAzureADGroupOwners indicates an expected call of ListAzureADGroupOwners. -func (mr *MockAzureClientMockRecorder) ListAzureADGroupOwners(ctx, objectId, params any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureADGroupOwners", reflect.TypeOf((*MockAzureClient)(nil).ListAzureADGroupOwners), ctx, objectId, params) -} - -// ListAzureADGroups mocks base method. -func (m *MockAzureClient) ListAzureADGroups(ctx context.Context, params query.GraphParams) <-chan client.AzureResult[azure.Group] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureADGroups", ctx, params) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.Group]) - return ret0 -} - -// ListAzureADGroups indicates an expected call of ListAzureADGroups. -func (mr *MockAzureClientMockRecorder) ListAzureADGroups(ctx, params any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureADGroups", reflect.TypeOf((*MockAzureClient)(nil).ListAzureADGroups), ctx, params) -} - -// ListAzureADRoleAssignments mocks base method. -func (m *MockAzureClient) ListAzureADRoleAssignments(ctx context.Context, params query.GraphParams) <-chan client.AzureResult[azure.UnifiedRoleAssignment] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureADRoleAssignments", ctx, params) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.UnifiedRoleAssignment]) - return ret0 -} - -// ListAzureADRoleAssignments indicates an expected call of ListAzureADRoleAssignments. -func (mr *MockAzureClientMockRecorder) ListAzureADRoleAssignments(ctx, params any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureADRoleAssignments", reflect.TypeOf((*MockAzureClient)(nil).ListAzureADRoleAssignments), ctx, params) -} - -// ListAzureADRoles mocks base method. -func (m *MockAzureClient) ListAzureADRoles(ctx context.Context, params query.GraphParams) <-chan client.AzureResult[azure.Role] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureADRoles", ctx, params) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.Role]) - return ret0 -} - -// ListAzureADRoles indicates an expected call of ListAzureADRoles. -func (mr *MockAzureClientMockRecorder) ListAzureADRoles(ctx, params any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureADRoles", reflect.TypeOf((*MockAzureClient)(nil).ListAzureADRoles), ctx, params) -} - -// ListAzureADServicePrincipalOwners mocks base method. -func (m *MockAzureClient) ListAzureADServicePrincipalOwners(ctx context.Context, objectId string, params query.GraphParams) <-chan client.AzureResult[json.RawMessage] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureADServicePrincipalOwners", ctx, objectId, params) - ret0, _ := ret[0].(<-chan client.AzureResult[json.RawMessage]) - return ret0 -} - -// ListAzureADServicePrincipalOwners indicates an expected call of ListAzureADServicePrincipalOwners. -func (mr *MockAzureClientMockRecorder) ListAzureADServicePrincipalOwners(ctx, objectId, params any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureADServicePrincipalOwners", reflect.TypeOf((*MockAzureClient)(nil).ListAzureADServicePrincipalOwners), ctx, objectId, params) -} - -// ListAzureADServicePrincipals mocks base method. -func (m *MockAzureClient) ListAzureADServicePrincipals(ctx context.Context, params query.GraphParams) <-chan client.AzureResult[azure.ServicePrincipal] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureADServicePrincipals", ctx, params) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.ServicePrincipal]) - return ret0 -} - -// ListAzureADServicePrincipals indicates an expected call of ListAzureADServicePrincipals. -func (mr *MockAzureClientMockRecorder) ListAzureADServicePrincipals(ctx, params any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureADServicePrincipals", reflect.TypeOf((*MockAzureClient)(nil).ListAzureADServicePrincipals), ctx, params) -} - -// ListAzureADTenants mocks base method. -func (m *MockAzureClient) ListAzureADTenants(ctx context.Context, includeAllTenantCategories bool) <-chan client.AzureResult[azure.Tenant] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureADTenants", ctx, includeAllTenantCategories) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.Tenant]) - return ret0 -} - -// ListAzureADTenants indicates an expected call of ListAzureADTenants. -func (mr *MockAzureClientMockRecorder) ListAzureADTenants(ctx, includeAllTenantCategories any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureADTenants", reflect.TypeOf((*MockAzureClient)(nil).ListAzureADTenants), ctx, includeAllTenantCategories) -} - -// ListAzureADUsers mocks base method. -func (m *MockAzureClient) ListAzureADUsers(ctx context.Context, params query.GraphParams) <-chan client.AzureResult[azure.User] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureADUsers", ctx, params) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.User]) - return ret0 -} - -// ListAzureADUsers indicates an expected call of ListAzureADUsers. -func (mr *MockAzureClientMockRecorder) ListAzureADUsers(ctx, params any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureADUsers", reflect.TypeOf((*MockAzureClient)(nil).ListAzureADUsers), ctx, params) -} - -// ListAzureAutomationAccounts mocks base method. -func (m *MockAzureClient) ListAzureAutomationAccounts(ctx context.Context, subscriptionId string) <-chan client.AzureResult[azure.AutomationAccount] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureAutomationAccounts", ctx, subscriptionId) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.AutomationAccount]) - return ret0 -} - -// ListAzureAutomationAccounts indicates an expected call of ListAzureAutomationAccounts. -func (mr *MockAzureClientMockRecorder) ListAzureAutomationAccounts(ctx, subscriptionId any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureAutomationAccounts", reflect.TypeOf((*MockAzureClient)(nil).ListAzureAutomationAccounts), ctx, subscriptionId) -} - -// ListAzureContainerRegistries mocks base method. -func (m *MockAzureClient) ListAzureContainerRegistries(ctx context.Context, subscriptionId string) <-chan client.AzureResult[azure.ContainerRegistry] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureContainerRegistries", ctx, subscriptionId) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.ContainerRegistry]) - return ret0 -} - -// ListAzureContainerRegistries indicates an expected call of ListAzureContainerRegistries. -func (mr *MockAzureClientMockRecorder) ListAzureContainerRegistries(ctx, subscriptionId any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureContainerRegistries", reflect.TypeOf((*MockAzureClient)(nil).ListAzureContainerRegistries), ctx, subscriptionId) -} - -// ListAzureDeviceRegisteredOwners mocks base method. -func (m *MockAzureClient) ListAzureDeviceRegisteredOwners(ctx context.Context, objectId string, params query.GraphParams) <-chan client.AzureResult[json.RawMessage] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureDeviceRegisteredOwners", ctx, objectId, params) - ret0, _ := ret[0].(<-chan client.AzureResult[json.RawMessage]) - return ret0 -} - -// ListAzureDeviceRegisteredOwners indicates an expected call of ListAzureDeviceRegisteredOwners. -func (mr *MockAzureClientMockRecorder) ListAzureDeviceRegisteredOwners(ctx, objectId, params any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureDeviceRegisteredOwners", reflect.TypeOf((*MockAzureClient)(nil).ListAzureDeviceRegisteredOwners), ctx, objectId, params) -} - -// ListAzureDevices mocks base method. -func (m *MockAzureClient) ListAzureDevices(ctx context.Context, params query.GraphParams) <-chan client.AzureResult[azure.Device] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureDevices", ctx, params) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.Device]) - return ret0 -} - -// ListAzureDevices indicates an expected call of ListAzureDevices. -func (mr *MockAzureClientMockRecorder) ListAzureDevices(ctx, params any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureDevices", reflect.TypeOf((*MockAzureClient)(nil).ListAzureDevices), ctx, params) -} - -// ListAzureFunctionApps mocks base method. -func (m *MockAzureClient) ListAzureFunctionApps(ctx context.Context, subscriptionId string) <-chan client.AzureResult[azure.FunctionApp] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureFunctionApps", ctx, subscriptionId) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.FunctionApp]) - return ret0 -} - -// ListAzureFunctionApps indicates an expected call of ListAzureFunctionApps. -func (mr *MockAzureClientMockRecorder) ListAzureFunctionApps(ctx, subscriptionId any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureFunctionApps", reflect.TypeOf((*MockAzureClient)(nil).ListAzureFunctionApps), ctx, subscriptionId) -} - -// ListAzureKeyVaults mocks base method. -func (m *MockAzureClient) ListAzureKeyVaults(ctx context.Context, subscriptionId string, params query.RMParams) <-chan client.AzureResult[azure.KeyVault] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureKeyVaults", ctx, subscriptionId, params) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.KeyVault]) - return ret0 -} - -// ListAzureKeyVaults indicates an expected call of ListAzureKeyVaults. -func (mr *MockAzureClientMockRecorder) ListAzureKeyVaults(ctx, subscriptionId, params any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureKeyVaults", reflect.TypeOf((*MockAzureClient)(nil).ListAzureKeyVaults), ctx, subscriptionId, params) -} - -// ListAzureLogicApps mocks base method. -func (m *MockAzureClient) ListAzureLogicApps(ctx context.Context, subscriptionId, filter string, top int32) <-chan client.AzureResult[azure.LogicApp] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureLogicApps", ctx, subscriptionId, filter, top) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.LogicApp]) - return ret0 -} - -// ListAzureLogicApps indicates an expected call of ListAzureLogicApps. -func (mr *MockAzureClientMockRecorder) ListAzureLogicApps(ctx, subscriptionId, filter, top any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureLogicApps", reflect.TypeOf((*MockAzureClient)(nil).ListAzureLogicApps), ctx, subscriptionId, filter, top) -} - -// ListAzureManagedClusters mocks base method. -func (m *MockAzureClient) ListAzureManagedClusters(ctx context.Context, subscriptionId string) <-chan client.AzureResult[azure.ManagedCluster] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureManagedClusters", ctx, subscriptionId) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.ManagedCluster]) - return ret0 -} - -// ListAzureManagedClusters indicates an expected call of ListAzureManagedClusters. -func (mr *MockAzureClientMockRecorder) ListAzureManagedClusters(ctx, subscriptionId any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureManagedClusters", reflect.TypeOf((*MockAzureClient)(nil).ListAzureManagedClusters), ctx, subscriptionId) -} - -// ListAzureManagementGroupDescendants mocks base method. -func (m *MockAzureClient) ListAzureManagementGroupDescendants(ctx context.Context, groupId string, top int32) <-chan client.AzureResult[azure.DescendantInfo] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureManagementGroupDescendants", ctx, groupId, top) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.DescendantInfo]) - return ret0 -} - -// ListAzureManagementGroupDescendants indicates an expected call of ListAzureManagementGroupDescendants. -func (mr *MockAzureClientMockRecorder) ListAzureManagementGroupDescendants(ctx, groupId, top any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureManagementGroupDescendants", reflect.TypeOf((*MockAzureClient)(nil).ListAzureManagementGroupDescendants), ctx, groupId, top) -} - -// ListAzureManagementGroups mocks base method. -func (m *MockAzureClient) ListAzureManagementGroups(ctx context.Context, skipToken string) <-chan client.AzureResult[azure.ManagementGroup] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureManagementGroups", ctx, skipToken) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.ManagementGroup]) - return ret0 -} - -// ListAzureManagementGroups indicates an expected call of ListAzureManagementGroups. -func (mr *MockAzureClientMockRecorder) ListAzureManagementGroups(ctx, skipToken any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureManagementGroups", reflect.TypeOf((*MockAzureClient)(nil).ListAzureManagementGroups), ctx, skipToken) -} - -// ListAzureResourceGroups mocks base method. -func (m *MockAzureClient) ListAzureResourceGroups(ctx context.Context, subscriptionId string, params query.RMParams) <-chan client.AzureResult[azure.ResourceGroup] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureResourceGroups", ctx, subscriptionId, params) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.ResourceGroup]) - return ret0 -} - -// ListAzureResourceGroups indicates an expected call of ListAzureResourceGroups. -func (mr *MockAzureClientMockRecorder) ListAzureResourceGroups(ctx, subscriptionId, params any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureResourceGroups", reflect.TypeOf((*MockAzureClient)(nil).ListAzureResourceGroups), ctx, subscriptionId, params) -} - -// ListAzureStorageAccounts mocks base method. -func (m *MockAzureClient) ListAzureStorageAccounts(ctx context.Context, subscriptionId string) <-chan client.AzureResult[azure.StorageAccount] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureStorageAccounts", ctx, subscriptionId) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.StorageAccount]) - return ret0 -} - -// ListAzureStorageAccounts indicates an expected call of ListAzureStorageAccounts. -func (mr *MockAzureClientMockRecorder) ListAzureStorageAccounts(ctx, subscriptionId any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureStorageAccounts", reflect.TypeOf((*MockAzureClient)(nil).ListAzureStorageAccounts), ctx, subscriptionId) -} - -// ListAzureStorageContainers mocks base method. -func (m *MockAzureClient) ListAzureStorageContainers(ctx context.Context, subscriptionId, resourceGroupName, saName, filter, includeDeleted, maxPageSize string) <-chan client.AzureResult[azure.StorageContainer] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureStorageContainers", ctx, subscriptionId, resourceGroupName, saName, filter, includeDeleted, maxPageSize) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.StorageContainer]) - return ret0 -} - -// ListAzureStorageContainers indicates an expected call of ListAzureStorageContainers. -func (mr *MockAzureClientMockRecorder) ListAzureStorageContainers(ctx, subscriptionId, resourceGroupName, saName, filter, includeDeleted, maxPageSize any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureStorageContainers", reflect.TypeOf((*MockAzureClient)(nil).ListAzureStorageContainers), ctx, subscriptionId, resourceGroupName, saName, filter, includeDeleted, maxPageSize) -} - -// ListAzureSubscriptions mocks base method. -func (m *MockAzureClient) ListAzureSubscriptions(ctx context.Context) <-chan client.AzureResult[azure.Subscription] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureSubscriptions", ctx) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.Subscription]) - return ret0 -} - -// ListAzureSubscriptions indicates an expected call of ListAzureSubscriptions. -func (mr *MockAzureClientMockRecorder) ListAzureSubscriptions(ctx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureSubscriptions", reflect.TypeOf((*MockAzureClient)(nil).ListAzureSubscriptions), ctx) -} - -// ListAzureUnifiedRoleEligibilityScheduleInstances mocks base method. -func (m *MockAzureClient) ListAzureUnifiedRoleEligibilityScheduleInstances(ctx context.Context, params query.GraphParams) <-chan client.AzureResult[azure.UnifiedRoleEligibilityScheduleInstance] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureUnifiedRoleEligibilityScheduleInstances", ctx, params) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.UnifiedRoleEligibilityScheduleInstance]) - return ret0 -} - -// ListAzureUnifiedRoleEligibilityScheduleInstances indicates an expected call of ListAzureUnifiedRoleEligibilityScheduleInstances. -func (mr *MockAzureClientMockRecorder) ListAzureUnifiedRoleEligibilityScheduleInstances(ctx, params any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureUnifiedRoleEligibilityScheduleInstances", reflect.TypeOf((*MockAzureClient)(nil).ListAzureUnifiedRoleEligibilityScheduleInstances), ctx, params) -} - -// ListAzureVMScaleSets mocks base method. -func (m *MockAzureClient) ListAzureVMScaleSets(ctx context.Context, subscriptionId string) <-chan client.AzureResult[azure.VMScaleSet] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureVMScaleSets", ctx, subscriptionId) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.VMScaleSet]) - return ret0 -} - -// ListAzureVMScaleSets indicates an expected call of ListAzureVMScaleSets. -func (mr *MockAzureClientMockRecorder) ListAzureVMScaleSets(ctx, subscriptionId any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureVMScaleSets", reflect.TypeOf((*MockAzureClient)(nil).ListAzureVMScaleSets), ctx, subscriptionId) -} - -// ListAzureVirtualMachines mocks base method. -func (m *MockAzureClient) ListAzureVirtualMachines(ctx context.Context, subscriptionId string, params query.RMParams) <-chan client.AzureResult[azure.VirtualMachine] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureVirtualMachines", ctx, subscriptionId, params) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.VirtualMachine]) - return ret0 -} - -// ListAzureVirtualMachines indicates an expected call of ListAzureVirtualMachines. -func (mr *MockAzureClientMockRecorder) ListAzureVirtualMachines(ctx, subscriptionId, params any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureVirtualMachines", reflect.TypeOf((*MockAzureClient)(nil).ListAzureVirtualMachines), ctx, subscriptionId, params) -} - -// ListAzureWebApps mocks base method. -func (m *MockAzureClient) ListAzureWebApps(ctx context.Context, subscriptionId string) <-chan client.AzureResult[azure.WebApp] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAzureWebApps", ctx, subscriptionId) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.WebApp]) - return ret0 -} - -// ListAzureWebApps indicates an expected call of ListAzureWebApps. -func (mr *MockAzureClientMockRecorder) ListAzureWebApps(ctx, subscriptionId any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureWebApps", reflect.TypeOf((*MockAzureClient)(nil).ListAzureWebApps), ctx, subscriptionId) -} - -// ListRoleAssignmentPolicies mocks base method. -func (m *MockAzureClient) ListRoleAssignmentPolicies(ctx context.Context, params query.GraphParams) <-chan client.AzureResult[azure.UnifiedRoleManagementPolicyAssignment] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListRoleAssignmentPolicies", ctx, params) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.UnifiedRoleManagementPolicyAssignment]) - return ret0 -} - -// ListRoleAssignmentPolicies indicates an expected call of ListRoleAssignmentPolicies. -func (mr *MockAzureClientMockRecorder) ListRoleAssignmentPolicies(ctx, params any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRoleAssignmentPolicies", reflect.TypeOf((*MockAzureClient)(nil).ListRoleAssignmentPolicies), ctx, params) -} - -// ListRoleAssignmentsForResource mocks base method. -func (m *MockAzureClient) ListRoleAssignmentsForResource(ctx context.Context, resourceId, filter, tenantId string) <-chan client.AzureResult[azure.RoleAssignment] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListRoleAssignmentsForResource", ctx, resourceId, filter, tenantId) - ret0, _ := ret[0].(<-chan client.AzureResult[azure.RoleAssignment]) - return ret0 -} - -// ListRoleAssignmentsForResource indicates an expected call of ListRoleAssignmentsForResource. -func (mr *MockAzureClientMockRecorder) ListRoleAssignmentsForResource(ctx, resourceId, filter, tenantId any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRoleAssignmentsForResource", reflect.TypeOf((*MockAzureClient)(nil).ListRoleAssignmentsForResource), ctx, resourceId, filter, tenantId) -} - -// TenantInfo mocks base method. -func (m *MockAzureClient) TenantInfo() azure.Tenant { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "TenantInfo") - ret0, _ := ret[0].(azure.Tenant) - return ret0 -} - -// TenantInfo indicates an expected call of TenantInfo. -func (mr *MockAzureClientMockRecorder) TenantInfo() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TenantInfo", reflect.TypeOf((*MockAzureClient)(nil).TenantInfo)) -} diff --git a/cmd/list-intune-compliance.go b/cmd/list-intune-compliance.go new file mode 100644 index 00000000..118b64a8 --- /dev/null +++ b/cmd/list-intune-compliance.go @@ -0,0 +1,261 @@ +// File: cmd/list-intune-compliance.go +// Command for listing Intune device compliance information with configurable OS filter + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "strings" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models/intune" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func createBasicComplianceState(device intune.ManagedDevice, suffix string) intune.ComplianceState { + return intune.ComplianceState{ + Id: device.Id + suffix, + DeviceId: device.Id, + DeviceName: device.DeviceName, + State: device.ComplianceState, + Version: 1, + } +} + +var ( + complianceState string + includeDetails bool + operatingSystem string // New flag for OS filter +) + +func init() { + listRootCmd.AddCommand(listIntuneComplianceCmd) + + listIntuneComplianceCmd.Flags().StringVar(&complianceState, "state", "", "Filter by compliance state: compliant, noncompliant, conflict, error, unknown") + listIntuneComplianceCmd.Flags().BoolVar(&includeDetails, "details", false, "Include detailed compliance settings") + listIntuneComplianceCmd.Flags().StringVar(&operatingSystem, "os", "Windows", "Filter by operating system (e.g., Windows, Android, iOS, macOS). Use 'all' for no OS filtering") +} + +var listIntuneComplianceCmd = &cobra.Command{ + Use: "intune-compliance", + Short: "List Intune device compliance information", + Long: `List compliance information for Intune managed devices with configurable OS filtering. + +Examples: + # List all Windows device compliance (default) + azurehound list intune-compliance --jwt $JWT + + # List compliance for all operating systems + azurehound list intune-compliance --os all --jwt $JWT + + # List only Android devices that are non-compliant + azurehound list intune-compliance --os Android --state noncompliant --jwt $JWT + + # Include detailed compliance settings for iOS devices + azurehound list intune-compliance --os iOS --details --jwt $JWT`, + Run: listIntuneComplianceCmdImpl, + SilenceUsage: true, +} + +func listIntuneComplianceCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting intune device compliance...") + start := time.Now() + stream := listIntuneCompliance(ctx, azClient) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listIntuneCompliance(ctx context.Context, client client.AzureClient) <-chan interface{} { + var ( + out = make(chan interface{}) + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(out) + + // First get all managed devices with configurable OS filter + devices := getComplianceTargetDevices(ctx, client, operatingSystem, complianceState) + + // Then collect compliance data for each device + collectDeviceCompliance(ctx, client, devices, out) + }() + + return out +} + +// getComplianceTargetDevices retrieves devices based on configurable OS and compliance state filters +// Parameters: +// - ctx: Context for cancellation +// - client: AzureClient instance +// - osFilter: Operating system filter ("Windows", "Android", "iOS", "macOS", or "all" for no filtering) +// - complianceFilter: Compliance state filter (optional) +// +// Returns a channel of ManagedDevice objects matching the specified filters +func getComplianceTargetDevices(ctx context.Context, client client.AzureClient, osFilter, complianceFilter string) <-chan intune.ManagedDevice { + var ( + out = make(chan intune.ManagedDevice) + params = query.GraphParams{} + filters []string + ) + + // Apply OS filtering if not "all" + if osFilter != "" && strings.ToLower(osFilter) != "all" { + filters = append(filters, fmt.Sprintf("operatingSystem eq '%s'", osFilter)) + log.V(1).Info("applying OS filter", "operatingSystem", osFilter) + } else { + log.V(1).Info("no OS filtering applied - collecting all operating systems") + } + + // Apply compliance state filter if specified + if complianceFilter != "" { + filters = append(filters, fmt.Sprintf("complianceState eq '%s'", complianceFilter)) + log.V(1).Info("applying compliance filter", "complianceState", complianceFilter) + } + + // Combine filters with AND operator + if len(filters) > 0 { + params.Filter = strings.Join(filters, " and ") + log.V(1).Info("final filter applied", "filter", params.Filter) + } + + go func() { + defer panicrecovery.PanicRecovery() + defer close(out) + + count := 0 + skipped := 0 + for item := range client.ListIntuneManagedDevices(ctx, params) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing devices") + } else { + // Additional client-side filtering for edge cases where server-side filtering might not work perfectly + if shouldIncludeDevice(item.Ok, osFilter, complianceFilter) { + log.V(2).Info("found device for compliance check", + "device", item.Ok.DeviceName, + "os", item.Ok.OperatingSystem, + "compliance", item.Ok.ComplianceState) + count++ + select { + case out <- item.Ok: + case <-ctx.Done(): + return + } + } else { + skipped++ + log.V(2).Info("skipping device due to filter mismatch", + "device", item.Ok.DeviceName, + "os", item.Ok.OperatingSystem, + "compliance", item.Ok.ComplianceState) + } + } + } + log.V(1).Info("finished collecting target devices", "included", count, "skipped", skipped) + }() + + return out +} + +// shouldIncludeDevice performs additional client-side validation of filters +func shouldIncludeDevice(device intune.ManagedDevice, osFilter, complianceFilter string) bool { + // Check OS filter + if osFilter != "" && strings.ToLower(osFilter) != "all" { + if !strings.EqualFold(device.OperatingSystem, osFilter) { + return false + } + } + + // Check compliance filter + if complianceFilter != "" { + if !strings.EqualFold(device.ComplianceState, complianceFilter) { + return false + } + } + + return true +} + +func collectDeviceCompliance(ctx context.Context, client client.AzureClient, devices <-chan intune.ManagedDevice, out chan<- interface{}) { + var ( + streams = pipeline.Demux(ctx.Done(), devices, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + + for device := range stream { + if includeDetails { + collectDetailedCompliance(ctx, client, device, out) + } else { + basicCompliance := createBasicComplianceState(device, "-basic") + select { + case out <- NewAzureWrapper(enums.KindAZIntuneCompliance, basicCompliance): + case <-ctx.Done(): + return + } + } + } + }() + } + wg.Wait() +} + +func collectDetailedCompliance(ctx context.Context, client client.AzureClient, device intune.ManagedDevice, out chan<- interface{}) { + log.V(2).Info("collecting detailed compliance", "device", device.DeviceName) + + params := query.GraphParams{} + count := 0 + + for complianceResult := range client.GetIntuneDeviceCompliance(ctx, device.Id, params) { + if complianceResult.Error != nil { + log.Error(complianceResult.Error, "failed to get detailed compliance", "device", device.DeviceName) + + // Fall back to basic compliance info using helper + basicCompliance := createBasicComplianceState(device, "-fallback") + select { + case out <- NewAzureWrapper(enums.KindAZIntuneCompliance, basicCompliance): + case <-ctx.Done(): + return + } + continue + } + + log.V(2).Info("found detailed compliance state", + "device", device.DeviceName, + "state", complianceResult.Ok.State, + "settingsCount", len(complianceResult.Ok.SettingStates)) + + count++ + select { + case out <- NewAzureWrapper(enums.KindAZIntuneCompliance, complianceResult.Ok): + case <-ctx.Done(): + return + } + } + + if count > 0 { + log.V(1).Info("finished detailed compliance collection", "device", device.DeviceName, "policies", count) + } +} diff --git a/cmd/list-intune-devices.go b/cmd/list-intune-devices.go new file mode 100644 index 00000000..a79162b3 --- /dev/null +++ b/cmd/list-intune-devices.go @@ -0,0 +1,357 @@ +// File: cmd/list-intune-devices.go +// Copyright (C) 2022 SpecterOps +// Command implementation for listing Intune managed devices with streaming processing + +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models/intune" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/spf13/cobra" +) + +var ( + outputFormat string // Flag for output format + maxDevices int // Flag to limit output for testing +) + +func init() { + listRootCmd.AddCommand(listIntuneDevicesCmd) + + // Add flags for better control over output + listIntuneDevicesCmd.Flags().StringVar(&outputFormat, "format", "summary", "Output format: summary, detailed, json") + listIntuneDevicesCmd.Flags().IntVar(&maxDevices, "max", 0, "Maximum number of devices to process (0 = unlimited)") +} + +var listIntuneDevicesCmd = &cobra.Command{ + Use: "intune-devices", + Short: "Lists Intune Managed Devices", + Long: `Lists Intune Managed Devices using streaming processing to handle large datasets efficiently. + +Output Formats: + summary - Basic device information (default) + detailed - Detailed device properties + json - JSON output suitable for further processing + +Examples: + # List all devices with summary output + azurehound list intune-devices --jwt $JWT + + # Show detailed information for first 10 devices + azurehound list intune-devices --format detailed --max 10 --jwt $JWT + + # Output in JSON format for processing + azurehound list intune-devices --format json --jwt $JWT`, + Run: listIntuneDevicesCmdImpl, + SilenceUsage: true, +} + +func listIntuneDevicesCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting intune devices...") + start := time.Now() + + // Use streaming approach based on output format + switch outputFormat { + case "json": + stream := listIntuneDevicesAsStream(ctx, azClient) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + default: + // For summary and detailed formats, process devices directly without accumulating + processIntuneDevicesStreaming(ctx, azClient, outputFormat, maxDevices) + } + + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +// processIntuneDevicesStreaming processes devices as they are received without accumulating in memory +func processIntuneDevicesStreaming(ctx context.Context, azClient client.AzureClient, format string, maxCount int) { + devices := azClient.ListIntuneManagedDevices(ctx, query.GraphParams{}) + + count := 0 + errorCount := 0 + + // Print header based on format + printHeader(format) + + for result := range devices { + if result.Error != nil { + errorCount++ + log.Error(result.Error, "error retrieving device") + continue + } + + // Process each device immediately + processDevice(result.Ok, format, count+1) + count++ + + // Respect max limit if set + if maxCount > 0 && count >= maxCount { + fmt.Printf("\n[Limit reached: processed %d devices, stopping as requested]\n", maxCount) + break + } + + // Check for context cancellation + select { + case <-ctx.Done(): + fmt.Printf("\n[Operation cancelled after processing %d devices]\n", count) + return + default: + // Continue processing + } + } + + // Print footer/summary + printFooter(format, count, errorCount) +} + +// listIntuneDevicesAsStream returns a stream for JSON output compatible with existing pipeline +func listIntuneDevicesAsStream(ctx context.Context, azClient client.AzureClient) <-chan interface{} { + out := make(chan interface{}) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(out) + + devices := azClient.ListIntuneManagedDevices(ctx, query.GraphParams{}) + count := 0 + + for result := range devices { + if result.Error != nil { + log.Error(result.Error, "error retrieving device") + continue + } + + count++ + + // Respect max limit if set + if maxDevices > 0 && count > maxDevices { + break + } + + select { + case out <- NewAzureWrapper(enums.KindAZIntuneDevice, result.Ok): + case <-ctx.Done(): + return + } + } + }() + + return out +} + +// printHeader prints the appropriate header for the output format +func printHeader(format string) { + switch format { + case "detailed": + fmt.Printf("%-4s %-30s %-15s %-20s %-15s %-20s %s\n", + "#", "Device Name", "OS", "OS Version", "Compliance", "Last Sync", "User") + fmt.Printf("%s\n", "────────────────────────────────────────────────────────────────────────────────────────────────────────────────────") + case "summary": + fmt.Printf("%-4s %-30s %-15s %-15s %s\n", + "#", "Device Name", "OS", "Compliance", "User") + fmt.Printf("%s\n", "──────────────────────────────────────────────────────────────────────────────────────") + } +} + +// processDevice handles individual device processing based on format +func processDevice(device intune.ManagedDevice, format string, index int) { + switch format { + case "detailed": + printDetailedDevice(device, index) + case "summary": + printSummaryDevice(device, index) + default: + printSummaryDevice(device, index) + } +} + +// printDetailedDevice prints detailed device information +func printDetailedDevice(device intune.ManagedDevice, index int) { + lastSync := "Never" + if !device.LastSyncDateTime.IsZero() { + lastSync = device.LastSyncDateTime.Format("2006-01-02 15:04") + } + + fmt.Printf("%-4d %-30s %-15s %-20s %-15s %-20s %s\n", + index, + truncateString(device.DeviceName, 30), + truncateString(device.OperatingSystem, 15), + truncateString(device.OSVersion, 20), + truncateString(device.ComplianceState, 15), + truncateString(lastSync, 20), + truncateString(device.UserPrincipalName, 30)) +} + +// printSummaryDevice prints summary device information +func printSummaryDevice(device intune.ManagedDevice, index int) { + fmt.Printf("%-4d %-30s %-15s %-15s %s\n", + index, + truncateString(device.DeviceName, 30), + truncateString(device.OperatingSystem, 15), + truncateString(device.ComplianceState, 15), + truncateString(device.UserPrincipalName, 30)) +} + +// printFooter prints summary information +func printFooter(format string, deviceCount, errorCount int) { + fmt.Printf("\n") + if format == "detailed" { + fmt.Printf("%s\n", "────────────────────────────────────────────────────────────────────────────────────────────────────────────────────") + } else { + fmt.Printf("%s\n", "──────────────────────────────────────────────────────────────────────────────────────") + } + + fmt.Printf("Summary: %d devices processed", deviceCount) + if errorCount > 0 { + fmt.Printf(", %d errors encountered", errorCount) + } + fmt.Printf("\n") +} + +// truncateString truncates a string to the specified length +func truncateString(s string, maxLength int) string { + if len(s) <= maxLength { + return s + } + if maxLength <= 3 { + return s[:maxLength] + } + return s[:maxLength-3] + "..." +} + +// DeviceProcessor defines an interface for processing devices +type DeviceProcessor interface { + ProcessDevice(device intune.ManagedDevice) error +} + +// StreamingDeviceProcessor provides a callback-based streaming processor +type StreamingDeviceProcessor struct { + ProcessFunc func(device intune.ManagedDevice) error + maxDevices int + processed int +} + +// NewStreamingDeviceProcessor creates a new streaming processor +func NewStreamingDeviceProcessor(processFunc func(intune.ManagedDevice) error, maxDevices int) *StreamingDeviceProcessor { + return &StreamingDeviceProcessor{ + ProcessFunc: processFunc, + maxDevices: maxDevices, + processed: 0, + } +} + +// ProcessDevice processes a single device +func (p *StreamingDeviceProcessor) ProcessDevice(device intune.ManagedDevice) error { + if p.maxDevices > 0 && p.processed >= p.maxDevices { + return fmt.Errorf("maximum device limit reached: %d", p.maxDevices) + } + + p.processed++ + return p.ProcessFunc(device) +} + +// GetProcessedCount returns the number of devices processed +func (p *StreamingDeviceProcessor) GetProcessedCount() int { + return p.processed +} + +// processIntuneDevicesWithCallback processes devices using a callback function (alternative streaming approach) +func processIntuneDevicesWithCallback(ctx context.Context, azClient client.AzureClient, processor DeviceProcessor) error { + devices := azClient.ListIntuneManagedDevices(ctx, query.GraphParams{}) + + for result := range devices { + if result.Error != nil { + log.Error(result.Error, "error retrieving device") + continue + } + + if err := processor.ProcessDevice(result.Ok); err != nil { + return err + } + + // Check for context cancellation + select { + case <-ctx.Done(): + return ctx.Err() + default: + // Continue processing + } + } + + return nil +} + +// Legacy function maintained for backward compatibility but now uses streaming +func listIntuneDevices(ctx context.Context, azClient client.AzureClient) ([]intune.ManagedDevice, error) { + log.V(1).Info("using legacy listIntuneDevices function - consider using streaming approach for better performance") + + var devices []intune.ManagedDevice + deviceStream := azClient.ListIntuneManagedDevices(ctx, query.GraphParams{}) + + for result := range deviceStream { + if result.Error != nil { + return nil, result.Error + } + devices = append(devices, result.Ok) + + // Add a safety check to prevent excessive memory usage + if len(devices) > 10000 { + log.V(1).Info("large dataset detected - consider using streaming mode", "deviceCount", len(devices)) + } + + // Check for context cancellation + select { + case <-ctx.Done(): + return devices, ctx.Err() + default: + // Continue processing + } + } + + return devices, nil +} + +// Example usage functions for the streaming processor + +// ExampleJSONProcessor demonstrates processing devices to JSON +func ExampleJSONProcessor() func(intune.ManagedDevice) error { + return func(device intune.ManagedDevice) error { + data, err := json.MarshalIndent(device, "", " ") + if err != nil { + return err + } + fmt.Println(string(data)) + return nil + } +} + +// ExampleSummaryProcessor demonstrates processing devices to summary format +func ExampleSummaryProcessor() func(intune.ManagedDevice) error { + count := 0 + return func(device intune.ManagedDevice) error { + count++ + fmt.Printf("%d. %s (%s) - %s\n", + count, + device.DeviceName, + device.OperatingSystem, + device.ComplianceState) + return nil + } +} diff --git a/cmd/list-intune-registry-analysis.go b/cmd/list-intune-registry-analysis.go new file mode 100644 index 00000000..7374f780 --- /dev/null +++ b/cmd/list-intune-registry-analysis.go @@ -0,0 +1,687 @@ +// cmd/list-intune-registry-analysis.go +package cmd + +import ( + "context" + "fmt" + "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" +) + +var ( + fullAnalysis bool // New flag for choosing analysis mode + skipValidation bool // New flag to skip script validation +) + +func init() { + listRootCmd.AddCommand(listIntuneRegistryAnalysisCmd) + + // Add command-line flags for analysis options + listIntuneRegistryAnalysisCmd.Flags().BoolVar(&fullAnalysis, "full", false, "Perform full registry analysis with script execution (requires deployed script)") + listIntuneRegistryAnalysisCmd.Flags().BoolVar(&skipValidation, "skip-validation", false, "Skip script deployment validation") +} + +var listIntuneRegistryAnalysisCmd = &cobra.Command{ + Use: "intune-registry-analysis", + Short: "Performs security analysis on Intune devices and formats for BloodHound", + Long: `Performs security analysis on collected registry data and formats for BloodHound. + +Analysis Modes: + Basic Mode (default): Analyzes devices based on Intune compliance data only + Full Mode (--full): Executes PowerShell scripts to collect and analyze registry data + +Examples: + # Basic analysis (compliance-based) + azurehound list intune-registry-analysis --jwt $JWT + + # Full analysis with script execution + azurehound list intune-registry-analysis --full --jwt $JWT + + # Skip script validation (useful for testing) + azurehound list intune-registry-analysis --full --skip-validation --jwt $JWT`, + Run: listIntuneRegistryAnalysisCmdImpl, + SilenceUsage: true, +} + +func listIntuneRegistryAnalysisCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := context.WithCancel(cmd.Context()) + defer stop() + + azClient := connectAndCreateClient() + + var analysisResults []azure.DeviceSecurityAnalysis + var err error + + if fullAnalysis { + fmt.Printf("šŸ” Starting FULL registry security analysis with script execution...\n") + + // Validate script deployment unless skipped + if !skipValidation { + fmt.Printf("šŸ”§ Validating script deployment...\n") + if err := azClient.ValidateScriptDeployment(ctx); err != nil { + fmt.Printf("āŒ Script validation failed: %v\n", err) + fmt.Printf("šŸ’” Use --skip-validation to bypass this check, or deploy the required PowerShell script first.\n") + exit(err) + } + fmt.Printf("āœ… Script validation successful\n") + } else { + fmt.Printf("āš ļø Skipping script validation as requested\n") + } + + analysisResults, err = performRealRegistrySecurityAnalysis(ctx, azClient) + if err != nil { + fmt.Printf("āŒ Full analysis failed, falling back to basic analysis: %v\n", err) + analysisResults, err = performDeviceAnalysisWithoutScripts(ctx, azClient) + } + } else { + fmt.Printf("šŸ“Š Starting BASIC device analysis (compliance-based)...\n") + analysisResults, err = performDeviceAnalysisWithoutScripts(ctx, azClient) + } + + if err != nil { + exit(err) + } + + displayAnalysisResults(analysisResults) +} + +// displayAnalysisResults shows the analysis results with emojis and formatting +func displayAnalysisResults(results []azure.DeviceSecurityAnalysis) { + fmt.Printf("\n=== šŸ›”ļø INTUNE DEVICE SECURITY ANALYSIS RESULTS ===\n\n") + + if len(results) == 0 { + fmt.Printf("āŒ No devices were analyzed\n") + return + } + + // Calculate summary statistics + summary := calculateSummaryStats(results) + displaySummary(summary, len(results)) + + // Display detailed results for each device + fmt.Printf("šŸ“‹ DEVICE DETAILS:\n") + fmt.Printf("═══════════════════════════════════════════════════════════════════════\n\n") + + for i, result := range results { + displayDeviceResult(i+1, result) + } + + // Display recommendations + displayRecommendations(results) +} + +func displaySummary(summary map[string]interface{}, totalDevices int) { + fmt.Printf("šŸ“ˆ ANALYSIS SUMMARY\n") + fmt.Printf("─────────────────────────────────────────────────────────────────────\n") + + // Compliance summary + if complianceSummary, ok := summary["compliance_summary"].(map[string]interface{}); ok { + fmt.Printf("āœ… Compliance Overview:\n") + fmt.Printf(" • Total Devices: %d\n", totalDevices) + fmt.Printf(" • Compliant: %v\n", complianceSummary["compliant"]) + fmt.Printf(" • Partially Compliant: %v\n", complianceSummary["partially_compliant"]) + fmt.Printf(" • Non-Compliant: %v\n", complianceSummary["non_compliant"]) + fmt.Printf(" • Compliance Rate: %v\n", complianceSummary["compliance_rate"]) + fmt.Printf("\n") + } + + // Risk summary + if riskSummary, ok := summary["risk_summary"].(map[string]interface{}); ok { + fmt.Printf("āš ļø Risk Assessment:\n") + fmt.Printf(" • Average Risk Score: %v/100\n", riskSummary["average_risk_score"]) + fmt.Printf(" • Total Security Findings: %v\n", riskSummary["total_findings"]) + + if findingsBySeverity, ok := riskSummary["findings_by_severity"].(map[string]int); ok { + fmt.Printf(" • Critical: %d | High: %d | Medium: %d | Low: %d\n", + findingsBySeverity["CRITICAL"], + findingsBySeverity["HIGH"], + findingsBySeverity["MEDIUM"], + findingsBySeverity["LOW"]) + } + fmt.Printf("\n") + } + + // Device breakdown + if deviceBreakdown, ok := summary["device_breakdown"].(map[string]interface{}); ok { + fmt.Printf("šŸŽÆ Risk Distribution:\n") + fmt.Printf(" • High Risk (70-100): %v devices\n", deviceBreakdown["high_risk_devices"]) + fmt.Printf(" • Medium Risk (30-69): %v devices\n", deviceBreakdown["medium_risk_devices"]) + fmt.Printf(" • Low Risk (0-29): %v devices\n", deviceBreakdown["low_risk_devices"]) + fmt.Printf("\n") + } +} + +func displayDeviceResult(index int, result azure.DeviceSecurityAnalysis) { + // Device header with risk level emoji + riskEmoji := getRiskEmoji(result.RiskScore) + statusEmoji := getComplianceEmoji(result.ComplianceStatus) + + fmt.Printf("%s %s Device #%d: %s\n", + riskEmoji, statusEmoji, index, result.Device.DeviceName) + + // Basic device info + fmt.Printf(" šŸ†” Device ID: %s\n", result.Device.ID) + fmt.Printf(" šŸ’» OS: %s %s\n", result.Device.OperatingSystem, result.Device.OSVersion) + fmt.Printf(" šŸ‘¤ User: %s\n", getDisplayValue(result.Device.UserPrincipalName)) + fmt.Printf(" šŸ“Š Risk Score: %d/100 (%s)\n", result.RiskScore, getRiskLevel(result.RiskScore)) + fmt.Printf(" āœ“ Compliance: %s\n", result.ComplianceStatus) + fmt.Printf(" ā° Last Analysis: %s\n", result.AnalysisTimestamp.Format("2006-01-02 15:04:05")) + fmt.Printf(" šŸ”„ Last Sync: %s\n", result.Device.LastSyncDateTime.Format("2006-01-02 15:04:05")) + + // Security findings + if len(result.SecurityFindings) > 0 { + fmt.Printf(" 🚨 Security Findings (%d):\n", len(result.SecurityFindings)) + + // Group findings by severity + findingsBySeverity := groupFindingsBySeverity(result.SecurityFindings) + + for _, severity := range []string{"CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"} { + if findings, exists := findingsBySeverity[severity]; exists && len(findings) > 0 { + for _, finding := range findings { + emoji := getSecurityEmoji(finding.Severity) + fmt.Printf(" %s %s\n", emoji, finding.Title) + fmt.Printf(" šŸ“ %s\n", finding.Description) + + // Show evidence for high/critical findings + if finding.Severity == "HIGH" || finding.Severity == "CRITICAL" { + if len(finding.Evidence) > 0 { + fmt.Printf(" šŸ” Evidence: %s\n", finding.Evidence[0]) + } + if len(finding.Recommendations) > 0 { + fmt.Printf(" šŸ’” Recommendation: %s\n", finding.Recommendations[0]) + } + } + } + } + } + } else { + fmt.Printf(" āœ… No security findings detected\n") + } + + // Escalation vectors + if len(result.EscalationVectors) > 0 { + fmt.Printf(" ā¬†ļø Privilege Escalation Vectors (%d):\n", len(result.EscalationVectors)) + for _, vector := range result.EscalationVectors { + fmt.Printf(" šŸŽÆ %s: %s → %s\n", vector.Type, vector.Source, vector.Target) + fmt.Printf(" Method: %s (Complexity: %s)\n", vector.Method, vector.Complexity) + } + } + + fmt.Printf("\n") +} + +func displayRecommendations(results []azure.DeviceSecurityAnalysis) { + criticalCount := 0 + highCount := 0 + nonCompliantCount := 0 + + for _, result := range results { + if result.ComplianceStatus == "NON_COMPLIANT" { + nonCompliantCount++ + } + + for _, finding := range result.SecurityFindings { + switch finding.Severity { + case "CRITICAL": + criticalCount++ + case "HIGH": + highCount++ + } + } + } + + if criticalCount > 0 || highCount > 0 || nonCompliantCount > 0 { + fmt.Printf("🚨 IMMEDIATE ACTIONS REQUIRED\n") + fmt.Printf("═══════════════════════════════════════════════════════════════════════\n") + + if criticalCount > 0 { + fmt.Printf("šŸ”„ CRITICAL: %d critical security issues need immediate attention\n", criticalCount) + } + + if highCount > 0 { + fmt.Printf("āš ļø HIGH: %d high-severity issues should be addressed soon\n", highCount) + } + + if nonCompliantCount > 0 { + fmt.Printf("šŸ“‹ COMPLIANCE: %d devices are non-compliant with policies\n", nonCompliantCount) + } + + fmt.Printf("\nšŸ’” Recommended Actions:\n") + fmt.Printf(" 1. Address all CRITICAL and HIGH severity findings immediately\n") + fmt.Printf(" 2. Review and remediate non-compliant devices\n") + fmt.Printf(" 3. Update device compliance policies if needed\n") + fmt.Printf(" 4. Schedule regular security assessments\n") + fmt.Printf(" 5. Consider additional endpoint protection measures\n\n") + } else { + fmt.Printf("āœ… GOOD NEWS!\n") + fmt.Printf("═══════════════════════════════════════════════════════════════════════\n") + fmt.Printf("No critical security issues were found in the analyzed devices.\n") + fmt.Printf("Continue regular monitoring to maintain security posture.\n\n") + } +} + +// Helper functions for display formatting + +func getRiskEmoji(riskScore int) string { + switch { + case riskScore >= 70: + return "šŸ”“" // High risk + case riskScore >= 30: + return "🟔" // Medium risk + default: + return "🟢" // Low risk + } +} + +func getComplianceEmoji(status string) string { + switch status { + case "COMPLIANT": + return "āœ…" + case "PARTIALLY_COMPLIANT": + return "āš ļø" + case "NON_COMPLIANT": + return "āŒ" + default: + return "ā“" + } +} + +func getSecurityEmoji(severity string) string { + switch severity { + case "CRITICAL": + return "šŸ”„" + case "HIGH": + return "🚨" + case "MEDIUM": + return "āš ļø" + case "LOW": + return "ā„¹ļø" + case "INFO": + return "šŸ“‹" + default: + return "ā“" + } +} + +func getRiskLevel(riskScore int) string { + switch { + case riskScore >= 70: + return "HIGH RISK" + case riskScore >= 30: + return "MEDIUM RISK" + default: + return "LOW RISK" + } +} + +func getDisplayValue(value string) string { + if value == "" { + return "Not specified" + } + return value +} + +func groupFindingsBySeverity(findings []azure.SecurityFinding) map[string][]azure.SecurityFinding { + grouped := make(map[string][]azure.SecurityFinding) + + for _, finding := range findings { + grouped[finding.Severity] = append(grouped[finding.Severity], finding) + } + + return grouped +} + +// calculateSummaryStats function (referenced in the display) +func calculateSummaryStats(results []azure.DeviceSecurityAnalysis) map[string]interface{} { + if len(results) == 0 { + return map[string]interface{}{} + } + + compliantCount := 0 + partiallyCompliantCount := 0 + nonCompliantCount := 0 + totalRiskScore := 0 + totalFindings := 0 + severityCounts := map[string]int{ + "CRITICAL": 0, + "HIGH": 0, + "MEDIUM": 0, + "LOW": 0, + "INFO": 0, + } + + for _, result := range results { + switch result.ComplianceStatus { + case "COMPLIANT": + compliantCount++ + case "PARTIALLY_COMPLIANT": + partiallyCompliantCount++ + case "NON_COMPLIANT": + nonCompliantCount++ + } + + totalRiskScore += result.RiskScore + totalFindings += len(result.SecurityFindings) + + for _, finding := range result.SecurityFindings { + severityCounts[finding.Severity]++ + } + } + + avgRiskScore := float64(totalRiskScore) / float64(len(results)) + complianceRate := float64(compliantCount) / float64(len(results)) * 100 + + return map[string]interface{}{ + "compliance_summary": map[string]interface{}{ + "compliant": compliantCount, + "partially_compliant": partiallyCompliantCount, + "non_compliant": nonCompliantCount, + "compliance_rate": fmt.Sprintf("%.1f%%", complianceRate), + }, + "risk_summary": map[string]interface{}{ + "average_risk_score": fmt.Sprintf("%.1f", avgRiskScore), + "total_findings": totalFindings, + "findings_by_severity": severityCounts, + }, + "device_breakdown": map[string]interface{}{ + "high_risk_devices": countDevicesByRiskLevel(results, 70, 100), + "medium_risk_devices": countDevicesByRiskLevel(results, 30, 69), + "low_risk_devices": countDevicesByRiskLevel(results, 0, 29), + }, + } +} + +func countDevicesByRiskLevel(results []azure.DeviceSecurityAnalysis, minRisk, maxRisk int) int { + count := 0 + for _, result := range results { + if result.RiskScore >= minRisk && result.RiskScore <= maxRisk { + count++ + } + } + return count +} + +func performDeviceAnalysisWithoutScripts(ctx context.Context, azClient client.AzureClient) ([]azure.DeviceSecurityAnalysis, error) { + fmt.Printf("šŸ“Š Starting device analysis without script execution...\n") + + var results []azure.DeviceSecurityAnalysis + + // Just analyze devices based on Intune compliance data + devices := azClient.ListIntuneDevices(ctx, query.GraphParams{}) + + for deviceResult := range devices { + if deviceResult.Error != nil { + fmt.Printf("āŒ Error getting device: %v\n", deviceResult.Error) + continue + } + + device := deviceResult.Ok + + // Skip non-Windows devices + if !strings.Contains(strings.ToLower(device.OperatingSystem), "windows") { + continue + } + + // Create analysis based on device compliance state + analysis := analyzeDeviceComplianceOnly(device) + results = append(results, analysis) + } + + fmt.Printf("āœ… Analyzed %d devices based on compliance data\n", len(results)) + return results, nil +} + +func analyzeDeviceComplianceOnly(device azure.IntuneDevice) azure.DeviceSecurityAnalysis { + analysis := azure.DeviceSecurityAnalysis{ + Device: device, + AnalysisTimestamp: time.Now(), + SecurityFindings: []azure.SecurityFinding{}, + EscalationVectors: []azure.EscalationVector{}, + RiskScore: 0, + ComplianceStatus: "COMPLIANT", + } + + // Analyze based on device properties + if device.ComplianceState != "compliant" { + finding := azure.SecurityFinding{ + ID: "DEVICE_NON_COMPLIANT", + Title: "Device Non-Compliant", + Severity: "MEDIUM", + Category: "Compliance", + Description: "Device does not meet compliance requirements", + Evidence: []string{fmt.Sprintf("State: %s", device.ComplianceState)}, + Recommendations: []string{"Review compliance policies"}, + MITREAttack: []string{"T1562"}, + } + analysis.SecurityFindings = append(analysis.SecurityFindings, finding) + analysis.RiskScore = 30 + analysis.ComplianceStatus = "NON_COMPLIANT" + } + + // Check for old sync dates + if time.Since(device.LastSyncDateTime) > 7*24*time.Hour { + finding := azure.SecurityFinding{ + ID: "DEVICE_STALE_SYNC", + Title: "Device Not Recently Synced", + Severity: "LOW", + Category: "Management", + Description: "Device hasn't synced with Intune recently", + Evidence: []string{fmt.Sprintf("Last sync: %s", device.LastSyncDateTime.Format("2006-01-02"))}, + Recommendations: []string{"Check device connectivity", "Force sync"}, + MITREAttack: []string{}, + } + analysis.SecurityFindings = append(analysis.SecurityFindings, finding) + analysis.RiskScore += 10 + } + + return analysis +} + +// performRealRegistrySecurityAnalysis performs actual registry data collection and analysis +func performRealRegistrySecurityAnalysis(ctx context.Context, azClient client.AzureClient) ([]azure.DeviceSecurityAnalysis, error) { + var ( + out = make([]azure.DeviceSecurityAnalysis, 0) + successCount = 0 + errorCount = 0 + ) + + fmt.Printf("šŸ” Starting real registry security analysis...\n") + + // Use the real registry collection function from your client + deviceRegistryData := azClient.CollectRegistryDataFromAllDevices(ctx) + + for registryResult := range deviceRegistryData { + if registryResult.Error != nil { + fmt.Printf("āŒ Error collecting registry data: %v\n", registryResult.Error) + errorCount++ + continue + } + + // Perform real security analysis on the collected registry data + analysis := performBasicDeviceSecurityAnalysis(registryResult.Ok) + + // Enhance the analysis with additional checks + enhanceSecurityAnalysis(&analysis, registryResult.Ok) + + out = append(out, analysis) + successCount++ + + fmt.Printf("āœ… Analyzed device %s: %d findings, risk score %d\n", + analysis.Device.DeviceName, + len(analysis.SecurityFindings), + analysis.RiskScore) + } + + fmt.Printf("šŸ“ˆ Registry analysis completed: %d successful, %d errors\n", successCount, errorCount) + + if successCount == 0 && errorCount > 0 { + return nil, fmt.Errorf("failed to analyze any devices successfully (%d errors)", errorCount) + } + + return out, nil +} + +// performBasicDeviceSecurityAnalysis - your existing real analysis function +func performBasicDeviceSecurityAnalysis(deviceData azure.DeviceRegistryData) azure.DeviceSecurityAnalysis { + analysis := azure.DeviceSecurityAnalysis{ + Device: deviceData.Device, + AnalysisTimestamp: deviceData.CollectedAt, + SecurityFindings: []azure.SecurityFinding{}, + EscalationVectors: []azure.EscalationVector{}, + RiskScore: 0, + ComplianceStatus: "COMPLIANT", + } + + // UAC Disabled Check + if deviceData.RegistryData.SecurityIndicators.UACDisabled { + finding := azure.SecurityFinding{ + ID: "UAC_DISABLED", + Title: "User Account Control Disabled", + Severity: "HIGH", + Category: "Privilege Escalation", + Description: "UAC is disabled, allowing privilege escalation attacks", + Evidence: []string{"UAC is disabled in registry"}, + Recommendations: []string{"Enable UAC through Group Policy or registry"}, + MITREAttack: []string{"T1548.002"}, + } + analysis.SecurityFindings = append(analysis.SecurityFindings, finding) + analysis.RiskScore += 25 + + // Add escalation vector for UAC bypass + vector := azure.EscalationVector{ + VectorID: "UAC_BYPASS_001", + Type: "Privilege Escalation", + Source: "Standard User", + Target: "Administrator", + Method: "UAC Disabled", + RequiredPrivs: []string{"User"}, + Complexity: "Low", + Impact: "High", + Conditions: []string{"UAC disabled"}, + } + analysis.EscalationVectors = append(analysis.EscalationVectors, vector) + } + + // Auto Admin Logon Check + if deviceData.RegistryData.SecurityIndicators.AutoAdminLogon { + finding := azure.SecurityFinding{ + ID: "AUTO_ADMIN_LOGON", + Title: "Automatic Administrator Logon Enabled", + Severity: "CRITICAL", + Category: "Credential Exposure", + Description: "Automatic administrator logon exposes admin credentials", + Evidence: []string{"AutoAdminLogon is enabled in registry"}, + Recommendations: []string{"Disable automatic administrator logon", "Use secure credential storage"}, + MITREAttack: []string{"T1552.002"}, + } + analysis.SecurityFindings = append(analysis.SecurityFindings, finding) + analysis.RiskScore += 40 + + // Add escalation vector for credential access + vector := azure.EscalationVector{ + VectorID: "CRED_ACCESS_001", + Type: "Credential Access", + Source: "Local Access", + Target: "Administrator Credentials", + Method: "Registry Credential Storage", + RequiredPrivs: []string{"Local Access"}, + Complexity: "Low", + Impact: "Critical", + Conditions: []string{"AutoAdminLogon enabled"}, + } + analysis.EscalationVectors = append(analysis.EscalationVectors, vector) + } + + // Weak Service Permissions Check + if deviceData.RegistryData.SecurityIndicators.WeakServicePermissions { + finding := azure.SecurityFinding{ + ID: "WEAK_SERVICE_PERMS", + Title: "Weak Service Permissions Detected", + Severity: "MEDIUM", + Category: "Privilege Escalation", + Description: "Services with weak permissions can be exploited for privilege escalation", + Evidence: []string{"Weak service permissions found in registry"}, + Recommendations: []string{"Review and restrict service permissions", "Apply principle of least privilege"}, + MITREAttack: []string{"T1543.003"}, + } + analysis.SecurityFindings = append(analysis.SecurityFindings, finding) + analysis.RiskScore += 15 + } + + // Suspicious Startup Items Check + if len(deviceData.RegistryData.SecurityIndicators.SuspiciousStartupItems) > 0 { + evidence := make([]string, 0, len(deviceData.RegistryData.SecurityIndicators.SuspiciousStartupItems)) + for _, item := range deviceData.RegistryData.SecurityIndicators.SuspiciousStartupItems { + evidence = append(evidence, fmt.Sprintf("%s: %s", item.Name, item.Value)) + } + + finding := azure.SecurityFinding{ + ID: "SUSPICIOUS_STARTUP", + Title: "Suspicious Startup Items Detected", + Severity: "MEDIUM", + Category: "Persistence", + Description: fmt.Sprintf("Found %d suspicious startup items", len(deviceData.RegistryData.SecurityIndicators.SuspiciousStartupItems)), + Evidence: evidence, + Recommendations: []string{"Review startup items", "Remove unauthorized persistence mechanisms"}, + MITREAttack: []string{"T1547.001"}, + } + analysis.SecurityFindings = append(analysis.SecurityFindings, finding) + analysis.RiskScore += 10 + } + + // Set compliance status based on risk score + if analysis.RiskScore >= 50 { + analysis.ComplianceStatus = "NON_COMPLIANT" + } else if analysis.RiskScore >= 25 { + analysis.ComplianceStatus = "PARTIALLY_COMPLIANT" + } + + return analysis +} + +// enhanceSecurityAnalysis adds additional security checks and analysis +func enhanceSecurityAnalysis(analysis *azure.DeviceSecurityAnalysis, deviceData azure.DeviceRegistryData) { + // Check device compliance state from Intune + if deviceData.Device.ComplianceState != "compliant" { + finding := azure.SecurityFinding{ + ID: "DEVICE_NON_COMPLIANT", + Title: "Device Non-Compliant with Intune Policies", + Severity: "MEDIUM", + Category: "Compliance", + Description: "Device does not meet Intune compliance requirements", + Evidence: []string{fmt.Sprintf("Compliance state: %s", deviceData.Device.ComplianceState)}, + Recommendations: []string{"Review device compliance policies", "Update device configuration"}, + MITREAttack: []string{"T1562"}, + } + analysis.SecurityFindings = append(analysis.SecurityFindings, finding) + analysis.RiskScore += 15 + } + + // Check for old OS versions (basic heuristic) + if deviceData.Device.OSVersion != "" && len(deviceData.Device.OSVersion) > 0 { + // Add informational finding about OS version + finding := azure.SecurityFinding{ + ID: "OS_VERSION_INFO", + Title: "Operating System Information", + Severity: "INFO", + Category: "Information", + Description: "Device OS version recorded for security posture assessment", + Evidence: []string{fmt.Sprintf("OS: %s, Version: %s", deviceData.Device.OperatingSystem, deviceData.Device.OSVersion)}, + Recommendations: []string{"Ensure OS is up to date with latest security patches"}, + MITREAttack: []string{}, + } + analysis.SecurityFindings = append(analysis.SecurityFindings, finding) + } + + // Update compliance status if it was degraded + if analysis.RiskScore >= 50 { + analysis.ComplianceStatus = "NON_COMPLIANT" + } else if analysis.RiskScore >= 25 { + analysis.ComplianceStatus = "PARTIALLY_COMPLIANT" + } +} diff --git a/enums/intune.go b/enums/intune.go new file mode 100644 index 00000000..27449cd0 --- /dev/null +++ b/enums/intune.go @@ -0,0 +1,52 @@ +package enums + +// Intune-specific Kind enumerations for data types +const ( + KindAZIntuneDevice Kind = "AZIntuneDevice" + KindAZIntuneDeviceCompliance Kind = "AZIntuneDeviceCompliance" + KindAZIntuneDeviceConfiguration Kind = "AZIntuneDeviceConfiguration" + KindAZIntuneCompliance Kind = "AZIntuneCompliance" +) + +// Device compliance states +type ComplianceState string + +const ( + ComplianceStateCompliant ComplianceState = "compliant" + ComplianceStateNoncompliant ComplianceState = "noncompliant" + ComplianceStateConflict ComplianceState = "conflict" + ComplianceStateError ComplianceState = "error" + ComplianceStateUnknown ComplianceState = "unknown" + ComplianceStateInGracePeriod ComplianceState = "inGracePeriod" +) + +// Device enrollment types +type EnrollmentType string + +const ( + EnrollmentTypeUserEnrollment EnrollmentType = "userEnrollment" + EnrollmentTypeDeviceEnrollmentManager EnrollmentType = "deviceEnrollmentManager" + EnrollmentTypeWindowsAzureADJoin EnrollmentType = "windowsAzureADJoin" + EnrollmentTypeWindowsAutoEnrollment EnrollmentType = "windowsAutoEnrollment" + EnrollmentTypeWindowsCoManagement EnrollmentType = "windowsCoManagement" +) + +// Management agent types +type ManagementAgent string + +const ( + ManagementAgentMDM ManagementAgent = "mdm" + ManagementAgentIntuneClient ManagementAgent = "intuneClient" + ManagementAgentConfigurationManagerClient ManagementAgent = "configurationManagerClient" + ManagementAgentUnknown ManagementAgent = "unknown" +) + +// Operating system types +type OperatingSystem string + +const ( + OperatingSystemWindows OperatingSystem = "windows" + OperatingSystemAndroid OperatingSystem = "android" + OperatingSystemIOS OperatingSystem = "iOS" + OperatingSystemMacOS OperatingSystem = "macOS" +) \ No newline at end of file diff --git a/models/azure/intune.go b/models/azure/intune.go new file mode 100644 index 00000000..28efe033 --- /dev/null +++ b/models/azure/intune.go @@ -0,0 +1,271 @@ +// models/azure/intune.go +package azure + +import ( + "time" +) + +// ChromeOSInfo represents Chrome OS device information with proper type safety +type ChromeOSInfo struct { + DeviceId string `json:"deviceId"` + OSVersion string `json:"osVersion"` + SupportEndDate time.Time `json:"supportEndDate"` + LastKnownNetwork string `json:"lastKnownNetwork"` + MACAddress string `json:"macAddress"` + SerialNumber string `json:"serialNumber"` + WillAutoRenew bool `json:"willAutoRenew"` + AnnotatedAssetId string `json:"annotatedAssetId"` + AnnotatedLocation string `json:"annotatedLocation"` + AnnotatedUser string `json:"annotatedUser"` + LastEnrollmentTime time.Time `json:"lastEnrollmentTime"` + OrgUnitPath string `json:"orgUnitPath"` + RecentUsers []string `json:"recentUsers"` + EthernetMacAddress string `json:"ethernetMacAddress"` + Model string `json:"model"` + OSBuildNumber string `json:"osBuildNumber"` + PlatformVersion string `json:"platformVersion"` + FirmwareVersion string `json:"firmwareVersion"` + LastPolicySync time.Time `json:"lastPolicySync"` + LastStatusReportTime time.Time `json:"lastStatusReportTime"` +} + +// IntuneDevice represents a device managed by Microsoft Intune +type IntuneDevice struct { + ID string `json:"id"` + DeviceName string `json:"deviceName"` + OperatingSystem string `json:"operatingSystem"` + OSVersion string `json:"osVersion"` + ComplianceState string `json:"complianceState"` + LastSyncDateTime time.Time `json:"lastSyncDateTime"` + EnrollmentType string `json:"enrollmentType"` + ManagementAgent string `json:"managementAgent"` + AzureADDeviceID string `json:"azureADDeviceId"` + UserPrincipalName string `json:"userPrincipalName"` + SerialNumber string `json:"serialNumber"` + Manufacturer string `json:"manufacturer"` + Model string `json:"model"` + TotalStorageSpaceInBytes int64 `json:"totalStorageSpaceInBytes"` + FreeStorageSpaceInBytes int64 `json:"freeStorageSpaceInBytes"` + ManagedDeviceName string `json:"managedDeviceName"` + PartnerReportedThreatState string `json:"partnerReportedThreatState"` + RequireUserEnrollmentApproval bool `json:"requireUserEnrollmentApproval"` + ManagementCertificateExpirationDate time.Time `json:"managementCertificateExpirationDate"` + ICCID string `json:"iccid"` + UDID string `json:"udid"` + Notes string `json:"notes"` + EthernetMacAddress string `json:"ethernetMacAddress"` + WiFiMacAddress string `json:"wiFiMacAddress"` + PhysicalMemoryInBytes int64 `json:"physicalMemoryInBytes"` + ProcessorArchitecture string `json:"processorArchitecture"` + SpecificationVersion string `json:"specificationVersion"` + JoinType string `json:"joinType"` + SkuFamily string `json:"skuFamily"` + SkuNumber int `json:"skuNumber"` + ManagementFeatures string `json:"managementFeatures"` + // Fixed: Use proper struct type instead of []interface{} + ChromeOSDeviceInfo []ChromeOSInfo `json:"chromeOSDeviceInfo"` + EnrolledDateTime time.Time `json:"enrolledDateTime"` + EmailAddress string `json:"emailAddress"` + UserID string `json:"userId"` + UserDisplayName string `json:"userDisplayName"` + DeviceRegistrationState string `json:"deviceRegistrationState"` + DeviceCategoryDisplayName string `json:"deviceCategoryDisplayName"` + IsSupervised bool `json:"isSupervised"` + ExchangeLastSuccessfulSyncDateTime time.Time `json:"exchangeLastSuccessfulSyncDateTime"` + ExchangeAccessState string `json:"exchangeAccessState"` + ExchangeAccessStateReason string `json:"exchangeAccessStateReason"` + RemoteAssistanceSessionURL string `json:"remoteAssistanceSessionUrl"` + RemoteAssistanceSessionErrorDetails string `json:"remoteAssistanceSessionErrorDetails"` + IsEncrypted bool `json:"isEncrypted"` + ComplianceGracePeriodExpirationDateTime time.Time `json:"complianceGracePeriodExpirationDateTime"` + ManagementAgents []string `json:"managementAgents"` + LostModeState string `json:"lostModeState"` + ActivationLockBypassCode string `json:"activationLockBypassCode"` +} + +// ScriptExecution represents the execution of a PowerShell script on an Intune device +type ScriptExecution struct { + ID string `json:"id"` + DeviceID string `json:"deviceId"` + Status string `json:"status"` + StartDateTime time.Time `json:"startDateTime"` + EndDateTime *time.Time `json:"endDateTime"` + ScriptName string `json:"scriptName"` + RunAsAccount string `json:"runAsAccount"` +} + +// ScriptExecutionResult represents the result of a PowerShell script execution +type ScriptExecutionResult struct { + ID string `json:"id"` + DeviceID string `json:"deviceId"` + DeviceName string `json:"deviceName"` + RunState string `json:"runState"` + ResultMessage string `json:"resultMessage"` + PreRemediationDetectionScriptOutput string `json:"preRemediationDetectionScriptOutput"` + RemediationScriptOutput string `json:"remediationScriptOutput"` + PostRemediationDetectionScriptOutput string `json:"postRemediationDetectionScriptOutput"` + ErrorCode int `json:"errorCode"` + LastStateUpdateDateTime time.Time `json:"lastStateUpdateDateTime"` +} + +// DeviceCompliancePolicy represents a device compliance policy +type DeviceCompliancePolicy struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + Description string `json:"description"` + Platform string `json:"platform"` + CreatedDateTime time.Time `json:"createdDateTime"` + LastModifiedDateTime time.Time `json:"lastModifiedDateTime"` +} + +// DeviceConfiguration represents a device configuration profile +type DeviceConfiguration struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + Description string `json:"description"` + Platform string `json:"platform"` + CreatedDateTime time.Time `json:"createdDateTime"` + LastModifiedDateTime time.Time `json:"lastModifiedDateTime"` + Settings map[string]interface{} `json:"settings"` +} + +// DeviceRegistryData combines device information with collected registry data +type DeviceRegistryData struct { + Device IntuneDevice `json:"device"` + RegistryData RegistryData `json:"registryData"` + CollectedAt time.Time `json:"collectedAt"` + + // BloodHound specific fields for integration + BloodHoundData BloodHoundDeviceData `json:"bloodhoundData"` +} + +// RegistryData represents the complete registry data collected from a device +type RegistryData struct { + DeviceInfo DeviceInfo `json:"deviceInfo"` + RegistryData []RegistryEntry `json:"registryData"` + SecurityIndicators SecurityIndicators `json:"securityIndicators"` + Summary RegistryDataSummary `json:"summary"` +} + +// DeviceInfo contains basic information about the device where data was collected +type DeviceInfo struct { + ComputerName string `json:"computerName"` + Domain string `json:"domain"` + User string `json:"user"` + Timestamp string `json:"timestamp"` + ScriptVersion string `json:"scriptVersion"` +} + +// RegistryEntry represents a single registry path and its collected values +type RegistryEntry struct { + Path string `json:"path"` + Purpose string `json:"purpose"` + Values map[string]interface{} `json:"values"` + Accessible bool `json:"accessible"` + Error *string `json:"error"` +} + +// SecurityIndicators contains analysis results of security-relevant registry settings +type SecurityIndicators struct { + UACDisabled bool `json:"uacDisabled"` + AutoAdminLogon bool `json:"autoAdminLogon"` + WeakServicePermissions bool `json:"weakServicePermissions"` + SuspiciousStartupItems []SuspiciousItem `json:"suspiciousStartupItems"` +} + +// SuspiciousItem represents a potentially malicious startup item +type SuspiciousItem struct { + Path string `json:"path"` + Name string `json:"name"` + Value string `json:"value"` +} + +// RegistryDataSummary provides high-level statistics about the collected data +type RegistryDataSummary struct { + TotalKeysChecked int `json:"totalKeysChecked"` + AccessibleKeys int `json:"accessibleKeys"` + HighRiskIndicators []string `json:"highRiskIndicators"` +} + +// BloodHoundDeviceData contains processed data formatted for BloodHound consumption +type BloodHoundDeviceData struct { + ObjectIdentifier string `json:"ObjectIdentifier"` + AzureDeviceID string `json:"AzureDeviceID"` + DisplayName string `json:"DisplayName"` + LocalGroups map[string][]string `json:"LocalGroups"` + UserRights map[string][]string `json:"UserRights"` + Sessions []SessionInfo `json:"Sessions"` + RegistryFindings []RegistryFinding `json:"RegistryFindings"` + SecurityFindings []SecurityFinding `json:"SecurityFindings"` + PrivilegeEscalation []EscalationVector `json:"PrivilegeEscalation"` +} + +// SessionInfo represents active user sessions on the device +type SessionInfo struct { + UserName string `json:"UserName"` + SessionType string `json:"SessionType"` + SessionID int `json:"SessionID"` + State string `json:"State"` + IdleTime string `json:"IdleTime"` + LogonTime time.Time `json:"LogonTime"` +} + +// RegistryFinding represents a specific registry-based security finding +type RegistryFinding struct { + Category string `json:"Category"` + Finding string `json:"Finding"` + Severity string `json:"Severity"` + RegistryPath string `json:"RegistryPath"` + ValueName string `json:"ValueName"` + CurrentValue interface{} `json:"CurrentValue"` + ExpectedValue interface{} `json:"ExpectedValue"` + Description string `json:"Description"` + Remediation string `json:"Remediation"` + AttackVector string `json:"AttackVector"` +} + +// SecurityFinding represents high-level security issues identified +type SecurityFinding struct { + ID string `json:"ID"` + Title string `json:"Title"` + Severity string `json:"Severity"` + Category string `json:"Category"` + Description string `json:"Description"` + Evidence []string `json:"Evidence"` + Recommendations []string `json:"Recommendations"` + MITREAttack []string `json:"MITREAttack"` +} + +// EscalationVector represents a potential privilege escalation path +type EscalationVector struct { + VectorID string `json:"VectorID"` + Type string `json:"Type"` + Source string `json:"Source"` + Target string `json:"Target"` + Method string `json:"Method"` + RequiredPrivs []string `json:"RequiredPrivs"` + Complexity string `json:"Complexity"` + Impact string `json:"Impact"` + Conditions []string `json:"Conditions"` +} + +// IntuneAppRegistration represents the Azure app registration for Intune access +type IntuneAppRegistration struct { + ClientID string `json:"clientId"` + TenantID string `json:"tenantId"` + ClientSecret string `json:"clientSecret"` + Permissions []string `json:"permissions"` +} + +// IntuneManagementScript represents a PowerShell script configured in Intune +type IntuneManagementScript struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + Description string `json:"description"` + ScriptContent string `json:"scriptContent"` + CreatedDateTime time.Time `json:"createdDateTime"` + LastModifiedDateTime time.Time `json:"lastModifiedDateTime"` + RunAsAccount string `json:"runAsAccount"` + FileName string `json:"fileName"` + RoleScopeTagIds []string `json:"roleScopeTagIds"` +} diff --git a/models/azure/intune_security.go b/models/azure/intune_security.go new file mode 100644 index 00000000..485128ac --- /dev/null +++ b/models/azure/intune_security.go @@ -0,0 +1,406 @@ +// models/azure/intune_security.go +package azure + +import ( + "time" +) + +// DeviceSecurityAnalysis represents the complete security analysis for a device +type DeviceSecurityAnalysis struct { + Device IntuneDevice `json:"device"` + AnalysisTimestamp time.Time `json:"analysisTimestamp"` + SecurityFindings []SecurityFinding `json:"securityFindings"` + EscalationVectors []EscalationVector `json:"escalationVectors"` + BloodHoundData BloodHoundDeviceData `json:"bloodhoundData"` + RiskScore int `json:"riskScore"` + ComplianceStatus string `json:"complianceStatus"` + LastUpdated time.Time `json:"lastUpdated"` +} + +// IntuneSecurityConfiguration represents security configuration collected from Intune +type IntuneSecurityConfiguration struct { + DeviceID string `json:"deviceId"` + DeviceName string `json:"deviceName"` + CompliancePolicies []DeviceCompliancePolicy `json:"compliancePolicies"` + ConfigurationProfiles []DeviceConfiguration `json:"configurationProfiles"` + SecurityBaselines []SecurityBaseline `json:"securityBaselines"` + BitLockerStatus BitLockerStatus `json:"bitLockerStatus"` + WindowsDefenderStatus WindowsDefenderStatus `json:"windowsDefenderStatus"` + FirewallStatus FirewallStatus `json:"firewallStatus"` + AppProtectionPolicies []AppProtectionPolicy `json:"appProtectionPolicies"` + ConditionalAccessPolicies []ConditionalAccessPolicy `json:"conditionalAccessPolicies"` + CollectedAt time.Time `json:"collectedAt"` +} + +// SecurityBaseline represents a security baseline configuration +type SecurityBaseline struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + Description string `json:"description"` + SecurityBaselineType string `json:"securityBaselineType"` + CreatedDateTime time.Time `json:"createdDateTime"` + LastModifiedDateTime time.Time `json:"lastModifiedDateTime"` + Settings []SecuritySetting `json:"settings"` +} + +// SecuritySetting represents an individual security setting +type SecuritySetting struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + SettingType string `json:"settingType"` + CurrentValue interface{} `json:"currentValue"` + RecommendedValue interface{} `json:"recommendedValue"` + ComplianceState string `json:"complianceState"` + Severity string `json:"severity"` +} + +// BitLockerStatus represents BitLocker encryption status +type BitLockerStatus struct { + EncryptionMethod string `json:"encryptionMethod"` + EncryptionStatus string `json:"encryptionStatus"` + ProtectionStatus string `json:"protectionStatus"` + KeyProtectors []string `json:"keyProtectors"` + RecoveryKeyBackupStatus string `json:"recoveryKeyBackupStatus"` + LastStatusUpdate time.Time `json:"lastStatusUpdate"` +} + +// WindowsDefenderStatus represents Windows Defender status +type WindowsDefenderStatus struct { + AntivirusEnabled bool `json:"antivirusEnabled"` + AntivirusSignatureVersion string `json:"antivirusSignatureVersion"` + AntivirusSignatureLastUpdate time.Time `json:"antivirusSignatureLastUpdate"` + RealTimeProtectionEnabled bool `json:"realTimeProtectionEnabled"` + BehaviorMonitorEnabled bool `json:"behaviorMonitorEnabled"` + FirewallEnabled bool `json:"firewallEnabled"` + SmartScreenEnabled bool `json:"smartScreenEnabled"` + CloudProtectionEnabled bool `json:"cloudProtectionEnabled"` + TamperProtectionEnabled bool `json:"tamperProtectionEnabled"` +} + +// FirewallStatus represents Windows Firewall status +type FirewallStatus struct { + DomainProfile FirewallProfile `json:"domainProfile"` + PrivateProfile FirewallProfile `json:"privateProfile"` + PublicProfile FirewallProfile `json:"publicProfile"` + LastStatusUpdate time.Time `json:"lastStatusUpdate"` +} + +// FirewallProfile represents a specific firewall profile configuration +type FirewallProfile struct { + Enabled bool `json:"enabled"` + DefaultInboundAction string `json:"defaultInboundAction"` + DefaultOutboundAction string `json:"defaultOutboundAction"` + NotificationsEnabled bool `json:"notificationsEnabled"` + StealthModeEnabled bool `json:"stealthModeEnabled"` + ExceptionRules []string `json:"exceptionRules"` +} + +// AppProtectionPolicy represents an app protection policy +type AppProtectionPolicy struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + Description string `json:"description"` + PlatformType string `json:"platformType"` + CreatedDateTime time.Time `json:"createdDateTime"` + LastModifiedDateTime time.Time `json:"lastModifiedDateTime"` + Settings map[string]interface{} `json:"settings"` + AssignedGroups []string `json:"assignedGroups"` +} + +// ConditionalAccessPolicy represents a conditional access policy +type ConditionalAccessPolicy struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + State string `json:"state"` + CreatedDateTime time.Time `json:"createdDateTime"` + ModifiedDateTime time.Time `json:"modifiedDateTime"` + Conditions map[string]interface{} `json:"conditions"` + GrantControls map[string]interface{} `json:"grantControls"` + SessionControls map[string]interface{} `json:"sessionControls"` +} + +// IntuneComplianceReport represents a comprehensive compliance report +type IntuneComplianceReport struct { + TenantID string `json:"tenantId"` + ReportTimestamp time.Time `json:"reportTimestamp"` + TotalDevices int `json:"totalDevices"` + CompliantDevices int `json:"compliantDevices"` + NonCompliantDevices int `json:"nonCompliantDevices"` + DeviceBreakdown DeviceBreakdown `json:"deviceBreakdown"` + SecurityFindings []SecurityFinding `json:"securityFindings"` + TopRisks []RiskSummary `json:"topRisks"` + ComplianceTrends []ComplianceTrend `json:"complianceTrends"` + Recommendations []SecurityRecommendation `json:"recommendations"` +} + +// Fixed: Consistent PascalCase naming for all exported fields +type DeviceBreakdown struct { + Windows int `json:"windows"` + MacOS int `json:"macOS"` // Fixed: Changed from macOS to MacOS + IOS int `json:"iOS"` // Fixed: Changed from iOS to IOS + Android int `json:"android"` + WindowsPhone int `json:"windowsPhone"` + Other int `json:"other"` +} + +// RiskSummary represents a high-level risk category summary +type RiskSummary struct { + RiskCategory string `json:"riskCategory"` + AffectedDevices int `json:"affectedDevices"` + Severity string `json:"severity"` + Description string `json:"description"` + ImpactScore float64 `json:"impactScore"` +} + +// ComplianceTrend represents compliance status over time +type ComplianceTrend struct { + Date time.Time `json:"date"` + CompliantCount int `json:"compliantCount"` + NonCompliantCount int `json:"nonCompliantCount"` + TotalCount int `json:"totalCount"` + ComplianceRate float64 `json:"complianceRate"` +} + +// SecurityRecommendation represents an actionable security recommendation +type SecurityRecommendation struct { + ID string `json:"id"` + Title string `json:"title"` + Priority string `json:"priority"` + Category string `json:"category"` + Description string `json:"description"` + Impact string `json:"impact"` + Implementation string `json:"implementation"` + AffectedDevices int `json:"affectedDevices"` + EstimatedEffort string `json:"estimatedEffort"` + MITREMitigations []string `json:"mitreMitigations"` +} + +// Fixed: Removed duplicate collections - keeping only the nested Data structure +// BloodHoundIntuneData represents data formatted specifically for BloodHound ingestion +type BloodHoundIntuneData struct { + Meta BloodHoundMeta `json:"meta"` + Data BloodHoundDataWrapper `json:"data"` + // Note: Removed duplicate top-level arrays to avoid confusion. + // All BloodHound data should be accessed through the Data field. +} + +// BloodHoundMeta contains metadata about the collection +type BloodHoundMeta struct { + Type string `json:"type"` + Count int `json:"count"` + Version string `json:"version"` + Methods int `json:"methods"` + CollectedBy string `json:"collectedBy"` + CollectedAt time.Time `json:"collectedAt"` +} + +// BloodHoundDataWrapper wraps the data arrays for BloodHound +type BloodHoundDataWrapper struct { + Computers []Computer `json:"computers"` + Users []BloodHoundUser `json:"users"` + Groups []BloodHoundGroup `json:"groups"` + LocalAdmins []LocalAdmin `json:"localAdmins"` + RemoteDesktopUsers []RemoteDesktopUser `json:"remoteDesktopUsers"` + DcomUsers []DcomUser `json:"dcomUsers"` + PSRemoteUsers []PSRemoteUser `json:"psRemoteUsers"` + Sessions []Session `json:"sessions"` + RegistryKeys []RegistryKey `json:"registryKeys"` +} + +// Computer represents a computer object for BloodHound +type Computer struct { + ObjectIdentifier string `json:"ObjectIdentifier"` + PrimaryGroupSID string `json:"PrimaryGroupSID"` + LocalAdmins []LocalAdminRelation `json:"LocalAdmins"` + RemoteDesktopUsers []RDPUsersRelation `json:"RemoteDesktopUsers"` + DcomUsers []DcomUsersRelation `json:"DcomUsers"` + PSRemoteUsers []PSRemoteRelation `json:"PSRemoteUsers"` + Properties ComputerProperties `json:"Properties"` + Aces []ACE `json:"Aces"` + Sessions []SessionRelation `json:"Sessions"` + RegistryFindings []RegistryFinding `json:"RegistryFindings"` + SecurityFindings []SecurityFinding `json:"SecurityFindings"` +} + +// Fixed: Consistent time types - using time.Time for better type safety +// ComputerProperties represents properties of a computer +type ComputerProperties struct { + Name string `json:"name"` + Domain string `json:"domain"` + ObjectID string `json:"objectid"` + PrimaryGroupSID string `json:"primarygroupsid"` + HasLAPS bool `json:"haslaps"` + // Fixed: Use time.Time for consistency and type safety + LastLogon time.Time `json:"lastlogon"` + LastLogonTimestamp time.Time `json:"lastlogontimestamp"` + PwdLastSet time.Time `json:"pwdlastset"` + ServicePrincipalNames []string `json:"serviceprincipalnames"` + Description string `json:"description"` + OperatingSystem string `json:"operatingsystem"` + Enabled bool `json:"enabled"` + UnconstrainedDelegation bool `json:"unconstraineddelegation"` + TrustedToAuth bool `json:"trustedtoauth"` + SamAccountName string `json:"samaccountname"` + DistinguishedName string `json:"distinguishedname"` + IntuneDeviceID string `json:"intunedeviceid"` + ComplianceState string `json:"compliancestate"` + LastSyncDateTime time.Time `json:"lastsyncdatetime"` + RiskScore int `json:"riskscore"` +} + +// BloodHoundUser represents a user object for BloodHound (renamed to avoid conflict) +type BloodHoundUser struct { + ObjectIdentifier string `json:"ObjectIdentifier"` + PrimaryGroupSID string `json:"PrimaryGroupSID"` + Properties BloodHoundUserProperties `json:"Properties"` + Aces []ACE `json:"Aces"` + Sessions []SessionRelation `json:"Sessions"` +} + +// Fixed: Consistent time types for all time-related fields +// BloodHoundUserProperties represents properties of a user (renamed to avoid conflict) +type BloodHoundUserProperties struct { + Name string `json:"name"` + Domain string `json:"domain"` + ObjectID string `json:"objectid"` + PrimaryGroupSID string `json:"primarygroupsid"` + HasSPN bool `json:"hasspn"` + ServicePrincipalNames []string `json:"serviceprincipalnames"` + DisplayName string `json:"displayname"` + Email string `json:"email"` + Title string `json:"title"` + Department string `json:"department"` + // Fixed: Use time.Time for consistency and type safety + LastLogon time.Time `json:"lastlogon"` + LastLogonTimestamp time.Time `json:"lastlogontimestamp"` + PwdLastSet time.Time `json:"pwdlastset"` + Enabled bool `json:"enabled"` + PasswordNeverExpires bool `json:"passwordneverexpires"` + PasswordNotRequired bool `json:"passwordnotrequired"` + UserCannotChangePassword bool `json:"usercannotchangepassword"` + DontRequirePreAuth bool `json:"dontreqpreauth"` + SamAccountName string `json:"samaccountname"` + DistinguishedName string `json:"distinguishedname"` + UnconstrainedDelegation bool `json:"unconstraineddelegation"` + Sensitive bool `json:"sensitive"` + AllowedToDelegate []string `json:"allowedtodelegate"` + AdminCount bool `json:"admincount"` + SIDHistory []string `json:"sidhistory"` +} + +// BloodHoundGroup represents a group object for BloodHound (renamed to avoid conflict) +type BloodHoundGroup struct { + ObjectIdentifier string `json:"ObjectIdentifier"` + Properties BloodHoundGroupProperties `json:"Properties"` + Aces []ACE `json:"Aces"` + Members []Member `json:"Members"` +} + +// BloodHoundGroupProperties represents properties of a group (renamed to avoid conflict) +type BloodHoundGroupProperties struct { + Name string `json:"name"` + Domain string `json:"domain"` + ObjectID string `json:"objectid"` + Description string `json:"description"` + AdminCount bool `json:"admincount"` + SamAccountName string `json:"samaccountname"` + DistinguishedName string `json:"distinguishedname"` +} + +// LocalAdmin represents a local administrator relationship +type LocalAdmin struct { + ObjectIdentifier string `json:"ObjectIdentifier"` + ObjectType string `json:"ObjectType"` + ComputerSID string `json:"ComputerSID"` +} + +// LocalAdminRelation represents a local admin relationship for BloodHound +type LocalAdminRelation struct { + ObjectIdentifier string `json:"ObjectIdentifier"` + ObjectType string `json:"ObjectType"` +} + +// RemoteDesktopUser represents a remote desktop user relationship +type RemoteDesktopUser struct { + ObjectIdentifier string `json:"ObjectIdentifier"` + ObjectType string `json:"ObjectType"` + ComputerSID string `json:"ComputerSID"` +} + +// RDPUsersRelation represents an RDP user relationship for BloodHound +type RDPUsersRelation struct { + ObjectIdentifier string `json:"ObjectIdentifier"` + ObjectType string `json:"ObjectType"` +} + +// DcomUser represents a DCOM user relationship +type DcomUser struct { + ObjectIdentifier string `json:"ObjectIdentifier"` + ObjectType string `json:"ObjectType"` + ComputerSID string `json:"ComputerSID"` +} + +// DcomUsersRelation represents a DCOM user relationship for BloodHound +type DcomUsersRelation struct { + ObjectIdentifier string `json:"ObjectIdentifier"` + ObjectType string `json:"ObjectType"` +} + +// PSRemoteUser represents a PowerShell remoting user relationship +type PSRemoteUser struct { + ObjectIdentifier string `json:"ObjectIdentifier"` + ObjectType string `json:"ObjectType"` + ComputerSID string `json:"ComputerSID"` +} + +// PSRemoteRelation represents a PS Remote relationship for BloodHound +type PSRemoteRelation struct { + ObjectIdentifier string `json:"ObjectIdentifier"` + ObjectType string `json:"ObjectType"` +} + +// Session represents a user session +type Session struct { + ComputerSID string `json:"ComputerSID"` + UserSID string `json:"UserSID"` + LogonType string `json:"LogonType"` +} + +// SessionRelation represents a session relationship for BloodHound +type SessionRelation struct { + UserSID string `json:"UserSID"` + LogonType string `json:"LogonType"` +} + +// ComputerDomain represents a computer's domain relationship +type ComputerDomain struct { + ComputerSID string `json:"ComputerSID"` + DomainSID string `json:"DomainSID"` +} + +// Member represents a group membership +type Member struct { + ObjectIdentifier string `json:"ObjectIdentifier"` + ObjectType string `json:"ObjectType"` +} + +// ACE represents an Access Control Entry +type ACE struct { + PrincipalSID string `json:"PrincipalSID"` + PrincipalType string `json:"PrincipalType"` + RightName string `json:"RightName"` + AceType string `json:"AceType"` + IsInherited bool `json:"IsInherited"` +} + +// RegistryKey represents a registry key finding for BloodHound +type RegistryKey struct { + ComputerSID string `json:"ComputerSID"` + RegistryPath string `json:"RegistryPath"` + ValueName string `json:"ValueName"` + ValueData interface{} `json:"ValueData"` + ValueType string `json:"ValueType"` + SecurityRisk string `json:"SecurityRisk"` + AttackVector string `json:"AttackVector"` + Properties map[string]interface{} `json:"Properties"` +} diff --git a/models/intune/models.go b/models/intune/models.go new file mode 100644 index 00000000..73045800 --- /dev/null +++ b/models/intune/models.go @@ -0,0 +1,61 @@ +// File: models/intune/models.go +// Copyright (C) 2022 SpecterOps +// Data models for Intune integration + +package intune + +import ( + "time" +) + +// ManagedDevice represents an Intune managed device +type ManagedDevice struct { + Id string `json:"id"` + DeviceName string `json:"deviceName"` + OperatingSystem string `json:"operatingSystem"` + OSVersion string `json:"osVersion"` + ComplianceState string `json:"complianceState"` + LastSyncDateTime time.Time `json:"lastSyncDateTime"` + EnrollmentType string `json:"enrollmentType"` + ManagementAgent string `json:"managementAgent"` + AzureADDeviceId string `json:"azureADDeviceId"` + UserPrincipalName string `json:"userPrincipalName"` + DeviceEnrollmentType string `json:"deviceEnrollmentType"` + JoinType string `json:"joinType"` +} + +// ComplianceState represents device compliance information +type ComplianceState struct { + Id string `json:"id"` + DeviceId string `json:"deviceId"` + DeviceName string `json:"deviceName"` + ComplianceGracePeriodExpirationDateTime time.Time `json:"complianceGracePeriodExpirationDateTime"` + State string `json:"state"` + Version int `json:"version"` + SettingStates []ComplianceSettingState `json:"settingStates"` +} + +// ComplianceSettingState represents individual compliance setting state +type ComplianceSettingState struct { + Setting string `json:"setting"` + State string `json:"state"` + CurrentValue string `json:"currentValue"` +} + +// ConfigurationState represents device configuration state +type ConfigurationState struct { + Id string `json:"id"` + DeviceId string `json:"deviceId"` + DeviceName string `json:"deviceName"` + State string `json:"state"` + Version int `json:"version"` + SettingStates []ConfigurationSettingState `json:"settingStates"` + PlatformType string `json:"platformType"` +} + +// ConfigurationSettingState represents individual configuration setting state +type ConfigurationSettingState struct { + Setting string `json:"setting"` + State string `json:"state"` + CurrentValue string `json:"currentValue"` +} \ No newline at end of file