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

95
AI-GENERATED-SCRIPTS.md Normal file
View File

@ -0,0 +1,95 @@
# AI-Generated Scripts - Important Notice
⚠️ **WARNING: The scripts listed below were generated by Claude AI and have NOT been tested in production environments.**
These scripts were created to complement the existing manually-written scripts in this repository. While they follow established patterns and best practices, **they require thorough review and testing before use in any production environment**.
## AI-Generated Scripts List
### Exchange Scripts
| Script | Purpose | Risk Level |
|--------|---------|------------|
| `Get-MailboxPermissions.ps1` | Audit mailbox delegate access (SendAs, SendOnBehalf, FullAccess) | Low (read-only) |
| `Get-InactiveMailboxes.ps1` | Identify stale mailboxes based on LastLogonTime | Low (read-only) |
| `Compare-MailboxDatabases.ps1` | Database health comparison with alerting | Low (read-only) |
| `Export-DistributionGroups.ps1` | Complete distribution group inventory | Low (read-only) |
| `Get-MailflowStats.ps1` | Transport log analysis for message flow patterns | Low (read-only) |
### Active Directory Scripts
| Script | Purpose | Risk Level |
|--------|---------|------------|
| `Get-ADUserLastLogon.ps1` | True LastLogon query across all domain controllers | Medium (queries all DCs) |
| `Export-OUStructure.ps1` | OU hierarchy documentation with GPO links | Low (read-only) |
| `Compare-ADGroupMemberships.ps1` | Group membership comparison between users | Low (read-only) |
### System Maintenance Scripts
| Script | Purpose | Risk Level |
|--------|---------|------------|
| `Get-ServerInventory.ps1` | Hardware/software inventory with Word/HTML report | Low (read-only) |
| `Monitor-DiskSpace.ps1` | Disk space monitoring with alerting | Low (read-only) |
| `Backup-ExchangeCertificates.ps1` | Certificate export to PFX files | Medium (exports sensitive keys) |
| `Test-ExchangeHealth.ps1` | Aggregated Exchange health checks | Low (read-only tests) |
## Testing Recommendations
### Before Production Use:
1. **Review the Code**
- Examine the logic and ensure it fits your environment
- Check for hardcoded paths, IP ranges, or environment-specific assumptions
- Verify parameter defaults are appropriate
2. **Test in Non-Production**
- Run scripts in a test environment first
- Verify output is accurate and complete
- Check performance impact (especially scripts that query all DCs or parse large logs)
3. **Verify Prerequisites**
- Ensure required modules are available (Exchange Management Shell, Active Directory)
- Confirm appropriate permissions (RBAC roles, AD rights, Administrator)
- Test with and without optional parameters
4. **Monitor Performance**
- Some scripts can be slow with large datasets:
- `Get-ADUserLastLogon.ps1` queries every domain controller
- `Get-MailflowStats.ps1` parses potentially large log files
- `Get-InactiveMailboxes.ps1` retrieves statistics for all mailboxes
- Consider running during maintenance windows or off-peak hours
5. **Security Considerations**
- `Backup-ExchangeCertificates.ps1` exports private keys - store PFX files securely
- `Get-MailboxPermissions.ps1` may reveal sensitive access patterns
- Review output before sharing to ensure no credentials or secrets are exposed
## How to Identify AI-Generated Scripts
All AI-generated scripts include:
1. Prominent warning in the `.SYNOPSIS` section: "⚠️ AI-GENERATED SCRIPT - UNTESTED"
2. Console output warning when executed
3. This file (`AI-GENERATED-SCRIPTS.md`) lists all AI-generated scripts
## Manually Written Scripts (Production-Tested)
The following scripts were manually written and have been used in production:
- `Exchange/Exchange-Inventory.ps1`
- `Exchange/Get-LargeMailboxes.ps1`
- `Exchange/Get-MailboxesPerType.ps1`
- `Exchange/Get-SMTPTraffic.ps1`
- `ActiveDirectory/CopyADgroups.ps1`
- `Misc/Fix-VSSBackup.ps1`
## Reporting Issues
If you find issues with AI-generated scripts, please:
1. Document the issue (error messages, unexpected behavior, environment details)
2. Note which script and version (check git commit)
3. Consider fixing the issue and contributing back to the repository
## License
These scripts are provided as-is under the same license as the repository. No warranty is provided, express or implied.
---
**Last Updated**: 2025-10-15
**Generated By**: Claude AI (Anthropic)

View File

