Connector Integration Examples

Real-world implementation examples for ConnectWise, Auvik, Microsoft Graph, Ninja RMM, and other key integrations using your existing database schema.

20+
Integrations Supported
90%
Evidence Auto-Collection
24/7
Continuous Monitoring
< 2s
Response Time

Using Your Existing Integration Infrastructure

These examples build on your existing API keys and connector mappings in the database:

📊 api_keys table

Stores credentials for 20+ integrations including ConnectWise, Auvik, Microsoft Graph, Ninja, Huntress, etc.

🔗 connector_client_mappings

Maps your internal clients to external system identifiers for seamless data collection

📋 assessment_events_questions

Stores evidence with control_evidence_source and control_evidence_location fields

ConnectWise Manage Integration

RMM/PSA data for user access, asset inventory, and security configurations

SOC 2 | ISO 27001

User Access Management (SOC 2 CC6.1)

Automatically collect and validate user access data from ConnectWise

Evidence Collection Flow:
  1. Retrieve API Key: Get ConnectWise credentials from api_keys table
  2. Map Client: Use connector_client_mappings to find ConnectWise company ID
  3. Collect Data: Fetch users via /company/companies/{id}/members endpoint
  4. AI Analysis: Analyze access patterns and compliance gaps
  5. Store Evidence: Update assessment_events_questions with results
