Add 12 AI-generated PowerShell scripts with documentation

⚠️ IMPORTANT: These scripts are AI-GENERATED and UNTESTED

Exchange Scripts (5):
- Get-MailboxPermissions.ps1: Audit delegate access permissions
- Get-InactiveMailboxes.ps1: Identify stale mailboxes
- Compare-MailboxDatabases.ps1: Database health comparison
- Export-DistributionGroups.ps1: Distribution group inventory
- Get-MailflowStats.ps1: Transport log analysis

Active Directory Scripts (3):
- Get-ADUserLastLogon.ps1: True LastLogon across all DCs
- Export-OUStructure.ps1: OU hierarchy with GPO links
- Compare-ADGroupMemberships.ps1: Compare user group memberships

System Maintenance Scripts (4):
- Get-ServerInventory.ps1: Hardware/software inventory report
- Monitor-DiskSpace.ps1: Disk space monitoring with alerts
- Backup-ExchangeCertificates.ps1: Certificate backup to PFX
- Test-ExchangeHealth.ps1: Aggregated Exchange health checks

Documentation:
- Updated CLAUDE.md with AI-generated scripts section
- Added AI-GENERATED-SCRIPTS.md with warnings and testing guide

All scripts include prominent warnings and follow established patterns
from existing scripts. Require thorough testing before production use.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Martien de Kleijn
2025-10-15 10:52:44 +02:00
parent 5e9d160d48
commit 62134801aa
14 changed files with 2565 additions and 0 deletions

View File

