Real-world implementation examples for ConnectWise, Auvik, Microsoft Graph, Ninja RMM, and other key integrations using your existing database schema.
These examples build on your existing API keys and connector mappings in the database:
Stores credentials for 20+ integrations including ConnectWise, Auvik, Microsoft Graph, Ninja, Huntress, etc.
Maps your internal clients to external system identifiers for seamless data collection
Stores evidence with control_evidence_source and control_evidence_location fields
RMM/PSA data for user access, asset inventory, and security configurations
Automatically collect and validate user access data from ConnectWise
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';
}
}
?>
GET /company/companies/{id}/members - User accountsGET /system/securityroles - Security rolesGET /company/configurations - Asset inventoryGET /service/tickets - Incident data
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)
];
}
}
?>
Network device inventory, security configurations, and vulnerability assessment
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;
}
}
?>
GET /inventory/device/info - Network device inventoryGET /inventory/device/detail - Detailed device informationGET /stat/alert - Network alerts and eventsGET /inventory/network/info - Network topology
Office 365 users, groups, applications, and security configurations
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;
}
}
?>
GET /users - User accounts and profilesGET /groups - Security and distribution groupsGET /applications - Registered applicationsGET /auditLogs/signIns - Sign-in activityGET /identity/conditionalAccess/policies - Conditional access policies
All connectors follow the same pattern:
Copy these connector classes into your Laravel application and start collecting evidence automatically.
Start DeploymentStart with FREE rules-based processing, add AI strategically for complex analysis only.
View Budget PlanAchieve 70-90% automation of compliance evidence collection with these integrations.
View Full Roadmap