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