initializeApi();
    }
    
    private function initializeApi(): void
    {
        // Get API credentials from existing api_keys table
        $apiKey = ApiKey::where('client_id', $this->clientId)
            ->where('type', 'connectwise')
            ->first();
            
        if (!$apiKey) {
            throw new \Exception("ConnectWise API key not found for client {$this->clientId}");
        }
        
        $this->baseUrl = rtrim($apiKey->url, '/');
        $this->headers = [
            'Authorization' => 'Basic ' . base64_encode($apiKey->api_company_id . '+' . $apiKey->key . ':' . $apiKey->secret),
            'Content-Type' => 'application/json',
            'Accept' => 'application/vnd.connectwise.com+json; version=2022.1'
        ];
    }
    
    public function collectUserAccessEvidence(): array
    {
        // Get client mapping to ConnectWise company
        $mapping = ConnectorClientMapping::where('client_id', $this->clientId)
            ->where('type', 'connectwise')
            ->first();
            
        if (!$mapping) {
            throw new \Exception("ConnectWise client mapping not found for {$this->clientId}");
        }
        
        $cwCompanyId = $mapping->connector_client_id;
        
        // Collect user data from ConnectWise
        $users = $this->apiCall("GET", "/company/companies/{$cwCompanyId}/members", [
            'pageSize' => 1000,
            'conditions' => 'inactiveFlag=false'
        ]);
        
        // Collect additional security data
        $securityRoles = $this->apiCall("GET", "/system/securityroles");
        $userSecurityRoles = $this->getUserSecurityRoles($cwCompanyId);
        
        // Enrich user data with security information
        $enrichedUsers = $this->enrichUserData($users, $securityRoles, $userSecurityRoles);
        
        return [
            'source' => 'connectwise',
            'company_id' => $cwCompanyId,
            'total_users' => count($enrichedUsers),
            'users' => $enrichedUsers,
            'security_roles' => $securityRoles,
            'collection_timestamp' => now()->toISOString(),
            'api_endpoint' => "/company/companies/{$cwCompanyId}/members"
        ];
    }
    
    private function enrichUserData(array $users, array $securityRoles, array $userSecurityRoles): array
    {
        $roleMap = collect($securityRoles)->keyBy('id');
        
        return array_map(function($user) use ($roleMap, $userSecurityRoles) {
            $userRoles = $userSecurityRoles[$user['id']] ?? [];
            
            return [
                'id' => $user['id'],
                'identifier' => $user['identifier'],
                'name' => trim(($user['firstName'] ?? '') . ' ' . ($user['lastName'] ?? '')),
                'email' => $user['emailAddress'] ?? null,
                'last_login' => $user['lastLogin'] ?? null,
                'inactive_flag' => $user['inactiveFlag'] ?? false,
                'admin_flag' => $user['adminFlag'] ?? false,
                'security_roles' => array_map(function($roleId) use ($roleMap) {
                    return [
                        'id' => $roleId,
                        'name' => $roleMap[$roleId]['description'] ?? 'Unknown',
                        'is_admin' => strpos(strtolower($roleMap[$roleId]['description'] ?? ''), 'admin') !== false
                    ];
                }, $userRoles),
                'is_privileged' => $this->isPrivilegedUser($user, $userRoles, $roleMap),
                'days_since_login' => $this->calculateDaysSinceLogin($user['lastLogin'] ?? null),
                'compliance_flags' => $this->generateComplianceFlags($user, $userRoles, $roleMap)
            ];
        }, $users);
    }
    
    private function isPrivilegedUser(array $user, array $userRoles, $roleMap): bool
    {
        if ($user['adminFlag'] ?? false) {
            return true;
        }
        
        $privilegedKeywords = ['admin', 'super', 'root', 'security', 'system'];
        
        foreach ($userRoles as $roleId) {
            $roleName = strtolower($roleMap[$roleId]['description'] ?? '');
            foreach ($privilegedKeywords as $keyword) {
                if (strpos($roleName, $keyword) !== false) {
                    return true;
                }
            }
        }
        
        return false;
    }
    
    private function calculateDaysSinceLogin(?string $lastLogin): ?int
    {
        if (!$lastLogin) {
            return null;
        }
        
        return Carbon::parse($lastLogin)->diffInDays(now());
    }
    
    private function generateComplianceFlags(array $user, array $userRoles, $roleMap): array
    {
        $flags = [];
        
        // Check for inactive users with recent login
        if (($user['inactiveFlag'] ?? false) && $this->calculateDaysSinceLogin($user['lastLogin'] ?? null) < 30) {
            $flags[] = 'inactive_user_recent_login';
        }
        
        // Check for users without recent login
        $daysSinceLogin = $this->calculateDaysSinceLogin($user['lastLogin'] ?? null);
        if ($daysSinceLogin && $daysSinceLogin > 90) {
            $flags[] = 'stale_user_account';
        }
        
        // Check for excessive privileges
        if (count($userRoles) > 5) {
            $flags[] = 'excessive_role_assignments';
        }
        
        // Check for admin users without proper naming convention
        if ($this->isPrivilegedUser($user, $userRoles, $roleMap)) {
            $identifier = strtolower($user['identifier'] ?? '');
            if (!preg_match('/admin|root|super/', $identifier)) {
                $flags[] = 'privileged_user_poor_naming';
            }
        }
        
        return $flags;
    }
    
    public function collectAssetInventory(): array
    {
        $mapping = ConnectorClientMapping::where('client_id', $this->clientId)
            ->where('type', 'connectwise')
            ->first();
            
        $cwCompanyId = $mapping->connector_client_id;
        
        // Collect configuration data (hardware assets)
        $configurations = $this->apiCall("GET", "/company/configurations", [
            'conditions' => "company/id={$cwCompanyId}",
            'pageSize' => 1000
        ]);
        
        // Enrich with additional data
        $enrichedAssets = array_map(function($config) {
            return [
                'id' => $config['id'],
                'name' => $config['name'] ?? 'Unknown',
                'type' => 'hardware',
                'category' => $this->categorizeAsset($config),
                'manufacturer' => $config['manufacturer']['name'] ?? null,
                'model' => $config['modelNumber'] ?? null,
                'serial_number' => $config['serialNumber'] ?? null,
                'location' => $config['locationName'] ?? null,
                'status' => $config['status']['name'] ?? 'Unknown',
                'install_date' => $config['installationDate'] ?? null,
                'purchase_date' => $config['purchaseDate'] ?? null,
                'warranty_expiration' => $config['warrantyExpirationDate'] ?? null,
                'last_updated' => $config['lastUpdate'] ?? null,
                'compliance_flags' => $this->generateAssetComplianceFlags($config),
                'source' => 'connectwise'
            ];
        }, $configurations);
        
        return [
            'source' => 'connectwise',
            'company_id' => $cwCompanyId,
            'total_assets' => count($enrichedAssets),
            'assets' => $enrichedAssets,
            'collection_timestamp' => now()->toISOString(),
            'api_endpoint' => "/company/configurations"
        ];
    }
    
    private function categorizeAsset(array $config): string
    {
        $type = strtolower($config['type']['name'] ?? '');
        $name = strtolower($config['name'] ?? '');
        
        $categories = [
            'server' => ['server', 'srv', 'host'],
            'workstation' => ['workstation', 'desktop', 'pc', 'computer'],
            'laptop' => ['laptop', 'notebook', 'mobile'],
            'network' => ['switch', 'router', 'firewall', 'ap', 'access point'],
            'storage' => ['storage', 'nas', 'san', 'disk'],
            'printer' => ['printer', 'mfp', 'copier'],
            'phone' => ['phone', 'voip', 'pbx']
        ];
        
        foreach ($categories as $category => $keywords) {
            foreach ($keywords as $keyword) {
                if (strpos($type, $keyword) !== false || strpos($name, $keyword) !== false) {
                    return $category;
                }
            }
        }
        
        return 'other';
    }
}
?>
Key API Endpoints:
• GET /company/companies/{id}/members - User accounts
• GET /system/securityroles - Security roles
• GET /company/configurations - Asset inventory
• GET /service/tickets - Incident data

