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:
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