@ -0,0 +1,215 @@
<#
.SYNOPSIS
Compare group memberships between two Active Directory users
.DESCRIPTION
Shows differences in group memberships between two users. Useful for
access troubleshooting, onboarding/offboarding validation, or role comparison.
.PARAMETER User1
First user (SamAccountName, UserPrincipalName, or DistinguishedName)
.PARAMETER User2
Second user (SamAccountName, UserPrincipalName, or DistinguishedName)
.PARAMETER OutputFolder
Destination folder for reports. Default: .\GroupComparison-<date>
.PARAMETER ShowCommonGroups
Include groups common to both users in report (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.
- Requires Active Directory PowerShell module
- Run with appropriate AD permissions
- Includes nested group memberships
.EXAMPLE
.\Compare-ADGroupMemberships.ps1 -User1 "jsmith" -User2 "jdoe"
.EXAMPLE
.\Compare-ADGroupMemberships.ps1 -User1 "john.smith@domain.com" -User2 "jane.doe@domain.com" -ShowCommonGroups $false
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$User1,
[Parameter(Mandatory = $true)]
[string]$User2,
[string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("GroupComparison-" + (Get-Date -Format "yyyyMMdd-HHmm"))),
[bool]$ShowCommonGroups = $true
)
function NowTag { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") }
Write-Host "[$(NowTag)] ⚠️ AI-GENERATED SCRIPT - UNTESTED" -ForegroundColor Yellow
Write-Host "[$(NowTag)] Comparing AD group memberships..." -ForegroundColor Green
# Import AD module
try {
Import-Module ActiveDirectory -ErrorAction Stop
} catch {
Write-Host "[$(NowTag)] ERROR: Active Directory module not available" -ForegroundColor Red
exit 1
}
# Create output folder
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
# Get user objects
Write-Host "[$(NowTag)] Retrieving user information..."
try {
$adUser1 = Get-ADUser -Identity $User1 -Properties MemberOf -ErrorAction Stop
Write-Host "[$(NowTag)] User 1: $($adUser1.Name) ($($adUser1.SamAccountName))"
} catch {
Write-Host "[$(NowTag)] ERROR: Could not find user: $User1" -ForegroundColor Red
exit 1
}
try {
$adUser2 = Get-ADUser -Identity $User2 -Properties MemberOf -ErrorAction Stop
Write-Host "[$(NowTag)] User 2: $($adUser2.Name) ($($adUser2.SamAccountName))"
} catch {
Write-Host "[$(NowTag)] ERROR: Could not find user: $User2" -ForegroundColor Red
exit 1
}
# Get group memberships
Write-Host "[$(NowTag)] Retrieving group memberships..."
$user1Groups = @()
if ($adUser1.MemberOf) {
foreach ($groupDN in $adUser1.MemberOf) {
try {
$group = Get-ADGroup -Identity $groupDN -Properties Description -ErrorAction SilentlyContinue
if ($group) {
$user1Groups += [PSCustomObject]@{
Name = $group.Name
DN = $group.DistinguishedName
Description = $group.Description
GroupScope = $group.GroupScope
GroupCategory = $group.GroupCategory
}
}
} catch {}
}
}
$user2Groups = @()
if ($adUser2.MemberOf) {
foreach ($groupDN in $adUser2.MemberOf) {
try {
$group = Get-ADGroup -Identity $groupDN -Properties Description -ErrorAction SilentlyContinue
if ($group) {
$user2Groups += [PSCustomObject]@{
Name = $group.Name
DN = $group.DistinguishedName
Description = $group.Description
GroupScope = $group.GroupScope
GroupCategory = $group.GroupCategory
}
}
} catch {}
}
}
Write-Host "[$(NowTag)] User 1 group count: $($user1Groups.Count)"
Write-Host "[$(NowTag)] User 2 group count: $($user2Groups.Count)"
# Compare memberships
$user1GroupNames = $user1Groups | Select-Object -ExpandProperty Name
$user2GroupNames = $user2Groups | Select-Object -ExpandProperty Name
$uniqueToUser1 = $user1Groups | Where-Object { $_.Name -notin $user2GroupNames }
$uniqueToUser2 = $user2Groups | Where-Object { $_.Name -notin $user1GroupNames }
$commonGroups = $user1Groups | Where-Object { $_.Name -in $user2GroupNames }
# Export results
Write-Host "[$(NowTag)] Exporting comparison results..."
# Unique to User 1
if ($uniqueToUser1.Count -gt 0) {
$file1 = Join-Path $OutputFolder "Groups-UniqueToUser1-$($adUser1.SamAccountName).csv"
$uniqueToUser1 | Sort-Object Name | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $file1
Write-Host "[$(NowTag)] Groups unique to $($adUser1.Name): $file1" -ForegroundColor Cyan
}
# Unique to User 2
if ($uniqueToUser2.Count -gt 0) {
$file2 = Join-Path $OutputFolder "Groups-UniqueToUser2-$($adUser2.SamAccountName).csv"
$uniqueToUser2 | Sort-Object Name | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $file2
Write-Host "[$(NowTag)] Groups unique to $($adUser2.Name): $file2" -ForegroundColor Cyan
}
# Common groups
if ($ShowCommonGroups -and $commonGroups.Count -gt 0) {
$fileCommon = Join-Path $OutputFolder "Groups-Common.csv"
$commonGroups | Sort-Object Name | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $fileCommon
Write-Host "[$(NowTag)] Common groups: $fileCommon" -ForegroundColor Green
}
# Summary report
$summaryFile = Join-Path $OutputFolder "Comparison-Summary.txt"
$summary = @"
Active Directory Group Membership Comparison
Generated: $(Get-Date)
User 1: $($adUser1.Name) ($($adUser1.SamAccountName))
User 2: $($adUser2.Name) ($($adUser2.SamAccountName))
SUMMARY:
User 1 Total Groups: $($user1Groups.Count)
User 2 Total Groups: $($user2Groups.Count)
Common Groups: $($commonGroups.Count)
Groups Unique to User 1: $($uniqueToUser1.Count)
Groups Unique to User 2: $($uniqueToUser2.Count)
GROUPS UNIQUE TO USER 1 ($($adUser1.Name)):
$($uniqueToUser1 | ForEach-Object { " - $($_.Name)" } | Out-String)
GROUPS UNIQUE TO USER 2 ($($adUser2.Name)):
$($uniqueToUser2 | ForEach-Object { " - $($_.Name)" } | Out-String)
$(if ($ShowCommonGroups) {
"COMMON GROUPS:
$($commonGroups | ForEach-Object { " - $($_.Name)" } | Out-String)"
} else { "" })
"@
$summary | Out-File -FilePath $summaryFile -Encoding UTF8
Write-Host "[$(NowTag)] Summary report: $summaryFile" -ForegroundColor Green
# Console output
Write-Host "`nCOMPARISON RESULTS:" -ForegroundColor Cyan
Write-Host " Common Groups: $($commonGroups.Count)" -ForegroundColor Green
Write-Host " Unique to $($adUser1.Name): $($uniqueToUser1.Count)" -ForegroundColor Yellow
Write-Host " Unique to $($adUser2.Name): $($uniqueToUser2.Count)" -ForegroundColor Yellow
if ($uniqueToUser1.Count -gt 0) {
Write-Host "`nGroups Only in $($adUser1.Name):" -ForegroundColor Yellow
$uniqueToUser1 | Select-Object -First 10 | ForEach-Object {
Write-Host " - $($_.Name)"
}
if ($uniqueToUser1.Count -gt 10) {
Write-Host " ... and $($uniqueToUser1.Count - 10) more (see CSV)"
}
}
if ($uniqueToUser2.Count -gt 0) {
Write-Host "`nGroups Only in $($adUser2.Name):" -ForegroundColor Yellow
$uniqueToUser2 | Select-Object -First 10 | ForEach-Object {
Write-Host " - $($_.Name)"
}
if ($uniqueToUser2.Count -gt 10) {
Write-Host " ... and $($uniqueToUser2.Count - 10) more (see CSV)"
}
}
Write-Host "`n[$(NowTag)] Comparison complete! Output folder: $OutputFolder"

View File

@ -0,0 +1,201 @@
<#
.SYNOPSIS
Export Active Directory OU structure with GPO links
.DESCRIPTION
Documents the complete OU hierarchy including GPO links, inheritance settings,
and description. Useful for documentation, disaster recovery, or comparing
environments.
.PARAMETER OutputFolder
Destination folder for reports. Default: .\OU-Structure-<date>
.PARAMETER IncludeGPODetails
Include detailed GPO information (default: $true)
.PARAMETER SearchBase
Optional starting point for OU export (default: domain root)
.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.
- Requires Active Directory PowerShell module
- Run with appropriate AD permissions
- Exports to both CSV (detailed) and TXT (tree view)
.EXAMPLE
.\Export-OUStructure.ps1
.EXAMPLE
.\Export-OUStructure.ps1 -SearchBase "OU=Departments,DC=domain,DC=com"
#>
[CmdletBinding()]
param(
[string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("OU-Structure-" + (Get-Date -Format "yyyyMMdd-HHmm"))),
[bool]$IncludeGPODetails = $true,
[string]$SearchBase = ""
)
function NowTag { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") }
Write-Host "[$(NowTag)] ⚠️ AI-GENERATED SCRIPT - UNTESTED" -ForegroundColor Yellow
Write-Host "[$(NowTag)] Exporting OU structure..." -ForegroundColor Green
# Import AD module
try {
Import-Module ActiveDirectory -ErrorAction Stop
} catch {
Write-Host "[$(NowTag)] ERROR: Active Directory module not available" -ForegroundColor Red
exit 1
}
# Create output folder
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
# Get domain info
$domain = Get-ADDomain
if (-not $SearchBase) {
$SearchBase = $domain.DistinguishedName
}
Write-Host "[$(NowTag)] Domain: $($domain.DNSRoot)"
Write-Host "[$(NowTag)] Search Base: $SearchBase"
# Get all OUs
Write-Host "[$(NowTag)] Retrieving organizational units..."
$ouParams = @{
Filter = '*'
SearchBase = $SearchBase
Properties = @('Description', 'gPLink', 'gPOptions', 'ProtectedFromAccidentalDeletion', 'WhenCreated', 'WhenChanged')
}
$ous = Get-ADOrganizationalUnit @ouParams | Sort-Object DistinguishedName
$ouCount = ($ous | Measure-Object).Count
Write-Host "[$(NowTag)] Found $ouCount organizational unit(s)"
# Build GPO name lookup if requested
$gpoLookup = @{}
if ($IncludeGPODetails) {
Write-Host "[$(NowTag)] Building GPO lookup table..."
try {
Import-Module GroupPolicy -ErrorAction Stop
$gpos = Get-GPO -All -ErrorAction SilentlyContinue
foreach ($gpo in $gpos) {
$gpoLookup[$gpo.Id.ToString()] = $gpo.DisplayName
}
Write-Host "[$(NowTag)] Found $($gpos.Count) GPO(s)"
} catch {
Write-Host "[$(NowTag)] WARNING: Could not load GroupPolicy module - GPO names will show as GUIDs" -ForegroundColor Yellow
}
}
# Process OUs
$ouDetails = @()
$treeLines = @()
foreach ($ou in $ous) {
# Calculate depth for tree view
$depth = ($ou.DistinguishedName -split ',OU=').Count - 1
$indent = " " * $depth
# Parse GPO links
$gpoLinks = @()
$gpoNames = @()
if ($ou.gPLink) {
# gPLink format: [LDAP://cn={GUID},cn=policies,cn=system,DC=domain,DC=com;0]
$links = $ou.gPLink -split '\]\[' -replace '^\[' -replace '\]$'
foreach ($link in $links) {
if ($link -match '\{([^\}]+)\}') {
$gpoGuid = $matches[1]
$gpoName = if ($gpoLookup.ContainsKey($gpoGuid)) {
$gpoLookup[$gpoGuid]
} else {
$gpoGuid
}
# Parse link order and enforcement
$linkOrder = if ($link -match ';(\d+)') { $matches[1] } else { "0" }
$enforced = $linkOrder -band 1 # Bit 0 indicates enforcement
$disabled = $linkOrder -band 2 # Bit 1 indicates disabled
$gpoNames += $gpoName
$gpoLinks += [PSCustomObject]@{
OU = $ou.Name
GPOName = $gpoName
GPOGUID = $gpoGuid
LinkOrder = $linkOrder
Enforced = ($enforced -eq 1)
Disabled = ($disabled -eq 2)
}
}
}
}
# GPO inheritance blocked?
$inheritanceBlocked = ($ou.gPOptions -eq 1)
# Tree view line
$protectedMark = if ($ou.ProtectedFromAccidentalDeletion) { "[P]" } else { "" }
$inheritMark = if ($inheritanceBlocked) { "[BLOCKED]" } else { "" }
$gpoMark = if ($gpoNames.Count -gt 0) { " (GPOs: $($gpoNames -join ', '))" } else { "" }
$treeLines += "$indent$($ou.Name) $protectedMark$inheritMark$gpoMark"
# Detailed record
$ouDetails += [PSCustomObject]@{
Name = $ou.Name
DistinguishedName = $ou.DistinguishedName
Description = $ou.Description
ProtectedFromAccidentalDeletion = $ou.ProtectedFromAccidentalDeletion
GPOInheritanceBlocked = $inheritanceBlocked
LinkedGPOs = ($gpoNames -join "; ")
LinkedGPOCount = $gpoNames.Count
WhenCreated = $ou.WhenCreated
WhenChanged = $ou.WhenChanged
}
}
# Export detailed CSV
$csvFile = Join-Path $OutputFolder "OU-Structure.csv"
$ouDetails | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $csvFile
Write-Host "[$(NowTag)] OU details exported: $csvFile" -ForegroundColor Green
# Export GPO links if available
if ($IncludeGPODetails -and $gpoLinks.Count -gt 0) {
$gpoLinksFile = Join-Path $OutputFolder "OU-GPO-Links.csv"
$gpoLinks | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $gpoLinksFile
Write-Host "[$(NowTag)] GPO links exported: $gpoLinksFile" -ForegroundColor Green
}
# Export tree view
$treeFile = Join-Path $OutputFolder "OU-Tree.txt"
$treeHeader = @"
Active Directory OU Structure
Domain: $($domain.DNSRoot)
Search Base: $SearchBase
Generated: $(Get-Date)
Legend:
[P] = Protected from Accidental Deletion
[BLOCKED] = GPO Inheritance Blocked
"@
$treeHeader | Out-File -FilePath $treeFile -Encoding UTF8
$treeLines | Out-File -FilePath $treeFile -Encoding UTF8 -Append
Write-Host "[$(NowTag)] Tree view exported: $treeFile" -ForegroundColor Green
# Summary
Write-Host "`nOU STRUCTURE SUMMARY:" -ForegroundColor Cyan
Write-Host " Total OUs: $ouCount"
Write-Host " Protected OUs: $(($ouDetails | Where-Object ProtectedFromAccidentalDeletion).Count)"
Write-Host " OUs with GPO Inheritance Blocked: $(($ouDetails | Where-Object GPOInheritanceBlocked).Count)"
Write-Host " OUs with Linked GPOs: $(($ouDetails | Where-Object { $_.LinkedGPOCount -gt 0 }).Count)"
Write-Host "`n[$(NowTag)] Export complete! Output folder: $OutputFolder"

View File

@ -0,0 +1,185 @@
<#
.SYNOPSIS
Report on inactive Active Directory user accounts based on true LastLogon
.DESCRIPTION
Queries all domain controllers to find the most recent LastLogon time for users.
The LastLogon attribute is not replicated, so each DC must be queried individually
to get accurate results. Identifies accounts inactive for specified days.
.PARAMETER InactiveDays
Number of days without logon to consider account inactive (default: 90)
.PARAMETER OutputFolder
Destination folder for reports. Default: .\InactiveADUsers-<date>
.PARAMETER SearchBase
Optional OU to limit search scope (default: entire domain)
.PARAMETER IncludeDisabled
Include disabled accounts 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.
- Requires Active Directory PowerShell module
- Run with appropriate AD permissions
- Can be slow in large environments (queries each DC)
- LastLogon attribute is not replicated between DCs
.EXAMPLE
.\Get-ADUserLastLogon.ps1
.EXAMPLE
.\Get-ADUserLastLogon.ps1 -InactiveDays 180 -SearchBase "OU=Users,DC=domain,DC=com"
#>
[CmdletBinding()]
param(
[int]$InactiveDays = 90,
[string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("InactiveADUsers-" + (Get-Date -Format "yyyyMMdd-HHmm"))),
[string]$SearchBase = "",
[bool]$IncludeDisabled = $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 AD user lastlogon analysis..." -ForegroundColor Green
# Import AD module
try {
Import-Module ActiveDirectory -ErrorAction Stop
} catch {
Write-Host "[$(NowTag)] ERROR: Active Directory module not available" -ForegroundColor Red
exit 1
}
# 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 all domain controllers
Write-Host "[$(NowTag)] Discovering domain controllers..."
$domainControllers = Get-ADDomainController -Filter * | Select-Object -ExpandProperty HostName
Write-Host "[$(NowTag)] Found $($domainControllers.Count) domain controller(s)"
# Get all users
Write-Host "[$(NowTag)] Retrieving AD users..."
$getUserParams = @{
Filter = "ObjectClass -eq 'user' -and Enabled -eq 'True'"
Properties = @('LastLogon', 'LastLogonDate', 'WhenCreated', 'Department', 'Title', 'Description', 'PasswordLastSet', 'PasswordNeverExpires', 'Enabled')
}
if ($SearchBase) {
$getUserParams['SearchBase'] = $SearchBase
}
if ($IncludeDisabled) {
$getUserParams['Filter'] = "ObjectClass -eq 'user'"
}
$users = Get-ADUser @getUserParams
$userCount = ($users | Measure-Object).Count
Write-Host "[$(NowTag)] Found $userCount user(s) to analyze"
# Query each DC for LastLogon and find the most recent
$userLogonData = @()
$current = 0
foreach ($user in $users) {
$current++
$pct = [int](($current / $userCount) * 100)
Write-Progress -Activity "Querying LastLogon from Domain Controllers" -Status "Processing $($user.SamAccountName) ($current/$userCount)" -PercentComplete $pct
$mostRecentLogon = $null
foreach ($dc in $domainControllers) {
try {
$userFromDC = Get-ADUser -Identity $user.SamAccountName -Server $dc -Properties LastLogon -ErrorAction SilentlyContinue
if ($userFromDC.LastLogon) {
$logonDate = [DateTime]::FromFileTime($userFromDC.LastLogon)
if ($null -eq $mostRecentLogon -or $logonDate -gt $mostRecentLogon) {
$mostRecentLogon = $logonDate
}
}
} catch {
Write-Verbose "Could not query $dc for $($user.SamAccountName)"
}
}
# Determine if inactive
$isInactive = $false
$reason = ""
$daysSinceLogon = "N/A"
if ($null -eq $mostRecentLogon) {
$isInactive = $true
$reason = "Never logged on"
} elseif ($mostRecentLogon -lt $cutoffDate) {
$isInactive = $true
$daysSinceLogon = [int]((Get-Date) - $mostRecentLogon).TotalDays
$reason = "Last logon: $($mostRecentLogon.ToString('yyyy-MM-dd'))"
} else {
$daysSinceLogon = [int]((Get-Date) - $mostRecentLogon).TotalDays
}
if ($isInactive) {
$userLogonData += [PSCustomObject]@{
SamAccountName = $user.SamAccountName
DisplayName = $user.Name
UserPrincipalName = $user.UserPrincipalName
Enabled = $user.Enabled
LastLogon = $mostRecentLogon
DaysSinceLogon = $daysSinceLogon
LastLogonDate = $user.LastLogonDate # Replicated attribute for comparison
WhenCreated = $user.WhenCreated
PasswordLastSet = $user.PasswordLastSet
PasswordNeverExpires = $user.PasswordNeverExpires
Department = $user.Department
Title = $user.Title
Description = $user.Description
DistinguishedName = $user.DistinguishedName
Reason = $reason
}
}
}
Write-Progress -Activity "Querying LastLogon from Domain Controllers" -Completed
# Export results
Write-Host "[$(NowTag)] Found $($userLogonData.Count) inactive user(s)"
if ($userLogonData.Count -gt 0) {
$csvFile = Join-Path $OutputFolder "Inactive-ADUsers.csv"
$userLogonData | Sort-Object DaysSinceLogon -Descending |
Export-Csv -NoTypeInformation -Encoding UTF8 -Path $csvFile
Write-Host "[$(NowTag)] Report exported: $csvFile" -ForegroundColor Green
# Summary statistics
Write-Host "`nINACTIVE USER SUMMARY:" -ForegroundColor Cyan
Write-Host " Total Inactive Users: $($userLogonData.Count)"
Write-Host " Never Logged On: $(($userLogonData | Where-Object { $_.DaysSinceLogon -eq 'N/A' }).Count)"
Write-Host " Enabled Accounts: $(($userLogonData | Where-Object Enabled).Count)"
Write-Host " Disabled Accounts: $(($userLogonData | Where-Object { -not $_.Enabled }).Count)"
Write-Host " Password Never Expires: $(($userLogonData | Where-Object PasswordNeverExpires).Count)"
# By department
$byDept = $userLogonData | Where-Object Department | Group-Object Department | Sort-Object Count -Descending | Select-Object -First 5
if ($byDept) {
Write-Host "`nTop 5 Departments with Inactive Users:" -ForegroundColor Cyan
$byDept | ForEach-Object {
Write-Host " $($_.Name): $($_.Count)"
}
}
} else {
Write-Host "[$(NowTag)] No inactive users found!" -ForegroundColor Green
}
Write-Host "`n[$(NowTag)] Analysis complete! Output folder: $OutputFolder"

View File

@ -107,8 +107,45 @@ These are operational scripts without formal test frameworks. When modifying:
2. **VSS script**: Test service restart logic on non-critical systems 2. **VSS script**: Test service restart logic on non-critical systems
3. **AD scripts**: Use test groups before production deployment 3. **AD scripts**: Use test groups before production deployment
## AI-Generated Scripts (UNTESTED)
The following scripts were generated by Claude AI in 2025 and have **NOT been tested in production environments**. They should be thoroughly reviewed and tested in non-production environments before use:
### Exchange Scripts (AI-Generated)
- **Get-MailboxPermissions.ps1** - Audits SendAs, SendOnBehalf, and FullAccess permissions
- **Get-InactiveMailboxes.ps1** - Identifies stale mailboxes based on LastLogonTime threshold
- **Compare-MailboxDatabases.ps1** - Database health comparison with alerting (whitespace, backup age)
- **Export-DistributionGroups.ps1** - Complete distribution group inventory with members
- **Get-MailflowStats.ps1** - Transport log analysis for message flow patterns and anomalies
### Active Directory Scripts (AI-Generated)
- **Get-ADUserLastLogon.ps1** - True LastLogon query across all DCs (non-replicated attribute)
- **Export-OUStructure.ps1** - OU hierarchy documentation with GPO links
- **Compare-ADGroupMemberships.ps1** - Group membership comparison between two users
### System Maintenance Scripts (AI-Generated)
- **Get-ServerInventory.ps1** - Comprehensive hardware/software inventory with Word/HTML report
- **Monitor-DiskSpace.ps1** - Disk space monitoring with alerting and optional email
- **Backup-ExchangeCertificates.ps1** - Certificate export to PFX with expiry warnings
- **Test-ExchangeHealth.ps1** - Aggregated Exchange health checks (services, replication, MAPI, databases)
### AI-Generated Script Patterns
These scripts follow the established patterns from the original scripts:
- Similar report generation (Word COM with HTML fallback where applicable)
- Progress tracking for long-running operations
- CSV export capabilities
- `-ErrorAction SilentlyContinue` for graceful degradation
- Comprehensive parameter documentation and examples
- Prominent "⚠️ AI-GENERATED SCRIPT - UNTESTED" warnings in output
**Testing Requirements**: All AI-generated scripts include detailed `.NOTES` sections with testing guidance. Pay special attention to:
- Permission requirements (RBAC, AD rights, Administrator)
- Performance impact (LastLogon queries across DCs, log parsing)
- Environment-specific assumptions (paths, IP ranges, thresholds)
## Important Constraints ## Important Constraints
- Scripts are environment-specific (hardcoded paths like `C:\Program Files\Microsoft\Exchange Server\V15\...`) - Scripts are environment-specific (hardcoded paths like `C:\Program Files\Microsoft\Exchange Server\V15\...`)
- Exchange-Inventory.ps1 assumes specific Exchange cmdlet availability - Exchange-Inventory.ps1 assumes specific Exchange cmdlet availability
- Get-SMTPTraffic.ps1 filters for specific IP range (192.168.1.*) - modify for different networks - Get-SMTPTraffic.ps1 filters for specific IP range (192.168.1.*) - modify for different networks
- AI-generated scripts have NOT been validated in production and require thorough testing

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"

View File

@ -0,0 +1,178 @@
<#
.SYNOPSIS
Backup Exchange certificates to PFX files
.DESCRIPTION
Exports all Exchange certificates to password-protected PFX files.
Alerts on certificates expiring within specified days.
.PARAMETER OutputFolder
Destination folder for PFX exports. Default: .\CertBackup-<date>
.PARAMETER Password
Password for PFX files (required)
.PARAMETER ExpiryWarningDays
Alert on certificates expiring within this many days (default: 90)
.PARAMETER IncludePrivateKey
Export with private key (required for restoration, 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
- Requires password for PFX protection
- Store PFX files securely - they contain private keys
- Tested compatibility: Exchange 2013/2016/2019 (not validated)
.EXAMPLE
$pwd = Read-Host "Enter PFX password" -AsSecureString
.\Backup-ExchangeCertificates.ps1 -Password $pwd
.EXAMPLE
$pwd = ConvertTo-SecureString "P@ssw0rd!" -AsPlainText -Force
.\Backup-ExchangeCertificates.ps1 -Password $pwd -ExpiryWarningDays 60
#>
[CmdletBinding()]
param(
[string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("CertBackup-" + (Get-Date -Format "yyyyMMdd-HHmm"))),
[Parameter(Mandatory = $true)]
[System.Security.SecureString]$Password,
[int]$ExpiryWarningDays = 90,
[bool]$IncludePrivateKey = $true
)
function NowTag { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") }
Write-Host "[$(NowTag)] ⚠️ AI-GENERATED SCRIPT - UNTESTED" -ForegroundColor Yellow
Write-Host "[$(NowTag)] Starting Exchange certificate backup..." -ForegroundColor Green
# Create output folder
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
Write-Host "[$(NowTag)] Output folder: $OutputFolder"
# Get all Exchange certificates
Write-Host "[$(NowTag)] Retrieving Exchange certificates..."
$certs = Get-ExchangeCertificate -ErrorAction SilentlyContinue
if (-not $certs) {
Write-Host "[$(NowTag)] No Exchange certificates found" -ForegroundColor Yellow
exit 0
}
$certCount = ($certs | Measure-Object).Count
Write-Host "[$(NowTag)] Found $certCount certificate(s)"
# Process each certificate
$exported = @()
$warnings = @()
foreach ($cert in $certs) {
$thumbprint = $cert.Thumbprint
$subject = $cert.Subject
$notAfter = $cert.NotAfter
$daysUntilExpiry = [int](($notAfter - (Get-Date)).TotalDays)
Write-Host "[$(NowTag)] Processing: $subject (Thumbprint: $thumbprint)"
# Check expiry
$status = "OK"
if ($daysUntilExpiry -lt 0) {
$status = "EXPIRED"
$warnings += [PSCustomObject]@{
Thumbprint = $thumbprint
Subject = $subject
NotAfter = $notAfter
DaysRemaining = $daysUntilExpiry
Status = $status
}
Write-Host " WARNING: Certificate EXPIRED on $($notAfter.ToString('yyyy-MM-dd'))" -ForegroundColor Red
} elseif ($daysUntilExpiry -lt $ExpiryWarningDays) {
$status = "EXPIRING SOON"
$warnings += [PSCustomObject]@{
Thumbprint = $thumbprint
Subject = $subject
NotAfter = $notAfter
DaysRemaining = $daysUntilExpiry
Status = $status
}
Write-Host " WARNING: Certificate expires in $daysUntilExpiry days ($($notAfter.ToString('yyyy-MM-dd')))" -ForegroundColor Yellow
} else {
Write-Host " Status: OK - Expires in $daysUntilExpiry days"
}
# Export to PFX
$fileName = "$($thumbprint).pfx"
$filePath = Join-Path $OutputFolder $fileName
try {
if ($IncludePrivateKey) {
# Export with private key
Export-ExchangeCertificate -Thumbprint $thumbprint -FileName $filePath -Password $Password -ErrorAction Stop | Out-Null
Write-Host " Exported: $fileName (with private key)" -ForegroundColor Green
} else {
# Export public key only (using .NET method)
$certObj = Get-Item -Path "Cert:\LocalMachine\My\$thumbprint" -ErrorAction SilentlyContinue
if ($certObj) {
$certBytes = $certObj.Export('Cert')
[System.IO.File]::WriteAllBytes($filePath, $certBytes)
Write-Host " Exported: $fileName (public key only)" -ForegroundColor Green
} else {
throw "Certificate not found in LocalMachine\My"
}
}
$exported += [PSCustomObject]@{
Thumbprint = $thumbprint
Subject = $subject
Services = ($cert.Services -join ", ")
NotBefore = $cert.NotBefore
NotAfter = $notAfter
DaysRemaining = $daysUntilExpiry
Status = $status
FileName = $fileName
FilePath = $filePath
FileSize = (Get-Item $filePath).Length
}
} catch {
Write-Host " ERROR exporting certificate: $($_.Exception.Message)" -ForegroundColor Red
}
}
# Export inventory
$inventoryFile = Join-Path $OutputFolder "Certificate-Inventory.csv"
$exported | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $inventoryFile
Write-Host "`n[$(NowTag)] Inventory exported: $inventoryFile" -ForegroundColor Green
# Export warnings if any
if ($warnings.Count -gt 0) {
$warningsFile = Join-Path $OutputFolder "Certificate-Warnings.csv"
$warnings | Sort-Object DaysRemaining | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $warningsFile
Write-Host "[$(NowTag)] Warnings exported: $warningsFile" -ForegroundColor Yellow
}
# Summary
Write-Host "`nBACKUP SUMMARY:" -ForegroundColor Cyan
Write-Host " Total Certificates: $certCount"
Write-Host " Successfully Exported: $($exported.Count)" -ForegroundColor Green
Write-Host " Failed: $($certCount - $exported.Count)" -ForegroundColor $(if ($certCount - $exported.Count -gt 0) { "Red" } else { "Green" })
Write-Host " Expired: $(($warnings | Where-Object Status -eq 'EXPIRED').Count)" -ForegroundColor Red
Write-Host " Expiring Soon: $(($warnings | Where-Object Status -eq 'EXPIRING SOON').Count)" -ForegroundColor Yellow
if ($warnings.Count -gt 0) {
Write-Host "`nCERTIFICATE WARNINGS:" -ForegroundColor Yellow
$warnings | Sort-Object DaysRemaining | ForEach-Object {
$color = if ($_.Status -eq "EXPIRED") { "Red" } else { "Yellow" }
Write-Host " $($_.Status): $($_.Subject) - $($_.DaysRemaining) days" -ForegroundColor $color
}
}
Write-Host "`n[$(NowTag)] Backup complete! Output folder: $OutputFolder"
Write-Host "`nIMPORTANT: Store PFX files securely - they contain private keys!" -ForegroundColor Cyan

View File

@ -0,0 +1,238 @@
<#
.SYNOPSIS
Comprehensive server hardware and software inventory report
.DESCRIPTION
Collects system information including CPU, RAM, disk, OS version, installed roles,
and key services. Generates Word document (with HTML fallback) similar to
Exchange-Inventory.ps1 structure.
.PARAMETER OutputFolder
Destination folder for reports. Default: .\ServerInventory-<date>
.PARAMETER IncludeInstalledSoftware
Include installed software list (can be slow, default: $false)
.PARAMETER IncludeWindowsFeatures
Include Windows Features/Roles (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 with Administrator privileges for complete information
- Works on Windows Server 2012+
- Report generation uses Word COM or HTML fallback
.EXAMPLE
.\Get-ServerInventory.ps1
.EXAMPLE
.\Get-ServerInventory.ps1 -IncludeInstalledSoftware $true -OutputFolder "D:\Inventory"
#>
[CmdletBinding()]
param(
[string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("ServerInventory-" + (Get-Date -Format "yyyyMMdd-HHmm"))),
[bool]$IncludeInstalledSoftware = $false,
[bool]$IncludeWindowsFeatures = $true
)
function NowTag { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") }
Write-Host "[$(NowTag)] ⚠️ AI-GENERATED SCRIPT - UNTESTED" -ForegroundColor Yellow
Write-Host "[$(NowTag)] Starting server inventory..." -ForegroundColor Green
# Create output folder
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
$ReportDocx = Join-Path $OutputFolder "Server-Inventory.docx"
$ReportHtml = Join-Path $OutputFolder "Server-Inventory.html"
# Word COM or HTML fallback (reused pattern from Exchange-Inventory.ps1)
$script:Word = $null
$script:Doc = $null
$script:UseHtml = $false
$script:HtmlSb = New-Object System.Text.StringBuilder
function Start-Report {
try {
$script:Word = New-Object -ComObject Word.Application -ErrorAction Stop
$script:Word.Visible = $false
$script:Doc = $script:Word.Documents.Add()
Add-Heading "Server Inventory Report" 1
Add-Paragraph ("Server: $env:COMPUTERNAME")
Add-Paragraph ("Generated: " + (Get-Date).ToString("yyyy-MM-dd HH:mm"))
} catch {
$script:UseHtml = $true
[void]$script:HtmlSb.AppendLine("<html><head><meta charset='utf-8'><title>Server Inventory</title>")
[void]$script:HtmlSb.AppendLine("<style>body{font-family:Segoe UI,Arial,sans-serif;margin:20px} h1{font-size:24px;color:#0066cc} h2{font-size:20px;margin-top:30px;color:#333} table{border-collapse:collapse;width:100%;margin:15px 0} th,td{border:1px solid #ccc;padding:8px;text-align:left} th{background:#f3f3f3;font-weight:bold}</style></head><body>")
[void]$script:HtmlSb.AppendLine("<h1>Server Inventory Report</h1>")
[void]$script:HtmlSb.AppendLine("<p><strong>Server:</strong> $env:COMPUTERNAME</p>")
[void]$script:HtmlSb.AppendLine("<p><strong>Generated:</strong> " + (Get-Date).ToString("yyyy-MM-dd HH:mm") + "</p>")
}
}
function Add-Heading([string]$Text, [int]$Level = 2) {
if ($script:UseHtml) {
[void]$script:HtmlSb.AppendLine("<h$Level>$Text</h$Level>")
} else {
$range = $script:Doc.Range()
$range.Collapse(0) | Out-Null
$range.Text = "$Text`r"
$style = switch ($Level) { 1 { 'Heading 1' } 2 { 'Heading 2' } default { 'Heading 2' } }
$range.set_Style($style)
$range.InsertParagraphAfter() | Out-Null
}
}
function Add-Paragraph([string]$Text) {
if ($script:UseHtml) {
$enc = [System.Web.HttpUtility]::HtmlEncode($Text)
[void]$script:HtmlSb.AppendLine("<p>$enc</p>")
} else {
$range = $script:Doc.Range()
$range.Collapse(0) | Out-Null
$range.Text = $Text
$range.InsertParagraphAfter() | Out-Null
}
}
function Add-Table([object]$Objects, [string[]]$PropOrder, [string]$Title) {
if ($null -eq $Objects) {
Add-Paragraph ("{0}: No data." -f $Title)
return
}
$arr = @()
if ($Objects -is [System.Collections.IEnumerable] -and -not ($Objects -is [string])) {
foreach ($o in $Objects) { $arr += $o }
} else { $arr = @($Objects) }
if ($arr.Count -eq 0) {
Add-Paragraph ("{0}: No data." -f $Title)
return
}
Add-Heading $Title 3
if ($script:UseHtml) {
[void]$script:HtmlSb.AppendLine("<table><thead><tr>")
foreach ($p in $PropOrder) { [void]$script:HtmlSb.AppendLine("<th>$([System.Web.HttpUtility]::HtmlEncode($p))</th>") }
[void]$script:HtmlSb.AppendLine("</tr></thead><tbody>")
foreach ($o in $arr) {
[void]$script:HtmlSb.AppendLine("<tr>")
foreach ($p in $PropOrder) {
$val = if ($o.$p) { [string]$o.$p } else { "" }
[void]$script:HtmlSb.AppendLine("<td>$([System.Web.HttpUtility]::HtmlEncode($val))</td>")
}
[void]$script:HtmlSb.AppendLine("</tr>")
}
[void]$script:HtmlSb.AppendLine("</tbody></table>")
} else {
$rows = $arr.Count
$cols = $PropOrder.Count
$range = $script:Doc.Range()
$table = $script:Doc.Tables.Add($range, [Math]::Max(1, $rows) + 1, $cols)
for ($c = 1; $c -le $cols; $c++) { $table.Cell(1, $c).Range.Text = $PropOrder[$c - 1] }
$table.Rows.Item(1).Range.Bold = $true
for ($r = 0; $r -lt $rows; $r++) {
for ($c = 0; $c -lt $cols; $c++) {
$val = if ($arr[$r].($PropOrder[$c])) { [string]$arr[$r].($PropOrder[$c]) } else { "" }
$table.Cell($r + 2, $c + 1).Range.Text = $val
}
}
$table.AutoFitBehavior(2) | Out-Null
$range.InsertParagraphAfter() | Out-Null
}
}
function End-Report {
if ($script:UseHtml) {
[void]$script:HtmlSb.AppendLine("</body></html>")
$script:HtmlSb.ToString() | Out-File -LiteralPath $ReportHtml -Encoding UTF8
Write-Host "[$(NowTag)] HTML report: $ReportHtml" -ForegroundColor Green
} else {
$wdFormatXMLDocument = 12
$script:Doc.SaveAs([ref]$ReportDocx, [ref]$wdFormatXMLDocument)
$script:Doc.Close(); $script:Word.Quit()
Write-Host "[$(NowTag)] Word report: $ReportDocx" -ForegroundColor Green
}
}
Start-Report
# System Information
Write-Host "[$(NowTag)] Collecting system information..."
$cs = Get-CimInstance -ClassName Win32_ComputerSystem
$os = Get-CimInstance -ClassName Win32_OperatingSystem
$bios = Get-CimInstance -ClassName Win32_BIOS
$sysInfo = [PSCustomObject]@{
ComputerName = $cs.Name
Domain = $cs.Domain
Manufacturer = $cs.Manufacturer
Model = $cs.Model
TotalPhysicalMemoryGB = [math]::Round($cs.TotalPhysicalMemory / 1GB, 2)
NumberOfProcessors = $cs.NumberOfProcessors
NumberOfLogicalProcessors = $cs.NumberOfLogicalProcessors
OSName = $os.Caption
OSVersion = $os.Version
OSArchitecture = $os.OSArchitecture
InstallDate = $os.InstallDate
LastBootUpTime = $os.LastBootUpTime
BIOSVersion = $bios.SMBIOSBIOSVersion
SerialNumber = $bios.SerialNumber
}
Add-Heading "System Information" 2
Add-Table $sysInfo @("ComputerName", "Domain", "Manufacturer", "Model", "TotalPhysicalMemoryGB", "NumberOfProcessors", "NumberOfLogicalProcessors", "OSName", "OSVersion", "OSArchitecture", "InstallDate", "LastBootUpTime", "BIOSVersion", "SerialNumber") "System Details"
# CPU Information
Write-Host "[$(NowTag)] Collecting CPU information..."
$cpus = Get-CimInstance -ClassName Win32_Processor | Select-Object Name, NumberOfCores, NumberOfLogicalProcessors, MaxClockSpeed, CurrentClockSpeed
Add-Table $cpus @("Name", "NumberOfCores", "NumberOfLogicalProcessors", "MaxClockSpeed", "CurrentClockSpeed") "Processors"
# Disk Information
Write-Host "[$(NowTag)] Collecting disk information..."
$disks = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=3" | Select-Object @{N = "Drive"; E = { $_.DeviceID } }, @{N = "SizeGB"; E = { [math]::Round($_.Size / 1GB, 2) } }, @{N = "FreeGB"; E = { [math]::Round($_.FreeSpace / 1GB, 2) } }, @{N = "UsedGB"; E = { [math]::Round(($_.Size - $_.FreeSpace) / 1GB, 2) } }, @{N = "PercentFree"; E = { [math]::Round(($_.FreeSpace / $_.Size) * 100, 2) } }, VolumeName, FileSystem
Add-Table $disks @("Drive", "SizeGB", "FreeGB", "UsedGB", "PercentFree", "VolumeName", "FileSystem") "Disk Drives"
# Network Adapters
Write-Host "[$(NowTag)] Collecting network adapter information..."
$nics = Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration -Filter "IPEnabled=True" | Select-Object Description, @{N = "IPAddress"; E = { $_.IPAddress -join ", " } }, @{N = "SubnetMask"; E = { $_.IPSubnet -join ", " } }, @{N = "DefaultGateway"; E = { $_.DefaultIPGateway -join ", " } }, @{N = "DNSServers"; E = { $_.DNSServerSearchOrder -join ", " } }, MACAddress, DHCPEnabled
Add-Table $nics @("Description", "IPAddress", "SubnetMask", "DefaultGateway", "DNSServers", "MACAddress", "DHCPEnabled") "Network Adapters"
# Windows Features/Roles
if ($IncludeWindowsFeatures) {
Write-Host "[$(NowTag)] Collecting Windows features..."
try {
Import-Module ServerManager -ErrorAction Stop
$features = Get-WindowsFeature | Where-Object Installed | Select-Object Name, DisplayName, FeatureType
Add-Table $features @("Name", "DisplayName", "FeatureType") "Installed Windows Features/Roles"
} catch {
Add-Paragraph "Windows Features: Could not retrieve (ServerManager module not available)"
}
}
# Services
Write-Host "[$(NowTag)] Collecting critical services..."
$services = Get-Service | Where-Object { $_.StartType -eq "Automatic" -and $_.Status -ne "Running" } | Select-Object Name, DisplayName, Status, StartType
if ($services) {
Add-Table $services @("Name", "DisplayName", "Status", "StartType") "Automatic Services Not Running (Potential Issues)"
} else {
Add-Paragraph "All automatic services are running"
}
# Installed Software (optional, can be slow)
if ($IncludeInstalledSoftware) {
Write-Host "[$(NowTag)] Collecting installed software (this may take a while)..."
$software = Get-CimInstance -ClassName Win32_Product | Select-Object Name, Version, Vendor, InstallDate | Sort-Object Name
Add-Table $software @("Name", "Version", "Vendor", "InstallDate") "Installed Software"
}
# Hotfixes
Write-Host "[$(NowTag)] Collecting installed hotfixes..."
$hotfixes = Get-HotFix | Sort-Object InstalledOn -Descending | Select-Object -First 20 | Select-Object HotFixID, Description, InstalledBy, InstalledOn
Add-Table $hotfixes @("HotFixID", "Description", "InstalledBy", "InstalledOn") "Recent Hotfixes (Last 20)"
End-Report
Write-Host "[$(NowTag)] Inventory complete! Output folder: $OutputFolder"

183
Misc/Monitor-DiskSpace.ps1 Normal file
View File

@ -0,0 +1,183 @@
<#
.SYNOPSIS
Monitor disk space and alert when below threshold
.DESCRIPTION
Checks all fixed drives for available space and alerts when below threshold.
Can be scheduled as a task for proactive monitoring.
.PARAMETER ThresholdPercent
Alert when free space falls below this percentage (default: 15)
.PARAMETER ThresholdGB
Alternative: alert when free space falls below this many GB (default: not used)
.PARAMETER LogPath
Path to log file for alerts (default: C:\Temp\DiskSpace-Monitor.log)
.PARAMETER SendEmail
Send email alerts (requires email parameters, default: $false)
.PARAMETER SmtpServer
SMTP server for email alerts
.PARAMETER EmailFrom
From address for email alerts
.PARAMETER EmailTo
To address(es) for email alerts (comma-separated)
.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.
- Suitable for scheduled task execution
- Exit codes: 0=OK, 1=Warning (below threshold), 2=Error
- Email functionality requires SMTP access
.EXAMPLE
.\Monitor-DiskSpace.ps1
.EXAMPLE
.\Monitor-DiskSpace.ps1 -ThresholdPercent 20 -LogPath "D:\Logs\DiskMonitor.log"
.EXAMPLE
.\Monitor-DiskSpace.ps1 -SendEmail $true -SmtpServer "smtp.domain.com" -EmailFrom "alerts@domain.com" -EmailTo "admin@domain.com"
#>
[CmdletBinding()]
param(
[int]$ThresholdPercent = 15,
[int]$ThresholdGB = 0,
[string]$LogPath = "C:\Temp\DiskSpace-Monitor.log",
[bool]$SendEmail = $false,
[string]$SmtpServer = "",
[string]$EmailFrom = "",
[string]$EmailTo = ""
)
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$stamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
$line = "[$stamp] [$Level] $Message"
Write-Output $line
# Ensure log directory exists
$logDir = Split-Path -Path $LogPath -Parent
if ($logDir -and -not (Test-Path -Path $logDir)) {
try {
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
} catch {}
}
try {
Add-Content -Path $LogPath -Value $line -Encoding UTF8 -ErrorAction SilentlyContinue
} catch {}
}
Write-Log "⚠️ AI-GENERATED SCRIPT - UNTESTED" "WARNING"
Write-Log "Starting disk space monitoring on $env:COMPUTERNAME"
# Get all fixed drives
$disks = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=3" -ErrorAction SilentlyContinue
if (-not $disks) {
Write-Log "ERROR: Could not retrieve disk information" "ERROR"
exit 2
}
$alerts = @()
$allDisksOK = $true
foreach ($disk in $disks) {
$drive = $disk.DeviceID
$sizeGB = [math]::Round($disk.Size / 1GB, 2)
$freeGB = [math]::Round($disk.FreeSpace / 1GB, 2)
$usedGB = [math]::Round(($disk.Size - $disk.FreeSpace) / 1GB, 2)
$percentFree = [math]::Round(($disk.FreeSpace / $disk.Size) * 100, 2)
Write-Log "Drive $drive - Size: $sizeGB GB, Free: $freeGB GB ($percentFree%)"
# Check thresholds
$alertTriggered = $false
$alertReason = ""
if ($ThresholdGB -gt 0 -and $freeGB -lt $ThresholdGB) {
$alertTriggered = $true
$alertReason = "Free space ($freeGB GB) below threshold ($ThresholdGB GB)"
} elseif ($percentFree -lt $ThresholdPercent) {
$alertTriggered = $true
$alertReason = "Free space ($percentFree%) below threshold ($ThresholdPercent%)"
}
if ($alertTriggered) {
$allDisksOK = $false
$alertMsg = "ALERT: Drive $drive - $alertReason"
Write-Log $alertMsg "ALERT"
$alerts += [PSCustomObject]@{
Drive = $drive
SizeGB = $sizeGB
FreeGB = $freeGB
UsedGB = $usedGB
PercentFree = $percentFree
Reason = $alertReason
}
}
}
# Send email if configured and alerts exist
if ($SendEmail -and $alerts.Count -gt 0) {
if (-not $SmtpServer -or -not $EmailFrom -or -not $EmailTo) {
Write-Log "Email alerts enabled but SMTP parameters incomplete" "WARNING"
} else {
Write-Log "Sending email alert to $EmailTo via $SmtpServer"
$subject = "DISK SPACE ALERT: $env:COMPUTERNAME"
$body = @"
Disk Space Alert on $env:COMPUTERNAME
Generated: $(Get-Date)
The following drives have triggered disk space alerts:
"@
foreach ($alert in $alerts) {
$body += @"
Drive: $($alert.Drive)
Size: $($alert.SizeGB) GB
Free: $($alert.FreeGB) GB ($($alert.PercentFree)%)
Used: $($alert.UsedGB) GB
Alert: $($alert.Reason)
"@
}
$body += @"
Threshold Settings:
Percent Threshold: $ThresholdPercent%
GB Threshold: $ThresholdGB GB
Please investigate and free up disk space as needed.
"@
try {
Send-MailMessage -To $EmailTo.Split(',') -From $EmailFrom -Subject $subject -Body $body -SmtpServer $SmtpServer -ErrorAction Stop
Write-Log "Email sent successfully"
} catch {
Write-Log "ERROR sending email: $($_.Exception.Message)" "ERROR"
}
}
}
# Exit with appropriate code
if ($allDisksOK) {
Write-Log "All disks have sufficient free space"
exit 0
} else {
Write-Log "$($alerts.Count) disk(s) below threshold"
exit 1
}

View File

@ -0,0 +1,290 @@
<#
.SYNOPSIS
Quick Exchange server health check with aggregated results
.DESCRIPTION
Runs multiple Exchange health cmdlets and aggregates results into a single
report. Includes service health, replication health, MAPI connectivity,
and database mount status.
.PARAMETER OutputFolder
Destination folder for reports. Default: .\ExchangeHealth-<date>
.PARAMETER IncludeMailflow
Test mail flow using Test-Mailflow cmdlet (default: $false, can be slow)
.PARAMETER ServerName
Specific server to test (default: local server)
.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
- Some tests require specific server roles
- Tests are non-intrusive (read-only)
.EXAMPLE
.\Test-ExchangeHealth.ps1
.EXAMPLE
.\Test-ExchangeHealth.ps1 -ServerName "EXCH01" -IncludeMailflow $true
#>
[CmdletBinding()]
param(
[string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("ExchangeHealth-" + (Get-Date -Format "yyyyMMdd-HHmm"))),
[bool]$IncludeMailflow = $false,
[string]$ServerName = $env:COMPUTERNAME
)
function NowTag { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") }
Write-Host "[$(NowTag)] ⚠️ AI-GENERATED SCRIPT - UNTESTED" -ForegroundColor Yellow
Write-Host "[$(NowTag)] Starting Exchange health check on $ServerName..." -ForegroundColor Green
# Create output folder
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
$allResults = @()
$criticalIssues = @()
$warnings = @()
# Test 1: Service Health
Write-Host "[$(NowTag)] Testing service health..."
try {
$serviceHealth = Test-ServiceHealth -Server $ServerName -ErrorAction SilentlyContinue
if ($serviceHealth) {
foreach ($svc in $serviceHealth) {
$result = [PSCustomObject]@{
TestCategory = "ServiceHealth"
TestName = $svc.Role
Server = $ServerName
Status = if ($svc.RequiredServicesRunning) { "PASS" } else { "FAIL" }
Details = "Required services: $($svc.RequiredServicesRunning)"
}
$allResults += $result
if (-not $svc.RequiredServicesRunning) {
$criticalIssues += "Service Health FAILED for role: $($svc.Role)"
}
}
Write-Host " Service Health: COMPLETED" -ForegroundColor Green
} else {
Write-Host " Service Health: No results (cmdlet may not be available)" -ForegroundColor Yellow
}
} catch {
Write-Host " Service Health: ERROR - $($_.Exception.Message)" -ForegroundColor Red
$allResults += [PSCustomObject]@{
TestCategory = "ServiceHealth"
TestName = "Test-ServiceHealth"
Server = $ServerName
Status = "ERROR"
Details = $_.Exception.Message
}
}
# Test 2: Replication Health (DAG servers only)
Write-Host "[$(NowTag)] Testing replication health..."
try {
$replHealth = Test-ReplicationHealth -Server $ServerName -ErrorAction SilentlyContinue
if ($replHealth) {
foreach ($test in $replHealth) {
$status = if ($test.Result -eq "Passed") { "PASS" } elseif ($test.Result -eq "Failed") { "FAIL" } else { "WARNING" }
$result = [PSCustomObject]@{
TestCategory = "ReplicationHealth"
TestName = $test.Check
Server = $ServerName
Status = $status
Details = $test.Error
}
$allResults += $result
if ($status -eq "FAIL") {
$criticalIssues += "Replication Health FAILED: $($test.Check) - $($test.Error)"
} elseif ($status -eq "WARNING") {
$warnings += "Replication Health WARNING: $($test.Check) - $($test.Error)"
}
}
Write-Host " Replication Health: COMPLETED" -ForegroundColor Green
} else {
Write-Host " Replication Health: Not applicable (not a DAG member)" -ForegroundColor Yellow
}
} catch {
Write-Host " Replication Health: ERROR or not applicable - $($_.Exception.Message)" -ForegroundColor Yellow
$allResults += [PSCustomObject]@{
TestCategory = "ReplicationHealth"
TestName = "Test-ReplicationHealth"
Server = $ServerName
Status = "N/A"
Details = "Not applicable or error: $($_.Exception.Message)"
}
}
# Test 3: MAPI Connectivity
Write-Host "[$(NowTag)] Testing MAPI connectivity..."
try {
$databases = Get-MailboxDatabase -Server $ServerName -Status -ErrorAction SilentlyContinue
foreach ($db in $databases) {
if ($db.Mounted) {
try {
$mapiTest = Test-MapiConnectivity -Database $db.Name -ErrorAction Stop
$status = if ($mapiTest.Result -eq "Success") { "PASS" } else { "FAIL" }
$result = [PSCustomObject]@{
TestCategory = "MapiConnectivity"
TestName = "Database: $($db.Name)"
Server = $ServerName
Status = $status
Details = "Latency: $($mapiTest.Latency.TotalMilliseconds) ms"
}
$allResults += $result
if ($status -eq "FAIL") {
$criticalIssues += "MAPI Connectivity FAILED for database: $($db.Name)"
}
} catch {
$result = [PSCustomObject]@{
TestCategory = "MapiConnectivity"
TestName = "Database: $($db.Name)"
Server = $ServerName
Status = "ERROR"
Details = $_.Exception.Message
}
$allResults += $result
$warnings += "MAPI test error for $($db.Name): $($_.Exception.Message)"
}
}
}
Write-Host " MAPI Connectivity: COMPLETED" -ForegroundColor Green
} catch {
Write-Host " MAPI Connectivity: ERROR - $($_.Exception.Message)" -ForegroundColor Red
}
# Test 4: Database Mount Status
Write-Host "[$(NowTag)] Checking database mount status..."
try {
$databases = Get-MailboxDatabase -Server $ServerName -Status -ErrorAction SilentlyContinue
foreach ($db in $databases) {
$status = if ($db.Mounted) { "PASS" } else { "FAIL" }
$result = [PSCustomObject]@{
TestCategory = "DatabaseMount"
TestName = "Database: $($db.Name)"
Server = $ServerName
Status = $status
Details = "Mounted: $($db.Mounted)"
}
$allResults += $result
if (-not $db.Mounted) {
$criticalIssues += "Database DISMOUNTED: $($db.Name)"
}
}
Write-Host " Database Mount Status: COMPLETED" -ForegroundColor Green
} catch {
Write-Host " Database Mount Status: ERROR - $($_.Exception.Message)" -ForegroundColor Red
}
# Test 5: Mail Flow (optional)
if ($IncludeMailflow) {
Write-Host "[$(NowTag)] Testing mail flow (this may take a minute)..."
try {
$mailflowTest = Test-Mailflow -TargetMailboxServer $ServerName -ErrorAction Stop
$status = if ($mailflowTest.TestMailflowResult -eq "Success") { "PASS" } else { "FAIL" }
$result = [PSCustomObject]@{
TestCategory = "Mailflow"
TestName = "Test-Mailflow"
Server = $ServerName
Status = $status
Details = "Result: $($mailflowTest.TestMailflowResult), Latency: $($mailflowTest.MessageLatencyTime.TotalMilliseconds) ms"
}
$allResults += $result
if ($status -eq "FAIL") {
$criticalIssues += "Mail Flow test FAILED"
}
Write-Host " Mail Flow: COMPLETED" -ForegroundColor Green
} catch {
Write-Host " Mail Flow: ERROR - $($_.Exception.Message)" -ForegroundColor Red
$allResults += [PSCustomObject]@{
TestCategory = "Mailflow"
TestName = "Test-Mailflow"
Server = $ServerName
Status = "ERROR"
Details = $_.Exception.Message
}
}
}
# Export results
Write-Host "[$(NowTag)] Exporting results..."
$csvFile = Join-Path $OutputFolder "Health-Check-Results.csv"
$allResults | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $csvFile
Write-Host "[$(NowTag)] Results exported: $csvFile" -ForegroundColor Green
# Summary
$passCount = ($allResults | Where-Object Status -eq "PASS").Count
$failCount = ($allResults | Where-Object Status -eq "FAIL").Count
$errorCount = ($allResults | Where-Object Status -eq "ERROR").Count
$totalTests = $allResults.Count
$summaryFile = Join-Path $OutputFolder "Health-Check-Summary.txt"
$summary = @"
Exchange Health Check Summary
Server: $ServerName
Generated: $(Get-Date)
TEST RESULTS:
Total Tests: $totalTests
Passed: $passCount
Failed: $failCount
Errors: $errorCount
Overall Status: $(if ($failCount -eq 0 -and $errorCount -eq 0) { "HEALTHY" } else { "ISSUES DETECTED" })
$(if ($criticalIssues.Count -gt 0) {
"CRITICAL ISSUES:
$($criticalIssues | ForEach-Object { " - $_" } | Out-String)
"} else { "" })
$(if ($warnings.Count -gt 0) {
"WARNINGS:
$($warnings | ForEach-Object { " - $_" } | Out-String)
"} else { "" })
DETAILED RESULTS:
$($allResults | ForEach-Object { " [$($_.Status)] $($_.TestCategory) - $($_.TestName): $($_.Details)" } | Out-String)
"@
$summary | Out-File -FilePath $summaryFile -Encoding UTF8
Write-Host "[$(NowTag)] Summary: $summaryFile" -ForegroundColor Green
# Console output
Write-Host "`nHEALTH CHECK SUMMARY:" -ForegroundColor Cyan
Write-Host " Total Tests: $totalTests"
Write-Host " Passed: $passCount" -ForegroundColor Green
Write-Host " Failed: $failCount" -ForegroundColor $(if ($failCount -gt 0) { "Red" } else { "Green" })
Write-Host " Errors: $errorCount" -ForegroundColor $(if ($errorCount -gt 0) { "Red" } else { "Green" })
if ($criticalIssues.Count -gt 0) {
Write-Host "`nCRITICAL ISSUES DETECTED:" -ForegroundColor Red
$criticalIssues | ForEach-Object { Write-Host " - $_" -ForegroundColor Red }
}
if ($warnings.Count -gt 0) {
Write-Host "`nWARNINGS:" -ForegroundColor Yellow
$warnings | ForEach-Object { Write-Host " - $_" -ForegroundColor Yellow }
}
Write-Host "`n[$(NowTag)] Health check complete! Output folder: $OutputFolder"
# Exit code
if ($failCount -gt 0 -or $errorCount -gt 0) {
exit 1
} else {
exit 0
}