@ -0,0 +1,190 @@
<#
.SYNOPSIS
Compare health metrics across all Exchange mailbox databases
.DESCRIPTION
Generates a comparison report of database sizes, whitespace, mount status,
backup age, and circular logging settings. Alerts on databases exceeding
thresholds or with configuration issues.
.PARAMETER WhitespaceThresholdGB
Alert if available whitespace exceeds this value (default: 50)
.PARAMETER BackupAgeThresholdDays
Alert if last backup is older than this many days (default: 2)
.PARAMETER OutputFolder
Destination folder for reports. Default: .\DatabaseComparison-<date>
.NOTES
⚠️ AI-GENERATED SCRIPT - UNTESTED
This script was generated by Claude AI and has not been tested in production.
Review and test thoroughly in a non-production environment before use.
- Run in Exchange Management Shell with appropriate RBAC permissions
- Requires -Status parameter support (Get-MailboxDatabase -Status)
- Tested compatibility: Exchange 2013/2016/2019 (not validated)
.EXAMPLE
.\Compare-MailboxDatabases.ps1
.EXAMPLE
.\Compare-MailboxDatabases.ps1 -WhitespaceThresholdGB 100 -BackupAgeThresholdDays 1
#>
[CmdletBinding()]
param(
[int]$WhitespaceThresholdGB = 50,
[int]$BackupAgeThresholdDays = 2,
[string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("DatabaseComparison-" + (Get-Date -Format "yyyyMMdd-HHmm")))
)
function NowTag { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") }
function Convert-BytesToGB([string]$sizeStr) {
if (-not $sizeStr) { return 0 }
try {
if ($sizeStr -match "([\d\.,]+)\s*(KB|MB|GB|TB)") {
$num = [double]($matches[1] -replace ',', '.')
$bytes = switch ($matches[2].ToUpper()) {
"KB" { $num * 1KB }
"MB" { $num * 1MB }
"GB" { $num * 1GB }
"TB" { $num * 1TB }
}
return [math]::Round($bytes / 1GB, 2)
}
} catch {}
return 0
}
Write-Host "[$(NowTag)] ⚠️ AI-GENERATED SCRIPT - UNTESTED" -ForegroundColor Yellow
Write-Host "[$(NowTag)] Starting database comparison analysis..." -ForegroundColor Green
# Create output folder
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
# Get all databases with status
Write-Host "[$(NowTag)] Retrieving mailbox databases..."
$databases = Get-MailboxDatabase -Status -ErrorAction SilentlyContinue | Sort-Object Name
$dbCount = ($databases | Measure-Object).Count
Write-Host "[$(NowTag)] Found $dbCount databases"
# Analyze each database
$dbAnalysis = @()
$alerts = @()
foreach ($db in $databases) {
Write-Host "[$(NowTag)] Analyzing $($db.Name)..."
$dbSizeGB = Convert-BytesToGB ([string]$db.DatabaseSize)
$whitespaceGB = Convert-BytesToGB ([string]$db.AvailableNewMailboxSpace)
$backupAge = $null
$backupStatus = "Unknown"
if ($db.LastFullBackup) {
$backupAge = [int]((Get-Date) - $db.LastFullBackup).TotalDays
$backupStatus = if ($backupAge -le $BackupAgeThresholdDays) { "OK" } else { "OLD" }
} else {
$backupStatus = "NEVER"
}
# Generate alerts
$dbAlerts = @()
if (-not $db.Mounted) {
$dbAlerts += "Database is DISMOUNTED"
$alerts += [PSCustomObject]@{
Database = $db.Name
Severity = "CRITICAL"
Alert = "Database is dismounted"
}
}
if ($whitespaceGB -gt $WhitespaceThresholdGB) {
$dbAlerts += "Excessive whitespace: $whitespaceGB GB"
$alerts += [PSCustomObject]@{
Database = $db.Name
Severity = "WARNING"
Alert = "Excessive whitespace: $whitespaceGB GB (threshold: $WhitespaceThresholdGB GB)"
}
}
if ($backupStatus -eq "NEVER") {
$dbAlerts += "Never backed up"
$alerts += [PSCustomObject]@{
Database = $db.Name
Severity = "CRITICAL"
Alert = "Database has never been backed up"
}
} elseif ($backupStatus -eq "OLD") {
$dbAlerts += "Backup is $backupAge days old"
$alerts += [PSCustomObject]@{
Database = $db.Name
Severity = "WARNING"
Alert = "Last backup is $backupAge days old (threshold: $BackupAgeThresholdDays days)"
}
}
if ($db.CircularLoggingEnabled) {
$dbAlerts += "Circular logging enabled"
$alerts += [PSCustomObject]@{
Database = $db.Name
Severity = "INFO"
Alert = "Circular logging is enabled"
}
}
$dbAnalysis += [PSCustomObject]@{
Database = $db.Name
Server = $db.Server
Mounted = $db.Mounted
DatabaseSizeGB = $dbSizeGB
WhitespaceGB = $whitespaceGB
WhitespacePercent = if ($dbSizeGB -gt 0) { [math]::Round(($whitespaceGB / $dbSizeGB) * 100, 2) } else { 0 }
CircularLogging = $db.CircularLoggingEnabled
LastFullBackup = $db.LastFullBackup
BackupAgeDays = $backupAge
BackupStatus = $backupStatus
EdbFilePath = $db.EdbFilePath
LogFolderPath = $db.LogFolderPath
Alerts = ($dbAlerts -join "; ")
}
}
# Export full analysis
$csvFile = Join-Path $OutputFolder "Database-Comparison.csv"
$dbAnalysis | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $csvFile
Write-Host "[$(NowTag)] Database analysis exported: $csvFile" -ForegroundColor Green
# Export alerts
if ($alerts.Count -gt 0) {
$alertsFile = Join-Path $OutputFolder "Database-Alerts.csv"
$alerts | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $alertsFile
Write-Host "[$(NowTag)] Alerts exported: $alertsFile" -ForegroundColor Yellow
Write-Host "`nALERTS FOUND:" -ForegroundColor Yellow
$alerts | Group-Object Severity | ForEach-Object {
Write-Host " $($_.Name): $($_.Count) alert(s)" -ForegroundColor $(
switch ($_.Name) {
"CRITICAL" { "Red" }
"WARNING" { "Yellow" }
default { "Cyan" }
}
)
}
} else {
Write-Host "`n[$(NowTag)] No alerts found - all databases healthy!" -ForegroundColor Green
}
# Summary statistics
Write-Host "`nDATABASE SUMMARY:" -ForegroundColor Cyan
Write-Host " Total Databases: $dbCount"
Write-Host " Mounted: $(($dbAnalysis | Where-Object Mounted).Count)"
Write-Host " Dismounted: $(($dbAnalysis | Where-Object { -not $_.Mounted }).Count)"
Write-Host " Total Size: $([math]::Round(($dbAnalysis | Measure-Object DatabaseSizeGB -Sum).Sum, 2)) GB"
Write-Host " Total Whitespace: $([math]::Round(($dbAnalysis | Measure-Object WhitespaceGB -Sum).Sum, 2)) GB"
Write-Host " Circular Logging Enabled: $(($dbAnalysis | Where-Object CircularLogging).Count)"
Write-Host "`n[$(NowTag)] Analysis complete! Output folder: $OutputFolder"

View File

@ -0,0 +1,163 @@
<#
.SYNOPSIS
Export complete inventory of distribution groups with members and settings
.DESCRIPTION
Documents all distribution groups including members, owners, email addresses,
and key settings. Useful for migration planning, documentation, or backup.
.PARAMETER OutputFolder
Destination folder for reports. Default: .\DistributionGroups-<date>
.PARAMETER IncludeMembers
Include detailed member lists (default: $true, can be slow)
.PARAMETER GroupFilter
Optional filter for specific groups (default: all)
.NOTES
⚠️ AI-GENERATED SCRIPT - UNTESTED
This script was generated by Claude AI and has not been tested in production.
Review and test thoroughly in a non-production environment before use.
- Run in Exchange Management Shell with appropriate RBAC permissions
- Large environments may take significant time
- Includes both Distribution Groups and Mail-Enabled Security Groups
.EXAMPLE
.\Export-DistributionGroups.ps1
.EXAMPLE
.\Export-DistributionGroups.ps1 -GroupFilter "Sales*" -OutputFolder "D:\Reports\Groups"
#>
[CmdletBinding()]
param(
[string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("DistributionGroups-" + (Get-Date -Format "yyyyMMdd-HHmm"))),
[bool]$IncludeMembers = $true,
[string]$GroupFilter = "*"
)
function NowTag { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") }
Write-Host "[$(NowTag)] ⚠️ AI-GENERATED SCRIPT - UNTESTED" -ForegroundColor Yellow
Write-Host "[$(NowTag)] Starting distribution group export..." -ForegroundColor Green
# Create output folder
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
# Get distribution groups
Write-Host "[$(NowTag)] Retrieving distribution groups..."
$groups = Get-DistributionGroup -Filter "Name -like '$GroupFilter'" -ResultSize Unlimited -ErrorAction SilentlyContinue
$groupCount = ($groups | Measure-Object).Count
Write-Host "[$(NowTag)] Found $groupCount distribution groups"
if ($groupCount -eq 0) {
Write-Host "[$(NowTag)] No groups found matching filter: $GroupFilter" -ForegroundColor Yellow
exit
}
# Collect group details
$groupDetails = @()
$groupMembers = @()
$current = 0
foreach ($group in $groups) {
$current++
$pct = [int](($current / $groupCount) * 100)
Write-Progress -Activity "Processing Distribution Groups" -Status "Processing $($group.Name) ($current/$groupCount)" -PercentComplete $pct
# Get managed by
$managedByList = @()
if ($group.ManagedBy) {
foreach ($mgr in $group.ManagedBy) {
try {
$mgrObj = Get-Recipient $mgr -ErrorAction SilentlyContinue
if ($mgrObj) {
$managedByList += $mgrObj.PrimarySmtpAddress
}
} catch {}
}
}
# Get email addresses
$emailAddresses = ($group.EmailAddresses | Where-Object { $_ -like "smtp:*" }) -join "; "
# Get member count
$memberCount = 0
try {
$members = Get-DistributionGroupMember -Identity $group.Identity -ResultSize Unlimited -ErrorAction SilentlyContinue
$memberCount = ($members | Measure-Object).Count
# Collect member details if requested
if ($IncludeMembers -and $members) {
foreach ($member in $members) {
$groupMembers += [PSCustomObject]@{
GroupName = $group.Name
GroupPrimarySmtp = $group.PrimarySmtpAddress
MemberName = $member.Name
MemberPrimarySmtp = $member.PrimarySmtpAddress
MemberType = $member.RecipientType
}
}
}
} catch {
Write-Host "[$(NowTag)] WARNING: Could not get members for $($group.Name)" -ForegroundColor Yellow
}
$groupDetails += [PSCustomObject]@{
Name = $group.Name
DisplayName = $group.DisplayName
Alias = $group.Alias
PrimarySmtpAddress = $group.PrimarySmtpAddress
EmailAddresses = $emailAddresses
RecipientTypeDetails = $group.RecipientTypeDetails
MemberCount = $memberCount
ManagedBy = ($managedByList -join "; ")
RequireSenderAuthentication = $group.RequireSenderAuthenticationEnabled
HiddenFromAddressLists = $group.HiddenFromAddressListsEnabled
ModerationEnabled = $group.ModerationEnabled
SendModerationNotifications = $group.SendModerationNotifications
AcceptMessagesOnlyFrom = (($group.AcceptMessagesOnlyFrom | ForEach-Object { $_.ToString() }) -join "; ")
RejectMessagesFrom = (($group.RejectMessagesFrom | ForEach-Object { $_.ToString() }) -join "; ")
BypassModerationFromSenders = (($group.BypassModerationFromSendersOrMembers | ForEach-Object { $_.ToString() }) -join "; ")
WhenCreated = $group.WhenCreated
WhenChanged = $group.WhenChanged
OrganizationalUnit = $group.OrganizationalUnit
}
}
Write-Progress -Activity "Processing Distribution Groups" -Completed
# Export results
Write-Host "[$(NowTag)] Exporting results..."
$groupsFile = Join-Path $OutputFolder "DistributionGroups.csv"
$groupDetails | Sort-Object Name | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $groupsFile
Write-Host "[$(NowTag)] Groups exported: $groupsFile" -ForegroundColor Green
if ($IncludeMembers -and $groupMembers.Count -gt 0) {
$membersFile = Join-Path $OutputFolder "DistributionGroup-Members.csv"
$groupMembers | Sort-Object GroupName, MemberName | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $membersFile
Write-Host "[$(NowTag)] Members exported: $membersFile" -ForegroundColor Green
}
# Summary
Write-Host "`nSUMMARY:" -ForegroundColor Cyan
Write-Host " Total Groups: $groupCount"
Write-Host " Total Members: $($groupMembers.Count)"
Write-Host " Hidden from Address Lists: $(($groupDetails | Where-Object HiddenFromAddressLists).Count)"
Write-Host " Moderation Enabled: $(($groupDetails | Where-Object ModerationEnabled).Count)"
Write-Host " Security Groups: $(($groupDetails | Where-Object { $_.RecipientTypeDetails -like '*Security*' }).Count)"
# Top 10 largest groups
$top10 = $groupDetails | Sort-Object MemberCount -Descending | Select-Object -First 10
if ($top10) {
Write-Host "`nTop 10 Largest Groups:" -ForegroundColor Cyan
$top10 | ForEach-Object {
Write-Host " $($_.Name): $($_.MemberCount) members"
}
}
Write-Host "`n[$(NowTag)] Export complete! Output folder: $OutputFolder"

View File

@ -0,0 +1,191 @@
<#
.SYNOPSIS
Identify inactive mailboxes based on LastLogonTime
.DESCRIPTION
Finds mailboxes with no logon activity within specified days.
Useful for identifying stale accounts, cost optimization, and cleanup planning.
.PARAMETER InactiveDays
Number of days without logon to consider mailbox inactive (default: 90)
.PARAMETER OutputFolder
Destination folder for reports. Default: .\InactiveMailboxes-<date>
.PARAMETER IncludeShared
Include shared mailboxes in the report (default: $false)
.PARAMETER IncludeSize
Include mailbox size information (slower, default: $true)
.NOTES
⚠️ AI-GENERATED SCRIPT - UNTESTED
This script was generated by Claude AI and has not been tested in production.
Review and test thoroughly in a non-production environment before use.
- Run in Exchange Management Shell with appropriate RBAC permissions
- LastLogonTime may be null for never-accessed mailboxes
- Size calculation can be slow with large mailbox counts
.EXAMPLE
.\Get-InactiveMailboxes.ps1
.EXAMPLE
.\Get-InactiveMailboxes.ps1 -InactiveDays 180 -IncludeShared $true
#>
[CmdletBinding()]
param(
[int]$InactiveDays = 90,
[string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("InactiveMailboxes-" + (Get-Date -Format "yyyyMMdd-HHmm"))),
[bool]$IncludeShared = $false,
[bool]$IncludeSize = $true
)
function NowTag { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") }
Write-Host "[$(NowTag)] ⚠️ AI-GENERATED SCRIPT - UNTESTED" -ForegroundColor Yellow
Write-Host "[$(NowTag)] Searching for mailboxes inactive for $InactiveDays+ days..." -ForegroundColor Green
# Create output folder
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
$cutoffDate = (Get-Date).AddDays(-$InactiveDays)
Write-Host "[$(NowTag)] Cutoff date: $($cutoffDate.ToString('yyyy-MM-dd'))"
# Get mailboxes
Write-Host "[$(NowTag)] Retrieving mailboxes..."
$filter = if ($IncludeShared) {
{ RecipientTypeDetails -notmatch "^Remote" -and Database }
} else {
{ RecipientTypeDetails -eq "UserMailbox" -and Database }
}
$mailboxes = Get-Mailbox -ResultSize Unlimited -ErrorAction SilentlyContinue |
Where-Object $filter
$mbCount = ($mailboxes | Measure-Object).Count
Write-Host "[$(NowTag)] Found $mbCount mailboxes to check"
# Get statistics per database to avoid prompts
Write-Host "[$(NowTag)] Retrieving mailbox statistics..."
$statsByDb = @{}
$databases = ($mailboxes.Database | Sort-Object -Unique)
$dbIdx = 0
foreach ($db in $databases) {
$dbIdx++
Write-Progress -Activity "Collecting Mailbox Statistics" -Status "Database $db ($dbIdx/$($databases.Count))" -PercentComplete ([int](($dbIdx / $databases.Count) * 100))
try {
$stats = Get-MailboxStatistics -Database $db -ErrorAction SilentlyContinue
foreach ($stat in $stats) {
$statsByDb[$stat.DisplayName] = $stat
}
} catch {
Write-Host "[$(NowTag)] WARNING: Could not get statistics for database $db" -ForegroundColor Yellow
}
}
Write-Progress -Activity "Collecting Mailbox Statistics" -Completed
# Analyze for inactive mailboxes
Write-Host "[$(NowTag)] Analyzing mailbox activity..."
$inactiveMailboxes = @()
$current = 0
foreach ($mb in $mailboxes) {
$current++
$pct = [int](($current / $mbCount) * 100)
Write-Progress -Activity "Analyzing Mailboxes" -Status "Processing $($mb.DisplayName) ($current/$mbCount)" -PercentComplete $pct
$stat = $statsByDb[$mb.DisplayName]
$lastLogon = if ($stat) { $stat.LastLogonTime } else { $null }
$isInactive = $false
$reason = ""
if ($null -eq $lastLogon) {
$isInactive = $true
$reason = "Never logged on"
} elseif ($lastLogon -lt $cutoffDate) {
$isInactive = $true
$reason = "Last logon: $($lastLogon.ToString('yyyy-MM-dd'))"
}
if ($isInactive) {
$daysSinceLogon = if ($lastLogon) {
[int]((Get-Date) - $lastLogon).TotalDays
} else {
"N/A"
}
$size = $null
$itemCount = $null
if ($IncludeSize -and $stat) {
$size = $stat.TotalItemSize
$itemCount = $stat.ItemCount
}
$inactiveMailboxes += [PSCustomObject]@{
DisplayName = $mb.DisplayName
PrimarySmtpAddress = $mb.PrimarySmtpAddress
RecipientTypeDetails = $mb.RecipientTypeDetails
Database = $mb.Database
LastLogonTime = $lastLogon
DaysSinceLogon = $daysSinceLogon
WhenCreated = $mb.WhenCreated
TotalItemSize = $size
ItemCount = $itemCount
Reason = $reason
}
}
}
Write-Progress -Activity "Analyzing Mailboxes" -Completed
# Export results
Write-Host "[$(NowTag)] Found $($inactiveMailboxes.Count) inactive mailboxes"
if ($inactiveMailboxes.Count -gt 0) {
$csvFile = Join-Path $OutputFolder "Inactive-Mailboxes.csv"
$inactiveMailboxes | Sort-Object DaysSinceLogon -Descending |
Export-Csv -NoTypeInformation -Encoding UTF8 -Path $csvFile
Write-Host "[$(NowTag)] Report exported: $csvFile" -ForegroundColor Green
# Summary by type
$byType = $inactiveMailboxes | Group-Object RecipientTypeDetails
Write-Host "`nInactive Mailboxes by Type:"
foreach ($type in $byType) {
Write-Host " $($type.Name): $($type.Count)"
}
# Calculate potential storage savings
if ($IncludeSize) {
$totalBytes = 0
foreach ($mb in $inactiveMailboxes) {
if ($mb.TotalItemSize) {
try {
$sizeStr = [string]$mb.TotalItemSize
if ($sizeStr -match "([\d\.,]+)\s*(KB|MB|GB|TB)") {
$num = [double]($matches[1] -replace ',', '.')
$bytes = switch ($matches[2].ToUpper()) {
"KB" { $num * 1KB }
"MB" { $num * 1MB }
"GB" { $num * 1GB }
"TB" { $num * 1TB }
}
$totalBytes += $bytes
}
} catch {}
}
}
$totalGB = [math]::Round($totalBytes / 1GB, 2)
Write-Host "`nPotential storage savings: $totalGB GB" -ForegroundColor Cyan
}
} else {
Write-Host "[$(NowTag)] No inactive mailboxes found!" -ForegroundColor Green
}
Write-Host "`n[$(NowTag)] Analysis complete! Output folder: $OutputFolder"

View File

@ -0,0 +1,173 @@
<#
.SYNOPSIS
Audit mailbox delegate access permissions across Exchange environment
.DESCRIPTION
Reports on SendAs, SendOnBehalf, and FullAccess permissions for all mailboxes.
Useful for security audits, compliance reviews, and migration planning.
.PARAMETER OutputFolder
Destination folder for CSV reports. Default: .\MailboxPermissions-<date>
.PARAMETER MailboxFilter
Optional filter for specific mailboxes. Default: all on-premises mailboxes
.PARAMETER IncludeInherited
Include inherited permissions in the report (default: $false)
.NOTES
⚠️ AI-GENERATED SCRIPT - UNTESTED
This script was generated by Claude AI and has not been tested in production.
Review and test thoroughly in a non-production environment before use.
- Run in Exchange Management Shell with appropriate RBAC permissions
- Can take significant time with large mailbox counts
- Tested compatibility: Exchange 2013/2016/2019 (not validated)
.EXAMPLE
.\Get-MailboxPermissions.ps1
.EXAMPLE
.\Get-MailboxPermissions.ps1 -OutputFolder "D:\Reports\Permissions" -IncludeInherited $true
#>
[CmdletBinding()]
param(
[string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("MailboxPermissions-" + (Get-Date -Format "yyyyMMdd-HHmm"))),
[string]$MailboxFilter = "*",
[bool]$IncludeInherited = $false
)
function NowTag { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") }
Write-Host "[$(NowTag)] ⚠️ AI-GENERATED SCRIPT - UNTESTED" -ForegroundColor Yellow
Write-Host "[$(NowTag)] Starting mailbox permissions audit..." -ForegroundColor Green
# Create output folder
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
# Get all on-premises mailboxes
Write-Host "[$(NowTag)] Retrieving mailboxes..."
$mailboxes = Get-Mailbox -Filter $MailboxFilter -ResultSize Unlimited -ErrorAction SilentlyContinue |
Where-Object { $_.RecipientTypeDetails -notmatch "^Remote" -and $_.Database }
$mbCount = ($mailboxes | Measure-Object).Count
Write-Host "[$(NowTag)] Found $mbCount mailboxes to audit"
# Collections
$fullAccessPerms = @()
$sendAsPerms = @()
$sendOnBehalfPerms = @()
$current = 0
foreach ($mb in $mailboxes) {
$current++
$pct = [int](($current / $mbCount) * 100)
Write-Progress -Activity "Auditing Mailbox Permissions" -Status "Processing $($mb.DisplayName) ($current/$mbCount)" -PercentComplete $pct
# FullAccess permissions
try {
$fullAccess = Get-MailboxPermission -Identity $mb.Identity -ErrorAction SilentlyContinue |
Where-Object {
$_.User -notlike "NT AUTHORITY\*" -and
$_.User -notlike "S-1-5-*" -and
$_.AccessRights -like "*FullAccess*" -and
($IncludeInherited -or -not $_.IsInherited)
}
foreach ($perm in $fullAccess) {
$fullAccessPerms += [PSCustomObject]@{
Mailbox = $mb.DisplayName
PrimarySmtpAddress = $mb.PrimarySmtpAddress
User = $perm.User
AccessRights = ($perm.AccessRights -join ", ")
IsInherited = $perm.IsInherited
Deny = $perm.Deny
}
}
} catch {
Write-Host "[$(NowTag)] ERROR getting FullAccess for $($mb.DisplayName): $_" -ForegroundColor Red
}
# SendAs permissions
try {
$sendAs = Get-ADPermission -Identity $mb.DistinguishedName -ErrorAction SilentlyContinue |
Where-Object {
$_.ExtendedRights -like "*Send-As*" -and
$_.User -notlike "NT AUTHORITY\*" -and
$_.User -notlike "S-1-5-*" -and
($IncludeInherited -or -not $_.IsInherited)
}
foreach ($perm in $sendAs) {
$sendAsPerms += [PSCustomObject]@{
Mailbox = $mb.DisplayName
PrimarySmtpAddress = $mb.PrimarySmtpAddress
User = $perm.User
IsInherited = $perm.IsInherited
Deny = $perm.Deny
}
}
} catch {
Write-Host "[$(NowTag)] ERROR getting SendAs for $($mb.DisplayName): $_" -ForegroundColor Red
}
# SendOnBehalf permissions
if ($mb.GrantSendOnBehalfTo -and $mb.GrantSendOnBehalfTo.Count -gt 0) {
foreach ($user in $mb.GrantSendOnBehalfTo) {
$sendOnBehalfPerms += [PSCustomObject]@{
Mailbox = $mb.DisplayName
PrimarySmtpAddress = $mb.PrimarySmtpAddress
User = $user
}
}
}
}
Write-Progress -Activity "Auditing Mailbox Permissions" -Completed
# Export results
Write-Host "[$(NowTag)] Exporting results..."
$fullAccessFile = Join-Path $OutputFolder "FullAccess-Permissions.csv"
$sendAsFile = Join-Path $OutputFolder "SendAs-Permissions.csv"
$sendOnBehalfFile = Join-Path $OutputFolder "SendOnBehalf-Permissions.csv"
$summaryFile = Join-Path $OutputFolder "Permissions-Summary.txt"
if ($fullAccessPerms.Count -gt 0) {
$fullAccessPerms | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $fullAccessFile
Write-Host "[$(NowTag)] FullAccess permissions: $fullAccessFile"
} else {
Write-Host "[$(NowTag)] No FullAccess permissions found"
}
if ($sendAsPerms.Count -gt 0) {
$sendAsPerms | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $sendAsFile
Write-Host "[$(NowTag)] SendAs permissions: $sendAsFile"
} else {
Write-Host "[$(NowTag)] No SendAs permissions found"
}
if ($sendOnBehalfPerms.Count -gt 0) {
$sendOnBehalfPerms | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $sendOnBehalfFile
Write-Host "[$(NowTag)] SendOnBehalf permissions: $sendOnBehalfFile"
} else {
Write-Host "[$(NowTag)] No SendOnBehalf permissions found"
}
# Summary
$summary = @"
Mailbox Permissions Audit Summary
Generated: $(Get-Date)
Mailboxes Audited: $mbCount
FullAccess Permissions: $($fullAccessPerms.Count)
SendAs Permissions: $($sendAsPerms.Count)
SendOnBehalf Permissions: $($sendOnBehalfPerms.Count)
Output Folder: $OutputFolder
"@
$summary | Out-File -FilePath $summaryFile -Encoding UTF8
Write-Host "`n$summary"
Write-Host "[$(NowTag)] Audit complete!" -ForegroundColor Green

View File

@ -0,0 +1,226 @@
<#
.SYNOPSIS
Analyze Exchange message flow statistics from transport logs
.DESCRIPTION
Aggregates statistics from Exchange transport logs including top senders/receivers,
message volume by time period, and potential anomalies. Builds on Get-SMTPTraffic.ps1
pattern but provides comprehensive analysis.
.PARAMETER LogPath
Path to transport logs (default: auto-detect from Exchange install)
.PARAMETER DaysBack
Number of days of logs to analyze (default: 7)
.PARAMETER OutputFolder
Destination folder for reports. Default: .\MailflowStats-<date>
.PARAMETER TopCount
Number of top senders/receivers to include in report (default: 25)
.NOTES
⚠️ AI-GENERATED SCRIPT - UNTESTED
This script was generated by Claude AI and has not been tested in production.
Review and test thoroughly in a non-production environment before use.
- Run in Exchange Management Shell
- Can be slow with large log volumes
- Analyzes SMTP Receive logs by default
.EXAMPLE
.\Get-MailflowStats.ps1
.EXAMPLE
.\Get-MailflowStats.ps1 -DaysBack 30 -TopCount 50
#>
[CmdletBinding()]
param(
[string]$LogPath = "",
[int]$DaysBack = 7,
[string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("MailflowStats-" + (Get-Date -Format "yyyyMMdd-HHmm"))),
[int]$TopCount = 25
)
function NowTag { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") }
Write-Host "[$(NowTag)] ⚠️ AI-GENERATED SCRIPT - UNTESTED" -ForegroundColor Yellow
Write-Host "[$(NowTag)] Starting mail flow analysis..." -ForegroundColor Green
# Auto-detect log path if not specified
if (-not $LogPath) {
$exchangePath = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\Setup" -ErrorAction SilentlyContinue).MsiInstallPath
if ($exchangePath) {
$LogPath = Join-Path $exchangePath "TransportRoles\Logs\FrontEnd\ProtocolLog\SmtpReceive"
} else {
Write-Host "[$(NowTag)] ERROR: Could not auto-detect Exchange log path. Please specify -LogPath parameter" -ForegroundColor Red
exit 1
}
}
if (-not (Test-Path $LogPath)) {
Write-Host "[$(NowTag)] ERROR: Log path not found: $LogPath" -ForegroundColor Red
exit 1
}
Write-Host "[$(NowTag)] Log path: $LogPath"
# Create output folder
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
# Get log files within date range
$cutoffDate = (Get-Date).AddDays(-$DaysBack)
Write-Host "[$(NowTag)] Analyzing logs from $($cutoffDate.ToString('yyyy-MM-dd')) to $(Get-Date -Format 'yyyy-MM-dd')"
$logFiles = Get-ChildItem -Path $LogPath -Filter "*.log" -ErrorAction SilentlyContinue |
Where-Object { $_.LastWriteTime -ge $cutoffDate } |
Sort-Object LastWriteTime -Descending
$fileCount = ($logFiles | Measure-Object).Count
if ($fileCount -eq 0) {
Write-Host "[$(NowTag)] No log files found in specified date range" -ForegroundColor Yellow
exit 0
}
Write-Host "[$(NowTag)] Found $fileCount log file(s) to analyze"
# Parse logs
$allMessages = @()
$fileIdx = 0
foreach ($logFile in $logFiles) {
$fileIdx++
Write-Progress -Activity "Parsing Transport Logs" -Status "Processing $($logFile.Name) ($fileIdx/$fileCount)" -PercentComplete ([int](($fileIdx / $fileCount) * 100))
# Get header from file
$header = Get-Content $logFile.FullName -TotalCount 50 | Where-Object { $_ -like '#Fields:*' } | Select-Object -First 1
if (-not $header) { continue }
$fields = $header -replace '^#Fields: ', ''
$columns = $fields -split ','
# Parse log entries
Get-Content $logFile.FullName | Where-Object { -not ($_ -like '#*') -and $_ -match ',' } | ForEach-Object {
try {
$row = $_ -split ',(?=(?:[^"]*"[^"]*")*[^"]*$)' # Handle quoted fields
$entry = [PSCustomObject]@{}
for ($i = 0; $i -lt $columns.Count -and $i -lt $row.Count; $i++) {
$entry | Add-Member -NotePropertyName $columns[$i].Trim() -NotePropertyValue ($row[$i] -replace '^"|"$', '') -Force
}
# Filter for relevant events (RECEIVE events)
if ($entry.event -eq 'RECEIVE' -or $entry.event -eq '+') {
$allMessages += $entry
}
} catch {}
}
}
Write-Progress -Activity "Parsing Transport Logs" -Completed
$totalMessages = $allMessages.Count
Write-Host "[$(NowTag)] Parsed $totalMessages message(s)"
if ($totalMessages -eq 0) {
Write-Host "[$(NowTag)] No messages found to analyze" -ForegroundColor Yellow
exit 0
}
# Analyze data
Write-Host "[$(NowTag)] Analyzing message flow patterns..."
# Top senders (by remote-endpoint or client-ip)
$topSenders = $allMessages | Where-Object { $_.'remote-endpoint' } |
Group-Object -Property 'remote-endpoint' |
Select-Object @{N = "IPAddress"; E = { $_.Name } }, Count |
Sort-Object Count -Descending |
Select-Object -First $TopCount
# Messages by hour
$messagesByHour = $allMessages | Where-Object { $_.'date-time' } |
ForEach-Object {
try {
$dt = [DateTime]::Parse($_.'date-time')
[PSCustomObject]@{ Hour = $dt.Hour }
} catch {}
} |
Group-Object Hour |
Select-Object @{N = "Hour"; E = { $_.Name } }, Count |
Sort-Object Hour
# Messages by day
$messagesByDay = $allMessages | Where-Object { $_.'date-time' } |
ForEach-Object {
try {
$dt = [DateTime]::Parse($_.'date-time')
[PSCustomObject]@{ Date = $dt.ToString('yyyy-MM-dd') }
} catch {}
} |
Group-Object Date |
Select-Object @{N = "Date"; E = { $_.Name } }, Count |
Sort-Object Date
# Connector usage
$connectorStats = $allMessages | Where-Object { $_.'connector-id' } |
Group-Object -Property 'connector-id' |
Select-Object @{N = "Connector"; E = { $_.Name } }, Count |
Sort-Object Count -Descending
# Export results
Write-Host "[$(NowTag)] Exporting results..."
$summaryFile = Join-Path $OutputFolder "Mailflow-Summary.txt"
$topSendersFile = Join-Path $OutputFolder "Top-Senders-By-IP.csv"
$byHourFile = Join-Path $OutputFolder "Messages-By-Hour.csv"
$byDayFile = Join-Path $OutputFolder "Messages-By-Day.csv"
$connectorFile = Join-Path $OutputFolder "Messages-By-Connector.csv"
$topSenders | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $topSendersFile
$messagesByHour | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $byHourFile
$messagesByDay | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $byDayFile
$connectorStats | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $connectorFile
Write-Host "[$(NowTag)] Top senders: $topSendersFile" -ForegroundColor Green
Write-Host "[$(NowTag)] Hourly distribution: $byHourFile" -ForegroundColor Green
Write-Host "[$(NowTag)] Daily distribution: $byDayFile" -ForegroundColor Green
Write-Host "[$(NowTag)] Connector stats: $connectorFile" -ForegroundColor Green
# Summary report
$summary = @"
Mail Flow Statistics Report
Generated: $(Get-Date)
Analysis Period: $($cutoffDate.ToString('yyyy-MM-dd')) to $(Get-Date -Format 'yyyy-MM-dd')
Log Path: $LogPath
SUMMARY:
Total Messages Analyzed: $totalMessages
Log Files Processed: $fileCount
Average Messages Per Day: $([math]::Round($totalMessages / $DaysBack, 0))
TOP $TopCount SENDERS (by IP):
$($topSenders | ForEach-Object { " $($_.IPAddress): $($_.Count) messages" } | Out-String)
BUSIEST HOURS (24-hour format):
$($messagesByHour | Sort-Object Count -Descending | Select-Object -First 5 | ForEach-Object { " Hour $($_.Hour): $($_.Count) messages" } | Out-String)
DAILY VOLUME:
$($messagesByDay | ForEach-Object { " $($_.Date): $($_.Count) messages" } | Out-String)
CONNECTOR USAGE:
$($connectorStats | ForEach-Object { " $($_.Connector): $($_.Count) messages" } | Out-String)
"@
$summary | Out-File -FilePath $summaryFile -Encoding UTF8
Write-Host "[$(NowTag)] Summary report: $summaryFile" -ForegroundColor Green
# Console output
Write-Host "`nMAIL FLOW SUMMARY:" -ForegroundColor Cyan
Write-Host " Total Messages: $totalMessages"
Write-Host " Average Per Day: $([math]::Round($totalMessages / $DaysBack, 0))"
Write-Host " Peak Hour: $(($messagesByHour | Sort-Object Count -Descending | Select-Object -First 1).Hour):00"
Write-Host " Busiest Day: $(($messagesByDay | Sort-Object Count -Descending | Select-Object -First 1).Date)"
Write-Host "`n[$(NowTag)] Analysis complete! Output folder: $OutputFolder"