Add 12 AI-generated PowerShell scripts with documentation
⚠️ IMPORTANT: These scripts are AI-GENERATED and UNTESTED Exchange Scripts (5): - Get-MailboxPermissions.ps1: Audit delegate access permissions - Get-InactiveMailboxes.ps1: Identify stale mailboxes - Compare-MailboxDatabases.ps1: Database health comparison - Export-DistributionGroups.ps1: Distribution group inventory - Get-MailflowStats.ps1: Transport log analysis Active Directory Scripts (3): - Get-ADUserLastLogon.ps1: True LastLogon across all DCs - Export-OUStructure.ps1: OU hierarchy with GPO links - Compare-ADGroupMemberships.ps1: Compare user group memberships System Maintenance Scripts (4): - Get-ServerInventory.ps1: Hardware/software inventory report - Monitor-DiskSpace.ps1: Disk space monitoring with alerts - Backup-ExchangeCertificates.ps1: Certificate backup to PFX - Test-ExchangeHealth.ps1: Aggregated Exchange health checks Documentation: - Updated CLAUDE.md with AI-generated scripts section - Added AI-GENERATED-SCRIPTS.md with warnings and testing guide All scripts include prominent warnings and follow established patterns from existing scripts. Require thorough testing before production use. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
95
AI-GENERATED-SCRIPTS.md
Normal file
95
AI-GENERATED-SCRIPTS.md
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
# AI-Generated Scripts - Important Notice
|
||||||
|
|
||||||
|
⚠️ **WARNING: The scripts listed below were generated by Claude AI and have NOT been tested in production environments.**
|
||||||
|
|
||||||
|
These scripts were created to complement the existing manually-written scripts in this repository. While they follow established patterns and best practices, **they require thorough review and testing before use in any production environment**.
|
||||||
|
|
||||||
|
## AI-Generated Scripts List
|
||||||
|
|
||||||
|
### Exchange Scripts
|
||||||
|
| Script | Purpose | Risk Level |
|
||||||
|
|--------|---------|------------|
|
||||||
|
| `Get-MailboxPermissions.ps1` | Audit mailbox delegate access (SendAs, SendOnBehalf, FullAccess) | Low (read-only) |
|
||||||
|
| `Get-InactiveMailboxes.ps1` | Identify stale mailboxes based on LastLogonTime | Low (read-only) |
|
||||||
|
| `Compare-MailboxDatabases.ps1` | Database health comparison with alerting | Low (read-only) |
|
||||||
|
| `Export-DistributionGroups.ps1` | Complete distribution group inventory | Low (read-only) |
|
||||||
|
| `Get-MailflowStats.ps1` | Transport log analysis for message flow patterns | Low (read-only) |
|
||||||
|
|
||||||
|
### Active Directory Scripts
|
||||||
|
| Script | Purpose | Risk Level |
|
||||||
|
|--------|---------|------------|
|
||||||
|
| `Get-ADUserLastLogon.ps1` | True LastLogon query across all domain controllers | Medium (queries all DCs) |
|
||||||
|
| `Export-OUStructure.ps1` | OU hierarchy documentation with GPO links | Low (read-only) |
|
||||||
|
| `Compare-ADGroupMemberships.ps1` | Group membership comparison between users | Low (read-only) |
|
||||||
|
|
||||||
|
### System Maintenance Scripts
|
||||||
|
| Script | Purpose | Risk Level |
|
||||||
|
|--------|---------|------------|
|
||||||
|
| `Get-ServerInventory.ps1` | Hardware/software inventory with Word/HTML report | Low (read-only) |
|
||||||
|
| `Monitor-DiskSpace.ps1` | Disk space monitoring with alerting | Low (read-only) |
|
||||||
|
| `Backup-ExchangeCertificates.ps1` | Certificate export to PFX files | Medium (exports sensitive keys) |
|
||||||
|
| `Test-ExchangeHealth.ps1` | Aggregated Exchange health checks | Low (read-only tests) |
|
||||||
|
|
||||||
|
## Testing Recommendations
|
||||||
|
|
||||||
|
### Before Production Use:
|
||||||
|
|
||||||
|
1. **Review the Code**
|
||||||
|
- Examine the logic and ensure it fits your environment
|
||||||
|
- Check for hardcoded paths, IP ranges, or environment-specific assumptions
|
||||||
|
- Verify parameter defaults are appropriate
|
||||||
|
|
||||||
|
2. **Test in Non-Production**
|
||||||
|
- Run scripts in a test environment first
|
||||||
|
- Verify output is accurate and complete
|
||||||
|
- Check performance impact (especially scripts that query all DCs or parse large logs)
|
||||||
|
|
||||||
|
3. **Verify Prerequisites**
|
||||||
|
- Ensure required modules are available (Exchange Management Shell, Active Directory)
|
||||||
|
- Confirm appropriate permissions (RBAC roles, AD rights, Administrator)
|
||||||
|
- Test with and without optional parameters
|
||||||
|
|
||||||
|
4. **Monitor Performance**
|
||||||
|
- Some scripts can be slow with large datasets:
|
||||||
|
- `Get-ADUserLastLogon.ps1` queries every domain controller
|
||||||
|
- `Get-MailflowStats.ps1` parses potentially large log files
|
||||||
|
- `Get-InactiveMailboxes.ps1` retrieves statistics for all mailboxes
|
||||||
|
- Consider running during maintenance windows or off-peak hours
|
||||||
|
|
||||||
|
5. **Security Considerations**
|
||||||
|
- `Backup-ExchangeCertificates.ps1` exports private keys - store PFX files securely
|
||||||
|
- `Get-MailboxPermissions.ps1` may reveal sensitive access patterns
|
||||||
|
- Review output before sharing to ensure no credentials or secrets are exposed
|
||||||
|
|
||||||
|
## How to Identify AI-Generated Scripts
|
||||||
|
|
||||||
|
All AI-generated scripts include:
|
||||||
|
1. Prominent warning in the `.SYNOPSIS` section: "⚠️ AI-GENERATED SCRIPT - UNTESTED"
|
||||||
|
2. Console output warning when executed
|
||||||
|
3. This file (`AI-GENERATED-SCRIPTS.md`) lists all AI-generated scripts
|
||||||
|
|
||||||
|
## Manually Written Scripts (Production-Tested)
|
||||||
|
|
||||||
|
The following scripts were manually written and have been used in production:
|
||||||
|
- `Exchange/Exchange-Inventory.ps1`
|
||||||
|
- `Exchange/Get-LargeMailboxes.ps1`
|
||||||
|
- `Exchange/Get-MailboxesPerType.ps1`
|
||||||
|
- `Exchange/Get-SMTPTraffic.ps1`
|
||||||
|
- `ActiveDirectory/CopyADgroups.ps1`
|
||||||
|
- `Misc/Fix-VSSBackup.ps1`
|
||||||
|
|
||||||
|
## Reporting Issues
|
||||||
|
|
||||||
|
If you find issues with AI-generated scripts, please:
|
||||||
|
1. Document the issue (error messages, unexpected behavior, environment details)
|
||||||
|
2. Note which script and version (check git commit)
|
||||||
|
3. Consider fixing the issue and contributing back to the repository
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
These scripts are provided as-is under the same license as the repository. No warranty is provided, express or implied.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: 2025-10-15
|
||||||
|
**Generated By**: Claude AI (Anthropic)
|
||||||
215
ActiveDirectory/Compare-ADGroupMemberships.ps1
Normal file
215
ActiveDirectory/Compare-ADGroupMemberships.ps1
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Compare group memberships between two Active Directory users
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Shows differences in group memberships between two users. Useful for
|
||||||
|
access troubleshooting, onboarding/offboarding validation, or role comparison.
|
||||||
|
|
||||||
|
.PARAMETER User1
|
||||||
|
First user (SamAccountName, UserPrincipalName, or DistinguishedName)
|
||||||
|
|
||||||
|
.PARAMETER User2
|
||||||
|
Second user (SamAccountName, UserPrincipalName, or DistinguishedName)
|
||||||
|
|
||||||
|
.PARAMETER OutputFolder
|
||||||
|
Destination folder for reports. Default: .\GroupComparison-<date>
|
||||||
|
|
||||||
|
.PARAMETER ShowCommonGroups
|
||||||
|
Include groups common to both users in report (default: $true)
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
⚠️ AI-GENERATED SCRIPT - UNTESTED
|
||||||
|
This script was generated by Claude AI and has not been tested in production.
|
||||||
|
Review and test thoroughly in a non-production environment before use.
|
||||||
|
|
||||||
|
- Requires Active Directory PowerShell module
|
||||||
|
- Run with appropriate AD permissions
|
||||||
|
- Includes nested group memberships
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Compare-ADGroupMemberships.ps1 -User1 "jsmith" -User2 "jdoe"
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Compare-ADGroupMemberships.ps1 -User1 "john.smith@domain.com" -User2 "jane.doe@domain.com" -ShowCommonGroups $false
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$User1,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$User2,
|
||||||
|
|
||||||
|
[string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("GroupComparison-" + (Get-Date -Format "yyyyMMdd-HHmm"))),
|
||||||
|
|
||||||
|
[bool]$ShowCommonGroups = $true
|
||||||
|
)
|
||||||
|
|
||||||
|
function NowTag { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") }
|
||||||
|
|
||||||
|
Write-Host "[$(NowTag)] ⚠️ AI-GENERATED SCRIPT - UNTESTED" -ForegroundColor Yellow
|
||||||
|
Write-Host "[$(NowTag)] Comparing AD group memberships..." -ForegroundColor Green
|
||||||
|
|
||||||
|
# Import AD module
|
||||||
|
try {
|
||||||
|
Import-Module ActiveDirectory -ErrorAction Stop
|
||||||
|
} catch {
|
||||||
|
Write-Host "[$(NowTag)] ERROR: Active Directory module not available" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create output folder
|
||||||
|
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
|
||||||
|
|
||||||
|
# Get user objects
|
||||||
|
Write-Host "[$(NowTag)] Retrieving user information..."
|
||||||
|
try {
|
||||||
|
$adUser1 = Get-ADUser -Identity $User1 -Properties MemberOf -ErrorAction Stop
|
||||||
|
Write-Host "[$(NowTag)] User 1: $($adUser1.Name) ($($adUser1.SamAccountName))"
|
||||||
|
} catch {
|
||||||
|
Write-Host "[$(NowTag)] ERROR: Could not find user: $User1" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$adUser2 = Get-ADUser -Identity $User2 -Properties MemberOf -ErrorAction Stop
|
||||||
|
Write-Host "[$(NowTag)] User 2: $($adUser2.Name) ($($adUser2.SamAccountName))"
|
||||||
|
} catch {
|
||||||
|
Write-Host "[$(NowTag)] ERROR: Could not find user: $User2" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get group memberships
|
||||||
|
Write-Host "[$(NowTag)] Retrieving group memberships..."
|
||||||
|
|
||||||
|
$user1Groups = @()
|
||||||
|
if ($adUser1.MemberOf) {
|
||||||
|
foreach ($groupDN in $adUser1.MemberOf) {
|
||||||
|
try {
|
||||||
|
$group = Get-ADGroup -Identity $groupDN -Properties Description -ErrorAction SilentlyContinue
|
||||||
|
if ($group) {
|
||||||
|
$user1Groups += [PSCustomObject]@{
|
||||||
|
Name = $group.Name
|
||||||
|
DN = $group.DistinguishedName
|
||||||
|
Description = $group.Description
|
||||||
|
GroupScope = $group.GroupScope
|
||||||
|
GroupCategory = $group.GroupCategory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$user2Groups = @()
|
||||||
|
if ($adUser2.MemberOf) {
|
||||||
|
foreach ($groupDN in $adUser2.MemberOf) {
|
||||||
|
try {
|
||||||
|
$group = Get-ADGroup -Identity $groupDN -Properties Description -ErrorAction SilentlyContinue
|
||||||
|
if ($group) {
|
||||||
|
$user2Groups += [PSCustomObject]@{
|
||||||
|
Name = $group.Name
|
||||||
|
DN = $group.DistinguishedName
|
||||||
|
Description = $group.Description
|
||||||
|
GroupScope = $group.GroupScope
|
||||||
|
GroupCategory = $group.GroupCategory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "[$(NowTag)] User 1 group count: $($user1Groups.Count)"
|
||||||
|
Write-Host "[$(NowTag)] User 2 group count: $($user2Groups.Count)"
|
||||||
|
|
||||||
|
# Compare memberships
|
||||||
|
$user1GroupNames = $user1Groups | Select-Object -ExpandProperty Name
|
||||||
|
$user2GroupNames = $user2Groups | Select-Object -ExpandProperty Name
|
||||||
|
|
||||||
|
$uniqueToUser1 = $user1Groups | Where-Object { $_.Name -notin $user2GroupNames }
|
||||||
|
$uniqueToUser2 = $user2Groups | Where-Object { $_.Name -notin $user1GroupNames }
|
||||||
|
$commonGroups = $user1Groups | Where-Object { $_.Name -in $user2GroupNames }
|
||||||
|
|
||||||
|
# Export results
|
||||||
|
Write-Host "[$(NowTag)] Exporting comparison results..."
|
||||||
|
|
||||||
|
# Unique to User 1
|
||||||
|
if ($uniqueToUser1.Count -gt 0) {
|
||||||
|
$file1 = Join-Path $OutputFolder "Groups-UniqueToUser1-$($adUser1.SamAccountName).csv"
|
||||||
|
$uniqueToUser1 | Sort-Object Name | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $file1
|
||||||
|
Write-Host "[$(NowTag)] Groups unique to $($adUser1.Name): $file1" -ForegroundColor Cyan
|
||||||
|
}
|
||||||
|
|
||||||
|
# Unique to User 2
|
||||||
|
if ($uniqueToUser2.Count -gt 0) {
|
||||||
|
$file2 = Join-Path $OutputFolder "Groups-UniqueToUser2-$($adUser2.SamAccountName).csv"
|
||||||
|
$uniqueToUser2 | Sort-Object Name | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $file2
|
||||||
|
Write-Host "[$(NowTag)] Groups unique to $($adUser2.Name): $file2" -ForegroundColor Cyan
|
||||||
|
}
|
||||||
|
|
||||||
|
# Common groups
|
||||||
|
if ($ShowCommonGroups -and $commonGroups.Count -gt 0) {
|
||||||
|
$fileCommon = Join-Path $OutputFolder "Groups-Common.csv"
|
||||||
|
$commonGroups | Sort-Object Name | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $fileCommon
|
||||||
|
Write-Host "[$(NowTag)] Common groups: $fileCommon" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# Summary report
|
||||||
|
$summaryFile = Join-Path $OutputFolder "Comparison-Summary.txt"
|
||||||
|
$summary = @"
|
||||||
|
Active Directory Group Membership Comparison
|
||||||
|
Generated: $(Get-Date)
|
||||||
|
|
||||||
|
User 1: $($adUser1.Name) ($($adUser1.SamAccountName))
|
||||||
|
User 2: $($adUser2.Name) ($($adUser2.SamAccountName))
|
||||||
|
|
||||||
|
SUMMARY:
|
||||||
|
User 1 Total Groups: $($user1Groups.Count)
|
||||||
|
User 2 Total Groups: $($user2Groups.Count)
|
||||||
|
Common Groups: $($commonGroups.Count)
|
||||||
|
Groups Unique to User 1: $($uniqueToUser1.Count)
|
||||||
|
Groups Unique to User 2: $($uniqueToUser2.Count)
|
||||||
|
|
||||||
|
GROUPS UNIQUE TO USER 1 ($($adUser1.Name)):
|
||||||
|
$($uniqueToUser1 | ForEach-Object { " - $($_.Name)" } | Out-String)
|
||||||
|
|
||||||
|
GROUPS UNIQUE TO USER 2 ($($adUser2.Name)):
|
||||||
|
$($uniqueToUser2 | ForEach-Object { " - $($_.Name)" } | Out-String)
|
||||||
|
|
||||||
|
$(if ($ShowCommonGroups) {
|
||||||
|
"COMMON GROUPS:
|
||||||
|
$($commonGroups | ForEach-Object { " - $($_.Name)" } | Out-String)"
|
||||||
|
} else { "" })
|
||||||
|
"@
|
||||||
|
|
||||||
|
$summary | Out-File -FilePath $summaryFile -Encoding UTF8
|
||||||
|
Write-Host "[$(NowTag)] Summary report: $summaryFile" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Console output
|
||||||
|
Write-Host "`nCOMPARISON RESULTS:" -ForegroundColor Cyan
|
||||||
|
Write-Host " Common Groups: $($commonGroups.Count)" -ForegroundColor Green
|
||||||
|
Write-Host " Unique to $($adUser1.Name): $($uniqueToUser1.Count)" -ForegroundColor Yellow
|
||||||
|
Write-Host " Unique to $($adUser2.Name): $($uniqueToUser2.Count)" -ForegroundColor Yellow
|
||||||
|
|
||||||
|
if ($uniqueToUser1.Count -gt 0) {
|
||||||
|
Write-Host "`nGroups Only in $($adUser1.Name):" -ForegroundColor Yellow
|
||||||
|
$uniqueToUser1 | Select-Object -First 10 | ForEach-Object {
|
||||||
|
Write-Host " - $($_.Name)"
|
||||||
|
}
|
||||||
|
if ($uniqueToUser1.Count -gt 10) {
|
||||||
|
Write-Host " ... and $($uniqueToUser1.Count - 10) more (see CSV)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($uniqueToUser2.Count -gt 0) {
|
||||||
|
Write-Host "`nGroups Only in $($adUser2.Name):" -ForegroundColor Yellow
|
||||||
|
$uniqueToUser2 | Select-Object -First 10 | ForEach-Object {
|
||||||
|
Write-Host " - $($_.Name)"
|
||||||
|
}
|
||||||
|
if ($uniqueToUser2.Count -gt 10) {
|
||||||
|
Write-Host " ... and $($uniqueToUser2.Count - 10) more (see CSV)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`n[$(NowTag)] Comparison complete! Output folder: $OutputFolder"
|
||||||
201
ActiveDirectory/Export-OUStructure.ps1
Normal file
201
ActiveDirectory/Export-OUStructure.ps1
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Export Active Directory OU structure with GPO links
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Documents the complete OU hierarchy including GPO links, inheritance settings,
|
||||||
|
and description. Useful for documentation, disaster recovery, or comparing
|
||||||
|
environments.
|
||||||
|
|
||||||
|
.PARAMETER OutputFolder
|
||||||
|
Destination folder for reports. Default: .\OU-Structure-<date>
|
||||||
|
|
||||||
|
.PARAMETER IncludeGPODetails
|
||||||
|
Include detailed GPO information (default: $true)
|
||||||
|
|
||||||
|
.PARAMETER SearchBase
|
||||||
|
Optional starting point for OU export (default: domain root)
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
⚠️ AI-GENERATED SCRIPT - UNTESTED
|
||||||
|
This script was generated by Claude AI and has not been tested in production.
|
||||||
|
Review and test thoroughly in a non-production environment before use.
|
||||||
|
|
||||||
|
- Requires Active Directory PowerShell module
|
||||||
|
- Run with appropriate AD permissions
|
||||||
|
- Exports to both CSV (detailed) and TXT (tree view)
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Export-OUStructure.ps1
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Export-OUStructure.ps1 -SearchBase "OU=Departments,DC=domain,DC=com"
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("OU-Structure-" + (Get-Date -Format "yyyyMMdd-HHmm"))),
|
||||||
|
[bool]$IncludeGPODetails = $true,
|
||||||
|
[string]$SearchBase = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
function NowTag { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") }
|
||||||
|
|
||||||
|
Write-Host "[$(NowTag)] ⚠️ AI-GENERATED SCRIPT - UNTESTED" -ForegroundColor Yellow
|
||||||
|
Write-Host "[$(NowTag)] Exporting OU structure..." -ForegroundColor Green
|
||||||
|
|
||||||
|
# Import AD module
|
||||||
|
try {
|
||||||
|
Import-Module ActiveDirectory -ErrorAction Stop
|
||||||
|
} catch {
|
||||||
|
Write-Host "[$(NowTag)] ERROR: Active Directory module not available" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create output folder
|
||||||
|
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
|
||||||
|
|
||||||
|
# Get domain info
|
||||||
|
$domain = Get-ADDomain
|
||||||
|
if (-not $SearchBase) {
|
||||||
|
$SearchBase = $domain.DistinguishedName
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "[$(NowTag)] Domain: $($domain.DNSRoot)"
|
||||||
|
Write-Host "[$(NowTag)] Search Base: $SearchBase"
|
||||||
|
|
||||||
|
# Get all OUs
|
||||||
|
Write-Host "[$(NowTag)] Retrieving organizational units..."
|
||||||
|
$ouParams = @{
|
||||||
|
Filter = '*'
|
||||||
|
SearchBase = $SearchBase
|
||||||
|
Properties = @('Description', 'gPLink', 'gPOptions', 'ProtectedFromAccidentalDeletion', 'WhenCreated', 'WhenChanged')
|
||||||
|
}
|
||||||
|
|
||||||
|
$ous = Get-ADOrganizationalUnit @ouParams | Sort-Object DistinguishedName
|
||||||
|
|
||||||
|
$ouCount = ($ous | Measure-Object).Count
|
||||||
|
Write-Host "[$(NowTag)] Found $ouCount organizational unit(s)"
|
||||||
|
|
||||||
|
# Build GPO name lookup if requested
|
||||||
|
$gpoLookup = @{}
|
||||||
|
if ($IncludeGPODetails) {
|
||||||
|
Write-Host "[$(NowTag)] Building GPO lookup table..."
|
||||||
|
try {
|
||||||
|
Import-Module GroupPolicy -ErrorAction Stop
|
||||||
|
$gpos = Get-GPO -All -ErrorAction SilentlyContinue
|
||||||
|
foreach ($gpo in $gpos) {
|
||||||
|
$gpoLookup[$gpo.Id.ToString()] = $gpo.DisplayName
|
||||||
|
}
|
||||||
|
Write-Host "[$(NowTag)] Found $($gpos.Count) GPO(s)"
|
||||||
|
} catch {
|
||||||
|
Write-Host "[$(NowTag)] WARNING: Could not load GroupPolicy module - GPO names will show as GUIDs" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Process OUs
|
||||||
|
$ouDetails = @()
|
||||||
|
$treeLines = @()
|
||||||
|
|
||||||
|
foreach ($ou in $ous) {
|
||||||
|
# Calculate depth for tree view
|
||||||
|
$depth = ($ou.DistinguishedName -split ',OU=').Count - 1
|
||||||
|
$indent = " " * $depth
|
||||||
|
|
||||||
|
# Parse GPO links
|
||||||
|
$gpoLinks = @()
|
||||||
|
$gpoNames = @()
|
||||||
|
|
||||||
|
if ($ou.gPLink) {
|
||||||
|
# gPLink format: [LDAP://cn={GUID},cn=policies,cn=system,DC=domain,DC=com;0]
|
||||||
|
$links = $ou.gPLink -split '\]\[' -replace '^\[' -replace '\]$'
|
||||||
|
|
||||||
|
foreach ($link in $links) {
|
||||||
|
if ($link -match '\{([^\}]+)\}') {
|
||||||
|
$gpoGuid = $matches[1]
|
||||||
|
$gpoName = if ($gpoLookup.ContainsKey($gpoGuid)) {
|
||||||
|
$gpoLookup[$gpoGuid]
|
||||||
|
} else {
|
||||||
|
$gpoGuid
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse link order and enforcement
|
||||||
|
$linkOrder = if ($link -match ';(\d+)') { $matches[1] } else { "0" }
|
||||||
|
$enforced = $linkOrder -band 1 # Bit 0 indicates enforcement
|
||||||
|
$disabled = $linkOrder -band 2 # Bit 1 indicates disabled
|
||||||
|
|
||||||
|
$gpoNames += $gpoName
|
||||||
|
$gpoLinks += [PSCustomObject]@{
|
||||||
|
OU = $ou.Name
|
||||||
|
GPOName = $gpoName
|
||||||
|
GPOGUID = $gpoGuid
|
||||||
|
LinkOrder = $linkOrder
|
||||||
|
Enforced = ($enforced -eq 1)
|
||||||
|
Disabled = ($disabled -eq 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# GPO inheritance blocked?
|
||||||
|
$inheritanceBlocked = ($ou.gPOptions -eq 1)
|
||||||
|
|
||||||
|
# Tree view line
|
||||||
|
$protectedMark = if ($ou.ProtectedFromAccidentalDeletion) { "[P]" } else { "" }
|
||||||
|
$inheritMark = if ($inheritanceBlocked) { "[BLOCKED]" } else { "" }
|
||||||
|
$gpoMark = if ($gpoNames.Count -gt 0) { " (GPOs: $($gpoNames -join ', '))" } else { "" }
|
||||||
|
|
||||||
|
$treeLines += "$indent$($ou.Name) $protectedMark$inheritMark$gpoMark"
|
||||||
|
|
||||||
|
# Detailed record
|
||||||
|
$ouDetails += [PSCustomObject]@{
|
||||||
|
Name = $ou.Name
|
||||||
|
DistinguishedName = $ou.DistinguishedName
|
||||||
|
Description = $ou.Description
|
||||||
|
ProtectedFromAccidentalDeletion = $ou.ProtectedFromAccidentalDeletion
|
||||||
|
GPOInheritanceBlocked = $inheritanceBlocked
|
||||||
|
LinkedGPOs = ($gpoNames -join "; ")
|
||||||
|
LinkedGPOCount = $gpoNames.Count
|
||||||
|
WhenCreated = $ou.WhenCreated
|
||||||
|
WhenChanged = $ou.WhenChanged
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Export detailed CSV
|
||||||
|
$csvFile = Join-Path $OutputFolder "OU-Structure.csv"
|
||||||
|
$ouDetails | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $csvFile
|
||||||
|
Write-Host "[$(NowTag)] OU details exported: $csvFile" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Export GPO links if available
|
||||||
|
if ($IncludeGPODetails -and $gpoLinks.Count -gt 0) {
|
||||||
|
$gpoLinksFile = Join-Path $OutputFolder "OU-GPO-Links.csv"
|
||||||
|
$gpoLinks | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $gpoLinksFile
|
||||||
|
Write-Host "[$(NowTag)] GPO links exported: $gpoLinksFile" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# Export tree view
|
||||||
|
$treeFile = Join-Path $OutputFolder "OU-Tree.txt"
|
||||||
|
$treeHeader = @"
|
||||||
|
Active Directory OU Structure
|
||||||
|
Domain: $($domain.DNSRoot)
|
||||||
|
Search Base: $SearchBase
|
||||||
|
Generated: $(Get-Date)
|
||||||
|
|
||||||
|
Legend:
|
||||||
|
[P] = Protected from Accidental Deletion
|
||||||
|
[BLOCKED] = GPO Inheritance Blocked
|
||||||
|
|
||||||
|
"@
|
||||||
|
|
||||||
|
$treeHeader | Out-File -FilePath $treeFile -Encoding UTF8
|
||||||
|
$treeLines | Out-File -FilePath $treeFile -Encoding UTF8 -Append
|
||||||
|
Write-Host "[$(NowTag)] Tree view exported: $treeFile" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
Write-Host "`nOU STRUCTURE SUMMARY:" -ForegroundColor Cyan
|
||||||
|
Write-Host " Total OUs: $ouCount"
|
||||||
|
Write-Host " Protected OUs: $(($ouDetails | Where-Object ProtectedFromAccidentalDeletion).Count)"
|
||||||
|
Write-Host " OUs with GPO Inheritance Blocked: $(($ouDetails | Where-Object GPOInheritanceBlocked).Count)"
|
||||||
|
Write-Host " OUs with Linked GPOs: $(($ouDetails | Where-Object { $_.LinkedGPOCount -gt 0 }).Count)"
|
||||||
|
|
||||||
|
Write-Host "`n[$(NowTag)] Export complete! Output folder: $OutputFolder"
|
||||||
185
ActiveDirectory/Get-ADUserLastLogon.ps1
Normal file
185
ActiveDirectory/Get-ADUserLastLogon.ps1
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Report on inactive Active Directory user accounts based on true LastLogon
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Queries all domain controllers to find the most recent LastLogon time for users.
|
||||||
|
The LastLogon attribute is not replicated, so each DC must be queried individually
|
||||||
|
to get accurate results. Identifies accounts inactive for specified days.
|
||||||
|
|
||||||
|
.PARAMETER InactiveDays
|
||||||
|
Number of days without logon to consider account inactive (default: 90)
|
||||||
|
|
||||||
|
.PARAMETER OutputFolder
|
||||||
|
Destination folder for reports. Default: .\InactiveADUsers-<date>
|
||||||
|
|
||||||
|
.PARAMETER SearchBase
|
||||||
|
Optional OU to limit search scope (default: entire domain)
|
||||||
|
|
||||||
|
.PARAMETER IncludeDisabled
|
||||||
|
Include disabled accounts in the report (default: $false)
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
⚠️ AI-GENERATED SCRIPT - UNTESTED
|
||||||
|
This script was generated by Claude AI and has not been tested in production.
|
||||||
|
Review and test thoroughly in a non-production environment before use.
|
||||||
|
|
||||||
|
- Requires Active Directory PowerShell module
|
||||||
|
- Run with appropriate AD permissions
|
||||||
|
- Can be slow in large environments (queries each DC)
|
||||||
|
- LastLogon attribute is not replicated between DCs
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Get-ADUserLastLogon.ps1
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Get-ADUserLastLogon.ps1 -InactiveDays 180 -SearchBase "OU=Users,DC=domain,DC=com"
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[int]$InactiveDays = 90,
|
||||||
|
[string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("InactiveADUsers-" + (Get-Date -Format "yyyyMMdd-HHmm"))),
|
||||||
|
[string]$SearchBase = "",
|
||||||
|
[bool]$IncludeDisabled = $false
|
||||||
|
)
|
||||||
|
|
||||||
|
function NowTag { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") }
|
||||||
|
|
||||||
|
Write-Host "[$(NowTag)] ⚠️ AI-GENERATED SCRIPT - UNTESTED" -ForegroundColor Yellow
|
||||||
|
Write-Host "[$(NowTag)] Starting AD user lastlogon analysis..." -ForegroundColor Green
|
||||||
|
|
||||||
|
# Import AD module
|
||||||
|
try {
|
||||||
|
Import-Module ActiveDirectory -ErrorAction Stop
|
||||||
|
} catch {
|
||||||
|
Write-Host "[$(NowTag)] ERROR: Active Directory module not available" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create output folder
|
||||||
|
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
|
||||||
|
|
||||||
|
$cutoffDate = (Get-Date).AddDays(-$InactiveDays)
|
||||||
|
Write-Host "[$(NowTag)] Cutoff date: $($cutoffDate.ToString('yyyy-MM-dd'))"
|
||||||
|
|
||||||
|
# Get all domain controllers
|
||||||
|
Write-Host "[$(NowTag)] Discovering domain controllers..."
|
||||||
|
$domainControllers = Get-ADDomainController -Filter * | Select-Object -ExpandProperty HostName
|
||||||
|
Write-Host "[$(NowTag)] Found $($domainControllers.Count) domain controller(s)"
|
||||||
|
|
||||||
|
# Get all users
|
||||||
|
Write-Host "[$(NowTag)] Retrieving AD users..."
|
||||||
|
$getUserParams = @{
|
||||||
|
Filter = "ObjectClass -eq 'user' -and Enabled -eq 'True'"
|
||||||
|
Properties = @('LastLogon', 'LastLogonDate', 'WhenCreated', 'Department', 'Title', 'Description', 'PasswordLastSet', 'PasswordNeverExpires', 'Enabled')
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($SearchBase) {
|
||||||
|
$getUserParams['SearchBase'] = $SearchBase
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($IncludeDisabled) {
|
||||||
|
$getUserParams['Filter'] = "ObjectClass -eq 'user'"
|
||||||
|
}
|
||||||
|
|
||||||
|
$users = Get-ADUser @getUserParams
|
||||||
|
$userCount = ($users | Measure-Object).Count
|
||||||
|
Write-Host "[$(NowTag)] Found $userCount user(s) to analyze"
|
||||||
|
|
||||||
|
# Query each DC for LastLogon and find the most recent
|
||||||
|
$userLogonData = @()
|
||||||
|
$current = 0
|
||||||
|
|
||||||
|
foreach ($user in $users) {
|
||||||
|
$current++
|
||||||
|
$pct = [int](($current / $userCount) * 100)
|
||||||
|
Write-Progress -Activity "Querying LastLogon from Domain Controllers" -Status "Processing $($user.SamAccountName) ($current/$userCount)" -PercentComplete $pct
|
||||||
|
|
||||||
|
$mostRecentLogon = $null
|
||||||
|
|
||||||
|
foreach ($dc in $domainControllers) {
|
||||||
|
try {
|
||||||
|
$userFromDC = Get-ADUser -Identity $user.SamAccountName -Server $dc -Properties LastLogon -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
if ($userFromDC.LastLogon) {
|
||||||
|
$logonDate = [DateTime]::FromFileTime($userFromDC.LastLogon)
|
||||||
|
|
||||||
|
if ($null -eq $mostRecentLogon -or $logonDate -gt $mostRecentLogon) {
|
||||||
|
$mostRecentLogon = $logonDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Verbose "Could not query $dc for $($user.SamAccountName)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determine if inactive
|
||||||
|
$isInactive = $false
|
||||||
|
$reason = ""
|
||||||
|
$daysSinceLogon = "N/A"
|
||||||
|
|
||||||
|
if ($null -eq $mostRecentLogon) {
|
||||||
|
$isInactive = $true
|
||||||
|
$reason = "Never logged on"
|
||||||
|
} elseif ($mostRecentLogon -lt $cutoffDate) {
|
||||||
|
$isInactive = $true
|
||||||
|
$daysSinceLogon = [int]((Get-Date) - $mostRecentLogon).TotalDays
|
||||||
|
$reason = "Last logon: $($mostRecentLogon.ToString('yyyy-MM-dd'))"
|
||||||
|
} else {
|
||||||
|
$daysSinceLogon = [int]((Get-Date) - $mostRecentLogon).TotalDays
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isInactive) {
|
||||||
|
$userLogonData += [PSCustomObject]@{
|
||||||
|
SamAccountName = $user.SamAccountName
|
||||||
|
DisplayName = $user.Name
|
||||||
|
UserPrincipalName = $user.UserPrincipalName
|
||||||
|
Enabled = $user.Enabled
|
||||||
|
LastLogon = $mostRecentLogon
|
||||||
|
DaysSinceLogon = $daysSinceLogon
|
||||||
|
LastLogonDate = $user.LastLogonDate # Replicated attribute for comparison
|
||||||
|
WhenCreated = $user.WhenCreated
|
||||||
|
PasswordLastSet = $user.PasswordLastSet
|
||||||
|
PasswordNeverExpires = $user.PasswordNeverExpires
|
||||||
|
Department = $user.Department
|
||||||
|
Title = $user.Title
|
||||||
|
Description = $user.Description
|
||||||
|
DistinguishedName = $user.DistinguishedName
|
||||||
|
Reason = $reason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Progress -Activity "Querying LastLogon from Domain Controllers" -Completed
|
||||||
|
|
||||||
|
# Export results
|
||||||
|
Write-Host "[$(NowTag)] Found $($userLogonData.Count) inactive user(s)"
|
||||||
|
|
||||||
|
if ($userLogonData.Count -gt 0) {
|
||||||
|
$csvFile = Join-Path $OutputFolder "Inactive-ADUsers.csv"
|
||||||
|
$userLogonData | Sort-Object DaysSinceLogon -Descending |
|
||||||
|
Export-Csv -NoTypeInformation -Encoding UTF8 -Path $csvFile
|
||||||
|
Write-Host "[$(NowTag)] Report exported: $csvFile" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Summary statistics
|
||||||
|
Write-Host "`nINACTIVE USER SUMMARY:" -ForegroundColor Cyan
|
||||||
|
Write-Host " Total Inactive Users: $($userLogonData.Count)"
|
||||||
|
Write-Host " Never Logged On: $(($userLogonData | Where-Object { $_.DaysSinceLogon -eq 'N/A' }).Count)"
|
||||||
|
Write-Host " Enabled Accounts: $(($userLogonData | Where-Object Enabled).Count)"
|
||||||
|
Write-Host " Disabled Accounts: $(($userLogonData | Where-Object { -not $_.Enabled }).Count)"
|
||||||
|
Write-Host " Password Never Expires: $(($userLogonData | Where-Object PasswordNeverExpires).Count)"
|
||||||
|
|
||||||
|
# By department
|
||||||
|
$byDept = $userLogonData | Where-Object Department | Group-Object Department | Sort-Object Count -Descending | Select-Object -First 5
|
||||||
|
if ($byDept) {
|
||||||
|
Write-Host "`nTop 5 Departments with Inactive Users:" -ForegroundColor Cyan
|
||||||
|
$byDept | ForEach-Object {
|
||||||
|
Write-Host " $($_.Name): $($_.Count)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "[$(NowTag)] No inactive users found!" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`n[$(NowTag)] Analysis complete! Output folder: $OutputFolder"
|
||||||
37
CLAUDE.md
37
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
|
2. **VSS script**: Test service restart logic on non-critical systems
|
||||||
3. **AD scripts**: Use test groups before production deployment
|
3. **AD scripts**: Use test groups before production deployment
|
||||||
|
|
||||||
|
## AI-Generated Scripts (UNTESTED)
|
||||||
|
|
||||||
|
The following scripts were generated by Claude AI in 2025 and have **NOT been tested in production environments**. They should be thoroughly reviewed and tested in non-production environments before use:
|
||||||
|
|
||||||
|
### Exchange Scripts (AI-Generated)
|
||||||
|
- **Get-MailboxPermissions.ps1** - Audits SendAs, SendOnBehalf, and FullAccess permissions
|
||||||
|
- **Get-InactiveMailboxes.ps1** - Identifies stale mailboxes based on LastLogonTime threshold
|
||||||
|
- **Compare-MailboxDatabases.ps1** - Database health comparison with alerting (whitespace, backup age)
|
||||||
|
- **Export-DistributionGroups.ps1** - Complete distribution group inventory with members
|
||||||
|
- **Get-MailflowStats.ps1** - Transport log analysis for message flow patterns and anomalies
|
||||||
|
|
||||||
|
### Active Directory Scripts (AI-Generated)
|
||||||
|
- **Get-ADUserLastLogon.ps1** - True LastLogon query across all DCs (non-replicated attribute)
|
||||||
|
- **Export-OUStructure.ps1** - OU hierarchy documentation with GPO links
|
||||||
|
- **Compare-ADGroupMemberships.ps1** - Group membership comparison between two users
|
||||||
|
|
||||||
|
### System Maintenance Scripts (AI-Generated)
|
||||||
|
- **Get-ServerInventory.ps1** - Comprehensive hardware/software inventory with Word/HTML report
|
||||||
|
- **Monitor-DiskSpace.ps1** - Disk space monitoring with alerting and optional email
|
||||||
|
- **Backup-ExchangeCertificates.ps1** - Certificate export to PFX with expiry warnings
|
||||||
|
- **Test-ExchangeHealth.ps1** - Aggregated Exchange health checks (services, replication, MAPI, databases)
|
||||||
|
|
||||||
|
### AI-Generated Script Patterns
|
||||||
|
These scripts follow the established patterns from the original scripts:
|
||||||
|
- Similar report generation (Word COM with HTML fallback where applicable)
|
||||||
|
- Progress tracking for long-running operations
|
||||||
|
- CSV export capabilities
|
||||||
|
- `-ErrorAction SilentlyContinue` for graceful degradation
|
||||||
|
- Comprehensive parameter documentation and examples
|
||||||
|
- Prominent "⚠️ AI-GENERATED SCRIPT - UNTESTED" warnings in output
|
||||||
|
|
||||||
|
**Testing Requirements**: All AI-generated scripts include detailed `.NOTES` sections with testing guidance. Pay special attention to:
|
||||||
|
- Permission requirements (RBAC, AD rights, Administrator)
|
||||||
|
- Performance impact (LastLogon queries across DCs, log parsing)
|
||||||
|
- Environment-specific assumptions (paths, IP ranges, thresholds)
|
||||||
|
|
||||||
## Important Constraints
|
## Important Constraints
|
||||||
|
|
||||||
- Scripts are environment-specific (hardcoded paths like `C:\Program Files\Microsoft\Exchange Server\V15\...`)
|
- Scripts are environment-specific (hardcoded paths like `C:\Program Files\Microsoft\Exchange Server\V15\...`)
|
||||||
- Exchange-Inventory.ps1 assumes specific Exchange cmdlet availability
|
- Exchange-Inventory.ps1 assumes specific Exchange cmdlet availability
|
||||||
- Get-SMTPTraffic.ps1 filters for specific IP range (192.168.1.*) - modify for different networks
|
- Get-SMTPTraffic.ps1 filters for specific IP range (192.168.1.*) - modify for different networks
|
||||||
|
- AI-generated scripts have NOT been validated in production and require thorough testing
|
||||||
|
|||||||
190
Exchange/Compare-MailboxDatabases.ps1
Normal file
190
Exchange/Compare-MailboxDatabases.ps1
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Compare health metrics across all Exchange mailbox databases
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Generates a comparison report of database sizes, whitespace, mount status,
|
||||||
|
backup age, and circular logging settings. Alerts on databases exceeding
|
||||||
|
thresholds or with configuration issues.
|
||||||
|
|
||||||
|
.PARAMETER WhitespaceThresholdGB
|
||||||
|
Alert if available whitespace exceeds this value (default: 50)
|
||||||
|
|
||||||
|
.PARAMETER BackupAgeThresholdDays
|
||||||
|
Alert if last backup is older than this many days (default: 2)
|
||||||
|
|
||||||
|
.PARAMETER OutputFolder
|
||||||
|
Destination folder for reports. Default: .\DatabaseComparison-<date>
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
⚠️ AI-GENERATED SCRIPT - UNTESTED
|
||||||
|
This script was generated by Claude AI and has not been tested in production.
|
||||||
|
Review and test thoroughly in a non-production environment before use.
|
||||||
|
|
||||||
|
- Run in Exchange Management Shell with appropriate RBAC permissions
|
||||||
|
- Requires -Status parameter support (Get-MailboxDatabase -Status)
|
||||||
|
- Tested compatibility: Exchange 2013/2016/2019 (not validated)
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Compare-MailboxDatabases.ps1
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Compare-MailboxDatabases.ps1 -WhitespaceThresholdGB 100 -BackupAgeThresholdDays 1
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[int]$WhitespaceThresholdGB = 50,
|
||||||
|
[int]$BackupAgeThresholdDays = 2,
|
||||||
|
[string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("DatabaseComparison-" + (Get-Date -Format "yyyyMMdd-HHmm")))
|
||||||
|
)
|
||||||
|
|
||||||
|
function NowTag { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") }
|
||||||
|
|
||||||
|
function Convert-BytesToGB([string]$sizeStr) {
|
||||||
|
if (-not $sizeStr) { return 0 }
|
||||||
|
try {
|
||||||
|
if ($sizeStr -match "([\d\.,]+)\s*(KB|MB|GB|TB)") {
|
||||||
|
$num = [double]($matches[1] -replace ',', '.')
|
||||||
|
$bytes = switch ($matches[2].ToUpper()) {
|
||||||
|
"KB" { $num * 1KB }
|
||||||
|
"MB" { $num * 1MB }
|
||||||
|
"GB" { $num * 1GB }
|
||||||
|
"TB" { $num * 1TB }
|
||||||
|
}
|
||||||
|
return [math]::Round($bytes / 1GB, 2)
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "[$(NowTag)] ⚠️ AI-GENERATED SCRIPT - UNTESTED" -ForegroundColor Yellow
|
||||||
|
Write-Host "[$(NowTag)] Starting database comparison analysis..." -ForegroundColor Green
|
||||||
|
|
||||||
|
# Create output folder
|
||||||
|
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
|
||||||
|
|
||||||
|
# Get all databases with status
|
||||||
|
Write-Host "[$(NowTag)] Retrieving mailbox databases..."
|
||||||
|
$databases = Get-MailboxDatabase -Status -ErrorAction SilentlyContinue | Sort-Object Name
|
||||||
|
|
||||||
|
$dbCount = ($databases | Measure-Object).Count
|
||||||
|
Write-Host "[$(NowTag)] Found $dbCount databases"
|
||||||
|
|
||||||
|
# Analyze each database
|
||||||
|
$dbAnalysis = @()
|
||||||
|
$alerts = @()
|
||||||
|
|
||||||
|
foreach ($db in $databases) {
|
||||||
|
Write-Host "[$(NowTag)] Analyzing $($db.Name)..."
|
||||||
|
|
||||||
|
$dbSizeGB = Convert-BytesToGB ([string]$db.DatabaseSize)
|
||||||
|
$whitespaceGB = Convert-BytesToGB ([string]$db.AvailableNewMailboxSpace)
|
||||||
|
|
||||||
|
$backupAge = $null
|
||||||
|
$backupStatus = "Unknown"
|
||||||
|
if ($db.LastFullBackup) {
|
||||||
|
$backupAge = [int]((Get-Date) - $db.LastFullBackup).TotalDays
|
||||||
|
$backupStatus = if ($backupAge -le $BackupAgeThresholdDays) { "OK" } else { "OLD" }
|
||||||
|
} else {
|
||||||
|
$backupStatus = "NEVER"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate alerts
|
||||||
|
$dbAlerts = @()
|
||||||
|
|
||||||
|
if (-not $db.Mounted) {
|
||||||
|
$dbAlerts += "Database is DISMOUNTED"
|
||||||
|
$alerts += [PSCustomObject]@{
|
||||||
|
Database = $db.Name
|
||||||
|
Severity = "CRITICAL"
|
||||||
|
Alert = "Database is dismounted"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($whitespaceGB -gt $WhitespaceThresholdGB) {
|
||||||
|
$dbAlerts += "Excessive whitespace: $whitespaceGB GB"
|
||||||
|
$alerts += [PSCustomObject]@{
|
||||||
|
Database = $db.Name
|
||||||
|
Severity = "WARNING"
|
||||||
|
Alert = "Excessive whitespace: $whitespaceGB GB (threshold: $WhitespaceThresholdGB GB)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($backupStatus -eq "NEVER") {
|
||||||
|
$dbAlerts += "Never backed up"
|
||||||
|
$alerts += [PSCustomObject]@{
|
||||||
|
Database = $db.Name
|
||||||
|
Severity = "CRITICAL"
|
||||||
|
Alert = "Database has never been backed up"
|
||||||
|
}
|
||||||
|
} elseif ($backupStatus -eq "OLD") {
|
||||||
|
$dbAlerts += "Backup is $backupAge days old"
|
||||||
|
$alerts += [PSCustomObject]@{
|
||||||
|
Database = $db.Name
|
||||||
|
Severity = "WARNING"
|
||||||
|
Alert = "Last backup is $backupAge days old (threshold: $BackupAgeThresholdDays days)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($db.CircularLoggingEnabled) {
|
||||||
|
$dbAlerts += "Circular logging enabled"
|
||||||
|
$alerts += [PSCustomObject]@{
|
||||||
|
Database = $db.Name
|
||||||
|
Severity = "INFO"
|
||||||
|
Alert = "Circular logging is enabled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$dbAnalysis += [PSCustomObject]@{
|
||||||
|
Database = $db.Name
|
||||||
|
Server = $db.Server
|
||||||
|
Mounted = $db.Mounted
|
||||||
|
DatabaseSizeGB = $dbSizeGB
|
||||||
|
WhitespaceGB = $whitespaceGB
|
||||||
|
WhitespacePercent = if ($dbSizeGB -gt 0) { [math]::Round(($whitespaceGB / $dbSizeGB) * 100, 2) } else { 0 }
|
||||||
|
CircularLogging = $db.CircularLoggingEnabled
|
||||||
|
LastFullBackup = $db.LastFullBackup
|
||||||
|
BackupAgeDays = $backupAge
|
||||||
|
BackupStatus = $backupStatus
|
||||||
|
EdbFilePath = $db.EdbFilePath
|
||||||
|
LogFolderPath = $db.LogFolderPath
|
||||||
|
Alerts = ($dbAlerts -join "; ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Export full analysis
|
||||||
|
$csvFile = Join-Path $OutputFolder "Database-Comparison.csv"
|
||||||
|
$dbAnalysis | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $csvFile
|
||||||
|
Write-Host "[$(NowTag)] Database analysis exported: $csvFile" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Export alerts
|
||||||
|
if ($alerts.Count -gt 0) {
|
||||||
|
$alertsFile = Join-Path $OutputFolder "Database-Alerts.csv"
|
||||||
|
$alerts | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $alertsFile
|
||||||
|
Write-Host "[$(NowTag)] Alerts exported: $alertsFile" -ForegroundColor Yellow
|
||||||
|
|
||||||
|
Write-Host "`nALERTS FOUND:" -ForegroundColor Yellow
|
||||||
|
$alerts | Group-Object Severity | ForEach-Object {
|
||||||
|
Write-Host " $($_.Name): $($_.Count) alert(s)" -ForegroundColor $(
|
||||||
|
switch ($_.Name) {
|
||||||
|
"CRITICAL" { "Red" }
|
||||||
|
"WARNING" { "Yellow" }
|
||||||
|
default { "Cyan" }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "`n[$(NowTag)] No alerts found - all databases healthy!" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# Summary statistics
|
||||||
|
Write-Host "`nDATABASE SUMMARY:" -ForegroundColor Cyan
|
||||||
|
Write-Host " Total Databases: $dbCount"
|
||||||
|
Write-Host " Mounted: $(($dbAnalysis | Where-Object Mounted).Count)"
|
||||||
|
Write-Host " Dismounted: $(($dbAnalysis | Where-Object { -not $_.Mounted }).Count)"
|
||||||
|
Write-Host " Total Size: $([math]::Round(($dbAnalysis | Measure-Object DatabaseSizeGB -Sum).Sum, 2)) GB"
|
||||||
|
Write-Host " Total Whitespace: $([math]::Round(($dbAnalysis | Measure-Object WhitespaceGB -Sum).Sum, 2)) GB"
|
||||||
|
Write-Host " Circular Logging Enabled: $(($dbAnalysis | Where-Object CircularLogging).Count)"
|
||||||
|
|
||||||
|
Write-Host "`n[$(NowTag)] Analysis complete! Output folder: $OutputFolder"
|
||||||
163
Exchange/Export-DistributionGroups.ps1
Normal file
163
Exchange/Export-DistributionGroups.ps1
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Export complete inventory of distribution groups with members and settings
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Documents all distribution groups including members, owners, email addresses,
|
||||||
|
and key settings. Useful for migration planning, documentation, or backup.
|
||||||
|
|
||||||
|
.PARAMETER OutputFolder
|
||||||
|
Destination folder for reports. Default: .\DistributionGroups-<date>
|
||||||
|
|
||||||
|
.PARAMETER IncludeMembers
|
||||||
|
Include detailed member lists (default: $true, can be slow)
|
||||||
|
|
||||||
|
.PARAMETER GroupFilter
|
||||||
|
Optional filter for specific groups (default: all)
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
⚠️ AI-GENERATED SCRIPT - UNTESTED
|
||||||
|
This script was generated by Claude AI and has not been tested in production.
|
||||||
|
Review and test thoroughly in a non-production environment before use.
|
||||||
|
|
||||||
|
- Run in Exchange Management Shell with appropriate RBAC permissions
|
||||||
|
- Large environments may take significant time
|
||||||
|
- Includes both Distribution Groups and Mail-Enabled Security Groups
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Export-DistributionGroups.ps1
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Export-DistributionGroups.ps1 -GroupFilter "Sales*" -OutputFolder "D:\Reports\Groups"
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("DistributionGroups-" + (Get-Date -Format "yyyyMMdd-HHmm"))),
|
||||||
|
[bool]$IncludeMembers = $true,
|
||||||
|
[string]$GroupFilter = "*"
|
||||||
|
)
|
||||||
|
|
||||||
|
function NowTag { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") }
|
||||||
|
|
||||||
|
Write-Host "[$(NowTag)] ⚠️ AI-GENERATED SCRIPT - UNTESTED" -ForegroundColor Yellow
|
||||||
|
Write-Host "[$(NowTag)] Starting distribution group export..." -ForegroundColor Green
|
||||||
|
|
||||||
|
# Create output folder
|
||||||
|
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
|
||||||
|
|
||||||
|
# Get distribution groups
|
||||||
|
Write-Host "[$(NowTag)] Retrieving distribution groups..."
|
||||||
|
$groups = Get-DistributionGroup -Filter "Name -like '$GroupFilter'" -ResultSize Unlimited -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
$groupCount = ($groups | Measure-Object).Count
|
||||||
|
Write-Host "[$(NowTag)] Found $groupCount distribution groups"
|
||||||
|
|
||||||
|
if ($groupCount -eq 0) {
|
||||||
|
Write-Host "[$(NowTag)] No groups found matching filter: $GroupFilter" -ForegroundColor Yellow
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
# Collect group details
|
||||||
|
$groupDetails = @()
|
||||||
|
$groupMembers = @()
|
||||||
|
|
||||||
|
$current = 0
|
||||||
|
foreach ($group in $groups) {
|
||||||
|
$current++
|
||||||
|
$pct = [int](($current / $groupCount) * 100)
|
||||||
|
Write-Progress -Activity "Processing Distribution Groups" -Status "Processing $($group.Name) ($current/$groupCount)" -PercentComplete $pct
|
||||||
|
|
||||||
|
# Get managed by
|
||||||
|
$managedByList = @()
|
||||||
|
if ($group.ManagedBy) {
|
||||||
|
foreach ($mgr in $group.ManagedBy) {
|
||||||
|
try {
|
||||||
|
$mgrObj = Get-Recipient $mgr -ErrorAction SilentlyContinue
|
||||||
|
if ($mgrObj) {
|
||||||
|
$managedByList += $mgrObj.PrimarySmtpAddress
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get email addresses
|
||||||
|
$emailAddresses = ($group.EmailAddresses | Where-Object { $_ -like "smtp:*" }) -join "; "
|
||||||
|
|
||||||
|
# Get member count
|
||||||
|
$memberCount = 0
|
||||||
|
try {
|
||||||
|
$members = Get-DistributionGroupMember -Identity $group.Identity -ResultSize Unlimited -ErrorAction SilentlyContinue
|
||||||
|
$memberCount = ($members | Measure-Object).Count
|
||||||
|
|
||||||
|
# Collect member details if requested
|
||||||
|
if ($IncludeMembers -and $members) {
|
||||||
|
foreach ($member in $members) {
|
||||||
|
$groupMembers += [PSCustomObject]@{
|
||||||
|
GroupName = $group.Name
|
||||||
|
GroupPrimarySmtp = $group.PrimarySmtpAddress
|
||||||
|
MemberName = $member.Name
|
||||||
|
MemberPrimarySmtp = $member.PrimarySmtpAddress
|
||||||
|
MemberType = $member.RecipientType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host "[$(NowTag)] WARNING: Could not get members for $($group.Name)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
$groupDetails += [PSCustomObject]@{
|
||||||
|
Name = $group.Name
|
||||||
|
DisplayName = $group.DisplayName
|
||||||
|
Alias = $group.Alias
|
||||||
|
PrimarySmtpAddress = $group.PrimarySmtpAddress
|
||||||
|
EmailAddresses = $emailAddresses
|
||||||
|
RecipientTypeDetails = $group.RecipientTypeDetails
|
||||||
|
MemberCount = $memberCount
|
||||||
|
ManagedBy = ($managedByList -join "; ")
|
||||||
|
RequireSenderAuthentication = $group.RequireSenderAuthenticationEnabled
|
||||||
|
HiddenFromAddressLists = $group.HiddenFromAddressListsEnabled
|
||||||
|
ModerationEnabled = $group.ModerationEnabled
|
||||||
|
SendModerationNotifications = $group.SendModerationNotifications
|
||||||
|
AcceptMessagesOnlyFrom = (($group.AcceptMessagesOnlyFrom | ForEach-Object { $_.ToString() }) -join "; ")
|
||||||
|
RejectMessagesFrom = (($group.RejectMessagesFrom | ForEach-Object { $_.ToString() }) -join "; ")
|
||||||
|
BypassModerationFromSenders = (($group.BypassModerationFromSendersOrMembers | ForEach-Object { $_.ToString() }) -join "; ")
|
||||||
|
WhenCreated = $group.WhenCreated
|
||||||
|
WhenChanged = $group.WhenChanged
|
||||||
|
OrganizationalUnit = $group.OrganizationalUnit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Progress -Activity "Processing Distribution Groups" -Completed
|
||||||
|
|
||||||
|
# Export results
|
||||||
|
Write-Host "[$(NowTag)] Exporting results..."
|
||||||
|
|
||||||
|
$groupsFile = Join-Path $OutputFolder "DistributionGroups.csv"
|
||||||
|
$groupDetails | Sort-Object Name | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $groupsFile
|
||||||
|
Write-Host "[$(NowTag)] Groups exported: $groupsFile" -ForegroundColor Green
|
||||||
|
|
||||||
|
if ($IncludeMembers -and $groupMembers.Count -gt 0) {
|
||||||
|
$membersFile = Join-Path $OutputFolder "DistributionGroup-Members.csv"
|
||||||
|
$groupMembers | Sort-Object GroupName, MemberName | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $membersFile
|
||||||
|
Write-Host "[$(NowTag)] Members exported: $membersFile" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
Write-Host "`nSUMMARY:" -ForegroundColor Cyan
|
||||||
|
Write-Host " Total Groups: $groupCount"
|
||||||
|
Write-Host " Total Members: $($groupMembers.Count)"
|
||||||
|
Write-Host " Hidden from Address Lists: $(($groupDetails | Where-Object HiddenFromAddressLists).Count)"
|
||||||
|
Write-Host " Moderation Enabled: $(($groupDetails | Where-Object ModerationEnabled).Count)"
|
||||||
|
Write-Host " Security Groups: $(($groupDetails | Where-Object { $_.RecipientTypeDetails -like '*Security*' }).Count)"
|
||||||
|
|
||||||
|
# Top 10 largest groups
|
||||||
|
$top10 = $groupDetails | Sort-Object MemberCount -Descending | Select-Object -First 10
|
||||||
|
if ($top10) {
|
||||||
|
Write-Host "`nTop 10 Largest Groups:" -ForegroundColor Cyan
|
||||||
|
$top10 | ForEach-Object {
|
||||||
|
Write-Host " $($_.Name): $($_.MemberCount) members"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`n[$(NowTag)] Export complete! Output folder: $OutputFolder"
|
||||||
191
Exchange/Get-InactiveMailboxes.ps1
Normal file
191
Exchange/Get-InactiveMailboxes.ps1
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Identify inactive mailboxes based on LastLogonTime
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Finds mailboxes with no logon activity within specified days.
|
||||||
|
Useful for identifying stale accounts, cost optimization, and cleanup planning.
|
||||||
|
|
||||||
|
.PARAMETER InactiveDays
|
||||||
|
Number of days without logon to consider mailbox inactive (default: 90)
|
||||||
|
|
||||||
|
.PARAMETER OutputFolder
|
||||||
|
Destination folder for reports. Default: .\InactiveMailboxes-<date>
|
||||||
|
|
||||||
|
.PARAMETER IncludeShared
|
||||||
|
Include shared mailboxes in the report (default: $false)
|
||||||
|
|
||||||
|
.PARAMETER IncludeSize
|
||||||
|
Include mailbox size information (slower, default: $true)
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
⚠️ AI-GENERATED SCRIPT - UNTESTED
|
||||||
|
This script was generated by Claude AI and has not been tested in production.
|
||||||
|
Review and test thoroughly in a non-production environment before use.
|
||||||
|
|
||||||
|
- Run in Exchange Management Shell with appropriate RBAC permissions
|
||||||
|
- LastLogonTime may be null for never-accessed mailboxes
|
||||||
|
- Size calculation can be slow with large mailbox counts
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Get-InactiveMailboxes.ps1
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Get-InactiveMailboxes.ps1 -InactiveDays 180 -IncludeShared $true
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[int]$InactiveDays = 90,
|
||||||
|
[string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("InactiveMailboxes-" + (Get-Date -Format "yyyyMMdd-HHmm"))),
|
||||||
|
[bool]$IncludeShared = $false,
|
||||||
|
[bool]$IncludeSize = $true
|
||||||
|
)
|
||||||
|
|
||||||
|
function NowTag { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") }
|
||||||
|
|
||||||
|
Write-Host "[$(NowTag)] ⚠️ AI-GENERATED SCRIPT - UNTESTED" -ForegroundColor Yellow
|
||||||
|
Write-Host "[$(NowTag)] Searching for mailboxes inactive for $InactiveDays+ days..." -ForegroundColor Green
|
||||||
|
|
||||||
|
# Create output folder
|
||||||
|
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
|
||||||
|
|
||||||
|
$cutoffDate = (Get-Date).AddDays(-$InactiveDays)
|
||||||
|
Write-Host "[$(NowTag)] Cutoff date: $($cutoffDate.ToString('yyyy-MM-dd'))"
|
||||||
|
|
||||||
|
# Get mailboxes
|
||||||
|
Write-Host "[$(NowTag)] Retrieving mailboxes..."
|
||||||
|
$filter = if ($IncludeShared) {
|
||||||
|
{ RecipientTypeDetails -notmatch "^Remote" -and Database }
|
||||||
|
} else {
|
||||||
|
{ RecipientTypeDetails -eq "UserMailbox" -and Database }
|
||||||
|
}
|
||||||
|
|
||||||
|
$mailboxes = Get-Mailbox -ResultSize Unlimited -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object $filter
|
||||||
|
|
||||||
|
$mbCount = ($mailboxes | Measure-Object).Count
|
||||||
|
Write-Host "[$(NowTag)] Found $mbCount mailboxes to check"
|
||||||
|
|
||||||
|
# Get statistics per database to avoid prompts
|
||||||
|
Write-Host "[$(NowTag)] Retrieving mailbox statistics..."
|
||||||
|
$statsByDb = @{}
|
||||||
|
$databases = ($mailboxes.Database | Sort-Object -Unique)
|
||||||
|
|
||||||
|
$dbIdx = 0
|
||||||
|
foreach ($db in $databases) {
|
||||||
|
$dbIdx++
|
||||||
|
Write-Progress -Activity "Collecting Mailbox Statistics" -Status "Database $db ($dbIdx/$($databases.Count))" -PercentComplete ([int](($dbIdx / $databases.Count) * 100))
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stats = Get-MailboxStatistics -Database $db -ErrorAction SilentlyContinue
|
||||||
|
foreach ($stat in $stats) {
|
||||||
|
$statsByDb[$stat.DisplayName] = $stat
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host "[$(NowTag)] WARNING: Could not get statistics for database $db" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Progress -Activity "Collecting Mailbox Statistics" -Completed
|
||||||
|
|
||||||
|
# Analyze for inactive mailboxes
|
||||||
|
Write-Host "[$(NowTag)] Analyzing mailbox activity..."
|
||||||
|
$inactiveMailboxes = @()
|
||||||
|
|
||||||
|
$current = 0
|
||||||
|
foreach ($mb in $mailboxes) {
|
||||||
|
$current++
|
||||||
|
$pct = [int](($current / $mbCount) * 100)
|
||||||
|
Write-Progress -Activity "Analyzing Mailboxes" -Status "Processing $($mb.DisplayName) ($current/$mbCount)" -PercentComplete $pct
|
||||||
|
|
||||||
|
$stat = $statsByDb[$mb.DisplayName]
|
||||||
|
|
||||||
|
$lastLogon = if ($stat) { $stat.LastLogonTime } else { $null }
|
||||||
|
$isInactive = $false
|
||||||
|
$reason = ""
|
||||||
|
|
||||||
|
if ($null -eq $lastLogon) {
|
||||||
|
$isInactive = $true
|
||||||
|
$reason = "Never logged on"
|
||||||
|
} elseif ($lastLogon -lt $cutoffDate) {
|
||||||
|
$isInactive = $true
|
||||||
|
$reason = "Last logon: $($lastLogon.ToString('yyyy-MM-dd'))"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isInactive) {
|
||||||
|
$daysSinceLogon = if ($lastLogon) {
|
||||||
|
[int]((Get-Date) - $lastLogon).TotalDays
|
||||||
|
} else {
|
||||||
|
"N/A"
|
||||||
|
}
|
||||||
|
|
||||||
|
$size = $null
|
||||||
|
$itemCount = $null
|
||||||
|
|
||||||
|
if ($IncludeSize -and $stat) {
|
||||||
|
$size = $stat.TotalItemSize
|
||||||
|
$itemCount = $stat.ItemCount
|
||||||
|
}
|
||||||
|
|
||||||
|
$inactiveMailboxes += [PSCustomObject]@{
|
||||||
|
DisplayName = $mb.DisplayName
|
||||||
|
PrimarySmtpAddress = $mb.PrimarySmtpAddress
|
||||||
|
RecipientTypeDetails = $mb.RecipientTypeDetails
|
||||||
|
Database = $mb.Database
|
||||||
|
LastLogonTime = $lastLogon
|
||||||
|
DaysSinceLogon = $daysSinceLogon
|
||||||
|
WhenCreated = $mb.WhenCreated
|
||||||
|
TotalItemSize = $size
|
||||||
|
ItemCount = $itemCount
|
||||||
|
Reason = $reason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Progress -Activity "Analyzing Mailboxes" -Completed
|
||||||
|
|
||||||
|
# Export results
|
||||||
|
Write-Host "[$(NowTag)] Found $($inactiveMailboxes.Count) inactive mailboxes"
|
||||||
|
|
||||||
|
if ($inactiveMailboxes.Count -gt 0) {
|
||||||
|
$csvFile = Join-Path $OutputFolder "Inactive-Mailboxes.csv"
|
||||||
|
$inactiveMailboxes | Sort-Object DaysSinceLogon -Descending |
|
||||||
|
Export-Csv -NoTypeInformation -Encoding UTF8 -Path $csvFile
|
||||||
|
Write-Host "[$(NowTag)] Report exported: $csvFile" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Summary by type
|
||||||
|
$byType = $inactiveMailboxes | Group-Object RecipientTypeDetails
|
||||||
|
Write-Host "`nInactive Mailboxes by Type:"
|
||||||
|
foreach ($type in $byType) {
|
||||||
|
Write-Host " $($type.Name): $($type.Count)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Calculate potential storage savings
|
||||||
|
if ($IncludeSize) {
|
||||||
|
$totalBytes = 0
|
||||||
|
foreach ($mb in $inactiveMailboxes) {
|
||||||
|
if ($mb.TotalItemSize) {
|
||||||
|
try {
|
||||||
|
$sizeStr = [string]$mb.TotalItemSize
|
||||||
|
if ($sizeStr -match "([\d\.,]+)\s*(KB|MB|GB|TB)") {
|
||||||
|
$num = [double]($matches[1] -replace ',', '.')
|
||||||
|
$bytes = switch ($matches[2].ToUpper()) {
|
||||||
|
"KB" { $num * 1KB }
|
||||||
|
"MB" { $num * 1MB }
|
||||||
|
"GB" { $num * 1GB }
|
||||||
|
"TB" { $num * 1TB }
|
||||||
|
}
|
||||||
|
$totalBytes += $bytes
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$totalGB = [math]::Round($totalBytes / 1GB, 2)
|
||||||
|
Write-Host "`nPotential storage savings: $totalGB GB" -ForegroundColor Cyan
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "[$(NowTag)] No inactive mailboxes found!" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`n[$(NowTag)] Analysis complete! Output folder: $OutputFolder"
|
||||||
173
Exchange/Get-MailboxPermissions.ps1
Normal file
173
Exchange/Get-MailboxPermissions.ps1
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Audit mailbox delegate access permissions across Exchange environment
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Reports on SendAs, SendOnBehalf, and FullAccess permissions for all mailboxes.
|
||||||
|
Useful for security audits, compliance reviews, and migration planning.
|
||||||
|
|
||||||
|
.PARAMETER OutputFolder
|
||||||
|
Destination folder for CSV reports. Default: .\MailboxPermissions-<date>
|
||||||
|
|
||||||
|
.PARAMETER MailboxFilter
|
||||||
|
Optional filter for specific mailboxes. Default: all on-premises mailboxes
|
||||||
|
|
||||||
|
.PARAMETER IncludeInherited
|
||||||
|
Include inherited permissions in the report (default: $false)
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
⚠️ AI-GENERATED SCRIPT - UNTESTED
|
||||||
|
This script was generated by Claude AI and has not been tested in production.
|
||||||
|
Review and test thoroughly in a non-production environment before use.
|
||||||
|
|
||||||
|
- Run in Exchange Management Shell with appropriate RBAC permissions
|
||||||
|
- Can take significant time with large mailbox counts
|
||||||
|
- Tested compatibility: Exchange 2013/2016/2019 (not validated)
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Get-MailboxPermissions.ps1
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Get-MailboxPermissions.ps1 -OutputFolder "D:\Reports\Permissions" -IncludeInherited $true
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("MailboxPermissions-" + (Get-Date -Format "yyyyMMdd-HHmm"))),
|
||||||
|
[string]$MailboxFilter = "*",
|
||||||
|
[bool]$IncludeInherited = $false
|
||||||
|
)
|
||||||
|
|
||||||
|
function NowTag { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") }
|
||||||
|
|
||||||
|
Write-Host "[$(NowTag)] ⚠️ AI-GENERATED SCRIPT - UNTESTED" -ForegroundColor Yellow
|
||||||
|
Write-Host "[$(NowTag)] Starting mailbox permissions audit..." -ForegroundColor Green
|
||||||
|
|
||||||
|
# Create output folder
|
||||||
|
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
|
||||||
|
|
||||||
|
# Get all on-premises mailboxes
|
||||||
|
Write-Host "[$(NowTag)] Retrieving mailboxes..."
|
||||||
|
$mailboxes = Get-Mailbox -Filter $MailboxFilter -ResultSize Unlimited -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { $_.RecipientTypeDetails -notmatch "^Remote" -and $_.Database }
|
||||||
|
|
||||||
|
$mbCount = ($mailboxes | Measure-Object).Count
|
||||||
|
Write-Host "[$(NowTag)] Found $mbCount mailboxes to audit"
|
||||||
|
|
||||||
|
# Collections
|
||||||
|
$fullAccessPerms = @()
|
||||||
|
$sendAsPerms = @()
|
||||||
|
$sendOnBehalfPerms = @()
|
||||||
|
|
||||||
|
$current = 0
|
||||||
|
foreach ($mb in $mailboxes) {
|
||||||
|
$current++
|
||||||
|
$pct = [int](($current / $mbCount) * 100)
|
||||||
|
Write-Progress -Activity "Auditing Mailbox Permissions" -Status "Processing $($mb.DisplayName) ($current/$mbCount)" -PercentComplete $pct
|
||||||
|
|
||||||
|
# FullAccess permissions
|
||||||
|
try {
|
||||||
|
$fullAccess = Get-MailboxPermission -Identity $mb.Identity -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object {
|
||||||
|
$_.User -notlike "NT AUTHORITY\*" -and
|
||||||
|
$_.User -notlike "S-1-5-*" -and
|
||||||
|
$_.AccessRights -like "*FullAccess*" -and
|
||||||
|
($IncludeInherited -or -not $_.IsInherited)
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($perm in $fullAccess) {
|
||||||
|
$fullAccessPerms += [PSCustomObject]@{
|
||||||
|
Mailbox = $mb.DisplayName
|
||||||
|
PrimarySmtpAddress = $mb.PrimarySmtpAddress
|
||||||
|
User = $perm.User
|
||||||
|
AccessRights = ($perm.AccessRights -join ", ")
|
||||||
|
IsInherited = $perm.IsInherited
|
||||||
|
Deny = $perm.Deny
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host "[$(NowTag)] ERROR getting FullAccess for $($mb.DisplayName): $_" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
# SendAs permissions
|
||||||
|
try {
|
||||||
|
$sendAs = Get-ADPermission -Identity $mb.DistinguishedName -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object {
|
||||||
|
$_.ExtendedRights -like "*Send-As*" -and
|
||||||
|
$_.User -notlike "NT AUTHORITY\*" -and
|
||||||
|
$_.User -notlike "S-1-5-*" -and
|
||||||
|
($IncludeInherited -or -not $_.IsInherited)
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($perm in $sendAs) {
|
||||||
|
$sendAsPerms += [PSCustomObject]@{
|
||||||
|
Mailbox = $mb.DisplayName
|
||||||
|
PrimarySmtpAddress = $mb.PrimarySmtpAddress
|
||||||
|
User = $perm.User
|
||||||
|
IsInherited = $perm.IsInherited
|
||||||
|
Deny = $perm.Deny
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host "[$(NowTag)] ERROR getting SendAs for $($mb.DisplayName): $_" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
# SendOnBehalf permissions
|
||||||
|
if ($mb.GrantSendOnBehalfTo -and $mb.GrantSendOnBehalfTo.Count -gt 0) {
|
||||||
|
foreach ($user in $mb.GrantSendOnBehalfTo) {
|
||||||
|
$sendOnBehalfPerms += [PSCustomObject]@{
|
||||||
|
Mailbox = $mb.DisplayName
|
||||||
|
PrimarySmtpAddress = $mb.PrimarySmtpAddress
|
||||||
|
User = $user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Progress -Activity "Auditing Mailbox Permissions" -Completed
|
||||||
|
|
||||||
|
# Export results
|
||||||
|
Write-Host "[$(NowTag)] Exporting results..."
|
||||||
|
|
||||||
|
$fullAccessFile = Join-Path $OutputFolder "FullAccess-Permissions.csv"
|
||||||
|
$sendAsFile = Join-Path $OutputFolder "SendAs-Permissions.csv"
|
||||||
|
$sendOnBehalfFile = Join-Path $OutputFolder "SendOnBehalf-Permissions.csv"
|
||||||
|
$summaryFile = Join-Path $OutputFolder "Permissions-Summary.txt"
|
||||||
|
|
||||||
|
if ($fullAccessPerms.Count -gt 0) {
|
||||||
|
$fullAccessPerms | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $fullAccessFile
|
||||||
|
Write-Host "[$(NowTag)] FullAccess permissions: $fullAccessFile"
|
||||||
|
} else {
|
||||||
|
Write-Host "[$(NowTag)] No FullAccess permissions found"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sendAsPerms.Count -gt 0) {
|
||||||
|
$sendAsPerms | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $sendAsFile
|
||||||
|
Write-Host "[$(NowTag)] SendAs permissions: $sendAsFile"
|
||||||
|
} else {
|
||||||
|
Write-Host "[$(NowTag)] No SendAs permissions found"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sendOnBehalfPerms.Count -gt 0) {
|
||||||
|
$sendOnBehalfPerms | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $sendOnBehalfFile
|
||||||
|
Write-Host "[$(NowTag)] SendOnBehalf permissions: $sendOnBehalfFile"
|
||||||
|
} else {
|
||||||
|
Write-Host "[$(NowTag)] No SendOnBehalf permissions found"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
$summary = @"
|
||||||
|
Mailbox Permissions Audit Summary
|
||||||
|
Generated: $(Get-Date)
|
||||||
|
|
||||||
|
Mailboxes Audited: $mbCount
|
||||||
|
FullAccess Permissions: $($fullAccessPerms.Count)
|
||||||
|
SendAs Permissions: $($sendAsPerms.Count)
|
||||||
|
SendOnBehalf Permissions: $($sendOnBehalfPerms.Count)
|
||||||
|
|
||||||
|
Output Folder: $OutputFolder
|
||||||
|
"@
|
||||||
|
|
||||||
|
$summary | Out-File -FilePath $summaryFile -Encoding UTF8
|
||||||
|
Write-Host "`n$summary"
|
||||||
|
Write-Host "[$(NowTag)] Audit complete!" -ForegroundColor Green
|
||||||
226
Exchange/Get-MailflowStats.ps1
Normal file
226
Exchange/Get-MailflowStats.ps1
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Analyze Exchange message flow statistics from transport logs
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Aggregates statistics from Exchange transport logs including top senders/receivers,
|
||||||
|
message volume by time period, and potential anomalies. Builds on Get-SMTPTraffic.ps1
|
||||||
|
pattern but provides comprehensive analysis.
|
||||||
|
|
||||||
|
.PARAMETER LogPath
|
||||||
|
Path to transport logs (default: auto-detect from Exchange install)
|
||||||
|
|
||||||
|
.PARAMETER DaysBack
|
||||||
|
Number of days of logs to analyze (default: 7)
|
||||||
|
|
||||||
|
.PARAMETER OutputFolder
|
||||||
|
Destination folder for reports. Default: .\MailflowStats-<date>
|
||||||
|
|
||||||
|
.PARAMETER TopCount
|
||||||
|
Number of top senders/receivers to include in report (default: 25)
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
⚠️ AI-GENERATED SCRIPT - UNTESTED
|
||||||
|
This script was generated by Claude AI and has not been tested in production.
|
||||||
|
Review and test thoroughly in a non-production environment before use.
|
||||||
|
|
||||||
|
- Run in Exchange Management Shell
|
||||||
|
- Can be slow with large log volumes
|
||||||
|
- Analyzes SMTP Receive logs by default
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Get-MailflowStats.ps1
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Get-MailflowStats.ps1 -DaysBack 30 -TopCount 50
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$LogPath = "",
|
||||||
|
[int]$DaysBack = 7,
|
||||||
|
[string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("MailflowStats-" + (Get-Date -Format "yyyyMMdd-HHmm"))),
|
||||||
|
[int]$TopCount = 25
|
||||||
|
)
|
||||||
|
|
||||||
|
function NowTag { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") }
|
||||||
|
|
||||||
|
Write-Host "[$(NowTag)] ⚠️ AI-GENERATED SCRIPT - UNTESTED" -ForegroundColor Yellow
|
||||||
|
Write-Host "[$(NowTag)] Starting mail flow analysis..." -ForegroundColor Green
|
||||||
|
|
||||||
|
# Auto-detect log path if not specified
|
||||||
|
if (-not $LogPath) {
|
||||||
|
$exchangePath = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\Setup" -ErrorAction SilentlyContinue).MsiInstallPath
|
||||||
|
if ($exchangePath) {
|
||||||
|
$LogPath = Join-Path $exchangePath "TransportRoles\Logs\FrontEnd\ProtocolLog\SmtpReceive"
|
||||||
|
} else {
|
||||||
|
Write-Host "[$(NowTag)] ERROR: Could not auto-detect Exchange log path. Please specify -LogPath parameter" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $LogPath)) {
|
||||||
|
Write-Host "[$(NowTag)] ERROR: Log path not found: $LogPath" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "[$(NowTag)] Log path: $LogPath"
|
||||||
|
|
||||||
|
# Create output folder
|
||||||
|
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
|
||||||
|
|
||||||
|
# Get log files within date range
|
||||||
|
$cutoffDate = (Get-Date).AddDays(-$DaysBack)
|
||||||
|
Write-Host "[$(NowTag)] Analyzing logs from $($cutoffDate.ToString('yyyy-MM-dd')) to $(Get-Date -Format 'yyyy-MM-dd')"
|
||||||
|
|
||||||
|
$logFiles = Get-ChildItem -Path $LogPath -Filter "*.log" -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { $_.LastWriteTime -ge $cutoffDate } |
|
||||||
|
Sort-Object LastWriteTime -Descending
|
||||||
|
|
||||||
|
$fileCount = ($logFiles | Measure-Object).Count
|
||||||
|
|
||||||
|
if ($fileCount -eq 0) {
|
||||||
|
Write-Host "[$(NowTag)] No log files found in specified date range" -ForegroundColor Yellow
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "[$(NowTag)] Found $fileCount log file(s) to analyze"
|
||||||
|
|
||||||
|
# Parse logs
|
||||||
|
$allMessages = @()
|
||||||
|
$fileIdx = 0
|
||||||
|
|
||||||
|
foreach ($logFile in $logFiles) {
|
||||||
|
$fileIdx++
|
||||||
|
Write-Progress -Activity "Parsing Transport Logs" -Status "Processing $($logFile.Name) ($fileIdx/$fileCount)" -PercentComplete ([int](($fileIdx / $fileCount) * 100))
|
||||||
|
|
||||||
|
# Get header from file
|
||||||
|
$header = Get-Content $logFile.FullName -TotalCount 50 | Where-Object { $_ -like '#Fields:*' } | Select-Object -First 1
|
||||||
|
if (-not $header) { continue }
|
||||||
|
|
||||||
|
$fields = $header -replace '^#Fields: ', ''
|
||||||
|
$columns = $fields -split ','
|
||||||
|
|
||||||
|
# Parse log entries
|
||||||
|
Get-Content $logFile.FullName | Where-Object { -not ($_ -like '#*') -and $_ -match ',' } | ForEach-Object {
|
||||||
|
try {
|
||||||
|
$row = $_ -split ',(?=(?:[^"]*"[^"]*")*[^"]*$)' # Handle quoted fields
|
||||||
|
$entry = [PSCustomObject]@{}
|
||||||
|
|
||||||
|
for ($i = 0; $i -lt $columns.Count -and $i -lt $row.Count; $i++) {
|
||||||
|
$entry | Add-Member -NotePropertyName $columns[$i].Trim() -NotePropertyValue ($row[$i] -replace '^"|"$', '') -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
# Filter for relevant events (RECEIVE events)
|
||||||
|
if ($entry.event -eq 'RECEIVE' -or $entry.event -eq '+') {
|
||||||
|
$allMessages += $entry
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Progress -Activity "Parsing Transport Logs" -Completed
|
||||||
|
|
||||||
|
$totalMessages = $allMessages.Count
|
||||||
|
Write-Host "[$(NowTag)] Parsed $totalMessages message(s)"
|
||||||
|
|
||||||
|
if ($totalMessages -eq 0) {
|
||||||
|
Write-Host "[$(NowTag)] No messages found to analyze" -ForegroundColor Yellow
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Analyze data
|
||||||
|
Write-Host "[$(NowTag)] Analyzing message flow patterns..."
|
||||||
|
|
||||||
|
# Top senders (by remote-endpoint or client-ip)
|
||||||
|
$topSenders = $allMessages | Where-Object { $_.'remote-endpoint' } |
|
||||||
|
Group-Object -Property 'remote-endpoint' |
|
||||||
|
Select-Object @{N = "IPAddress"; E = { $_.Name } }, Count |
|
||||||
|
Sort-Object Count -Descending |
|
||||||
|
Select-Object -First $TopCount
|
||||||
|
|
||||||
|
# Messages by hour
|
||||||
|
$messagesByHour = $allMessages | Where-Object { $_.'date-time' } |
|
||||||
|
ForEach-Object {
|
||||||
|
try {
|
||||||
|
$dt = [DateTime]::Parse($_.'date-time')
|
||||||
|
[PSCustomObject]@{ Hour = $dt.Hour }
|
||||||
|
} catch {}
|
||||||
|
} |
|
||||||
|
Group-Object Hour |
|
||||||
|
Select-Object @{N = "Hour"; E = { $_.Name } }, Count |
|
||||||
|
Sort-Object Hour
|
||||||
|
|
||||||
|
# Messages by day
|
||||||
|
$messagesByDay = $allMessages | Where-Object { $_.'date-time' } |
|
||||||
|
ForEach-Object {
|
||||||
|
try {
|
||||||
|
$dt = [DateTime]::Parse($_.'date-time')
|
||||||
|
[PSCustomObject]@{ Date = $dt.ToString('yyyy-MM-dd') }
|
||||||
|
} catch {}
|
||||||
|
} |
|
||||||
|
Group-Object Date |
|
||||||
|
Select-Object @{N = "Date"; E = { $_.Name } }, Count |
|
||||||
|
Sort-Object Date
|
||||||
|
|
||||||
|
# Connector usage
|
||||||
|
$connectorStats = $allMessages | Where-Object { $_.'connector-id' } |
|
||||||
|
Group-Object -Property 'connector-id' |
|
||||||
|
Select-Object @{N = "Connector"; E = { $_.Name } }, Count |
|
||||||
|
Sort-Object Count -Descending
|
||||||
|
|
||||||
|
# Export results
|
||||||
|
Write-Host "[$(NowTag)] Exporting results..."
|
||||||
|
|
||||||
|
$summaryFile = Join-Path $OutputFolder "Mailflow-Summary.txt"
|
||||||
|
$topSendersFile = Join-Path $OutputFolder "Top-Senders-By-IP.csv"
|
||||||
|
$byHourFile = Join-Path $OutputFolder "Messages-By-Hour.csv"
|
||||||
|
$byDayFile = Join-Path $OutputFolder "Messages-By-Day.csv"
|
||||||
|
$connectorFile = Join-Path $OutputFolder "Messages-By-Connector.csv"
|
||||||
|
|
||||||
|
$topSenders | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $topSendersFile
|
||||||
|
$messagesByHour | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $byHourFile
|
||||||
|
$messagesByDay | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $byDayFile
|
||||||
|
$connectorStats | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $connectorFile
|
||||||
|
|
||||||
|
Write-Host "[$(NowTag)] Top senders: $topSendersFile" -ForegroundColor Green
|
||||||
|
Write-Host "[$(NowTag)] Hourly distribution: $byHourFile" -ForegroundColor Green
|
||||||
|
Write-Host "[$(NowTag)] Daily distribution: $byDayFile" -ForegroundColor Green
|
||||||
|
Write-Host "[$(NowTag)] Connector stats: $connectorFile" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Summary report
|
||||||
|
$summary = @"
|
||||||
|
Mail Flow Statistics Report
|
||||||
|
Generated: $(Get-Date)
|
||||||
|
Analysis Period: $($cutoffDate.ToString('yyyy-MM-dd')) to $(Get-Date -Format 'yyyy-MM-dd')
|
||||||
|
Log Path: $LogPath
|
||||||
|
|
||||||
|
SUMMARY:
|
||||||
|
Total Messages Analyzed: $totalMessages
|
||||||
|
Log Files Processed: $fileCount
|
||||||
|
Average Messages Per Day: $([math]::Round($totalMessages / $DaysBack, 0))
|
||||||
|
|
||||||
|
TOP $TopCount SENDERS (by IP):
|
||||||
|
$($topSenders | ForEach-Object { " $($_.IPAddress): $($_.Count) messages" } | Out-String)
|
||||||
|
|
||||||
|
BUSIEST HOURS (24-hour format):
|
||||||
|
$($messagesByHour | Sort-Object Count -Descending | Select-Object -First 5 | ForEach-Object { " Hour $($_.Hour): $($_.Count) messages" } | Out-String)
|
||||||
|
|
||||||
|
DAILY VOLUME:
|
||||||
|
$($messagesByDay | ForEach-Object { " $($_.Date): $($_.Count) messages" } | Out-String)
|
||||||
|
|
||||||
|
CONNECTOR USAGE:
|
||||||
|
$($connectorStats | ForEach-Object { " $($_.Connector): $($_.Count) messages" } | Out-String)
|
||||||
|
"@
|
||||||
|
|
||||||
|
$summary | Out-File -FilePath $summaryFile -Encoding UTF8
|
||||||
|
Write-Host "[$(NowTag)] Summary report: $summaryFile" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Console output
|
||||||
|
Write-Host "`nMAIL FLOW SUMMARY:" -ForegroundColor Cyan
|
||||||
|
Write-Host " Total Messages: $totalMessages"
|
||||||
|
Write-Host " Average Per Day: $([math]::Round($totalMessages / $DaysBack, 0))"
|
||||||
|
Write-Host " Peak Hour: $(($messagesByHour | Sort-Object Count -Descending | Select-Object -First 1).Hour):00"
|
||||||
|
Write-Host " Busiest Day: $(($messagesByDay | Sort-Object Count -Descending | Select-Object -First 1).Date)"
|
||||||
|
|
||||||
|
Write-Host "`n[$(NowTag)] Analysis complete! Output folder: $OutputFolder"
|
||||||
178
Misc/Backup-ExchangeCertificates.ps1
Normal file
178
Misc/Backup-ExchangeCertificates.ps1
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Backup Exchange certificates to PFX files
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Exports all Exchange certificates to password-protected PFX files.
|
||||||
|
Alerts on certificates expiring within specified days.
|
||||||
|
|
||||||
|
.PARAMETER OutputFolder
|
||||||
|
Destination folder for PFX exports. Default: .\CertBackup-<date>
|
||||||
|
|
||||||
|
.PARAMETER Password
|
||||||
|
Password for PFX files (required)
|
||||||
|
|
||||||
|
.PARAMETER ExpiryWarningDays
|
||||||
|
Alert on certificates expiring within this many days (default: 90)
|
||||||
|
|
||||||
|
.PARAMETER IncludePrivateKey
|
||||||
|
Export with private key (required for restoration, default: $true)
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
⚠️ AI-GENERATED SCRIPT - UNTESTED
|
||||||
|
This script was generated by Claude AI and has not been tested in production.
|
||||||
|
Review and test thoroughly in a non-production environment before use.
|
||||||
|
|
||||||
|
- Run in Exchange Management Shell with appropriate RBAC permissions
|
||||||
|
- Requires password for PFX protection
|
||||||
|
- Store PFX files securely - they contain private keys
|
||||||
|
- Tested compatibility: Exchange 2013/2016/2019 (not validated)
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
$pwd = Read-Host "Enter PFX password" -AsSecureString
|
||||||
|
.\Backup-ExchangeCertificates.ps1 -Password $pwd
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
$pwd = ConvertTo-SecureString "P@ssw0rd!" -AsPlainText -Force
|
||||||
|
.\Backup-ExchangeCertificates.ps1 -Password $pwd -ExpiryWarningDays 60
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("CertBackup-" + (Get-Date -Format "yyyyMMdd-HHmm"))),
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[System.Security.SecureString]$Password,
|
||||||
|
|
||||||
|
[int]$ExpiryWarningDays = 90,
|
||||||
|
|
||||||
|
[bool]$IncludePrivateKey = $true
|
||||||
|
)
|
||||||
|
|
||||||
|
function NowTag { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") }
|
||||||
|
|
||||||
|
Write-Host "[$(NowTag)] ⚠️ AI-GENERATED SCRIPT - UNTESTED" -ForegroundColor Yellow
|
||||||
|
Write-Host "[$(NowTag)] Starting Exchange certificate backup..." -ForegroundColor Green
|
||||||
|
|
||||||
|
# Create output folder
|
||||||
|
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
|
||||||
|
Write-Host "[$(NowTag)] Output folder: $OutputFolder"
|
||||||
|
|
||||||
|
# Get all Exchange certificates
|
||||||
|
Write-Host "[$(NowTag)] Retrieving Exchange certificates..."
|
||||||
|
$certs = Get-ExchangeCertificate -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
if (-not $certs) {
|
||||||
|
Write-Host "[$(NowTag)] No Exchange certificates found" -ForegroundColor Yellow
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
$certCount = ($certs | Measure-Object).Count
|
||||||
|
Write-Host "[$(NowTag)] Found $certCount certificate(s)"
|
||||||
|
|
||||||
|
# Process each certificate
|
||||||
|
$exported = @()
|
||||||
|
$warnings = @()
|
||||||
|
|
||||||
|
foreach ($cert in $certs) {
|
||||||
|
$thumbprint = $cert.Thumbprint
|
||||||
|
$subject = $cert.Subject
|
||||||
|
$notAfter = $cert.NotAfter
|
||||||
|
$daysUntilExpiry = [int](($notAfter - (Get-Date)).TotalDays)
|
||||||
|
|
||||||
|
Write-Host "[$(NowTag)] Processing: $subject (Thumbprint: $thumbprint)"
|
||||||
|
|
||||||
|
# Check expiry
|
||||||
|
$status = "OK"
|
||||||
|
if ($daysUntilExpiry -lt 0) {
|
||||||
|
$status = "EXPIRED"
|
||||||
|
$warnings += [PSCustomObject]@{
|
||||||
|
Thumbprint = $thumbprint
|
||||||
|
Subject = $subject
|
||||||
|
NotAfter = $notAfter
|
||||||
|
DaysRemaining = $daysUntilExpiry
|
||||||
|
Status = $status
|
||||||
|
}
|
||||||
|
Write-Host " WARNING: Certificate EXPIRED on $($notAfter.ToString('yyyy-MM-dd'))" -ForegroundColor Red
|
||||||
|
} elseif ($daysUntilExpiry -lt $ExpiryWarningDays) {
|
||||||
|
$status = "EXPIRING SOON"
|
||||||
|
$warnings += [PSCustomObject]@{
|
||||||
|
Thumbprint = $thumbprint
|
||||||
|
Subject = $subject
|
||||||
|
NotAfter = $notAfter
|
||||||
|
DaysRemaining = $daysUntilExpiry
|
||||||
|
Status = $status
|
||||||
|
}
|
||||||
|
Write-Host " WARNING: Certificate expires in $daysUntilExpiry days ($($notAfter.ToString('yyyy-MM-dd')))" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host " Status: OK - Expires in $daysUntilExpiry days"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Export to PFX
|
||||||
|
$fileName = "$($thumbprint).pfx"
|
||||||
|
$filePath = Join-Path $OutputFolder $fileName
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($IncludePrivateKey) {
|
||||||
|
# Export with private key
|
||||||
|
Export-ExchangeCertificate -Thumbprint $thumbprint -FileName $filePath -Password $Password -ErrorAction Stop | Out-Null
|
||||||
|
Write-Host " Exported: $fileName (with private key)" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
# Export public key only (using .NET method)
|
||||||
|
$certObj = Get-Item -Path "Cert:\LocalMachine\My\$thumbprint" -ErrorAction SilentlyContinue
|
||||||
|
if ($certObj) {
|
||||||
|
$certBytes = $certObj.Export('Cert')
|
||||||
|
[System.IO.File]::WriteAllBytes($filePath, $certBytes)
|
||||||
|
Write-Host " Exported: $fileName (public key only)" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
throw "Certificate not found in LocalMachine\My"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$exported += [PSCustomObject]@{
|
||||||
|
Thumbprint = $thumbprint
|
||||||
|
Subject = $subject
|
||||||
|
Services = ($cert.Services -join ", ")
|
||||||
|
NotBefore = $cert.NotBefore
|
||||||
|
NotAfter = $notAfter
|
||||||
|
DaysRemaining = $daysUntilExpiry
|
||||||
|
Status = $status
|
||||||
|
FileName = $fileName
|
||||||
|
FilePath = $filePath
|
||||||
|
FileSize = (Get-Item $filePath).Length
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host " ERROR exporting certificate: $($_.Exception.Message)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Export inventory
|
||||||
|
$inventoryFile = Join-Path $OutputFolder "Certificate-Inventory.csv"
|
||||||
|
$exported | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $inventoryFile
|
||||||
|
Write-Host "`n[$(NowTag)] Inventory exported: $inventoryFile" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Export warnings if any
|
||||||
|
if ($warnings.Count -gt 0) {
|
||||||
|
$warningsFile = Join-Path $OutputFolder "Certificate-Warnings.csv"
|
||||||
|
$warnings | Sort-Object DaysRemaining | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $warningsFile
|
||||||
|
Write-Host "[$(NowTag)] Warnings exported: $warningsFile" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
Write-Host "`nBACKUP SUMMARY:" -ForegroundColor Cyan
|
||||||
|
Write-Host " Total Certificates: $certCount"
|
||||||
|
Write-Host " Successfully Exported: $($exported.Count)" -ForegroundColor Green
|
||||||
|
Write-Host " Failed: $($certCount - $exported.Count)" -ForegroundColor $(if ($certCount - $exported.Count -gt 0) { "Red" } else { "Green" })
|
||||||
|
Write-Host " Expired: $(($warnings | Where-Object Status -eq 'EXPIRED').Count)" -ForegroundColor Red
|
||||||
|
Write-Host " Expiring Soon: $(($warnings | Where-Object Status -eq 'EXPIRING SOON').Count)" -ForegroundColor Yellow
|
||||||
|
|
||||||
|
if ($warnings.Count -gt 0) {
|
||||||
|
Write-Host "`nCERTIFICATE WARNINGS:" -ForegroundColor Yellow
|
||||||
|
$warnings | Sort-Object DaysRemaining | ForEach-Object {
|
||||||
|
$color = if ($_.Status -eq "EXPIRED") { "Red" } else { "Yellow" }
|
||||||
|
Write-Host " $($_.Status): $($_.Subject) - $($_.DaysRemaining) days" -ForegroundColor $color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`n[$(NowTag)] Backup complete! Output folder: $OutputFolder"
|
||||||
|
Write-Host "`nIMPORTANT: Store PFX files securely - they contain private keys!" -ForegroundColor Cyan
|
||||||
238
Misc/Get-ServerInventory.ps1
Normal file
238
Misc/Get-ServerInventory.ps1
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Comprehensive server hardware and software inventory report
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Collects system information including CPU, RAM, disk, OS version, installed roles,
|
||||||
|
and key services. Generates Word document (with HTML fallback) similar to
|
||||||
|
Exchange-Inventory.ps1 structure.
|
||||||
|
|
||||||
|
.PARAMETER OutputFolder
|
||||||
|
Destination folder for reports. Default: .\ServerInventory-<date>
|
||||||
|
|
||||||
|
.PARAMETER IncludeInstalledSoftware
|
||||||
|
Include installed software list (can be slow, default: $false)
|
||||||
|
|
||||||
|
.PARAMETER IncludeWindowsFeatures
|
||||||
|
Include Windows Features/Roles (default: $true)
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
⚠️ AI-GENERATED SCRIPT - UNTESTED
|
||||||
|
This script was generated by Claude AI and has not been tested in production.
|
||||||
|
Review and test thoroughly in a non-production environment before use.
|
||||||
|
|
||||||
|
- Run with Administrator privileges for complete information
|
||||||
|
- Works on Windows Server 2012+
|
||||||
|
- Report generation uses Word COM or HTML fallback
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Get-ServerInventory.ps1
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Get-ServerInventory.ps1 -IncludeInstalledSoftware $true -OutputFolder "D:\Inventory"
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("ServerInventory-" + (Get-Date -Format "yyyyMMdd-HHmm"))),
|
||||||
|
[bool]$IncludeInstalledSoftware = $false,
|
||||||
|
[bool]$IncludeWindowsFeatures = $true
|
||||||
|
)
|
||||||
|
|
||||||
|
function NowTag { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") }
|
||||||
|
|
||||||
|
Write-Host "[$(NowTag)] ⚠️ AI-GENERATED SCRIPT - UNTESTED" -ForegroundColor Yellow
|
||||||
|
Write-Host "[$(NowTag)] Starting server inventory..." -ForegroundColor Green
|
||||||
|
|
||||||
|
# Create output folder
|
||||||
|
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
|
||||||
|
|
||||||
|
$ReportDocx = Join-Path $OutputFolder "Server-Inventory.docx"
|
||||||
|
$ReportHtml = Join-Path $OutputFolder "Server-Inventory.html"
|
||||||
|
|
||||||
|
# Word COM or HTML fallback (reused pattern from Exchange-Inventory.ps1)
|
||||||
|
$script:Word = $null
|
||||||
|
$script:Doc = $null
|
||||||
|
$script:UseHtml = $false
|
||||||
|
$script:HtmlSb = New-Object System.Text.StringBuilder
|
||||||
|
|
||||||
|
function Start-Report {
|
||||||
|
try {
|
||||||
|
$script:Word = New-Object -ComObject Word.Application -ErrorAction Stop
|
||||||
|
$script:Word.Visible = $false
|
||||||
|
$script:Doc = $script:Word.Documents.Add()
|
||||||
|
Add-Heading "Server Inventory Report" 1
|
||||||
|
Add-Paragraph ("Server: $env:COMPUTERNAME")
|
||||||
|
Add-Paragraph ("Generated: " + (Get-Date).ToString("yyyy-MM-dd HH:mm"))
|
||||||
|
} catch {
|
||||||
|
$script:UseHtml = $true
|
||||||
|
[void]$script:HtmlSb.AppendLine("<html><head><meta charset='utf-8'><title>Server Inventory</title>")
|
||||||
|
[void]$script:HtmlSb.AppendLine("<style>body{font-family:Segoe UI,Arial,sans-serif;margin:20px} h1{font-size:24px;color:#0066cc} h2{font-size:20px;margin-top:30px;color:#333} table{border-collapse:collapse;width:100%;margin:15px 0} th,td{border:1px solid #ccc;padding:8px;text-align:left} th{background:#f3f3f3;font-weight:bold}</style></head><body>")
|
||||||
|
[void]$script:HtmlSb.AppendLine("<h1>Server Inventory Report</h1>")
|
||||||
|
[void]$script:HtmlSb.AppendLine("<p><strong>Server:</strong> $env:COMPUTERNAME</p>")
|
||||||
|
[void]$script:HtmlSb.AppendLine("<p><strong>Generated:</strong> " + (Get-Date).ToString("yyyy-MM-dd HH:mm") + "</p>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Add-Heading([string]$Text, [int]$Level = 2) {
|
||||||
|
if ($script:UseHtml) {
|
||||||
|
[void]$script:HtmlSb.AppendLine("<h$Level>$Text</h$Level>")
|
||||||
|
} else {
|
||||||
|
$range = $script:Doc.Range()
|
||||||
|
$range.Collapse(0) | Out-Null
|
||||||
|
$range.Text = "$Text`r"
|
||||||
|
$style = switch ($Level) { 1 { 'Heading 1' } 2 { 'Heading 2' } default { 'Heading 2' } }
|
||||||
|
$range.set_Style($style)
|
||||||
|
$range.InsertParagraphAfter() | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Add-Paragraph([string]$Text) {
|
||||||
|
if ($script:UseHtml) {
|
||||||
|
$enc = [System.Web.HttpUtility]::HtmlEncode($Text)
|
||||||
|
[void]$script:HtmlSb.AppendLine("<p>$enc</p>")
|
||||||
|
} else {
|
||||||
|
$range = $script:Doc.Range()
|
||||||
|
$range.Collapse(0) | Out-Null
|
||||||
|
$range.Text = $Text
|
||||||
|
$range.InsertParagraphAfter() | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Add-Table([object]$Objects, [string[]]$PropOrder, [string]$Title) {
|
||||||
|
if ($null -eq $Objects) {
|
||||||
|
Add-Paragraph ("{0}: No data." -f $Title)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$arr = @()
|
||||||
|
if ($Objects -is [System.Collections.IEnumerable] -and -not ($Objects -is [string])) {
|
||||||
|
foreach ($o in $Objects) { $arr += $o }
|
||||||
|
} else { $arr = @($Objects) }
|
||||||
|
if ($arr.Count -eq 0) {
|
||||||
|
Add-Paragraph ("{0}: No data." -f $Title)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Add-Heading $Title 3
|
||||||
|
|
||||||
|
if ($script:UseHtml) {
|
||||||
|
[void]$script:HtmlSb.AppendLine("<table><thead><tr>")
|
||||||
|
foreach ($p in $PropOrder) { [void]$script:HtmlSb.AppendLine("<th>$([System.Web.HttpUtility]::HtmlEncode($p))</th>") }
|
||||||
|
[void]$script:HtmlSb.AppendLine("</tr></thead><tbody>")
|
||||||
|
foreach ($o in $arr) {
|
||||||
|
[void]$script:HtmlSb.AppendLine("<tr>")
|
||||||
|
foreach ($p in $PropOrder) {
|
||||||
|
$val = if ($o.$p) { [string]$o.$p } else { "" }
|
||||||
|
[void]$script:HtmlSb.AppendLine("<td>$([System.Web.HttpUtility]::HtmlEncode($val))</td>")
|
||||||
|
}
|
||||||
|
[void]$script:HtmlSb.AppendLine("</tr>")
|
||||||
|
}
|
||||||
|
[void]$script:HtmlSb.AppendLine("</tbody></table>")
|
||||||
|
} else {
|
||||||
|
$rows = $arr.Count
|
||||||
|
$cols = $PropOrder.Count
|
||||||
|
$range = $script:Doc.Range()
|
||||||
|
$table = $script:Doc.Tables.Add($range, [Math]::Max(1, $rows) + 1, $cols)
|
||||||
|
for ($c = 1; $c -le $cols; $c++) { $table.Cell(1, $c).Range.Text = $PropOrder[$c - 1] }
|
||||||
|
$table.Rows.Item(1).Range.Bold = $true
|
||||||
|
for ($r = 0; $r -lt $rows; $r++) {
|
||||||
|
for ($c = 0; $c -lt $cols; $c++) {
|
||||||
|
$val = if ($arr[$r].($PropOrder[$c])) { [string]$arr[$r].($PropOrder[$c]) } else { "" }
|
||||||
|
$table.Cell($r + 2, $c + 1).Range.Text = $val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$table.AutoFitBehavior(2) | Out-Null
|
||||||
|
$range.InsertParagraphAfter() | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function End-Report {
|
||||||
|
if ($script:UseHtml) {
|
||||||
|
[void]$script:HtmlSb.AppendLine("</body></html>")
|
||||||
|
$script:HtmlSb.ToString() | Out-File -LiteralPath $ReportHtml -Encoding UTF8
|
||||||
|
Write-Host "[$(NowTag)] HTML report: $ReportHtml" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
$wdFormatXMLDocument = 12
|
||||||
|
$script:Doc.SaveAs([ref]$ReportDocx, [ref]$wdFormatXMLDocument)
|
||||||
|
$script:Doc.Close(); $script:Word.Quit()
|
||||||
|
Write-Host "[$(NowTag)] Word report: $ReportDocx" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Report
|
||||||
|
|
||||||
|
# System Information
|
||||||
|
Write-Host "[$(NowTag)] Collecting system information..."
|
||||||
|
$cs = Get-CimInstance -ClassName Win32_ComputerSystem
|
||||||
|
$os = Get-CimInstance -ClassName Win32_OperatingSystem
|
||||||
|
$bios = Get-CimInstance -ClassName Win32_BIOS
|
||||||
|
|
||||||
|
$sysInfo = [PSCustomObject]@{
|
||||||
|
ComputerName = $cs.Name
|
||||||
|
Domain = $cs.Domain
|
||||||
|
Manufacturer = $cs.Manufacturer
|
||||||
|
Model = $cs.Model
|
||||||
|
TotalPhysicalMemoryGB = [math]::Round($cs.TotalPhysicalMemory / 1GB, 2)
|
||||||
|
NumberOfProcessors = $cs.NumberOfProcessors
|
||||||
|
NumberOfLogicalProcessors = $cs.NumberOfLogicalProcessors
|
||||||
|
OSName = $os.Caption
|
||||||
|
OSVersion = $os.Version
|
||||||
|
OSArchitecture = $os.OSArchitecture
|
||||||
|
InstallDate = $os.InstallDate
|
||||||
|
LastBootUpTime = $os.LastBootUpTime
|
||||||
|
BIOSVersion = $bios.SMBIOSBIOSVersion
|
||||||
|
SerialNumber = $bios.SerialNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
Add-Heading "System Information" 2
|
||||||
|
Add-Table $sysInfo @("ComputerName", "Domain", "Manufacturer", "Model", "TotalPhysicalMemoryGB", "NumberOfProcessors", "NumberOfLogicalProcessors", "OSName", "OSVersion", "OSArchitecture", "InstallDate", "LastBootUpTime", "BIOSVersion", "SerialNumber") "System Details"
|
||||||
|
|
||||||
|
# CPU Information
|
||||||
|
Write-Host "[$(NowTag)] Collecting CPU information..."
|
||||||
|
$cpus = Get-CimInstance -ClassName Win32_Processor | Select-Object Name, NumberOfCores, NumberOfLogicalProcessors, MaxClockSpeed, CurrentClockSpeed
|
||||||
|
Add-Table $cpus @("Name", "NumberOfCores", "NumberOfLogicalProcessors", "MaxClockSpeed", "CurrentClockSpeed") "Processors"
|
||||||
|
|
||||||
|
# Disk Information
|
||||||
|
Write-Host "[$(NowTag)] Collecting disk information..."
|
||||||
|
$disks = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=3" | Select-Object @{N = "Drive"; E = { $_.DeviceID } }, @{N = "SizeGB"; E = { [math]::Round($_.Size / 1GB, 2) } }, @{N = "FreeGB"; E = { [math]::Round($_.FreeSpace / 1GB, 2) } }, @{N = "UsedGB"; E = { [math]::Round(($_.Size - $_.FreeSpace) / 1GB, 2) } }, @{N = "PercentFree"; E = { [math]::Round(($_.FreeSpace / $_.Size) * 100, 2) } }, VolumeName, FileSystem
|
||||||
|
Add-Table $disks @("Drive", "SizeGB", "FreeGB", "UsedGB", "PercentFree", "VolumeName", "FileSystem") "Disk Drives"
|
||||||
|
|
||||||
|
# Network Adapters
|
||||||
|
Write-Host "[$(NowTag)] Collecting network adapter information..."
|
||||||
|
$nics = Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration -Filter "IPEnabled=True" | Select-Object Description, @{N = "IPAddress"; E = { $_.IPAddress -join ", " } }, @{N = "SubnetMask"; E = { $_.IPSubnet -join ", " } }, @{N = "DefaultGateway"; E = { $_.DefaultIPGateway -join ", " } }, @{N = "DNSServers"; E = { $_.DNSServerSearchOrder -join ", " } }, MACAddress, DHCPEnabled
|
||||||
|
Add-Table $nics @("Description", "IPAddress", "SubnetMask", "DefaultGateway", "DNSServers", "MACAddress", "DHCPEnabled") "Network Adapters"
|
||||||
|
|
||||||
|
# Windows Features/Roles
|
||||||
|
if ($IncludeWindowsFeatures) {
|
||||||
|
Write-Host "[$(NowTag)] Collecting Windows features..."
|
||||||
|
try {
|
||||||
|
Import-Module ServerManager -ErrorAction Stop
|
||||||
|
$features = Get-WindowsFeature | Where-Object Installed | Select-Object Name, DisplayName, FeatureType
|
||||||
|
Add-Table $features @("Name", "DisplayName", "FeatureType") "Installed Windows Features/Roles"
|
||||||
|
} catch {
|
||||||
|
Add-Paragraph "Windows Features: Could not retrieve (ServerManager module not available)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Services
|
||||||
|
Write-Host "[$(NowTag)] Collecting critical services..."
|
||||||
|
$services = Get-Service | Where-Object { $_.StartType -eq "Automatic" -and $_.Status -ne "Running" } | Select-Object Name, DisplayName, Status, StartType
|
||||||
|
if ($services) {
|
||||||
|
Add-Table $services @("Name", "DisplayName", "Status", "StartType") "Automatic Services Not Running (Potential Issues)"
|
||||||
|
} else {
|
||||||
|
Add-Paragraph "All automatic services are running"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Installed Software (optional, can be slow)
|
||||||
|
if ($IncludeInstalledSoftware) {
|
||||||
|
Write-Host "[$(NowTag)] Collecting installed software (this may take a while)..."
|
||||||
|
$software = Get-CimInstance -ClassName Win32_Product | Select-Object Name, Version, Vendor, InstallDate | Sort-Object Name
|
||||||
|
Add-Table $software @("Name", "Version", "Vendor", "InstallDate") "Installed Software"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hotfixes
|
||||||
|
Write-Host "[$(NowTag)] Collecting installed hotfixes..."
|
||||||
|
$hotfixes = Get-HotFix | Sort-Object InstalledOn -Descending | Select-Object -First 20 | Select-Object HotFixID, Description, InstalledBy, InstalledOn
|
||||||
|
Add-Table $hotfixes @("HotFixID", "Description", "InstalledBy", "InstalledOn") "Recent Hotfixes (Last 20)"
|
||||||
|
|
||||||
|
End-Report
|
||||||
|
Write-Host "[$(NowTag)] Inventory complete! Output folder: $OutputFolder"
|
||||||
183
Misc/Monitor-DiskSpace.ps1
Normal file
183
Misc/Monitor-DiskSpace.ps1
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Monitor disk space and alert when below threshold
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Checks all fixed drives for available space and alerts when below threshold.
|
||||||
|
Can be scheduled as a task for proactive monitoring.
|
||||||
|
|
||||||
|
.PARAMETER ThresholdPercent
|
||||||
|
Alert when free space falls below this percentage (default: 15)
|
||||||
|
|
||||||
|
.PARAMETER ThresholdGB
|
||||||
|
Alternative: alert when free space falls below this many GB (default: not used)
|
||||||
|
|
||||||
|
.PARAMETER LogPath
|
||||||
|
Path to log file for alerts (default: C:\Temp\DiskSpace-Monitor.log)
|
||||||
|
|
||||||
|
.PARAMETER SendEmail
|
||||||
|
Send email alerts (requires email parameters, default: $false)
|
||||||
|
|
||||||
|
.PARAMETER SmtpServer
|
||||||
|
SMTP server for email alerts
|
||||||
|
|
||||||
|
.PARAMETER EmailFrom
|
||||||
|
From address for email alerts
|
||||||
|
|
||||||
|
.PARAMETER EmailTo
|
||||||
|
To address(es) for email alerts (comma-separated)
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
⚠️ AI-GENERATED SCRIPT - UNTESTED
|
||||||
|
This script was generated by Claude AI and has not been tested in production.
|
||||||
|
Review and test thoroughly in a non-production environment before use.
|
||||||
|
|
||||||
|
- Suitable for scheduled task execution
|
||||||
|
- Exit codes: 0=OK, 1=Warning (below threshold), 2=Error
|
||||||
|
- Email functionality requires SMTP access
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Monitor-DiskSpace.ps1
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Monitor-DiskSpace.ps1 -ThresholdPercent 20 -LogPath "D:\Logs\DiskMonitor.log"
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Monitor-DiskSpace.ps1 -SendEmail $true -SmtpServer "smtp.domain.com" -EmailFrom "alerts@domain.com" -EmailTo "admin@domain.com"
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[int]$ThresholdPercent = 15,
|
||||||
|
[int]$ThresholdGB = 0,
|
||||||
|
[string]$LogPath = "C:\Temp\DiskSpace-Monitor.log",
|
||||||
|
[bool]$SendEmail = $false,
|
||||||
|
[string]$SmtpServer = "",
|
||||||
|
[string]$EmailFrom = "",
|
||||||
|
[string]$EmailTo = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
function Write-Log {
|
||||||
|
param([string]$Message, [string]$Level = "INFO")
|
||||||
|
$stamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
|
||||||
|
$line = "[$stamp] [$Level] $Message"
|
||||||
|
Write-Output $line
|
||||||
|
|
||||||
|
# Ensure log directory exists
|
||||||
|
$logDir = Split-Path -Path $LogPath -Parent
|
||||||
|
if ($logDir -and -not (Test-Path -Path $logDir)) {
|
||||||
|
try {
|
||||||
|
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Add-Content -Path $LogPath -Value $line -Encoding UTF8 -ErrorAction SilentlyContinue
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log "⚠️ AI-GENERATED SCRIPT - UNTESTED" "WARNING"
|
||||||
|
Write-Log "Starting disk space monitoring on $env:COMPUTERNAME"
|
||||||
|
|
||||||
|
# Get all fixed drives
|
||||||
|
$disks = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=3" -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
if (-not $disks) {
|
||||||
|
Write-Log "ERROR: Could not retrieve disk information" "ERROR"
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
$alerts = @()
|
||||||
|
$allDisksOK = $true
|
||||||
|
|
||||||
|
foreach ($disk in $disks) {
|
||||||
|
$drive = $disk.DeviceID
|
||||||
|
$sizeGB = [math]::Round($disk.Size / 1GB, 2)
|
||||||
|
$freeGB = [math]::Round($disk.FreeSpace / 1GB, 2)
|
||||||
|
$usedGB = [math]::Round(($disk.Size - $disk.FreeSpace) / 1GB, 2)
|
||||||
|
$percentFree = [math]::Round(($disk.FreeSpace / $disk.Size) * 100, 2)
|
||||||
|
|
||||||
|
Write-Log "Drive $drive - Size: $sizeGB GB, Free: $freeGB GB ($percentFree%)"
|
||||||
|
|
||||||
|
# Check thresholds
|
||||||
|
$alertTriggered = $false
|
||||||
|
$alertReason = ""
|
||||||
|
|
||||||
|
if ($ThresholdGB -gt 0 -and $freeGB -lt $ThresholdGB) {
|
||||||
|
$alertTriggered = $true
|
||||||
|
$alertReason = "Free space ($freeGB GB) below threshold ($ThresholdGB GB)"
|
||||||
|
} elseif ($percentFree -lt $ThresholdPercent) {
|
||||||
|
$alertTriggered = $true
|
||||||
|
$alertReason = "Free space ($percentFree%) below threshold ($ThresholdPercent%)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($alertTriggered) {
|
||||||
|
$allDisksOK = $false
|
||||||
|
$alertMsg = "ALERT: Drive $drive - $alertReason"
|
||||||
|
Write-Log $alertMsg "ALERT"
|
||||||
|
|
||||||
|
$alerts += [PSCustomObject]@{
|
||||||
|
Drive = $drive
|
||||||
|
SizeGB = $sizeGB
|
||||||
|
FreeGB = $freeGB
|
||||||
|
UsedGB = $usedGB
|
||||||
|
PercentFree = $percentFree
|
||||||
|
Reason = $alertReason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send email if configured and alerts exist
|
||||||
|
if ($SendEmail -and $alerts.Count -gt 0) {
|
||||||
|
if (-not $SmtpServer -or -not $EmailFrom -or -not $EmailTo) {
|
||||||
|
Write-Log "Email alerts enabled but SMTP parameters incomplete" "WARNING"
|
||||||
|
} else {
|
||||||
|
Write-Log "Sending email alert to $EmailTo via $SmtpServer"
|
||||||
|
|
||||||
|
$subject = "DISK SPACE ALERT: $env:COMPUTERNAME"
|
||||||
|
$body = @"
|
||||||
|
Disk Space Alert on $env:COMPUTERNAME
|
||||||
|
Generated: $(Get-Date)
|
||||||
|
|
||||||
|
The following drives have triggered disk space alerts:
|
||||||
|
|
||||||
|
"@
|
||||||
|
|
||||||
|
foreach ($alert in $alerts) {
|
||||||
|
$body += @"
|
||||||
|
|
||||||
|
Drive: $($alert.Drive)
|
||||||
|
Size: $($alert.SizeGB) GB
|
||||||
|
Free: $($alert.FreeGB) GB ($($alert.PercentFree)%)
|
||||||
|
Used: $($alert.UsedGB) GB
|
||||||
|
Alert: $($alert.Reason)
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
$body += @"
|
||||||
|
|
||||||
|
|
||||||
|
Threshold Settings:
|
||||||
|
Percent Threshold: $ThresholdPercent%
|
||||||
|
GB Threshold: $ThresholdGB GB
|
||||||
|
|
||||||
|
Please investigate and free up disk space as needed.
|
||||||
|
"@
|
||||||
|
|
||||||
|
try {
|
||||||
|
Send-MailMessage -To $EmailTo.Split(',') -From $EmailFrom -Subject $subject -Body $body -SmtpServer $SmtpServer -ErrorAction Stop
|
||||||
|
Write-Log "Email sent successfully"
|
||||||
|
} catch {
|
||||||
|
Write-Log "ERROR sending email: $($_.Exception.Message)" "ERROR"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Exit with appropriate code
|
||||||
|
if ($allDisksOK) {
|
||||||
|
Write-Log "All disks have sufficient free space"
|
||||||
|
exit 0
|
||||||
|
} else {
|
||||||
|
Write-Log "$($alerts.Count) disk(s) below threshold"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
290
Misc/Test-ExchangeHealth.ps1
Normal file
290
Misc/Test-ExchangeHealth.ps1
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Quick Exchange server health check with aggregated results
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Runs multiple Exchange health cmdlets and aggregates results into a single
|
||||||
|
report. Includes service health, replication health, MAPI connectivity,
|
||||||
|
and database mount status.
|
||||||
|
|
||||||
|
.PARAMETER OutputFolder
|
||||||
|
Destination folder for reports. Default: .\ExchangeHealth-<date>
|
||||||
|
|
||||||
|
.PARAMETER IncludeMailflow
|
||||||
|
Test mail flow using Test-Mailflow cmdlet (default: $false, can be slow)
|
||||||
|
|
||||||
|
.PARAMETER ServerName
|
||||||
|
Specific server to test (default: local server)
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
⚠️ AI-GENERATED SCRIPT - UNTESTED
|
||||||
|
This script was generated by Claude AI and has not been tested in production.
|
||||||
|
Review and test thoroughly in a non-production environment before use.
|
||||||
|
|
||||||
|
- Run in Exchange Management Shell
|
||||||
|
- Some tests require specific server roles
|
||||||
|
- Tests are non-intrusive (read-only)
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Test-ExchangeHealth.ps1
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Test-ExchangeHealth.ps1 -ServerName "EXCH01" -IncludeMailflow $true
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("ExchangeHealth-" + (Get-Date -Format "yyyyMMdd-HHmm"))),
|
||||||
|
[bool]$IncludeMailflow = $false,
|
||||||
|
[string]$ServerName = $env:COMPUTERNAME
|
||||||
|
)
|
||||||
|
|
||||||
|
function NowTag { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") }
|
||||||
|
|
||||||
|
Write-Host "[$(NowTag)] ⚠️ AI-GENERATED SCRIPT - UNTESTED" -ForegroundColor Yellow
|
||||||
|
Write-Host "[$(NowTag)] Starting Exchange health check on $ServerName..." -ForegroundColor Green
|
||||||
|
|
||||||
|
# Create output folder
|
||||||
|
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
|
||||||
|
|
||||||
|
$allResults = @()
|
||||||
|
$criticalIssues = @()
|
||||||
|
$warnings = @()
|
||||||
|
|
||||||
|
# Test 1: Service Health
|
||||||
|
Write-Host "[$(NowTag)] Testing service health..."
|
||||||
|
try {
|
||||||
|
$serviceHealth = Test-ServiceHealth -Server $ServerName -ErrorAction SilentlyContinue
|
||||||
|
if ($serviceHealth) {
|
||||||
|
foreach ($svc in $serviceHealth) {
|
||||||
|
$result = [PSCustomObject]@{
|
||||||
|
TestCategory = "ServiceHealth"
|
||||||
|
TestName = $svc.Role
|
||||||
|
Server = $ServerName
|
||||||
|
Status = if ($svc.RequiredServicesRunning) { "PASS" } else { "FAIL" }
|
||||||
|
Details = "Required services: $($svc.RequiredServicesRunning)"
|
||||||
|
}
|
||||||
|
$allResults += $result
|
||||||
|
|
||||||
|
if (-not $svc.RequiredServicesRunning) {
|
||||||
|
$criticalIssues += "Service Health FAILED for role: $($svc.Role)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Host " Service Health: COMPLETED" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " Service Health: No results (cmdlet may not be available)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host " Service Health: ERROR - $($_.Exception.Message)" -ForegroundColor Red
|
||||||
|
$allResults += [PSCustomObject]@{
|
||||||
|
TestCategory = "ServiceHealth"
|
||||||
|
TestName = "Test-ServiceHealth"
|
||||||
|
Server = $ServerName
|
||||||
|
Status = "ERROR"
|
||||||
|
Details = $_.Exception.Message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test 2: Replication Health (DAG servers only)
|
||||||
|
Write-Host "[$(NowTag)] Testing replication health..."
|
||||||
|
try {
|
||||||
|
$replHealth = Test-ReplicationHealth -Server $ServerName -ErrorAction SilentlyContinue
|
||||||
|
if ($replHealth) {
|
||||||
|
foreach ($test in $replHealth) {
|
||||||
|
$status = if ($test.Result -eq "Passed") { "PASS" } elseif ($test.Result -eq "Failed") { "FAIL" } else { "WARNING" }
|
||||||
|
|
||||||
|
$result = [PSCustomObject]@{
|
||||||
|
TestCategory = "ReplicationHealth"
|
||||||
|
TestName = $test.Check
|
||||||
|
Server = $ServerName
|
||||||
|
Status = $status
|
||||||
|
Details = $test.Error
|
||||||
|
}
|
||||||
|
$allResults += $result
|
||||||
|
|
||||||
|
if ($status -eq "FAIL") {
|
||||||
|
$criticalIssues += "Replication Health FAILED: $($test.Check) - $($test.Error)"
|
||||||
|
} elseif ($status -eq "WARNING") {
|
||||||
|
$warnings += "Replication Health WARNING: $($test.Check) - $($test.Error)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Host " Replication Health: COMPLETED" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " Replication Health: Not applicable (not a DAG member)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host " Replication Health: ERROR or not applicable - $($_.Exception.Message)" -ForegroundColor Yellow
|
||||||
|
$allResults += [PSCustomObject]@{
|
||||||
|
TestCategory = "ReplicationHealth"
|
||||||
|
TestName = "Test-ReplicationHealth"
|
||||||
|
Server = $ServerName
|
||||||
|
Status = "N/A"
|
||||||
|
Details = "Not applicable or error: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test 3: MAPI Connectivity
|
||||||
|
Write-Host "[$(NowTag)] Testing MAPI connectivity..."
|
||||||
|
try {
|
||||||
|
$databases = Get-MailboxDatabase -Server $ServerName -Status -ErrorAction SilentlyContinue
|
||||||
|
foreach ($db in $databases) {
|
||||||
|
if ($db.Mounted) {
|
||||||
|
try {
|
||||||
|
$mapiTest = Test-MapiConnectivity -Database $db.Name -ErrorAction Stop
|
||||||
|
$status = if ($mapiTest.Result -eq "Success") { "PASS" } else { "FAIL" }
|
||||||
|
|
||||||
|
$result = [PSCustomObject]@{
|
||||||
|
TestCategory = "MapiConnectivity"
|
||||||
|
TestName = "Database: $($db.Name)"
|
||||||
|
Server = $ServerName
|
||||||
|
Status = $status
|
||||||
|
Details = "Latency: $($mapiTest.Latency.TotalMilliseconds) ms"
|
||||||
|
}
|
||||||
|
$allResults += $result
|
||||||
|
|
||||||
|
if ($status -eq "FAIL") {
|
||||||
|
$criticalIssues += "MAPI Connectivity FAILED for database: $($db.Name)"
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
$result = [PSCustomObject]@{
|
||||||
|
TestCategory = "MapiConnectivity"
|
||||||
|
TestName = "Database: $($db.Name)"
|
||||||
|
Server = $ServerName
|
||||||
|
Status = "ERROR"
|
||||||
|
Details = $_.Exception.Message
|
||||||
|
}
|
||||||
|
$allResults += $result
|
||||||
|
$warnings += "MAPI test error for $($db.Name): $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Host " MAPI Connectivity: COMPLETED" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host " MAPI Connectivity: ERROR - $($_.Exception.Message)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test 4: Database Mount Status
|
||||||
|
Write-Host "[$(NowTag)] Checking database mount status..."
|
||||||
|
try {
|
||||||
|
$databases = Get-MailboxDatabase -Server $ServerName -Status -ErrorAction SilentlyContinue
|
||||||
|
foreach ($db in $databases) {
|
||||||
|
$status = if ($db.Mounted) { "PASS" } else { "FAIL" }
|
||||||
|
|
||||||
|
$result = [PSCustomObject]@{
|
||||||
|
TestCategory = "DatabaseMount"
|
||||||
|
TestName = "Database: $($db.Name)"
|
||||||
|
Server = $ServerName
|
||||||
|
Status = $status
|
||||||
|
Details = "Mounted: $($db.Mounted)"
|
||||||
|
}
|
||||||
|
$allResults += $result
|
||||||
|
|
||||||
|
if (-not $db.Mounted) {
|
||||||
|
$criticalIssues += "Database DISMOUNTED: $($db.Name)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Host " Database Mount Status: COMPLETED" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host " Database Mount Status: ERROR - $($_.Exception.Message)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test 5: Mail Flow (optional)
|
||||||
|
if ($IncludeMailflow) {
|
||||||
|
Write-Host "[$(NowTag)] Testing mail flow (this may take a minute)..."
|
||||||
|
try {
|
||||||
|
$mailflowTest = Test-Mailflow -TargetMailboxServer $ServerName -ErrorAction Stop
|
||||||
|
$status = if ($mailflowTest.TestMailflowResult -eq "Success") { "PASS" } else { "FAIL" }
|
||||||
|
|
||||||
|
$result = [PSCustomObject]@{
|
||||||
|
TestCategory = "Mailflow"
|
||||||
|
TestName = "Test-Mailflow"
|
||||||
|
Server = $ServerName
|
||||||
|
Status = $status
|
||||||
|
Details = "Result: $($mailflowTest.TestMailflowResult), Latency: $($mailflowTest.MessageLatencyTime.TotalMilliseconds) ms"
|
||||||
|
}
|
||||||
|
$allResults += $result
|
||||||
|
|
||||||
|
if ($status -eq "FAIL") {
|
||||||
|
$criticalIssues += "Mail Flow test FAILED"
|
||||||
|
}
|
||||||
|
Write-Host " Mail Flow: COMPLETED" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host " Mail Flow: ERROR - $($_.Exception.Message)" -ForegroundColor Red
|
||||||
|
$allResults += [PSCustomObject]@{
|
||||||
|
TestCategory = "Mailflow"
|
||||||
|
TestName = "Test-Mailflow"
|
||||||
|
Server = $ServerName
|
||||||
|
Status = "ERROR"
|
||||||
|
Details = $_.Exception.Message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Export results
|
||||||
|
Write-Host "[$(NowTag)] Exporting results..."
|
||||||
|
|
||||||
|
$csvFile = Join-Path $OutputFolder "Health-Check-Results.csv"
|
||||||
|
$allResults | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $csvFile
|
||||||
|
Write-Host "[$(NowTag)] Results exported: $csvFile" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
$passCount = ($allResults | Where-Object Status -eq "PASS").Count
|
||||||
|
$failCount = ($allResults | Where-Object Status -eq "FAIL").Count
|
||||||
|
$errorCount = ($allResults | Where-Object Status -eq "ERROR").Count
|
||||||
|
$totalTests = $allResults.Count
|
||||||
|
|
||||||
|
$summaryFile = Join-Path $OutputFolder "Health-Check-Summary.txt"
|
||||||
|
$summary = @"
|
||||||
|
Exchange Health Check Summary
|
||||||
|
Server: $ServerName
|
||||||
|
Generated: $(Get-Date)
|
||||||
|
|
||||||
|
TEST RESULTS:
|
||||||
|
Total Tests: $totalTests
|
||||||
|
Passed: $passCount
|
||||||
|
Failed: $failCount
|
||||||
|
Errors: $errorCount
|
||||||
|
Overall Status: $(if ($failCount -eq 0 -and $errorCount -eq 0) { "HEALTHY" } else { "ISSUES DETECTED" })
|
||||||
|
|
||||||
|
$(if ($criticalIssues.Count -gt 0) {
|
||||||
|
"CRITICAL ISSUES:
|
||||||
|
$($criticalIssues | ForEach-Object { " - $_" } | Out-String)
|
||||||
|
"} else { "" })
|
||||||
|
|
||||||
|
$(if ($warnings.Count -gt 0) {
|
||||||
|
"WARNINGS:
|
||||||
|
$($warnings | ForEach-Object { " - $_" } | Out-String)
|
||||||
|
"} else { "" })
|
||||||
|
|
||||||
|
DETAILED RESULTS:
|
||||||
|
$($allResults | ForEach-Object { " [$($_.Status)] $($_.TestCategory) - $($_.TestName): $($_.Details)" } | Out-String)
|
||||||
|
"@
|
||||||
|
|
||||||
|
$summary | Out-File -FilePath $summaryFile -Encoding UTF8
|
||||||
|
Write-Host "[$(NowTag)] Summary: $summaryFile" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Console output
|
||||||
|
Write-Host "`nHEALTH CHECK SUMMARY:" -ForegroundColor Cyan
|
||||||
|
Write-Host " Total Tests: $totalTests"
|
||||||
|
Write-Host " Passed: $passCount" -ForegroundColor Green
|
||||||
|
Write-Host " Failed: $failCount" -ForegroundColor $(if ($failCount -gt 0) { "Red" } else { "Green" })
|
||||||
|
Write-Host " Errors: $errorCount" -ForegroundColor $(if ($errorCount -gt 0) { "Red" } else { "Green" })
|
||||||
|
|
||||||
|
if ($criticalIssues.Count -gt 0) {
|
||||||
|
Write-Host "`nCRITICAL ISSUES DETECTED:" -ForegroundColor Red
|
||||||
|
$criticalIssues | ForEach-Object { Write-Host " - $_" -ForegroundColor Red }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($warnings.Count -gt 0) {
|
||||||
|
Write-Host "`nWARNINGS:" -ForegroundColor Yellow
|
||||||
|
$warnings | ForEach-Object { Write-Host " - $_" -ForegroundColor Yellow }
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`n[$(NowTag)] Health check complete! Output folder: $OutputFolder"
|
||||||
|
|
||||||
|
# Exit code
|
||||||
|
if ($failCount -gt 0 -or $errorCount -gt 0) {
|
||||||
|
exit 1
|
||||||
|
} else {
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user