Production-ready Microsoft 365 integration package for comprehensive compliance automation. Covers SOC 2, ISO 27001, HIPAA, and NIST CSF requirements with real-world PHP implementation.
Everything your developers need to implement M365 compliance automation:
Full Laravel PHP classes ready to copy into your codebase
Migrations and schema updates for M365 integration
Environment setup, API credentials, and service registration
Unit tests, integration tests, and validation procedures
Base connector class with authentication and common functionality
Core connector class with OAuth authentication and base functionality
httpClient = new Client(['timeout' => 30]);
$this->initializeAuthentication();
}
private function initializeAuthentication(): 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}");
}
$this->tenantId = $apiKey->api_company_id;
// Check if we have a valid cached token
$cacheKey = "msgraph_token_{$this->clientId}";
$cachedToken = Cache::get($cacheKey);
if ($cachedToken && $cachedToken['expires_at'] > now()->addMinutes(5)) {
$this->accessToken = $cachedToken['access_token'];
} else {
// Get new token using client credentials flow
$this->refreshAccessToken($apiKey);
}
$this->headers = [
'Authorization' => 'Bearer ' . $this->accessToken,
'Content-Type' => 'application/json',
'Accept' => 'application/json'
];
}
private function refreshAccessToken(ApiKey $apiKey): void
{
$tokenUrl = "https://login.microsoftonline.com/{$this->tenantId}/oauth2/v2.0/token";
try {
$response = $this->httpClient->post($tokenUrl, [
'form_params' => [
'client_id' => $apiKey->key,
'client_secret' => $apiKey->secret,
'scope' => 'https://graph.microsoft.com/.default',
'grant_type' => 'client_credentials'
]
]);
$tokenData = json_decode($response->getBody()->getContents(), true);
$this->accessToken = $tokenData['access_token'];
$expiresIn = $tokenData['expires_in'] ?? 3600;
// Cache the token
$cacheKey = "msgraph_token_{$this->clientId}";
Cache::put($cacheKey, [
'access_token' => $this->accessToken,
'expires_at' => now()->addSeconds($expiresIn)
], $expiresIn - 300); // Cache for 5 minutes less than expiry
// Update database with new token info
$apiKey->update([
'access_token' => $this->accessToken,
'expires_at' => now()->addSeconds($expiresIn)
]);
} catch (RequestException $e) {
Log::error("Failed to refresh Microsoft Graph token for client {$this->clientId}: " . $e->getMessage());
throw new \Exception("Failed to authenticate with Microsoft Graph API");
}
}
protected function graphApiCall(string $method, string $endpoint, array $params = [], bool $useBeta = false): array
{
$baseUrl = $useBeta ? $this->betaUrl : $this->baseUrl;
$url = $baseUrl . '/' . ltrim($endpoint, '/');
$options = [
'headers' => $this->headers,
'query' => $params
];
try {
$response = $this->httpClient->request($method, $url, $options);
$data = json_decode($response->getBody()->getContents(), true);
// Handle paginated responses
if (isset($data['@odata.nextLink'])) {
$data['hasMore'] = true;
$data['nextLink'] = $data['@odata.nextLink'];
}
return $data;
} catch (RequestException $e) {
$statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 0;
// Handle token expiration
if ($statusCode === 401) {
$this->handleTokenExpiration();
// Retry once with new token
return $this->graphApiCall($method, $endpoint, $params, $useBeta);
}
Log::error("Graph API call failed: {$method} {$url} - " . $e->getMessage());
throw new \Exception("Microsoft Graph API call failed: " . $e->getMessage());
}
}
private function handleTokenExpiration(): void
{
$apiKey = ApiKey::where('client_id', $this->clientId)
->where('type', 'msgraph')
->first();
$this->refreshAccessToken($apiKey);
$this->headers['Authorization'] = 'Bearer ' . $this->accessToken;
}
protected function getAllPaginatedResults(string $endpoint, array $params = [], bool $useBeta = false): array
{
$allResults = [];
$nextLink = null;
do {
if ($nextLink) {
// For subsequent calls, use the full nextLink URL
$response = $this->httpClient->get($nextLink, ['headers' => $this->headers]);
$data = json_decode($response->getBody()->getContents(), true);
} else {
$data = $this->graphApiCall('GET', $endpoint, $params, $useBeta);
}
if (isset($data['value'])) {
$allResults = array_merge($allResults, $data['value']);
}
$nextLink = $data['@odata.nextLink'] ?? null;
// Safety break to prevent infinite loops
if (count($allResults) > 10000) {
Log::warning("Graph API pagination exceeded 10,000 results for endpoint: {$endpoint}");
break;
}
} while ($nextLink);
return $allResults;
}
public function validateConnection(): array
{
try {
// Test basic connectivity
$organization = $this->graphApiCall('GET', '/organization');
// Test required permissions
$permissionTests = [
'users' => '/users?$top=1',
'groups' => '/groups?$top=1',
'applications' => '/applications?$top=1',
'directoryRoles' => '/directoryRoles?$top=1'
];
$permissions = [];
foreach ($permissionTests as $scope => $endpoint) {
try {
$this->graphApiCall('GET', $endpoint);
$permissions[$scope] = true;
} catch (\Exception $e) {
$permissions[$scope] = false;
Log::warning("Missing Graph API permission for {$scope}: " . $e->getMessage());
}
}
return [
'connected' => true,
'tenant_id' => $this->tenantId,
'organization' => $organization['value'][0] ?? null,
'permissions' => $permissions,
'required_scopes' => $this->requiredScopes,
'missing_permissions' => array_keys(array_filter($permissions, fn($v) => !$v))
];
} catch (\Exception $e) {
return [
'connected' => false,
'error' => $e->getMessage(),
'tenant_id' => $this->tenantId
];
}
}
public function getTenantInfo(): array
{
$organization = $this->graphApiCall('GET', '/organization');
$org = $organization['value'][0] ?? [];
return [
'tenant_id' => $this->tenantId,
'display_name' => $org['displayName'] ?? 'Unknown',
'verified_domains' => array_column($org['verifiedDomains'] ?? [], 'name'),
'created_date' => $org['createdDateTime'] ?? null,
'country' => $org['countryLetterCode'] ?? null,
'privacy_profile' => $org['privacyProfile'] ?? null,
'technical_contacts' => $org['technicalNotificationMails'] ?? []
];
}
}
?>
Step-by-step Azure configuration for your dev team
User.Read.All - Read all user profilesGroup.Read.All - Read all groupsApplication.Read.All - Read applicationsDirectory.Read.All - Read directory dataPolicy.Read.All - Read organization policiesRoleManagement.Read.All - Read role assignmentsAuditLog.Read.All - Read audit logsSecurityEvents.Read.All - Read security events
# Environment Configuration (.env)
MSGRAPH_CLIENT_ID=your_azure_app_client_id
MSGRAPH_CLIENT_SECRET=your_azure_app_client_secret
MSGRAPH_TENANT_ID=your_azure_tenant_id
MSGRAPH_REDIRECT_URI=https://your-app.com/auth/msgraph/callback
# Database Setup - Add to existing api_keys table
INSERT INTO api_keys (
id, company_id, client_id, type,
`key`, secret, api_company_id,
created_by, updated_by, created_at, updated_at
) VALUES (
'generated_id', 'company_id', 'client_id', 'msgraph',
'your_azure_app_client_id', 'your_azure_app_client_secret', 'your_azure_tenant_id',
'user_id', 'user_id', NOW(), NOW()
);
SOC 2 CC6.1, ISO 27001 A.9.2, HIPAA Access Controls
Comprehensive user access analysis for multiple compliance frameworks
clientId}");
// Collect core user data
$users = $this->collectUsers();
$adminRoles = $this->collectAdminRoles();
$groups = $this->collectSecurityGroups();
$signInData = $this->collectSignInActivity();
$conditionalAccess = $this->collectConditionalAccessPolicies();
$mfaStatus = $this->collectMFAStatus();
// Enrich users with security context
$enrichedUsers = $this->enrichUsersWithSecurityData($users, $adminRoles, $groups, $signInData, $mfaStatus);
// Generate compliance analysis
$complianceAnalysis = $this->generateComplianceAnalysis($enrichedUsers, $conditionalAccess);
return [
'source' => 'msgraph',
'collection_type' => 'user_access_comprehensive',
'tenant_info' => $this->getTenantInfo(),
'total_users' => count($enrichedUsers),
'users' => $enrichedUsers,
'admin_roles' => $adminRoles,
'security_groups' => $groups,
'conditional_access_policies' => $conditionalAccess,
'compliance_analysis' => $complianceAnalysis,
'collection_timestamp' => now()->toISOString(),
'api_endpoints_used' => [
'/users',
'/directoryRoles',
'/groups',
'/auditLogs/signIns',
'/identity/conditionalAccess/policies',
'/reports/credentialUserRegistrationDetails'
]
];
}
private function collectUsers(): array
{
return $this->getAllPaginatedResults('/users', [
'$select' => implode(',', [
'id', 'userPrincipalName', 'displayName', 'givenName', 'surname',
'accountEnabled', 'createdDateTime', 'lastSignInDateTime',
'mail', 'department', 'jobTitle', 'companyName', 'country',
'assignedLicenses', 'assignedPlans', 'usageLocation'
])
]);
}
private function collectAdminRoles(): array
{
$directoryRoles = $this->getAllPaginatedResults('/directoryRoles');
$roleAssignments = [];
foreach ($directoryRoles as $role) {
$members = $this->getAllPaginatedResults("/directoryRoles/{$role['id']}/members");
$roleAssignments[$role['id']] = [
'role' => $role,
'members' => $members
];
}
return $roleAssignments;
}
private function collectSecurityGroups(): array
{
return $this->getAllPaginatedResults('/groups', [
'$filter' => "securityEnabled eq true",
'$select' => 'id,displayName,description,groupTypes,securityEnabled,mailEnabled,createdDateTime'
]);
}
private function collectSignInActivity(): array
{
// Get sign-in data for last 30 days
$startDate = now()->subDays(30)->toISOString();
try {
return $this->getAllPaginatedResults('/auditLogs/signIns', [
'$filter' => "createdDateTime ge {$startDate}",
'$select' => 'id,createdDateTime,userPrincipalName,userId,appDisplayName,clientAppUsed,deviceDetail,location,riskDetail,riskLevelAggregated,riskLevelDuringSignIn,riskState,status',
'$top' => 1000
]);
} catch (\Exception $e) {
Log::warning("Could not collect sign-in data: " . $e->getMessage());
return [];
}
}
private function collectConditionalAccessPolicies(): array
{
try {
return $this->getAllPaginatedResults('/identity/conditionalAccess/policies');
} catch (\Exception $e) {
Log::warning("Could not collect conditional access policies: " . $e->getMessage());
return [];
}
}
private function collectMFAStatus(): array
{
try {
return $this->getAllPaginatedResults('/reports/credentialUserRegistrationDetails');
} catch (\Exception $e) {
Log::warning("Could not collect MFA status: " . $e->getMessage());
return [];
}
}
private function enrichUsersWithSecurityData(array $users, array $adminRoles, array $groups, array $signInData, array $mfaStatus): array
{
// Index data for fast lookup
$userSignIns = $this->indexSignInsByUser($signInData);
$userMFA = $this->indexMFAByUser($mfaStatus);
$adminUsers = $this->getAdminUserIds($adminRoles);
return array_map(function($user) use ($userSignIns, $userMFA, $adminUsers) {
$userId = $user['id'];
$upn = $user['userPrincipalName'];
// Calculate user metrics
$signIns = $userSignIns[$userId] ?? [];
$lastSignIn = $this->getLastSignInDate($signIns, $user);
$daysSinceSignIn = $lastSignIn ? Carbon::parse($lastSignIn)->diffInDays(now()) : null;
return [
'id' => $userId,
'user_principal_name' => $upn,
'display_name' => $user['displayName'],
'email' => $user['mail'] ?? $upn,
'account_enabled' => $user['accountEnabled'],
'created_date' => $user['createdDateTime'],
'last_signin_date' => $lastSignIn,
'days_since_signin' => $daysSinceSignIn,
'department' => $user['department'],
'job_title' => $user['jobTitle'],
'company' => $user['companyName'],
'country' => $user['country'],
// License information
'assigned_licenses' => $this->formatLicenseInfo($user['assignedLicenses'] ?? []),
'has_license' => !empty($user['assignedLicenses']),
// Security information
'is_admin' => in_array($userId, $adminUsers),
'admin_roles' => $this->getUserAdminRoles($userId, $adminRoles),
'mfa_status' => $userMFA[$upn] ?? null,
'signin_activity' => [
'total_signins_30d' => count($signIns),
'unique_apps_30d' => count(array_unique(array_column($signIns, 'appDisplayName'))),
'failed_signins_30d' => count(array_filter($signIns, fn($s) => ($s['status']['errorCode'] ?? 0) !== 0)),
'risky_signins_30d' => count(array_filter($signIns, fn($s) => ($s['riskLevelDuringSignIn'] ?? 'none') !== 'none'))
],
// Compliance flags
'compliance_flags' => $this->generateUserComplianceFlags($user, $signIns, $userMFA[$upn] ?? null, $adminUsers),
'risk_score' => $this->calculateUserRiskScore($user, $signIns, $userMFA[$upn] ?? null, $adminUsers)
];
}, $users);
}
private function generateUserComplianceFlags(array $user, array $signIns, ?array $mfaStatus, array $adminUsers): array
{
$flags = [];
$userId = $user['id'];
$upn = $user['userPrincipalName'];
// Check for inactive users
$lastSignIn = $this->getLastSignInDate($signIns, $user);
$daysSinceSignIn = $lastSignIn ? Carbon::parse($lastSignIn)->diffInDays(now()) : null;
if ($daysSinceSignIn === null || $daysSinceSignIn > 90) {
$flags[] = [
'flag' => 'inactive_user',
'severity' => 'high',
'description' => 'User has not signed in for 90+ days or never signed in',
'framework_relevance' => ['SOC2_CC6.1', 'ISO27001_A.9.2.6', 'HIPAA_164.308.a.4']
];
}
// Check for enabled accounts without licenses
if ($user['accountEnabled'] && empty($user['assignedLicenses'])) {
$flags[] = [
'flag' => 'enabled_unlicensed',
'severity' => 'medium',
'description' => 'Account is enabled but has no Office 365 licenses assigned',
'framework_relevance' => ['SOC2_CC6.1', 'ISO27001_A.9.2.1']
];
}
// Check admin users without MFA
if (in_array($userId, $adminUsers)) {
if (!$mfaStatus || !($mfaStatus['isMfaRegistered'] ?? false)) {
$flags[] = [
'flag' => 'admin_no_mfa',
'severity' => 'critical',
'description' => 'Administrative user does not have MFA registered',
'framework_relevance' => ['SOC2_CC6.1', 'ISO27001_A.9.4.2', 'NIST_CSF_PR.AC-7', 'HIPAA_164.308.a.5.ii.D']
];
}
}
// Check for excessive failed sign-ins
$failedSignIns = array_filter($signIns, fn($s) => ($s['status']['errorCode'] ?? 0) !== 0);
if (count($failedSignIns) > 10) {
$flags[] = [
'flag' => 'excessive_failed_signins',
'severity' => 'medium',
'description' => 'User has more than 10 failed sign-ins in the last 30 days',
'framework_relevance' => ['SOC2_CC6.1', 'ISO27001_A.9.4.3', 'NIST_CSF_PR.AC-7']
];
}
// Check for risky sign-ins
$riskySignIns = array_filter($signIns, fn($s) => in_array($s['riskLevelDuringSignIn'] ?? 'none', ['medium', 'high']));
if (count($riskySignIns) > 0) {
$flags[] = [
'flag' => 'risky_signin_activity',
'severity' => 'high',
'description' => 'User has ' . count($riskySignIns) . ' risky sign-ins detected by Azure AD',
'framework_relevance' => ['SOC2_CC6.1', 'ISO27001_A.9.4.3', 'NIST_CSF_DE.AE-2']
];
}
// Check admin naming convention
if (in_array($userId, $adminUsers)) {
if (!preg_match('/admin|adm|root|svc/i', $upn)) {
$flags[] = [
'flag' => 'admin_poor_naming',
'severity' => 'low',
'description' => 'Administrative account does not follow naming convention',
'framework_relevance' => ['SOC2_CC6.1', 'ISO27001_A.9.2.1']
];
}
}
return $flags;
}
private function calculateUserRiskScore(array $user, array $signIns, ?array $mfaStatus, array $adminUsers): int
{
$riskScore = 0;
$userId = $user['id'];
// Base risk factors
if (!$user['accountEnabled']) {
return 0; // Disabled accounts have no risk
}
// Inactive user risk
$lastSignIn = $this->getLastSignInDate($signIns, $user);
$daysSinceSignIn = $lastSignIn ? Carbon::parse($lastSignIn)->diffInDays(now()) : null;
if ($daysSinceSignIn === null) {
$riskScore += 15; // Never signed in
} elseif ($daysSinceSignIn > 90) {
$riskScore += 20; // Very stale
} elseif ($daysSinceSignIn > 30) {
$riskScore += 10; // Stale
}
// Admin user risk
if (in_array($userId, $adminUsers)) {
$riskScore += 15; // Base admin risk
if (!$mfaStatus || !($mfaStatus['isMfaRegistered'] ?? false)) {
$riskScore += 35; // Admin without MFA is critical
}
}
// Sign-in activity risk
$failedSignIns = array_filter($signIns, fn($s) => ($s['status']['errorCode'] ?? 0) !== 0);
$riskScore += min(count($failedSignIns), 15); // Max 15 points for failed sign-ins
$riskySignIns = array_filter($signIns, fn($s) => in_array($s['riskLevelDuringSignIn'] ?? 'none', ['medium', 'high']));
$riskScore += count($riskySignIns) * 5; // 5 points per risky sign-in
// License risk
if (empty($user['assignedLicenses'])) {
$riskScore += 5; // Unlicensed users are suspicious
}
return min($riskScore, 100); // Cap at 100
}
private function generateComplianceAnalysis(array $enrichedUsers, array $conditionalAccessPolicies): array
{
$totalUsers = count($enrichedUsers);
$enabledUsers = array_filter($enrichedUsers, fn($u) => $u['account_enabled']);
$adminUsers = array_filter($enrichedUsers, fn($u) => $u['is_admin']);
$usersWithMFA = array_filter($enrichedUsers, fn($u) => $u['mfa_status']['isMfaRegistered'] ?? false);
$inactiveUsers = array_filter($enrichedUsers, fn($u) => ($u['days_since_signin'] ?? 999) > 90);
$riskyUsers = array_filter($enrichedUsers, fn($u) => $u['risk_score'] > 50);
// Compliance scoring by framework
$frameworks = [
'SOC2_CC6.1' => $this->calculateSOC2Score($enrichedUsers),
'ISO27001_A.9.2' => $this->calculateISO27001Score($enrichedUsers),
'HIPAA_ACCESS' => $this->calculateHIPAAScore($enrichedUsers),
'NIST_CSF_PR.AC' => $this->calculateNISTScore($enrichedUsers)
];
return [
'summary_metrics' => [
'total_users' => $totalUsers,
'enabled_users' => count($enabledUsers),
'admin_users' => count($adminUsers),
'mfa_enabled_users' => count($usersWithMFA),
'inactive_users' => count($inactiveUsers),
'high_risk_users' => count($riskyUsers),
'mfa_adoption_rate' => $totalUsers > 0 ? round((count($usersWithMFA) / $totalUsers) * 100, 1) : 0,
'admin_mfa_rate' => count($adminUsers) > 0 ? round((count(array_filter($adminUsers, fn($u) => $u['mfa_status']['isMfaRegistered'] ?? false)) / count($adminUsers)) * 100, 1) : 0
],
'framework_scores' => $frameworks,
'policy_analysis' => [
'conditional_access_policies' => count($conditionalAccessPolicies),
'enabled_policies' => count(array_filter($conditionalAccessPolicies, fn($p) => $p['state'] === 'enabled')),
'mfa_policies' => count(array_filter($conditionalAccessPolicies, fn($p) => in_array('mfa', $p['grantControls']['builtInControls'] ?? [])))
],
'recommendations' => $this->generateRecommendations($enrichedUsers, $conditionalAccessPolicies),
'compliance_flags' => $this->aggregateComplianceFlags($enrichedUsers)
];
}
private function calculateSOC2Score(array $users): array
{
$enabledUsers = array_filter($users, fn($u) => $u['account_enabled']);
$totalEnabled = count($enabledUsers);
if ($totalEnabled === 0) {
return ['score' => 100, 'details' => 'No enabled users found'];
}
$score = 100;
$deductions = [];
// Deduct for inactive users
$inactiveUsers = array_filter($enabledUsers, fn($u) => ($u['days_since_signin'] ?? 999) > 90);
if (count($inactiveUsers) > 0) {
$deduction = min(30, (count($inactiveUsers) / $totalEnabled) * 50);
$score -= $deduction;
$deductions[] = "Inactive users: -{$deduction} points";
}
// Deduct for admin users without MFA
$adminUsers = array_filter($enabledUsers, fn($u) => $u['is_admin']);
$adminWithoutMFA = array_filter($adminUsers, fn($u) => !($u['mfa_status']['isMfaRegistered'] ?? false));
if (count($adminWithoutMFA) > 0) {
$deduction = count($adminWithoutMFA) * 15;
$score -= $deduction;
$deductions[] = "Admin users without MFA: -{$deduction} points";
}
return [
'score' => max(0, $score),
'details' => $deductions,
'requirements_met' => $score >= 80
];
}
// ... Additional framework scoring methods would be implemented similarly
}
?>
SOC 2 CC6.7, ISO 27001 A.14.2, NIST CSF PR.DS-2
Analyze registered applications, service principals, and API permissions
clientId}");
// Collect application data
$applications = $this->collectApplications();
$servicePrincipals = $this->collectServicePrincipals();
$oauthConsents = $this->collectOAuthConsents();
$appRoleAssignments = $this->collectAppRoleAssignments();
// Analyze security posture
$securityAnalysis = $this->analyzeApplicationSecurity($applications, $servicePrincipals, $oauthConsents);
return [
'source' => 'msgraph',
'collection_type' => 'application_security',
'tenant_info' => $this->getTenantInfo(),
'applications' => $applications,
'service_principals' => $servicePrincipals,
'oauth_consents' => $oauthConsents,
'app_role_assignments' => $appRoleAssignments,
'security_analysis' => $securityAnalysis,
'collection_timestamp' => now()->toISOString(),
'api_endpoints_used' => [
'/applications',
'/servicePrincipals',
'/oauth2PermissionGrants',
'/servicePrincipals/{id}/appRoleAssignments'
]
];
}
private function collectApplications(): array
{
$apps = $this->getAllPaginatedResults('/applications', [
'$select' => implode(',', [
'id', 'appId', 'displayName', 'createdDateTime', 'publisherDomain',
'signInAudience', 'web', 'spa', 'api', 'requiredResourceAccess',
'passwordCredentials', 'keyCredentials', 'identifierUris'
])
]);
return array_map(function($app) {
return [
'id' => $app['id'],
'app_id' => $app['appId'],
'display_name' => $app['displayName'],
'created_date' => $app['createdDateTime'],
'publisher_domain' => $app['publisherDomain'],
'signin_audience' => $app['signInAudience'],
'redirect_uris' => $this->extractRedirectUris($app),
'api_permissions' => $app['requiredResourceAccess'] ?? [],
'has_secrets' => !empty($app['passwordCredentials']) || !empty($app['keyCredentials']),
'secret_count' => count($app['passwordCredentials'] ?? []),
'cert_count' => count($app['keyCredentials'] ?? []),
'identifier_uris' => $app['identifierUris'] ?? [],
'security_analysis' => $this->analyzeApplicationSecurityProfile($app),
'compliance_flags' => $this->generateApplicationComplianceFlags($app)
];
}, $apps);
}
private function analyzeApplicationSecurityProfile(array $app): array
{
$security = [
'risk_level' => 'low',
'risk_factors' => [],
'security_score' => 100
];
// Check publisher verification
if (empty($app['publisherDomain']) || $app['publisherDomain'] === 'mssubstratus.com') {
$security['risk_factors'][] = 'Unverified publisher';
$security['security_score'] -= 20;
}
// Check audience scope
$audience = $app['signInAudience'] ?? '';
if (in_array($audience, ['AzureADMultipleOrgs', 'AzureADandPersonalMicrosoftAccount'])) {
$security['risk_factors'][] = 'Broad sign-in audience allows external users';
$security['security_score'] -= 15;
}
// Check redirect URIs security
$redirectUris = $this->extractRedirectUris($app);
foreach ($redirectUris as $uri) {
if (!str_starts_with($uri, 'https://')) {
$security['risk_factors'][] = 'Insecure redirect URI (non-HTTPS)';
$security['security_score'] -= 25;
break;
}
if (str_contains($uri, 'localhost') || str_contains($uri, '127.0.0.1')) {
$security['risk_factors'][] = 'Development redirect URI in production';
$security['security_score'] -= 10;
}
}
// Check API permissions risk
$permissions = $app['requiredResourceAccess'] ?? [];
$highRiskPermissions = $this->checkHighRiskPermissions($permissions);
if (!empty($highRiskPermissions)) {
$security['risk_factors'][] = 'High-risk API permissions: ' . implode(', ', $highRiskPermissions);
$security['security_score'] -= count($highRiskPermissions) * 10;
}
// Check credential security
$passwordCreds = $app['passwordCredentials'] ?? [];
foreach ($passwordCreds as $cred) {
if (isset($cred['endDateTime'])) {
$expiryDate = Carbon::parse($cred['endDateTime']);
if ($expiryDate->isPast()) {
$security['risk_factors'][] = 'Expired client secret';
$security['security_score'] -= 15;
} elseif ($expiryDate->diffInDays(now()) < 30) {
$security['risk_factors'][] = 'Client secret expires soon';
$security['security_score'] -= 5;
}
}
}
// Determine risk level
if ($security['security_score'] >= 80) {
$security['risk_level'] = 'low';
} elseif ($security['security_score'] >= 60) {
$security['risk_level'] = 'medium';
} else {
$security['risk_level'] = 'high';
}
return $security;
}
private function checkHighRiskPermissions(array $requiredResourceAccess): array
{
$highRiskPermissions = [];
// Define high-risk permission patterns
$riskPatterns = [
'Directory.ReadWrite.All' => 'Full directory write access',
'User.ReadWrite.All' => 'Read/write all user profiles',
'Group.ReadWrite.All' => 'Read/write all groups',
'Application.ReadWrite.All' => 'Read/write all applications',
'RoleManagement.ReadWrite.Directory' => 'Manage directory roles',
'Policy.ReadWrite.ConditionalAccess' => 'Manage conditional access policies',
'DeviceManagementConfiguration.ReadWrite.All' => 'Manage device configuration',
'SecurityEvents.ReadWrite.All' => 'Read/write security events'
];
foreach ($requiredResourceAccess as $resource) {
foreach ($resource['resourceAccess'] ?? [] as $permission) {
// This would need to be enhanced with actual permission ID to name mapping
// For now, we'll check against common high-risk permission IDs
$permissionId = $permission['id'];
if ($this->isHighRiskPermissionId($permissionId)) {
$highRiskPermissions[] = $permissionId;
}
}
}
return $highRiskPermissions;
}
private function generateApplicationComplianceFlags(array $app): array
{
$flags = [];
// SOC 2 CC6.7 - Logical and Physical Access Controls
if (empty($app['publisherDomain']) || $app['publisherDomain'] === 'mssubstratus.com') {
$flags[] = [
'flag' => 'unverified_publisher',
'severity' => 'medium',
'description' => 'Application publisher is not verified',
'framework_relevance' => ['SOC2_CC6.7', 'ISO27001_A.14.2.5']
];
}
// Check for overprivileged applications
$permissions = $app['requiredResourceAccess'] ?? [];
if (count($permissions) > 10) {
$flags[] = [
'flag' => 'excessive_permissions',
'severity' => 'medium',
'description' => 'Application requests excessive API permissions',
'framework_relevance' => ['SOC2_CC6.7', 'ISO27001_A.14.2.2', 'NIST_CSF_PR.AC-4']
];
}
// HTTPS redirect requirement
$redirectUris = $this->extractRedirectUris($app);
$insecureUris = array_filter($redirectUris, fn($uri) => !str_starts_with($uri, 'https://'));
if (!empty($insecureUris)) {
$flags[] = [
'flag' => 'insecure_redirect_uris',
'severity' => 'high',
'description' => 'Application uses non-HTTPS redirect URIs',
'framework_relevance' => ['SOC2_CC6.7', 'ISO27001_A.14.1.3', 'NIST_CSF_PR.DS-2']
];
}
return $flags;
}
private function collectOAuthConsents(): array
{
try {
return $this->getAllPaginatedResults('/oauth2PermissionGrants');
} catch (\Exception $e) {
Log::warning("Could not collect OAuth consents: " . $e->getMessage());
return [];
}
}
private function analyzeApplicationSecurity(array $applications, array $servicePrincipals, array $oauthConsents): array
{
$totalApps = count($applications);
$highRiskApps = array_filter($applications, fn($app) => $app['security_analysis']['risk_level'] === 'high');
$unverifiedApps = array_filter($applications, fn($app) => empty($app['publisher_domain']) || $app['publisher_domain'] === 'mssubstratus.com');
$overPrivilegedApps = array_filter($applications, fn($app) => count($app['api_permissions']) > 10);
return [
'summary_metrics' => [
'total_applications' => $totalApps,
'high_risk_applications' => count($highRiskApps),
'unverified_publishers' => count($unverifiedApps),
'overprivileged_applications' => count($overPrivilegedApps),
'total_service_principals' => count($servicePrincipals),
'oauth_consents' => count($oauthConsents)
],
'risk_distribution' => [
'high' => count(array_filter($applications, fn($app) => $app['security_analysis']['risk_level'] === 'high')),
'medium' => count(array_filter($applications, fn($app) => $app['security_analysis']['risk_level'] === 'medium')),
'low' => count(array_filter($applications, fn($app) => $app['security_analysis']['risk_level'] === 'low'))
],
'compliance_summary' => [
'soc2_cc67_score' => $this->calculateSOC2ApplicationScore($applications),
'iso27001_a142_score' => $this->calculateISO27001ApplicationScore($applications),
'nist_csf_prds2_score' => $this->calculateNISTApplicationScore($applications)
],
'recommendations' => $this->generateApplicationRecommendations($applications)
];
}
}
?>
Collects device compliance data for mobile device management and endpoint protection validation.
authenticate($clientId);
return [
'managed_devices' => $this->collectManagedDevices(),
'compliance_policies' => $this->collectCompliancePolicies(),
'device_configurations' => $this->collectDeviceConfigurations(),
'compliance_status' => $this->analyzeDeviceCompliance(),
'framework_mapping' => $this->mapToComplianceFrameworks()
];
}
private function collectManagedDevices(): array
{
try {
$devices = $this->getAllPaginatedResults('/deviceManagement/managedDevices');
return array_map(function($device) {
return [
'id' => $device['id'],
'device_name' => $device['deviceName'],
'platform' => $device['operatingSystem'],
'compliance_state' => $device['complianceState'],
'last_sync' => $device['lastSyncDateTime'],
'enrollment_type' => $device['managementType'],
'ownership' => $device['ownerType'],
'encryption_status' => $device['isEncrypted'] ?? null,
'security_analysis' => $this->analyzeDeviceSecurity($device)
];
}, $devices);
} catch (\Exception $e) {
Log::error("Failed to collect managed devices: " . $e->getMessage());
return [];
}
}
private function collectCompliancePolicies(): array
{
try {
return $this->getAllPaginatedResults('/deviceManagement/deviceCompliancePolicies');
} catch (\Exception $e) {
Log::error("Failed to collect compliance policies: " . $e->getMessage());
return [];
}
}
private function collectDeviceConfigurations(): array
{
try {
return $this->getAllPaginatedResults('/deviceManagement/deviceConfigurations');
} catch (\Exception $e) {
Log::error("Failed to collect device configurations: " . $e->getMessage());
return [];
}
}
private function analyzeDeviceCompliance(): array
{
$devices = $this->collectManagedDevices();
$total = count($devices);
$compliant = count(array_filter($devices, fn($d) => $d['compliance_state'] === 'compliant'));
$nonCompliant = count(array_filter($devices, fn($d) => $d['compliance_state'] === 'noncompliant'));
$unknown = $total - $compliant - $nonCompliant;
return [
'total_devices' => $total,
'compliant_devices' => $compliant,
'non_compliant_devices' => $nonCompliant,
'unknown_compliance' => $unknown,
'compliance_percentage' => $total > 0 ? round(($compliant / $total) * 100, 2) : 0,
'encryption_compliance' => $this->analyzeEncryptionCompliance($devices),
'platform_breakdown' => $this->analyzePlatformCompliance($devices)
];
}
private function mapToComplianceFrameworks(): array
{
return [
'SOC2_CC6.1' => [
'control_description' => 'Logical and Physical Access Controls',
'evidence_collected' => [
'managed_device_inventory',
'device_compliance_status',
'encryption_requirements'
],
'compliance_score' => $this->calculateSOC2DeviceScore()
],
'ISO27001_A.12.6.2' => [
'control_description' => 'Restrictions on software installation',
'evidence_collected' => [
'device_configuration_policies',
'software_restriction_policies'
],
'compliance_score' => $this->calculateISO27001DeviceScore()
],
'NIST_CSF_PR.AC-3' => [
'control_description' => 'Remote access is managed',
'evidence_collected' => [
'mobile_device_management_policies',
'device_compliance_enforcement'
],
'compliance_score' => $this->calculateNISTDeviceScore()
]
];
}
}
?>
Collects security events and alerts for incident response and continuous monitoring.
authenticate($clientId);
return [
'security_alerts' => $this->collectSecurityAlerts(),
'security_incidents' => $this->collectSecurityIncidents(),
'threat_indicators' => $this->collectThreatIndicators(),
'security_score' => $this->collectSecurityScore(),
'incident_analysis' => $this->analyzeSecurityIncidents(),
'framework_mapping' => $this->mapSecurityToFrameworks()
];
}
private function collectSecurityAlerts(): array
{
try {
$alerts = $this->getAllPaginatedResults('/security/alerts_v2');
return array_map(function($alert) {
return [
'id' => $alert['id'],
'title' => $alert['title'],
'severity' => $alert['severity'],
'status' => $alert['status'],
'category' => $alert['category'],
'created_datetime' => $alert['createdDateTime'],
'determination' => $alert['determination'],
'classification' => $alert['classification'],
'assigned_to' => $alert['assignedTo'],
'mitre_techniques' => $alert['mitreTechniques'] ?? [],
'evidence' => $alert['evidence'] ?? [],
'compliance_impact' => $this->assessComplianceImpact($alert)
];
}, $alerts);
} catch (\Exception $e) {
Log::error("Failed to collect security alerts: " . $e->getMessage());
return [];
}
}
private function collectSecurityScore(): array
{
try {
$response = $this->makeRequest('/security/secureScores', 'GET', [], [
'$top' => 1,
'$orderby' => 'createdDateTime desc'
]);
$latestScore = $response['value'][0] ?? null;
if (!$latestScore) {
return ['error' => 'No security score data available'];
}
return [
'current_score' => $latestScore['currentScore'],
'max_score' => $latestScore['maxScore'],
'percentage' => round(($latestScore['currentScore'] / $latestScore['maxScore']) * 100, 2),
'created_date' => $latestScore['createdDateTime'],
'control_scores' => $latestScore['controlScores'] ?? [],
'average_comparative_scores' => $latestScore['averageComparativeScores'] ?? []
];
} catch (\Exception $e) {
Log::error("Failed to collect security score: " . $e->getMessage());
return ['error' => $e->getMessage()];
}
}
private function analyzeSecurityIncidents(): array
{
$alerts = $this->collectSecurityAlerts();
$totalAlerts = count($alerts);
$highSeverity = count(array_filter($alerts, fn($a) => $a['severity'] === 'high'));
$openAlerts = count(array_filter($alerts, fn($a) => $a['status'] === 'new' || $a['status'] === 'inProgress'));
$resolvedAlerts = count(array_filter($alerts, fn($a) => $a['status'] === 'resolved'));
return [
'total_alerts' => $totalAlerts,
'high_severity_alerts' => $highSeverity,
'open_alerts' => $openAlerts,
'resolved_alerts' => $resolvedAlerts,
'resolution_rate' => $totalAlerts > 0 ? round(($resolvedAlerts / $totalAlerts) * 100, 2) : 0,
'average_resolution_time' => $this->calculateAverageResolutionTime($alerts),
'top_threat_categories' => $this->getTopThreatCategories($alerts),
'mitre_attack_coverage' => $this->analyzeMitreCoverage($alerts)
];
}
private function mapSecurityToFrameworks(): array
{
return [
'SOC2_CC7.1' => [
'control_description' => 'System Monitoring',
'evidence_collected' => [
'security_alerts_monitoring',
'incident_response_metrics',
'threat_detection_capabilities'
],
'compliance_score' => $this->calculateSOC2SecurityScore()
],
'ISO27001_A.16.1.4' => [
'control_description' => 'Assessment of and decision on information security events',
'evidence_collected' => [
'security_incident_classification',
'incident_response_procedures',
'security_event_analysis'
],
'compliance_score' => $this->calculateISO27001SecurityScore()
],
'NIST_CSF_DE.CM-1' => [
'control_description' => 'The network is monitored to detect potential cybersecurity events',
'evidence_collected' => [
'continuous_security_monitoring',
'threat_indicator_collection',
'security_score_tracking'
],
'compliance_score' => $this->calculateNISTSecurityScore()
]
];
}
}
?>
Centralized analysis engine that correlates data from all M365 collectors to generate compliance insights.
userCollector = $userCollector;
$this->appCollector = $appCollector;
$this->deviceCollector = $deviceCollector;
$this->securityCollector = $securityCollector;
}
public function generateComprehensiveAnalysis(int $clientId): array
{
// Collect data from all sources
$userData = $this->userCollector->collect($clientId);
$appData = $this->appCollector->collect($clientId);
$deviceData = $this->deviceCollector->collect($clientId);
$securityData = $this->securityCollector->collect($clientId);
// Generate cross-domain analysis
return [
'executive_summary' => $this->generateExecutiveSummary($userData, $appData, $deviceData, $securityData),
'compliance_scores' => $this->calculateComplianceScores($userData, $appData, $deviceData, $securityData),
'risk_assessment' => $this->performRiskAssessment($userData, $appData, $deviceData, $securityData),
'framework_compliance' => $this->assessFrameworkCompliance($userData, $appData, $deviceData, $securityData),
'recommendations' => $this->generateRecommendations($userData, $appData, $deviceData, $securityData),
'evidence_packages' => $this->createEvidencePackages($userData, $appData, $deviceData, $securityData),
'automation_opportunities' => $this->identifyAutomationOpportunities($userData, $appData, $deviceData, $securityData)
];
}
private function generateExecutiveSummary(array $userData, array $appData, array $deviceData, array $securityData): array
{
$totalUsers = count($userData['user_accounts'] ?? []);
$privilegedUsers = count(array_filter($userData['user_accounts'] ?? [], fn($u) => $u['is_privileged']));
$totalApps = count($appData['applications'] ?? []);
$highRiskApps = count(array_filter($appData['applications'] ?? [], fn($a) => $a['security_analysis']['risk_level'] === 'high'));
$totalDevices = $deviceData['compliance_status']['total_devices'] ?? 0;
$compliantDevices = $deviceData['compliance_status']['compliant_devices'] ?? 0;
$openSecurityAlerts = $securityData['incident_analysis']['open_alerts'] ?? 0;
return [
'environment_overview' => [
'total_users' => $totalUsers,
'privileged_users' => $privilegedUsers,
'privileged_user_percentage' => $totalUsers > 0 ? round(($privilegedUsers / $totalUsers) * 100, 2) : 0,
'total_applications' => $totalApps,
'high_risk_applications' => $highRiskApps,
'total_managed_devices' => $totalDevices,
'device_compliance_rate' => $totalDevices > 0 ? round(($compliantDevices / $totalDevices) * 100, 2) : 0,
'open_security_alerts' => $openSecurityAlerts
],
'overall_compliance_score' => $this->calculateOverallComplianceScore($userData, $appData, $deviceData, $securityData),
'top_risks' => $this->identifyTopRisks($userData, $appData, $deviceData, $securityData),
'immediate_actions_required' => $this->identifyImmediateActions($userData, $appData, $deviceData, $securityData)
];
}
private function calculateComplianceScores(array $userData, array $appData, array $deviceData, array $securityData): array
{
return [
'SOC2' => [
'CC6.1_user_access' => $this->calculateSOC2UserScore($userData),
'CC6.7_application_security' => $this->calculateSOC2AppScore($appData),
'CC7.1_system_monitoring' => $this->calculateSOC2SecurityScore($securityData),
'overall_soc2_score' => $this->calculateOverallSOC2Score($userData, $appData, $deviceData, $securityData)
],
'ISO27001' => [
'A.9.2.1_user_registration' => $this->calculateISO27001UserScore($userData),
'A.14.2.5_secure_development' => $this->calculateISO27001AppScore($appData),
'A.12.6.2_software_restrictions' => $this->calculateISO27001DeviceScore($deviceData),
'A.16.1.4_incident_assessment' => $this->calculateISO27001SecurityScore($securityData),
'overall_iso27001_score' => $this->calculateOverallISO27001Score($userData, $appData, $deviceData, $securityData)
],
'NIST_CSF' => [
'PR.AC_1_identity_management' => $this->calculateNISTUserScore($userData),
'PR.DS_2_data_transit_protection' => $this->calculateNISTAppScore($appData),
'PR.AC_3_remote_access' => $this->calculateNISTDeviceScore($deviceData),
'DE.CM_1_network_monitoring' => $this->calculateNISTSecurityScore($securityData),
'overall_nist_csf_score' => $this->calculateOverallNISTScore($userData, $appData, $deviceData, $securityData)
]
];
}
private function createEvidencePackages(array $userData, array $appData, array $deviceData, array $securityData): array
{
return [
'SOC2_Evidence_Package' => [
'CC6.1_User_Access_Controls' => [
'user_access_review' => $userData['access_analysis'],
'privileged_user_monitoring' => $userData['privileged_users'],
'mfa_compliance' => $userData['mfa_analysis']
],
'CC6.7_Application_Security' => [
'application_inventory' => $appData['applications'],
'application_risk_assessment' => $appData['security_analysis'],
'oauth_consent_analysis' => $appData['oauth_consents']
],
'CC7.1_System_Monitoring' => [
'security_alerts' => $securityData['security_alerts'],
'incident_response_metrics' => $securityData['incident_analysis'],
'security_score_trends' => $securityData['security_score']
]
],
'Audit_Ready_Reports' => [
'user_access_certification' => $this->generateUserAccessCertification($userData),
'application_security_assessment' => $this->generateApplicationSecurityAssessment($appData),
'device_compliance_report' => $this->generateDeviceComplianceReport($deviceData),
'security_incident_summary' => $this->generateSecurityIncidentSummary($securityData)
]
];
}
public function storeEvidenceInDatabase(int $clientId, array $analysisResults): void
{
// Store comprehensive analysis results in assessment_events_questions table
foreach ($analysisResults['framework_compliance'] as $framework => $controls) {
foreach ($controls as $controlId => $controlData) {
AssessmentEventsQuestion::updateOrCreate(
[
'client_id' => $clientId,
'framework' => $framework,
'control_id' => $controlId,
'evidence_source' => 'M365_Graph_API'
],
[
'compliance_score' => $controlData['compliance_score'],
'evidence_collected' => json_encode($controlData['evidence_collected']),
'analysis_date' => now(),
'automated_collection' => true,
'risk_level' => $this->determineRiskLevel($controlData['compliance_score']),
'recommendations' => json_encode($controlData['recommendations'] ?? [])
]
);
}
}
}
}
?>
collector = new M365UserAccessCollector();
}
/** @test */
public function it_can_collect_user_data()
{
// Mock Graph API responses
$this->mockGraphApiResponse('/users', [
'value' => [
[
'id' => 'user-1',
'userPrincipalName' => 'test@example.com',
'accountEnabled' => true
]
]
]);
$result = $this->collector->collect(1);
$this->assertArrayHasKey('user_accounts', $result);
$this->assertArrayHasKey('privileged_users', $result);
$this->assertArrayHasKey('mfa_analysis', $result);
}
/** @test */
public function it_identifies_privileged_users()
{
$users = [
['id' => '1', 'roles' => ['Global Administrator']],
['id' => '2', 'roles' => ['User']]
];
$privileged = $this->collector->identifyPrivilegedUsers($users);
$this->assertCount(1, $privileged);
$this->assertEquals('1', $privileged[0]['id']);
}
}
?>
create();
// Set up test API keys
$client->apiKeys()->create([
'provider' => 'microsoft_graph',
'client_id' => 'test-client-id',
'client_secret' => encrypt('test-secret'),
'tenant_id' => 'test-tenant-id'
]);
$analyzer = app(M365ComplianceAnalyzer::class);
$result = $analyzer->generateComprehensiveAnalysis($client->id);
$this->assertArrayHasKey('executive_summary', $result);
$this->assertArrayHasKey('compliance_scores', $result);
$this->assertArrayHasKey('evidence_packages', $result);
}
}
?>
Complete commands to deploy the M365 integration:
# 1. Install the M365 collectors
cp -r app/Services/Collectors/M365/ /path/to/your/laravel/app/Services/Collectors/
cp -r app/Services/Analysis/M365/ /path/to/your/laravel/app/Services/Analysis/
# 2. Register the service provider
echo "App\\Providers\\M365CollectorServiceProvider::class," >> config/app.php
# 3. Run database migrations
php artisan migrate
# 4. Publish config files
php artisan vendor:publish --tag=m365-config
# 5. Set up queue worker for background collection
php artisan queue:work --queue=m365-collection
# 6. Schedule automated collection (add to crontab)
# 0 2 * * * cd /path/to/laravel && php artisan m365:collect-all
# 7. Test the integration
php artisan m365:test-connection --client-id=1
php artisan m365:collect-evidence --client-id=1 --framework=SOC2