Integration with AI Rules Engine

How ConnectWise data flows through the AI processing pipeline

 count($users),
            'privileged_users' => 0,
            'inactive_users' => 0,
            'stale_accounts' => 0,
            'compliance_flags' => []
        ];
        
        foreach ($users as $user) {
            // Count privileged users
            if ($user['is_privileged']) {
                $metrics['privileged_users']++;
            }
            
            // Check for violations based on compliance flags
            foreach ($user['compliance_flags'] as $flag) {
                $metrics['compliance_flags'][] = $flag;
                
                switch ($flag) {
                    case 'stale_user_account':
                        $violations[] = [
                            'rule' => 'stale_user_account',
                            'severity' => 2, // High
                            'user' => $user['name'],
                            'days_since_login' => $user['days_since_login'],
                            'description' => "User {$user['name']} has not logged in for {$user['days_since_login']} days"
                        ];
                        $metrics['stale_accounts']++;
                        break;
                        
                    case 'excessive_role_assignments':
                        $warnings[] = [
                            'rule' => 'excessive_role_assignments',
                            'user' => $user['name'],
                            'role_count' => count($user['security_roles']),
                            'description' => "User {$user['name']} has " . count($user['security_roles']) . " security roles assigned"
                        ];
                        break;
                        
                    case 'privileged_user_poor_naming':
                        $warnings[] = [
                            'rule' => 'privileged_user_poor_naming',
                            'user' => $user['name'],
                            'identifier' => $user['identifier'],
                            'description' => "Privileged user {$user['name']} ({$user['identifier']}) doesn't follow naming convention"
                        ];
                        break;
                }
            }
        }
        
        // Calculate compliance percentages
        $metrics['privileged_ratio'] = $metrics['total_users'] > 0 ? 
            round(($metrics['privileged_users'] / $metrics['total_users']) * 100, 1) : 0;
        $metrics['stale_ratio'] = $metrics['total_users'] > 0 ? 
            round(($metrics['stale_accounts'] / $metrics['total_users']) * 100, 1) : 0;
        
        return [
            'connector' => 'connectwise',
            'control_framework' => 'SOC2_CC6.1',
            'metrics' => $metrics,
            'violations' => $violations,
            'warnings' => $warnings,
            'analysis_timestamp' => now()->toISOString()
        ];
    }
    
    public function getRecommendations(array $analysis): array
    {
        $recommendations = [];
        
        // Analyze violation patterns
        $violationCounts = array_count_values(array_column($analysis['violations'], 'rule'));
        
        if (isset($violationCounts['stale_user_account']) && $violationCounts['stale_user_account'] > 0) {
            $recommendations[] = [
                'priority' => 'high',
                'category' => 'user_lifecycle',
                'title' => 'Remove Stale User Accounts',
                'description' => "Disable or remove {$violationCounts['stale_user_account']} user accounts that haven't been used in 90+ days",
                'action_items' => [
                    'Review inactive user list in ConnectWise',
                    'Confirm with managers before disabling accounts',
                    'Implement automated account deactivation policy',
                    'Set up quarterly access reviews'
                ]
            ];
        }
        
        if ($analysis['metrics']['privileged_ratio'] > 30) {
            $recommendations[] = [
                'priority' => 'medium',
                'category' => 'privilege_management',
                'title' => 'Review Privileged Access',
                'description' => "High percentage of privileged users ({$analysis['metrics']['privileged_ratio']}%) - review for least privilege compliance",
                'action_items' => [
                    'Audit security role assignments',
                    'Remove unnecessary administrative privileges',
                    'Implement role-based access control',
                    'Document privilege escalation procedures'
                ]
            ];
        }
        
        return $recommendations;
    }
}

