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
$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
$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
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:
- 15-minute polling interval for production tenants (balance between timely detection and API rate limits)
- 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
- Historical trending: Store health data for 12 months to analyze service reliability patterns (which services have most issues? when?)
- 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!
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:
-
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)
-
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!
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!
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!
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:** 
## 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