Start-ADEHM.ps1

<#
.SYNOPSIS
    ADEHM - Active Directory Enterprise Health Monitor
    Main entry point of the monitoring engine.
 
.DESCRIPTION
    Monitors every domain controller defined in the configuration, collects
    system and Active Directory indicators through CIM/WinRM, generates a
    professional HTML report and sends it by email.
 
    Execution flow:
    Read Configuration -> Create Logger -> Read DC list -> CIM Connection
    -> Collection -> Report Generation -> Mail Delivery -> Archiving -> End
 
.PARAMETER ConfigPath
    Path to the ADEHM.config.psd1 configuration file.
    Default: .\Config\ADEHM.config.psd1
 
.PARAMETER Credential
    Service account used for remote CIM connections and SMTP delivery.
    Must hold the minimal delegations described in Docs/PERMISSIONS.md
    (least-privilege principle).
 
.PARAMETER DemoMode
    Generates simulated data, with no real network connection, to validate
    the installation, the HTML report rendering and the mail flow. Useful
    for acceptance testing before production rollout.
 
.EXAMPLE
    .\Start-ADEHM.ps1 -ConfigPath .\Config\ADEHM.config.psd1 -Credential (Get-Credential)
 
.EXAMPLE
    .\Start-ADEHM.ps1 -DemoMode
#>


[CmdletBinding()]
param(
    [string]$ConfigPath = (Join-Path $PSScriptRoot 'Config\ADEHM.config.psd1'),
    [System.Management.Automation.PSCredential]$Credential,
    [switch]$DemoMode
)

$ErrorActionPreference = 'Stop'
$startTime = Get-Date

# ---------------------------------------------------------------------
# 0. Module loading (modular architecture)
# ---------------------------------------------------------------------
$moduleRoot = Join-Path $PSScriptRoot 'Modules'
Get-ChildItem -Path $moduleRoot -Filter '*.psm1' | ForEach-Object {
    Import-Module $_.FullName -Force
}