// Usage in SmartComplianceRouter
class SmartComplianceRouter 
{
    public function processConnectWiseUserAccess(string $questionId): array
    {
        $question = AssessmentEventsQuestion::find($questionId);
        
        // Collect evidence from ConnectWise
        $connector = new ConnectWiseConnector($question->client_id);
        $evidenceData = $connector->collectUserAccessEvidence();
        
        // Process through rules engine
        $rules = new ConnectWiseUserAccessRules();
        $analysis = $rules->analyze($evidenceData);
        
        // Calculate compliance score
        $score = $this->calculateComplianceScore($analysis);
        
        // Update assessment question
        DB::table('assessment_events_questions')
            ->where('id', $questionId)
            ->update([
                'control_evidence_source' => 'connectwise',
                'control_evidence_location' => $evidenceData['api_endpoint'],
                'evidence_location' => json_encode([
                    'evidence_summary' => [
                        'total_users' => $analysis['metrics']['total_users'],
                        'privileged_users' => $analysis['metrics']['privileged_users'],
                        'stale_accounts' => $analysis['metrics']['stale_accounts'],
                        'compliance_score' => $score
                    ],
                    'detailed_evidence' => $evidenceData,
                    'analysis_results' => $analysis
                ]),
                'evidence_location_type' => 'text',
                'responsibility' => 'tool',
                'selected_option_id' => $this->getOptionByScore($score),
                'auditor_notes' => $this->generateAuditorNotes($analysis),
                'auditor_conformity_mark' => $score >= 80 ? 'met' : 'not met',
                'question_notes' => implode("\n", array_column($rules->getRecommendations($analysis), 'title'))
            ]);
        
        return [
            'compliance_score' => $score,
            'analysis' => $analysis,
            'evidence_data' => $evidenceData,
            'recommendations' => $rules->getRecommendations($analysis)
        ];
    }
}
?>

Auvik Network Monitoring

Network device inventory, security configurations, and vulnerability assessment

ISO 27001 | NIST CSF

Network Asset Discovery (ISO 27001 A.8.1.1)

Automatically discover and inventory network devices from Auvik

