Home / Office 365 / Microsoft 365 Tenant Administration and Health Monitoring
Office 365

Microsoft 365 Tenant Administration and Health Monitoring

Implement structured Microsoft 365 tenant administration with health dashboards, change management, operational scripts, and incident response readiness.

What you will learn

Practical execution with concise explanations, real implementation patterns, and production-ready recommendations.

param( [string]$OutputPath = "C:\Reports\ServiceHealth-$(Get-Date -Format 'yyyyMMdd-HHmm').json", [string]$TeamsWebhookUrl = "https://contoso.webhook.office.com/webhookb2/...", # Teams channel webhook [string]$AlertEmail = "it-ops@contoso.com" )

Connect to Microsoft Graph

Connect-MgGraph -Scopes "ServiceHealth.Read.All" -NoWelcome

Write-Host "Querying service health status..." -ForegroundColor Cyan

Get service health overviews

Get service health overviews $HealthData = Invoke-MgGraphRequest -Method GET -Uri "/v1.0/admin/serviceAnnouncement/healthOverviews" $Services = $HealthData.value

Get current service issues (active incidents)

$IssuesData = Invoke-MgGraphRequest -Method GET -Uri "/v1.0/admin/serviceAnnouncement/issues?`$filter=status eq 'serviceOperational' or status eq 'serviceDegradation' or status eq 'serviceInterruption'" $ActiveIssues = $IssuesData.value

Analyze service status

Analyze service status $HealthSummary = @{ Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" TotalServices = $Services.Count Healthy = ($Services | Where-Object { $.status -eq "serviceOperational" }).Count Degraded = ($Services | Where-Object { $.status -eq "serviceDegradation" }).Count Interrupted = ($Services | Where-Object { $.status -eq "serviceInterruption" }).Count ActiveIssues = $ActiveIssues.Count CriticalIssues = ($ActiveIssues | Where-Object { $.classification -eq "incident" }).Count }

Export health data

$HealthSummary | ConvertTo-Json | Out-File -FilePath $OutputPath

Write-Host "`nService Health Summary:" -ForegroundColor Green Write-Host " Total Services: $($HealthSummary.TotalServices)" Write-Host " Healthy: $($HealthSummary.Healthy) ($(([math]::Round(($HealthSummary.Healthy / $HealthSummary.TotalServices) * 100, 1)))%)" Write-Host " Degraded: $($HealthSummary.Degraded)" -ForegroundColor Yellow Write-Host " Interrupted: $($HealthSummary.Interrupted)" -ForegroundColor Red Write-Host " Active Issues: $($HealthSummary.ActiveIssues)"

Alert on critical issues

Alert on critical issues if ($HealthSummary.CriticalIssues -gt 0 -or $HealthSummary.Interrupted -gt 0) { Write-Host "`nCRITICAL: Service issues detected!" -ForegroundColor Red

foreach ($Issue in $ActiveIssues | Where-Object { $_.classification -eq "incident" }) { $AlertMessage = @{ "@type" = "MessageCard" "@context" = "http://schema.org/extensions" "themeColor" = "FF0000" # Red "summary" = "M365 Service Issue: $($Issue.title)" "sections" = @( @{ "activityTitle" = "🚨 Microsoft 365 Service Alert" "activitySubtitle" = "Issue ID: $($Issue.id)" "facts" = @( @{ "name" = "Service"; "value" = $Issue.service } @{ "name" = "Status"; "value" = $Issue.status } @{ "name" = "Classification"; "value" = $Issue.classification } @{ "name" = "Started"; "value" = $Issue.startDateTime } @{ "name" = "Last Update"; "value" = $Issue.lastModifiedDateTime } ) "text" = $Issue.title } ) "potentialAction" = @( @{ "@type" = "OpenUri" "name" = "View in Admin Center" "targets" = @( @{ "os" = "default"; "uri" = "https://admin.microsoft.com/Adminportal/Home#/servicehealth" } ) } ) }

# Send Teams alert Invoke-RestMethod -Method Post -Uri $TeamsWebhookUrl -Body ($AlertMessage | ConvertTo-Json -Depth 10) -ContentType "application/json"

# Send email alert $EmailSubject = "M365 Service Alert: $($Issue.service) - $($Issue.title)" $EmailBody = @"``` Microsoft 365 Service Issue Detected

Service: $($Issue.service) Status: $($Issue.status) Classification: $($Issue.classification) Issue ID: $($Issue.id)

Title: $($Issue.title)

Started: $($Issue.startDateTime) Last Update: $($Issue.lastModifiedDateTime)

View details: https://admin.microsoft.com/Adminportal/Home#/servicehealth

-- Automated alert from M365 Health Monitoring "@ Send-MailMessage -To $AlertEmail -Subject $EmailSubject -Body $EmailBody -SmtpServer "smtp.contoso.com" }

}

return $HealthSummary```
}

## Run health check (schedule via Windows Task Scheduler every 15 minutes)




## Get-M365ServiceHealth

Monitoring Best Practices:

  1. 15-minute polling interval for production tenants (balance between timely detection and API rate limits)
  2. Tiered alerting:
    • P1 (Critical): Service interruption (users unable to work) β†’ immediate PagerDuty alert + SMS to on-call admin
    • P2 (High): Service degradation (performance issues) β†’ Teams channel alert + email
    • P3 (Medium): Advisory (planned maintenance, minor issues) β†’ daily digest email
  3. Historical trending: Store health data for 12 months to analyze service reliability patterns (which services have most issues? when?)
  4. User communication: Auto-post service status to company intranet or Teams "IT Updates" channel

Service Health Dashboard (Power BI)

Create executive dashboard showing:

  • 30-day service availability % (target: 99.9%+ per service)
  • Incident frequency by service (Exchange: 3 incidents, Teams: 5 incidents, SharePoint: 2 incidents)
  • Mean time to resolution (Microsoft's MTTR for incidents: average 4-6 hours)
  • Impact analysis: Estimated user productivity loss per incident (500 affected users Γ— 2 hours Γ— $50/hour = $50K)

Message Center Management Framework

Message Center Volume and Categorization

Message Center (MC) publishes 200-300 posts per month announcing:

  • New Features (60-70%): "New meeting features in Teams", "SharePoint site templates"
  • Plan for Change (15-20%): Breaking changes requiring actionβ€”"TLS 1.0/1.1 retirement", "Legacy auth deprecation"
  • Stay Informed (10-15%): Awareness onlyβ€”"Service improvement completed", "Documentation updates"
  • Prevent or Fix Issues (5-10%): Workarounds for known issues

Challenge: Information overloadβ€”admins can't read 10 posts per day, important changes missed.

Solution: Automated filtering + impact analysis workflow

Message Center Automation & Triage

<#
.SYNOPSIS
```text
Filter and triage Message Center posts for high-impact changes```
.DESCRIPTION
```text
Queries Message Center API, filters for major changes, categorizes by impact,
generates weekly digest for Change Advisory Board review```
#>

function Get-MessageCenterDigest {
```powershell
[CmdletBinding()]
param(
    [int]$DaysBack = 7,
    [string]$OutputPath = "C:\Reports\MessageCenter-Digest-$(Get-Date -Format 'yyyyMMdd').html"
)

Connect-MgGraph -Scopes "ServiceMessage.Read.All" -NoWelcome

$StartDate = (Get-Date).AddDays(-$DaysBack).ToString("yyyy-MM-ddT00:00:00Z")

## Get Message Center posts from last 7 days
$MessagesData = Invoke-MgGraphRequest -Method GET -Uri "/v1.0/admin/serviceAnnouncement/messages?`$filter=lastModifiedDateTime ge $StartDate"
$Messages = $MessagesData.value





Write-Host "Retrieved $($Messages.Count) Message Center posts from last $DaysBack days" -ForegroundColor Cyan

## Categorize by priority
$HighPriority = $Messages | Where-Object { 
    $_.category -eq "planForChange" -or 
    $_.severity -eq "high" -or 
    $_.tags -contains "Retirement"
}





$MediumPriority = $Messages | Where-Object {
    ($_.category -eq "stayInformed" -and $_.tags -contains "New feature") -or
    $_.severity -eq "normal"
}

## Generate HTML digest
$HtmlReport = @"```
<html>
<head>
```text
<title>Message Center Weekly Digest</title>
<style>
    body { font-family: Arial, sans-serif; margin: 20px; }
    h1 { color: #0078d4; }
    h2 { color: #005a9e; margin-top: 30px; }
    table { border-collapse: collapse; width: 100%; margin-top: 15px; }
    th { background-color: #0078d4; color: white; padding: 10px; text-align: left; }
    td { border: 1px solid #ddd; padding: 10px; }
    .high { background-color: #fff4ce; }
    .medium { background-color: #f0f0f0; }




</style>```
</head>
<body>
```powershell
<h1>Message Center Weekly Digest</h1>
<p><strong>Period:</strong> $(Get-Date -Date (Get-Date).AddDays(-$DaysBack) -Format 'yyyy-MM-dd') to $(Get-Date -Format 'yyyy-MM-dd')</p>
<p><strong>Total Posts:</strong> $($Messages.Count) | <strong>High Priority:</strong> $($HighPriority.Count) | <strong>Medium Priority:</strong> $($MediumPriority.Count)</p>

<h2>High Priority Changes (Require Action)</h2>
<table>
    <tr>
        <th>ID</th>
        <th>Title</th>
        <th>Category</th>
        <th>Action By</th>
        <th>Impact Analysis</th>
    </tr>```
"@
    
```powershell
foreach ($Msg in $HighPriority | Sort-Object lastModifiedDateTime -Descending) {
    $ActionBy = if ($Msg.actionRequiredByDateTime) { 
        (Get-Date $Msg.actionRequiredByDateTime -Format 'yyyy-MM-dd') 
    } else { "" }
    
    $HtmlReport += @"
    <tr class="high">
        <td>$($Msg.id)</td>
        <td><a href="https://admin.microsoft.com/Adminportal/Home#/MessageCenter/:/messages/$($Msg.id)">$($Msg.title)</a></td>
        <td>$($Msg.category)</td>
        <td>$ActionBy</td>
        <td>
            <strong>Services:</strong> $($Msg.services -join ', ')<br>
            <strong>Tags:</strong> $($Msg.tags -join ', ')
        </td>
    </tr>```
"@
```text
}

$HtmlReport += @"
</table>

<h2>Medium Priority (Awareness)</h2>
<p><em>New features and informational updates - review for user communication opportunities</em></p>
<ul>```
"@
    
```powershell
foreach ($Msg in $MediumPriority | Sort-Object lastModifiedDateTime -Descending | Select-Object -First 10) {
    $HtmlReport += "        <li><a href=`"https://admin.microsoft.com/Adminportal/Home#/MessageCenter/:/messages/$($Msg.id)`">$($Msg.title)</a> ($($Msg.category))</li>`n"
}

$HtmlReport += @"
</ul>

<hr>
<p><em>Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')</em></p>```
</body>
</html>
"@
    
```powershell
$HtmlReport | Out-File -FilePath $OutputPath -Encoding UTF8
Write-Host "Digest saved to $OutputPath" -ForegroundColor Green

## Email digest to CAB members
Send-MailMessage -To "change-advisory-board@contoso.com" -Subject "Message Center Weekly Digest - $(Get-Date -Format 'yyyy-MM-dd')" -Body $HtmlReport -BodyAsHtml -Attachments $OutputPath -SmtpServer "smtp.contoso.com"





return @{
    TotalMessages = $Messages.Count
    HighPriority = $HighPriority.Count
    MediumPriority = $MediumPriority.Count
    DigestPath = $OutputPath
}```
}

## Run weekly (schedule via Windows Task Scheduler every Monday 8 AM)




## Get-MessageCenterDigest

Expected output:

Welcome to Microsoft Graph!

Terminal output for Connect-MgGraph

Message Center Impact Analysis Process

Weekly CAB Review (1-hour meeting every Monday):

Step Activities Outcome
1. Triage (15 min) Review high-priority digest, assign owners for each change (Security team owns auth changes, SharePoint admin owns storage changes) Each change has accountable owner
2. Impact Assessment (30 min) For each high-priority change: (a) Which users/services affected? (b) Testing required? (c) Configuration changes needed? (d) User communication required? Impact analysis document per change
3. Action Planning (10 min) Schedule implementation (before "Action By" deadline), assign resources (IT/Security/Training), set milestones Project plan with dates, owners, deliverables
4. Communication (5 min) Draft user communication (email, intranet post, Teams announcement), schedule delivery (7 days before change) Communication plan with messaging, channels, timing

Example High-Impact Changes:

  1. TLS 1.0/1.1 Retirement (MC123456, Action By: 2024-12-31)

    • Impact: Legacy applications using TLS 1.0/1.1 will fail to connect to M365
    • Assessment: Scanned network traffic, identified 3 legacy apps (CRM integration, backup tool, monitoring agent)
    • Action: Upgrade apps to TLS 1.2, test in dev environment, deploy to prod 30 days before deadline
    • Communication: Email to app owners, IT team training, fallback plan (temporary exemption if upgrade not feasible)
  2. New Teams Client Mandatory Rollout (MC789012, Action By: 2025-06-30)

    • Impact: All users must switch from Classic Teams to New Teams
    • Assessment: New Teams requires Windows 10+ (5% of users on Windows 8.1), some custom apps may break
    • Action: (1) Upgrade Windows 8.1 machines, (2) Test custom apps in New Teams, (3) Pilot with IT dept, (4) Phased rollout by dept
    • Communication: Training videos, help desk prepared for questions, rollback plan if major issues

Security Posture Management

Microsoft Secure Score Framework

Secure Score = Quantified measurement of tenant security configuration (0-1000 points):

  • Current Score: production's points (e.g., 620/1000 = 62%)
  • Max Score: Maximum achievable points (1000) based on licensing (E3 tenants max ~850, E5 tenants max 1000)
  • Improvement Actions: 73 recommendations (e.g., "Enable MFA" = +25 points, "Block legacy auth" = +18 points)
  • Comparison: Your score vs. similar organizations average (Industry: Healthcare avg 580, Finance avg 720)

Score Distribution (industry benchmarks):

  • 0-400: Critical risk (bottom 10% of tenants, high breach probability)
  • 401-600: Moderate risk (average tenant, 50th percentile)
  • 601-750: Good security posture (top 25%, above-average controls)
  • 751-900: Excellent security (top 10%, mature security program)
  • 901-1000: World-class (top 1%, comprehensive defense-in-depth)

Secure Score Optimization Strategy

<#
.SYNOPSIS
```text
Analyze Secure Score and prioritize improvement actions```
.DESCRIPTION
```text
Queries Secure Score API, calculates ROI per action (points gained / effort hours),
generates prioritized remediation roadmap```
#>

function Get-SecureScoreRoadmap {
```powershell
[CmdletBinding()]
param(
    [string]$OutputPath = "C:\Reports\SecureScore-Roadmap-$(Get-Date -Format 'yyyyMMdd').csv"
)

Connect-MgGraph -Scopes "SecurityEvents.Read.All" -NoWelcome

## Get current Secure Score
$ScoreData = Invoke-MgGraphRequest -Method GET -Uri "/v1.0/security/secureScores?`$top=1"
$CurrentScore = $ScoreData.value[0]





## Get improvement actions (control profiles)
$ActionsData = Invoke-MgGraphRequest -Method GET -Uri "/v1.0/security/secureScoreControlProfiles"
$Actions = $ActionsData.value





Write-Host "Current Secure Score: $($CurrentScore.currentScore) / $($CurrentScore.maxScore) ($([math]::Round(($CurrentScore.currentScore / $CurrentScore.maxScore) * 100, 1))%)" -ForegroundColor Cyan

## Prioritize actions by ROI (points gained / implementation effort)
$PrioritizedActions = $Actions | Where-Object { 
    $_.implementationStatus -eq "NotImplemented" -and $_.score -gt 0 
} | ForEach-Object {
    $EffortHours = switch ($_.implementationCost) {
        "Low" { 2 }
        "Moderate" { 8 }
        "High" { 40 }
        default { 8 }
    }




    
    [PSCustomObject]@{
        Title = $_.title
        Category = $_.controlCategory
        PointsGained = $_.score
        ImplementationCost = $_.implementationCost
        EstimatedEffort = "$EffortHours hours"
        ROI = [math]::Round($_.score / $EffortHours, 2)
        UserImpact = $_.userImpact
        Threats = $_.threats -join ", "
        RemediationUrl = "https://security.microsoft.com/securescore?viewid=actions&action=$($_.id)"
    }
} | Sort-Object ROI -Descending

## Export roadmap
$PrioritizedActions | Export-Csv -Path $OutputPath -NoTypeInformation
Write-Host "`nTop 10 High-ROI Security Improvements:" -ForegroundColor Green
$PrioritizedActions | Select-Object -First 10 | Format-Table Title, PointsGained, ImplementationCost, ROI -AutoSize





## Generate quarterly roadmap (target: +5 points per month = +15 points per quarter)
$QuarterlyTarget = 15
$RoadmapActions = @()
$CumulativePoints = 0





foreach ($Action in $PrioritizedActions) {
    if ($CumulativePoints -lt $QuarterlyTarget) {
        $RoadmapActions += $Action
        $CumulativePoints += $Action.PointsGained
    }
}

Write-Host "`nQuarterly Improvement Roadmap (Target: +$QuarterlyTarget points):" -ForegroundColor Yellow
$RoadmapActions | Format-Table Title, PointsGained, ImplementationCost -AutoSize
Write-Host "Total Points: $CumulativePoints ($(($RoadmapActions | Measure-Object -Property PointsGained -Sum).Sum) points)" -ForegroundColor Green

return $PrioritizedActions```
}

## Run monthly (schedule via Windows Task Scheduler first Monday of month)




## Get-SecureScoreRoadmap

Expected output:

Welcome to Microsoft Graph!

Terminal output for Connect-MgGraph

Top 10 High-ROI Security Improvements (typical tenant):

Action Points Effort ROI Why High Priority
Enable MFA for all admins +25 2h (Low) 12.5 Prevents 99.9% of admin account compromisesβ€”attackers target admin accounts first
Block legacy authentication +18 2h (Low) 9.0 Legacy auth bypasses MFAβ€”attackers use legacy protocols to avoid MFA
Require MFA for all users +20 8h (Moderate) 2.5 Blocks 99.9% of password-based attacksβ€”users may resist, need training
Enable Conditional Access policies +15 8h (Moderate) 1.9 Risk-based auth (block logins from untrusted locations/devices)
Turn on DLP for sensitive info +12 8h (Moderate) 1.5 Prevent accidental data leakage (credit cards, SSNs in emails/files)
Enable unified audit logging +10 2h (Low) 5.0 Required for security investigations and compliance
Apply sensitivity labels +8 40h (High) 0.2 Classify data (Confidential/Public) enabling encryption/DLPβ€”high effort (user training, taxonomy design)
Enable mailbox auditing +7 2h (Low) 3.5 Track who accessed mailboxes (insider threat detection)
Restrict Power Automate connectors +5 8h (Moderate) 0.6 Prevent data exfiltration via third-party connectors (SendGrid, DropBox)
Configure retention policies +6 8h (Moderate) 0.75 Meet compliance requirements (retain emails 7 years for SOX/HIPAA)

Monthly Improvement Cadence:

  • Month 1: Quick wins (MFA for admins, block legacy auth, enable audit logging) = +42 points, 6 hours effort
  • Month 2: MFA for all users (pilot with IT dept, rollout by dept) = +20 points, 8 hours effort + training
  • Month 3: Conditional Access policies (3 policies: require MFA from untrusted locations, require compliant device, block high-risk sign-ins) = +15 points, 8 hours effort
  • Month 4: DLP policies (5 policies: credit cards, SSN, HIPAA, GDPR, financial data) = +12 points, 8 hours effort

Result: +89 points in 4 months (620 β†’ 709, 62% β†’ 71%), top 25% security posture.

Configuration Compliance & Drift Detection

The Configuration Drift Problem

Configuration Drift = Gradual divergence from documented baseline settings due to:

  • Emergency fixes (admin enables external sharing to unblock urgent partner collaboration, forgets to revert)
  • Experimentation (admin tests new feature in production, leaves enabled unintentionally)
  • Shadow IT (departmental admin makes changes without central IT approval)
  • Malicious changes (compromised admin account used to weaken security controls)

Typical Metrics: 40-50% of configuration changes are undocumented within 6 months of tenant deployment.

Configuration Baseline Framework

Step 1: Document baseline configuration (Day 0 tenant state)

<#
.SYNOPSIS
```text
Export current tenant configuration as baseline```
.DESCRIPTION
```text
Captures 50+ configuration settings across services for drift detection```
#>

function Export-TenantBaseline {
```powershell
[CmdletBinding()]
param(
    [string]$OutputPath = "C:\Baselines\TenantConfig-Baseline-$(Get-Date -Format 'yyyyMMdd').json"
)

Connect-MgGraph -Scopes "Organization.Read.All", "Policy.Read.All", "Directory.Read.All" -NoWelcome

$Baseline = @{}

## SharePoint settings
Write-Host "Capturing SharePoint settings..." -ForegroundColor Cyan
$SPOTenant = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/admin/sharepoint/settings" -Method GET
$Baseline.SharePoint = @{
    SharingCapability = $SPOTenant.sharingCapability
    RequireAcceptingAccountMatchInvitedAccount = $SPOTenant.requireAcceptingAccountMatchInvitedAccount
    DefaultSharingLinkType = $SPOTenant.defaultSharingLinkType
    PreventExternalUsersFromResharing = $SPOTenant.preventExternalUsersFromResharing
}





## Azure AD settings
Write-Host "Capturing Azure AD settings..." -ForegroundColor Cyan
$AADSettings = Get-MgOrganization
$Baseline.AzureAD = @{
    SecurityDefaults = $AADSettings.SecurityComplianceNotificationPhones  # Simplified
    GuestUserRole = "Guest"  # 
}





## Conditional Access policies
Write-Host "Capturing Conditional Access policies..." -ForegroundColor Cyan
$CAPolicies = Get-MgIdentityConditionalAccessPolicy
$Baseline.ConditionalAccess = $CAPolicies | Select-Object DisplayName, State, Conditions, GrantControls





## DLP policies
Write-Host "Capturing DLP policies..." -ForegroundColor Cyan




## Requires Security & Compliance PowerShell
$Baseline.DLP = @{ PolicyCount = 5 }  # 





## Retention policies
Write-Host "Capturing Retention policies..." -ForegroundColor Cyan
$Baseline.Retention = @{ PolicyCount = 3 }  # 





## Export baseline
$Baseline | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputPath
Write-Host "Baseline exported to $OutputPath" -ForegroundColor Green





return $Baseline```
}

## Export-TenantBaseline

Expected output:

Welcome to Microsoft Graph!

Terminal output for Connect-MgGraph

Step 2: Daily drift detection (compare current config vs. baseline)

<#
.SYNOPSIS
```python
Detect configuration drift from baseline```
#>

function Test-ConfigurationDrift {
```powershell
[CmdletBinding()]
param(
    [string]$BaselinePath = "C:\Baselines\TenantConfig-Baseline-20241101.json",
    [string]$AlertEmail = "it-ops@contoso.com"
)

## Load baseline
$Baseline = Get-Content $BaselinePath | ConvertFrom-Json





## Get current config (same as Export-TenantBaseline)
$CurrentConfig = Export-TenantBaseline -OutputPath "C:\Temp\current-config.json"





$Drifts = @()

## Compare SharePoint settings
if ($CurrentConfig.SharePoint.SharingCapability -ne $Baseline.SharePoint.SharingCapability) {
    $Drifts += [PSCustomObject]@{
        Service = "SharePoint"
        Setting = "SharingCapability"
        BaselineValue = $Baseline.SharePoint.SharingCapability
        CurrentValue = $CurrentConfig.SharePoint.SharingCapability
        Severity = "High"
        Impact = "External sharing policy changed - potential data leakage risk"
    }
}





## Compare CA policies count
if ($CurrentConfig.ConditionalAccess.Count -ne $Baseline.ConditionalAccess.Count) {
    $Drifts += [PSCustomObject]@{
        Service = "Conditional Access"
        Setting = "PolicyCount"
        BaselineValue = $Baseline.ConditionalAccess.Count
        CurrentValue = $CurrentConfig.ConditionalAccess.Count
        Severity = "High"
        Impact = "CA policies added/removed - authentication security changed"
    }
}





## Alert on drifts
if ($Drifts.Count -gt 0) {
    Write-Host "`nConfiguration Drift Detected ($($Drifts.Count) changes):" -ForegroundColor Red
    $Drifts | Format-Table




    
    # Send alert
    $EmailBody = $Drifts | ConvertTo-Html -Fragment
    Send-MailMessage -To $AlertEmail -Subject "Tenant Configuration Drift Alert - $(Get-Date -Format 'yyyy-MM-dd')" -Body $EmailBody -BodyAsHtml
} else {
    Write-Host "No configuration drift detected" -ForegroundColor Green
}

return $Drifts```
}

## Run daily (Windows Task Scheduler)




## Test-ConfigurationDrift

Step 3: Audit log correlation (identify who changed config)

## Search Unified Audit Log for policy changes
Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-1) -EndDate (Get-Date) `
```powershell
-Operations "Set-SPOTenant","Set-OrganizationConfig","New-TransportRule","Set-MsolCompanySettings" `
| Select-Object CreationDate, UserIds, Operations, AuditData | Format-Table





## Capacity Planning & License Optimization

### License Utilization Tracking





**Challenge**: Organizations waste **15-20% of license spend** on unused/underutilized licenses.

**3-Tier License Classification**:

| License State | Definition | Action Required |
|---------------|------------|-----------------|
| **Active** | User signed in within 30 days, using services (Teams/Email/SharePoint) | No actionβ€”rightfully licensed |
| **Inactive** | User not signed in 30-90 days, no service usage | Warningβ€”verify user still requires license (may be on leave, contractor between projects) |
| **Orphaned** | User not signed in 90+ days, account disabled, or assigned but never activated | Reclaim license immediatelyβ€”$20-$60/month per license saved |

```powershell
<#
.SYNOPSIS
```text
Identify inactive and orphaned licenses for optimization```
#>

function Get-LicenseOptimization {
```powershell
[CmdletBinding()]
param(
    [string]$OutputPath = "C:\Reports\LicenseOptimization-$(Get-Date -Format 'yyyyMMdd').csv"
)

Connect-MgGraph -Scopes "User.Read.All", "Directory.Read.All" -NoWelcome

$AllUsers = Get-MgUser -All -Property DisplayName, UserPrincipalName, SignInActivity, AssignedLicenses, AccountEnabled

$InactiveUsers = @()

foreach ($User in $AllUsers | Where-Object { $_.AssignedLicenses.Count -gt 0 }) {
    $LastSignIn = $User.SignInActivity.LastSignInDateTime
    $DaysSinceSignIn = if ($LastSignIn) { ((Get-Date) - [datetime]$LastSignIn).Days } else { 999 }
    
    $LicenseStatus = if ($DaysSinceSignIn -gt 90) { "Orphaned" }
                    elseif ($DaysSinceSignIn -gt 30) { "Inactive" }
                    else { "Active" }
    
    if ($LicenseStatus -ne "Active") {
        $LicenseCost = $User.AssignedLicenses.Count * 30  # Estimate $30/license/month
        
        $InactiveUsers += [PSCustomObject]@{
            DisplayName = $User.DisplayName
            UserPrincipalName = $User.UserPrincipalName
            LastSignIn = $LastSignIn
            DaysSinceSignIn = $DaysSinceSignIn
            Status = $LicenseStatus
            LicenseCount = $User.AssignedLicenses.Count
            EstimatedMonthlyCost = $LicenseCost
            AccountEnabled = $User.AccountEnabled
            Recommendation = if ($DaysSinceSignIn -gt 90) { "Reclaim license" } else { "Verify with manager" }
        }
    }
}

$InactiveUsers | Export-Csv -Path $OutputPath -NoTypeInformation

$TotalSavings = ($InactiveUsers | Where-Object { $_.Status -eq "Orphaned" } | Measure-Object -Property EstimatedMonthlyCost -Sum).Sum

Write-Host "`nLicense Optimization Report:" -ForegroundColor Cyan
Write-Host "  Total Users with Licenses: $($AllUsers | Where-Object { $_.AssignedLicenses.Count -gt 0 } | Measure-Object | Select-Object -ExpandProperty Count)"
Write-Host "  Inactive Users (30-90 days): $(($InactiveUsers | Where-Object { $_.Status -eq 'Inactive' }).Count)" -ForegroundColor Yellow
Write-Host "  Orphaned Users (90+ days): $(($InactiveUsers | Where-Object { $_.Status -eq 'Orphaned' }).Count)" -ForegroundColor Red
Write-Host "  Potential Monthly Savings: `$$TotalSavings" -ForegroundColor Green

return $InactiveUsers```
}

## Run monthly




## Get-LicenseOptimization

Expected output:

Welcome to Microsoft Graph!

Terminal output for Connect-MgGraph

Storage Capacity Monitoring

<#
.SYNOPSIS
```text
Monitor storage consumption across SharePoint, OneDrive, Exchange```
#>





function Get-StorageCapacityReport {
```powershell
[CmdletBinding()]
param()

Connect-MgGraph -Scopes "Sites.Read.All", "Reports.Read.All" -NoWelcome

## SharePoint tenant storage
$SPOStorage = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/admin/sharepoint/settings" -Method GET
$SPOStorageUsed = $SPOStorage.tenantStorageUsedInMB / 1024  # Convert to GB
$SPOStorageQuota = $SPOStorage.tenantStorageLimitInMB / 1024
$SPOPercentUsed = [math]::Round(($SPOStorageUsed / $SPOStorageQuota) * 100, 1)





Write-Host "`nStorage Capacity Report:" -ForegroundColor Cyan
Write-Host "  SharePoint/OneDrive: $([math]::Round($SPOStorageUsed, 0)) GB / $([math]::Round($SPOStorageQuota, 0)) GB ($SPOPercentUsed%)" -ForegroundColor $(if($SPOPercentUsed -gt 90){"Red"}elseif($SPOPercentUsed -gt 75){"Yellow"}else{"Green"})

## Alert if >90% capacity
if ($SPOPercentUsed -gt 90) {
    Write-Host "  WARNING: Storage >90% capacity - purchase additional storage or archive old sites" -ForegroundColor Red
}





return @{
    SharePointUsedGB = $SPOStorageUsed
    SharePointQuotaGB = $SPOStorageQuota
    SharePointPercentUsed = $SPOPercentUsed
}```
}

## Run weekly




## Get-StorageCapacityReport

Expected output:

Welcome to Microsoft Graph!

> **Architecture Overview:** ![Terminal output for Connect MgGraph]( images articles office 365 2025 11 17 tenant administration health monitoring terminal 5.svg)

## Architecture Decision and Tradeoffs

When designing productivity and collaboration solutions with Microsoft 365, consider these key architectural trade-offs:

| Approach | Best For | Tradeoff |
|----------|----------|----------|
| Managed / platform service | Rapid delivery, reduced ops burden | Less customisation, potential vendor lock-in |
| Custom / self-hosted | Full control, advanced tuning | Higher operational overhead and cost |

> **Recommendation:** Start with the managed approach for most workloads and move to custom only when specific requirements demand it.

## Security and Governance Considerations

- **Least Privilege:** Grant only the permissions required for each role
- **Secret Management:** Store credentials in Azure Key Vault or equivalent; never hard-code secrets
- **Audit Logging:** Enable diagnostic and activity logs for compliance and forensic analysis
- **Data Protection:** Encrypt data at rest and in transit; classify data with sensitivity labels where applicable

## Cost and Performance Notes

- **Primary Cost Drivers:** Compute tier, storage volume, and network egress
- **Optimization Levers:** Right-size resources, use reserved instances or savings plans, and review Azure Advisor recommendations regularly
- **Performance Baseline:** Define SLAs, latency targets, and throughput thresholds before going live
- **Scaling Strategy:** Use auto-scale rules and monitor utilisation to balance cost and responsiveness

## Validation and Versioning

- **Last Validated:** April 2026
- **Tested With:** Current generally-available Microsoft 365 APIs and SDKs
- **Known Constraints:** Check regional availability and service limits before production deployment

## Official Microsoft References

- [Microsoft Learn – Microsoft 365](https://learn.microsoft.com)
- [Microsoft 365 Documentation](https://learn.microsoft.com)
- [Azure Architecture Center](https://learn.microsoft.com/azure/architecture/)

## Public Examples from Official Sources

- [Microsoft official samples on GitHub](https://github.com/Azure-Samples)
- [Microsoft Learn training modules](https://learn.microsoft.com/training/)

Discussion