try {
    # -------------------------------------------------------------
    # 1. Read Configuration
    # -------------------------------------------------------------
    $config = Import-ADEHMConfig -Path $ConfigPath

    # -------------------------------------------------------------
    # 2. Create Logger
    # -------------------------------------------------------------
    $logPath = Initialize-ADEHMLogger -LogDirectory $config.Paths.Logs -DebugMode:$config.General.DebugMode
    Write-ADEHMLog -Level INFO -Module Core -Message 'ADEHM Started'
    Write-ADEHMLog -Level INFO -Module Core -Message 'Reading Configuration'
    if ($DemoMode) {
        Write-ADEHMLog -Level WARN -Module Core -Message 'DemoMode active: simulated data, no real network connection.'
    }

    # -------------------------------------------------------------
    # 3. Read DC list
    # -------------------------------------------------------------
    $dcList = $config.DomainControllers
    Write-ADEHMLog -Level INFO -Module Core -Message "Domain Controllers to check: $($dcList.Count)"

    $results = New-Object System.Collections.Generic.List[object]
    $svcNames = $config.Services | ForEach-Object { $_.Name }

    foreach ($dc in $dcList) {

        Write-ADEHMLog -Level INFO -Module Core -Message "Connecting $dc"

        $row = [ordered]@{
            Hostname        = $dc
            Site            = $null
            IPAddress       = $null
            Ping            = $false
            WinRM           = $false
            NTDS            = $null
            DNS             = $null
            DFSR            = $null
            Netlogon        = $null
            KDC             = $null
            ADWS            = $null
            SYSVOL          = $false
            NetlogonShare   = $false   # \\DC\NETLOGON share (distinct from the Netlogon service: PowerShell hashtable keys are case-insensitive)
            CpuPercent      = $null
            RamPercent      = $null
            DiskFreePercent = $null
            DiskDetail      = @()
            OSCaption       = $null
            BuildNumber     = $null
            LastBootTime    = $null
            UptimeDays      = $null
        }

        if ($DemoMode) {
            # -----------------------------------------------------
            # Simulated data: validates the HTML report and the mail
            # flow without a real AD infrastructure.
            # -----------------------------------------------------
            $row.Site      = 'Default-First-Site-Name'
            $row.IPAddress = '10.10.{0}.{1}' -f (Get-Random -Minimum 0 -Maximum 5), (Get-Random -Minimum 2 -Maximum 254)
            $row.Ping      = $true
            $row.WinRM     = $true
            foreach ($svc in $svcNames) {
                $row[$svc] = ((Get-Random -Minimum 0 -Maximum 10) -gt 0)
            }
            $row.SYSVOL        = $true
            $row.NetlogonShare = $true
            $row.CpuPercent = Get-Random -Minimum 5 -Maximum 95
            $row.RamPercent = Get-Random -Minimum 20 -Maximum 95
            $row.DiskFreePercent = Get-Random -Minimum 2 -Maximum 80
            $row.DiskDetail = @([PSCustomObject]@{
                DeviceID    = 'C:'
                SizeGB      = 120
                FreeGB      = [math]::Round(120 * $row.DiskFreePercent / 100, 1)
                FreePercent = $row.DiskFreePercent
            })
            $row.OSCaption   = 'Microsoft Windows Server 2022 Standard'
            $row.BuildNumber = '20348'
            $row.LastBootTime = (Get-Date).AddDays(-(Get-Random -Minimum 1 -Maximum 90))
            $row.UptimeDays   = [math]::Round(((Get-Date) - $row.LastBootTime).TotalDays, 1)

            Write-ADEHMLog -Level INFO -Module Connectivity -Message "$dc Ping OK (demo)"
            Write-ADEHMLog -Level INFO -Module System -Message "$dc CPU OK (demo)"
            Write-ADEHMLog -Level INFO -Module System -Message "$dc RAM OK (demo)"
        }
        else {
            # -----------------------------------------------------
            # 4a. Basic connectivity
            # -----------------------------------------------------
            $row.Ping  = Test-ADEHMPing -ComputerName $dc -TimeoutSeconds $config.General.TimeoutSeconds
            $dns       = Resolve-ADEHMDns -ComputerName $dc
            $row.IPAddress = $dns.IPAddress
            $row.WinRM = Test-ADEHMWinRM -ComputerName $dc -TimeoutSeconds $config.General.TimeoutSeconds

            if (-not $row.Ping -or -not $row.WinRM) {
                Write-ADEHMError -Server $dc -Module 'Connectivity' `
                    -Exception 'Ping or WinRM unavailable' `
                    -ProbableCause 'Server down, firewall blocking ICMP/WinRM, or WinRM service stopped.' `
                    -Recommendation "Check the state of $dc and the WinRM configuration (winrm quickconfig) / firewall."
                $results.Add([PSCustomObject]$row)
                continue
            }

            # -----------------------------------------------------
            # 4b. CIM connection (one single session per server)
            # -----------------------------------------------------
            $cim = $null
            try {
                $cim = New-ADEHMCimSession -ComputerName $dc -TimeoutSeconds $config.General.TimeoutSeconds -Credential $Credential
                Write-ADEHMLog -Level DEBUG -Module System -Message "$dc CIM session opened"

                # -------------------------------------------------
                # 4c. Collection
                # -------------------------------------------------
                $sysInfo    = Get-ADEHMSystemInfo -CimSession $cim
                $diskInfo   = Get-ADEHMDiskInfo -CimSession $cim
                $services   = Get-ADEHMServiceStatus -CimSession $cim -ServiceNames $svcNames
                $domainInfo = Get-ADEHMDomainInfo -ComputerName $dc

                $row.Site         = $domainInfo.Site
                $row.CpuPercent   = $sysInfo.CpuLoadPercent
                $row.RamPercent   = $sysInfo.RamUsedPercent
                $row.OSCaption    = $sysInfo.OSCaption
                $row.BuildNumber  = $sysInfo.BuildNumber
                $row.LastBootTime = $sysInfo.LastBootTime
                $row.UptimeDays   = $sysInfo.UptimeDays
                $row.DiskDetail   = $diskInfo

                $cDisk = $diskInfo | Where-Object { $_.DeviceID -eq 'C:' } | Select-Object -First 1
                $row.DiskFreePercent = if ($cDisk) { $cDisk.FreePercent } else { ($diskInfo | Sort-Object FreePercent | Select-Object -First 1).FreePercent }

                foreach ($svc in $svcNames) {
                    $row[$svc] = $services[$svc].Running
                }

                $row.SYSVOL        = Test-ADEHMShare -ComputerName $dc -ShareName 'SYSVOL'
                $row.NetlogonShare = Test-ADEHMShare -ComputerName $dc -ShareName 'NETLOGON'

                Write-ADEHMLog -Level INFO -Module System -Message "$dc CPU OK"
                Write-ADEHMLog -Level INFO -Module System -Message "$dc RAM OK"
            }
            catch {
                $msg = $_.Exception.Message

                # The probable cause is inferred from the error type to avoid
                # sending the diagnosis down the wrong path.
                if ($msg -match 'parameter') {
                    $cause = 'Client-side error (ADEHM host), raised BEFORE any connection to the DC: cmdlet incompatibility or engine bug.'
                    $reco  = "The issue concerns neither $dc nor permissions. Check the host PowerShell version and report the error."
                }
                elseif ($msg -match 'denied|access') {
                    $cause = "Insufficient rights for the service account on $dc."
                    $reco  = 'Check the CIM/WMI and WinRM delegations of the service account (see Docs/PERMISSIONS.md).'
                }
                elseif ($msg -match 'timeout|timed out') {
                    $cause = "Response timeout: $dc overloaded, or WinRM traffic (5985) filtered by a firewall."
                    $reco  = "Run Test-NetConnection $dc -Port 5985 and raise General.TimeoutSeconds if the server is slow."
                }
                else {
                    $cause = "CIM/WinRM service unavailable on $dc, or unexpected error."
                    $reco  = 'Review the exception message above and the debug-level log (General.DebugMode = $true).'
                }

                Write-ADEHMError -Server $dc -Module 'System/ActiveDirectory' -Exception $msg `
                    -ProbableCause $cause -Recommendation $reco
            }
            finally {
                # Systematic session cleanup
                if ($cim) {
                    Remove-CimSession -CimSession $cim -ErrorAction SilentlyContinue
                    Write-ADEHMLog -Level DEBUG -Module System -Message "$dc CIM session closed"
                }
            }
        }

        $results.Add([PSCustomObject]$row)
    }

    # -------------------------------------------------------------
    # 5. Report Generation
    # -------------------------------------------------------------
    $endTime = Get-Date
    $reportFileName = 'ADEHM_Report_{0}.html' -f (Get-Date -Format 'yyyyMMdd_HHmmss')
    $reportPath = Join-Path $config.Paths.Reports $reportFileName

    New-ADEHMHtmlReport -Config $config -Results $results -StartTime $startTime -EndTime $endTime `
        -ErrorLog (Get-ADEHMErrorLog) -OutputPath $reportPath | Out-Null
    Write-ADEHMLog -Level INFO -Module Report -Message 'Report Generated'

    # -------------------------------------------------------------
    # 6. Mail Delivery
    # -------------------------------------------------------------
    $emailBody = $null
    if ($config.Mail.Enabled -and $config.Mail.EmbedReportInBody) {
        # Email-optimized twin rendering: same data, built with HTML tables
        # + inline styles for faithful display in Outlook.
        $emailBody = New-ADEHMEmailReport -Config $config -Results $results `
            -StartTime $startTime -EndTime $endTime -ErrorLog (Get-ADEHMErrorLog)
    }

    $sent = Send-ADEHMReport -Config $config -ReportPath $reportPath -RunDate $startTime `
        -BodyHtml $emailBody -Credential $Credential
    if ($sent) {
        Write-ADEHMLog -Level INFO -Module Mail -Message 'Mail Sent'
    }

    # -------------------------------------------------------------
    # 7. Archiving
    # -------------------------------------------------------------
    if ($config.General.ArchiveReports) {
        $cutoff = (Get-Date).AddDays(-$config.General.ArchiveDays)

        Get-ChildItem -Path $config.Paths.Reports -Filter 'ADEHM_Report_*.html' -ErrorAction SilentlyContinue |
            Where-Object { $_.LastWriteTime -lt $cutoff } |
            ForEach-Object { Move-Item -LiteralPath $_.FullName -Destination $config.Paths.Archive -Force }

        Get-ChildItem -Path $config.Paths.Logs -Filter 'ADEHM_*.log' -ErrorAction SilentlyContinue |
            Where-Object { $_.LastWriteTime -lt $cutoff } |
            ForEach-Object { Move-Item -LiteralPath $_.FullName -Destination $config.Paths.Archive -Force }

        Write-ADEHMLog -Level DEBUG -Module Core -Message "Archiving: items older than $($cutoff.ToString('yyyy-MM-dd')) moved."
    }

    # -------------------------------------------------------------
    # 8. End
    # -------------------------------------------------------------
    Write-ADEHMLog -Level INFO -Module Core -Message 'ADEHM Finished'

    return [PSCustomObject]@{
        ReportPath = $reportPath
        LogPath    = $logPath
        Results    = $results
        Errors     = Get-ADEHMErrorLog
    }
}
catch {
    Write-Host "FATAL ERROR: $($_.Exception.Message)" -ForegroundColor Red
    if (Get-Command Write-ADEHMLog -ErrorAction SilentlyContinue) {
        Write-ADEHMLog -Level ERROR -Module Core -Message "Fatal error: $($_.Exception.Message)"
    }
    exit 1
}