initializeApi();
    }
    
    private function initializeApi(): void
    {
        $apiKey = ApiKey::where('client_id', $this->clientId)
            ->where('type', 'auvik')
            ->first();
            
        if (!$apiKey) {
            throw new \Exception("Auvik API key not found for client {$this->clientId}");
        }
        
        $this->headers = [
            'Authorization' => 'Basic ' . base64_encode($apiKey->key . ':' . $apiKey->secret),
            'Content-Type' => 'application/json',
            'Accept' => 'application/vnd.api+json'
        ];
    }
    
    public function collectNetworkAssets(): array
    {
        // Get client's Auvik tenant mapping
        $mapping = ConnectorClientMapping::where('client_id', $this->clientId)
            ->where('type', 'auvik')
            ->first();
            
        $tenantDomainPrefix = $mapping ? $mapping->connector_client_id : null;
        $filterParams = $tenantDomainPrefix ? ['filter[networks.domainPrefix]' => $tenantDomainPrefix] : [];
        
        // Collect network devices
        $devices = $this->apiCall("GET", "/inventory/device/info", array_merge([
            'page[first]' => 100,
            'include' => 'deviceDetail'
        ], $filterParams));
        
        // Collect network details for each device
        $enrichedDevices = [];
        foreach ($devices['data'] as $device) {
            $deviceDetail = $this->getDeviceDetails($device['id']);
            $networkInterfaces = $this->getNetworkInterfaces($device['id']);
            $deviceConfiguration = $this->getDeviceConfiguration($device['id']);
            
            $enrichedDevices[] = [
                'id' => $device['id'],
                'name' => $device['attributes']['deviceName'],
                'type' => 'network',
                'category' => $device['attributes']['deviceType'],
                'manufacturer' => $device['attributes']['vendorName'] ?? null,
                'model' => $device['attributes']['modelName'] ?? null,
                'ip_addresses' => $device['attributes']['ipAddresses'] ?? [],
                'mac_address' => $device['attributes']['macAddress'] ?? null,
                'serial_number' => $deviceDetail['serialNumber'] ?? null,
                'firmware_version' => $device['attributes']['firmwareVersion'] ?? null,
                'device_status' => $device['attributes']['deviceStatus'],
                'last_seen' => $device['attributes']['lastSeenTime'],
                'snmp_enabled' => $device['attributes']['snmpEnabled'] ?? false,
                'management_ip' => $device['attributes']['managementIp'] ?? null,
                'network_interfaces' => $networkInterfaces,
                'configuration_data' => $deviceConfiguration,
                'compliance_analysis' => $this->analyzeDeviceCompliance($device, $deviceDetail, $networkInterfaces),
                'source' => 'auvik'
            ];
        }
        
        return [
            'source' => 'auvik',
            'tenant_domain' => $tenantDomainPrefix,
            'total_devices' => count($enrichedDevices),
            'devices' => $enrichedDevices,
            'device_types' => $this->categorizeDevices($enrichedDevices),
            'collection_timestamp' => now()->toISOString(),
            'api_endpoint' => "/inventory/device/info"
        ];
    }
    
    private function analyzeDeviceCompliance(array $device, array $deviceDetail, array $interfaces): array
    {
        $compliance = [
            'security_flags' => [],
            'configuration_issues' => [],
            'risk_score' => 0
        ];
        
        // Check for security configurations
        if (!($device['attributes']['snmpEnabled'] ?? false)) {
            $compliance['configuration_issues'][] = 'SNMP monitoring not enabled';
            $compliance['risk_score'] += 10;
        }
        
        // Check firmware currency (if we have version data)
        $firmwareVersion = $device['attributes']['firmwareVersion'] ?? null;
        if ($firmwareVersion && $this->isFirmwareOutdated($firmwareVersion, $device['attributes']['vendorName'] ?? '')) {
            $compliance['security_flags'][] = 'Outdated firmware detected';
            $compliance['risk_score'] += 25;
        }
        
        // Check for open/insecure interfaces
        foreach ($interfaces as $interface) {
            if (isset($interface['adminStatus']) && $interface['adminStatus'] === 'up' && 
                isset($interface['description']) && 
                stripos($interface['description'], 'unused') !== false) {
                $compliance['configuration_issues'][] = "Unused interface {$interface['name']} is administratively up";
                $compliance['risk_score'] += 5;
            }
        }
        
        // Device status checks
        if ($device['attributes']['deviceStatus'] !== 'online') {
            $compliance['security_flags'][] = 'Device not online - monitoring gap';
            $compliance['risk_score'] += 15;
        }
        
        // Calculate final risk level
        $compliance['risk_level'] = $this->calculateRiskLevel($compliance['risk_score']);
        
        return $compliance;
    }
    
    private function categorizeDevices(array $devices): array
    {
        $categories = [
            'routers' => 0,
            'switches' => 0,
            'firewalls' => 0,
            'access_points' => 0,
            'servers' => 0,
            'printers' => 0,
            'other' => 0
        ];
        
        foreach ($devices as $device) {
            $type = strtolower($device['category']);
            
            if (strpos($type, 'router') !== false) {
                $categories['routers']++;
            } elseif (strpos($type, 'switch') !== false) {
                $categories['switches']++;
            } elseif (strpos($type, 'firewall') !== false) {
                $categories['firewalls']++;
            } elseif (strpos($type, 'access point') !== false || strpos($type, 'wireless') !== false) {
                $categories['access_points']++;
            } elseif (strpos($type, 'server') !== false) {
                $categories['servers']++;
            } elseif (strpos($type, 'printer') !== false) {
                $categories['printers']++;
            } else {
                $categories['other']++;
            }
        }
        
        return $categories;
    }
    
    public function collectSecurityEvents(): array
    {
        // Collect network alerts and security events
        $alerts = $this->apiCall("GET", "/stat/alert", [
            'filter[alertTime]' => 'lastWeek',
            'filter[severity]' => 'critical,warning',
            'page[first]' => 100
        ]);
        
        $securityEvents = [];
        foreach ($alerts['data'] as $alert) {
            if ($this->isSecurityRelated($alert)) {
                $securityEvents[] = [
                    'id' => $alert['id'],
                    'device_name' => $alert['relationships']['entity']['data']['id'] ?? 'Unknown',
                    'alert_type' => $alert['attributes']['alertType'],
                    'severity' => $alert['attributes']['severity'],
                    'description' => $alert['attributes']['alertDescription'],
                    'timestamp' => $alert['attributes']['alertTime'],
                    'status' => $alert['attributes']['status'],
                    'category' => $this->categorizeSecurityEvent($alert),
                    'compliance_impact' => $this->assessComplianceImpact($alert)
                ];
            }
        }
        
        return [
            'source' => 'auvik',
            'total_security_events' => count($securityEvents),
            'events' => $securityEvents,
            'event_categories' => array_count_values(array_column($securityEvents, 'category')),
            'collection_timestamp' => now()->toISOString()
        ];
    }
    
    private function isSecurityRelated(array $alert): bool
    {
        $securityKeywords = [
            'security', 'intrusion', 'malware', 'breach', 'unauthorized',
            'vulnerability', 'attack', 'suspicious', 'failed login',
            'configuration change', 'firmware', 'certificate'
        ];
        
        $alertText = strtolower($alert['attributes']['alertDescription']);
        
        foreach ($securityKeywords as $keyword) {
            if (strpos($alertText, $keyword) !== false) {
                return true;
            }
        }
        
        return false;
    }
}
?>
Key Auvik API Endpoints:
• GET /inventory/device/info - Network device inventory
• GET /inventory/device/detail - Detailed device information
• GET /stat/alert - Network alerts and events
• GET /inventory/network/info - Network topology

