From 62134801aa885abc516a8377aa635eab27bc3328 Mon Sep 17 00:00:00 2001 From: Martien de Kleijn Date: Wed, 15 Oct 2025 10:52:44 +0200 Subject: [PATCH] Add 12 AI-generated PowerShell scripts with documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠️ 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 --- AI-GENERATED-SCRIPTS.md | 95 ++++++ .../Compare-ADGroupMemberships.ps1 | 215 +++++++++++++ ActiveDirectory/Export-OUStructure.ps1 | 201 ++++++++++++ ActiveDirectory/Get-ADUserLastLogon.ps1 | 185 +++++++++++ CLAUDE.md | 37 +++ Exchange/Compare-MailboxDatabases.ps1 | 190 ++++++++++++ Exchange/Export-DistributionGroups.ps1 | 163 ++++++++++ Exchange/Get-InactiveMailboxes.ps1 | 191 ++++++++++++ Exchange/Get-MailboxPermissions.ps1 | 173 +++++++++++ Exchange/Get-MailflowStats.ps1 | 226 ++++++++++++++ Misc/Backup-ExchangeCertificates.ps1 | 178 +++++++++++ Misc/Get-ServerInventory.ps1 | 238 ++++++++++++++ Misc/Monitor-DiskSpace.ps1 | 183 +++++++++++ Misc/Test-ExchangeHealth.ps1 | 290 ++++++++++++++++++ 14 files changed, 2565 insertions(+) create mode 100644 AI-GENERATED-SCRIPTS.md create mode 100644 ActiveDirectory/Compare-ADGroupMemberships.ps1 create mode 100644 ActiveDirectory/Export-OUStructure.ps1 create mode 100644 ActiveDirectory/Get-ADUserLastLogon.ps1 create mode 100644 Exchange/Compare-MailboxDatabases.ps1 create mode 100644 Exchange/Export-DistributionGroups.ps1 create mode 100644 Exchange/Get-InactiveMailboxes.ps1 create mode 100644 Exchange/Get-MailboxPermissions.ps1 create mode 100644 Exchange/Get-MailflowStats.ps1 create mode 100644 Misc/Backup-ExchangeCertificates.ps1 create mode 100644 Misc/Get-ServerInventory.ps1 create mode 100644 Misc/Monitor-DiskSpace.ps1 create mode 100644 Misc/Test-ExchangeHealth.ps1 diff --git a/AI-GENERATED-SCRIPTS.md b/AI-GENERATED-SCRIPTS.md new file mode 100644 index 0000000..11d55f8 --- /dev/null +++ b/AI-GENERATED-SCRIPTS.md @@ -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) diff --git a/ActiveDirectory/Compare-ADGroupMemberships.ps1 b/ActiveDirectory/Compare-ADGroupMemberships.ps1 new file mode 100644 index 0000000..3ff1ad6 --- /dev/null +++ b/ActiveDirectory/Compare-ADGroupMemberships.ps1 @@ -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- + +.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" diff --git a/ActiveDirectory/Export-OUStructure.ps1 b/ActiveDirectory/Export-OUStructure.ps1 new file mode 100644 index 0000000..17ed8e1 --- /dev/null +++ b/ActiveDirectory/Export-OUStructure.ps1 @@ -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- + +.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" diff --git a/ActiveDirectory/Get-ADUserLastLogon.ps1 b/ActiveDirectory/Get-ADUserLastLogon.ps1 new file mode 100644 index 0000000..87b8adc --- /dev/null +++ b/ActiveDirectory/Get-ADUserLastLogon.ps1 @@ -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- + +.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" diff --git a/CLAUDE.md b/CLAUDE.md index 92f7215..0843ab7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 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 - Scripts are environment-specific (hardcoded paths like `C:\Program Files\Microsoft\Exchange Server\V15\...`) - Exchange-Inventory.ps1 assumes specific Exchange cmdlet availability - 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 diff --git a/Exchange/Compare-MailboxDatabases.ps1 b/Exchange/Compare-MailboxDatabases.ps1 new file mode 100644 index 0000000..3cf9bf5 --- /dev/null +++ b/Exchange/Compare-MailboxDatabases.ps1 @@ -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- + +.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" diff --git a/Exchange/Export-DistributionGroups.ps1 b/Exchange/Export-DistributionGroups.ps1 new file mode 100644 index 0000000..fe61604 --- /dev/null +++ b/Exchange/Export-DistributionGroups.ps1 @@ -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- + +.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" diff --git a/Exchange/Get-InactiveMailboxes.ps1 b/Exchange/Get-InactiveMailboxes.ps1 new file mode 100644 index 0000000..06ffcfe --- /dev/null +++ b/Exchange/Get-InactiveMailboxes.ps1 @@ -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- + +.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" diff --git a/Exchange/Get-MailboxPermissions.ps1 b/Exchange/Get-MailboxPermissions.ps1 new file mode 100644 index 0000000..2e0ba7b --- /dev/null +++ b/Exchange/Get-MailboxPermissions.ps1 @@ -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- + +.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 diff --git a/Exchange/Get-MailflowStats.ps1 b/Exchange/Get-MailflowStats.ps1 new file mode 100644 index 0000000..82208df --- /dev/null +++ b/Exchange/Get-MailflowStats.ps1 @@ -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- + +.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" diff --git a/Misc/Backup-ExchangeCertificates.ps1 b/Misc/Backup-ExchangeCertificates.ps1 new file mode 100644 index 0000000..bb40068 --- /dev/null +++ b/Misc/Backup-ExchangeCertificates.ps1 @@ -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- + +.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 diff --git a/Misc/Get-ServerInventory.ps1 b/Misc/Get-ServerInventory.ps1 new file mode 100644 index 0000000..f1a4ac4 --- /dev/null +++ b/Misc/Get-ServerInventory.ps1 @@ -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- + +.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("Server Inventory") + [void]$script:HtmlSb.AppendLine("") + [void]$script:HtmlSb.AppendLine("

Server Inventory Report

") + [void]$script:HtmlSb.AppendLine("

Server: $env:COMPUTERNAME

") + [void]$script:HtmlSb.AppendLine("

Generated: " + (Get-Date).ToString("yyyy-MM-dd HH:mm") + "

") + } +} + +function Add-Heading([string]$Text, [int]$Level = 2) { + if ($script:UseHtml) { + [void]$script:HtmlSb.AppendLine("$Text") + } 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("

$enc

") + } 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("") + foreach ($p in $PropOrder) { [void]$script:HtmlSb.AppendLine("") } + [void]$script:HtmlSb.AppendLine("") + foreach ($o in $arr) { + [void]$script:HtmlSb.AppendLine("") + foreach ($p in $PropOrder) { + $val = if ($o.$p) { [string]$o.$p } else { "" } + [void]$script:HtmlSb.AppendLine("") + } + [void]$script:HtmlSb.AppendLine("") + } + [void]$script:HtmlSb.AppendLine("
$([System.Web.HttpUtility]::HtmlEncode($p))
$([System.Web.HttpUtility]::HtmlEncode($val))
") + } 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("") + $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" diff --git a/Misc/Monitor-DiskSpace.ps1 b/Misc/Monitor-DiskSpace.ps1 new file mode 100644 index 0000000..9ef24fc --- /dev/null +++ b/Misc/Monitor-DiskSpace.ps1 @@ -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 +} diff --git a/Misc/Test-ExchangeHealth.ps1 b/Misc/Test-ExchangeHealth.ps1 new file mode 100644 index 0000000..6b0a2d7 --- /dev/null +++ b/Misc/Test-ExchangeHealth.ps1 @@ -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- + +.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 +}