Microsoft Graph API

Office 365 users, groups, applications, and security configurations

SOC 2 | HIPAA | ISO 27001

Office 365 User & Security Analysis

Comprehensive Office 365 compliance evidence collection

initializeGraphClient();
    }
    
    private function initializeGraphClient(): void
    {
        $apiKey = ApiKey::where('client_id', $this->clientId)
            ->where('type', 'msgraph')
            ->first();
            
        if (!$apiKey) {
            throw new \Exception("Microsoft Graph API key not found for client {$this->clientId}");
        }
        
        // Initialize Graph client with stored credentials
        $tokenProvider = new GraphPhpLeagueAccessTokenProvider([
            'clientId' => $apiKey->key,
            'clientSecret' => $apiKey->secret,
            'tenantId' => $apiKey->api_company_id
        ]);
        
        $this->graphClient = new GraphServiceClient($tokenProvider);
    }
    
    public function collectUserAccessEvidence(): array
    {
        // Collect users with detailed information
        $users = $this->graphClient->users()
            ->get([
                'select' => [
                    'id', 'userPrincipalName', 'displayName', 'givenName', 'surname',
                    'accountEnabled', 'createdDateTime', 'lastSignInDateTime',
                    'assignedLicenses', 'department', 'jobTitle'
                ],
                'expand' => ['memberOf']
            ])->wait();
        
        // Collect admin roles and assignments
        $directoryRoles = $this->graphClient->directoryRoles()->get()->wait();
        $adminRoleAssignments = $this->getAdminRoleAssignments();
        
        // Collect conditional access policies
        $conditionalAccessPolicies = $this->graphClient->identity()
            ->conditionalAccess()
            ->policies()
            ->get()
            ->wait();
        
        // Enrich user data with security information
        $enrichedUsers = [];
        foreach ($users->getValue() as $user) {
            $userGroups = $this->getUserGroups($user->getId());
            $userRoles = $this->getUserDirectoryRoles($user->getId(), $adminRoleAssignments);
            
            $enrichedUsers[] = [
                'id' => $user->getId(),
                'user_principal_name' => $user->getUserPrincipalName(),
                'display_name' => $user->getDisplayName(),
                'email' => $user->getUserPrincipalName(),
                'account_enabled' => $user->getAccountEnabled(),
                'created_date' => $user->getCreatedDateTime()?->format('c'),
                'last_signin' => $user->getLastSignInDateTime()?->format('c'),
                'department' => $user->getDepartment(),
                'job_title' => $user->getJobTitle(),
                'assigned_licenses' => $this->parseLicenses($user->getAssignedLicenses()),
                'groups' => $userGroups,
                'directory_roles' => $userRoles,
                'is_admin' => $this->isAdminUser($userRoles),
                'compliance_analysis' => $this->analyzeUserCompliance($user, $userGroups, $userRoles),
                'source' => 'msgraph'
            ];
        }
        
        return [
            'source' => 'msgraph',
            'tenant_id' => $this->getTenantId(),
            'total_users' => count($enrichedUsers),
            'users' => $enrichedUsers,
            'directory_roles' => $this->formatDirectoryRoles($directoryRoles->getValue()),
            'conditional_access_policies' => $this->formatConditionalAccessPolicies($conditionalAccessPolicies->getValue()),
            'security_summary' => $this->generateSecuritySummary($enrichedUsers),
            'collection_timestamp' => now()->toISOString(),
            'api_endpoint' => '/users'
        ];
    }
    
    private function analyzeUserCompliance($user, array $groups, array $roles): array
    {
        $compliance = [
            'flags' => [],
            'risk_score' => 0,
            'recommendations' => []
        ];
        
        // Check for inactive users
        $lastSignin = $user->getLastSignInDateTime();
        if (!$lastSignin || $lastSignin->diffInDays(now()) > 90) {
            $compliance['flags'][] = 'inactive_user';
            $compliance['risk_score'] += 20;
            $compliance['recommendations'][] = 'Review inactive user account for potential deactivation';
        }
        
        // Check for disabled accounts with recent activity
        if (!$user->getAccountEnabled() && $lastSignin && $lastSignin->diffInDays(now()) < 7) {
            $compliance['flags'][] = 'recently_disabled_account';
            $compliance['risk_score'] += 15;
        }
        
        // Check for excessive group memberships
        if (count($groups) > 20) {
            $compliance['flags'][] = 'excessive_group_memberships';
            $compliance['risk_score'] += 10;
            $compliance['recommendations'][] = 'Review group memberships for least privilege compliance';
        }
        
        // Check for admin users without proper controls
        if ($this->isAdminUser($roles)) {
            $compliance['flags'][] = 'privileged_user';
            
            // Check if admin has MFA enabled (would need additional API call)
            // This is a placeholder for MFA check
            $compliance['recommendations'][] = 'Ensure MFA is enabled for administrative account';
            
            // Check admin naming convention
            $upn = strtolower($user->getUserPrincipalName());
            if (!preg_match('/admin|adm|root/', $upn)) {
                $compliance['flags'][] = 'admin_poor_naming';
                $compliance['risk_score'] += 5;
            }
        }
        
        // Check license assignments
        $licenses = $this->parseLicenses($user->getAssignedLicenses());
        if (empty($licenses)) {
            $compliance['flags'][] = 'no_license_assigned';
            $compliance['recommendations'][] = 'Verify if user requires Office 365 license or should be deactivated';
        }
        
        return $compliance;
    }
    
    public function collectApplicationSecurity(): array
    {
        // Collect registered applications
        $applications = $this->graphClient->applications()
            ->get([
                'select' => [
                    'id', 'appId', 'displayName', 'createdDateTime',
                    'publisherDomain', 'signInAudience', 'web', 'api'
                ]
            ])->wait();
        
        // Collect service principals (enterprise applications)
        $servicePrincipals = $this->graphClient->servicePrincipals()
            ->get([
                'select' => [
                    'id', 'appId', 'displayName', 'accountEnabled',
                    'appRoles', 'oauth2PermissionScopes'
                ]
            ])->wait();
        
        $appSecurityAnalysis = [];
        foreach ($applications->getValue() as $app) {
            $appSecurityAnalysis[] = [
                'id' => $app->getId(),
                'app_id' => $app->getAppId(),
                'display_name' => $app->getDisplayName(),
                'created_date' => $app->getCreatedDateTime()?->format('c'),
                'publisher_domain' => $app->getPublisherDomain(),
                'signin_audience' => $app->getSignInAudience(),
                'security_analysis' => $this->analyzeApplicationSecurity($app),
                'permissions' => $this->getApplicationPermissions($app->getAppId()),
                'source' => 'msgraph'
            ];
        }
        
        return [
            'source' => 'msgraph',
            'total_applications' => count($appSecurityAnalysis),
            'applications' => $appSecurityAnalysis,
            'service_principals' => $this->formatServicePrincipals($servicePrincipals->getValue()),
            'security_summary' => $this->generateAppSecuritySummary($appSecurityAnalysis),
            'collection_timestamp' => now()->toISOString()
        ];
    }
    
    private function analyzeApplicationSecurity($app): array
    {
        $security = [
            'risk_flags' => [],
            'risk_score' => 0,
            'recommendations' => []
        ];
        
        // Check publisher domain
        $publisherDomain = $app->getPublisherDomain();
        if (!$publisherDomain || $publisherDomain === 'mssubstratus.com') {
            $security['risk_flags'][] = 'unverified_publisher';
            $security['risk_score'] += 15;
            $security['recommendations'][] = 'Verify application publisher and review permissions';
        }
        
        // Check sign-in audience
        $audience = $app->getSignInAudience();
        if ($audience === 'AzureADMultipleOrgs' || $audience === 'AzureADandPersonalMicrosoftAccount') {
            $security['risk_flags'][] = 'broad_signin_audience';
            $security['risk_score'] += 10;
            $security['recommendations'][] = 'Review if broad sign-in audience is necessary';
        }
        
        // Check redirect URIs
        $web = $app->getWeb();
        if ($web && $web->getRedirectUris()) {
            foreach ($web->getRedirectUris() as $uri) {
                if (!str_starts_with($uri, 'https://')) {
                    $security['risk_flags'][] = 'insecure_redirect_uri';
                    $security['risk_score'] += 20;
                    $security['recommendations'][] = 'Ensure all redirect URIs use HTTPS';
                    break;
                }
            }
        }
        
        return $security;
    }
}
?>
Key Microsoft Graph Endpoints:
• GET /users - User accounts and profiles
• GET /groups - Security and distribution groups
• GET /applications - Registered applications
• GET /auditLogs/signIns - Sign-in activity
• GET /identity/conditionalAccess/policies - Conditional access policies

Integration Summary & Next Steps

Complete Integration Framework

Covered Integrations
  • ConnectWise: User access, asset inventory, incident data
  • Auvik: Network devices, security events, configurations
  • Microsoft Graph: Office 365 users, apps, security policies
Additional Connectors Available
  • • Ninja RMM: Endpoint management and security
  • • Huntress: Threat detection and response
  • • CyberCNS: Vulnerability scanning
  • • Hudu: Documentation and asset management
  • • Google Workspace: User and application security
Implementation Pattern

All connectors follow the same pattern:

  1. Extend BaseConnector: Inherit common functionality
  2. Read API Keys: Get credentials from api_keys table
  3. Map Clients: Use connector_client_mappings for ID translation
  4. Collect Evidence: Call external APIs and enrich data
  5. Analyze Compliance: Apply rules-based or AI analysis
  6. Store Results: Update assessment_events_questions table
Ready to Implement

Copy these connector classes into your Laravel application and start collecting evidence automatically.

Start Deployment
Cost-Effective

Start with FREE rules-based processing, add AI strategically for complex analysis only.

View Budget Plan
Immediate ROI

Achieve 70-90% automation of compliance evidence collection with these integrations.

View Full Roadmap