MAD-Computers.ps1

<###########################
     Computers
############################>

# Collecte et analyse du parc informatique AD.
#
# Variables consommees (fournies par le contexte Get-MADReport) :
# $MaxSearchObjects - limite de recherche AD
# $createdlastdays - date seuil (calculee depuis $RecentObjectsDays, defaut 30j, initialisee dans MAD-Users)
# $barcreateobject - liste partagee users/PC pour le graphe timeline (initialisee dans MAD-Helpers)
#
# Variables produites (utilisees par EoL, Resume, HTML MultiPage et OnePage) :
# $ComputersTable - liste complete des ordinateurs normalises
# $ClientList - postes clients uniquement
# $ServerList - serveurs uniquement
# $UnknownOSTable - OS non identifies
# $GraphComputerOS - agregats OS pour graphes
# $ClientOSStats - stats clients par OS_Full
# $ServerOSStats - stats serveurs par OS_Full
# $ClientObsolete / $ClientSupported
# $ServerObsolete / $ServerSupported
# $TOPComputersTable - tableau recapitulatif
# $totalcomputers, $lastcreatedpc
# $ComputersProtected / $ComputersNotProtected
# $ComputerEnabled / $ComputerDisabled
# $endofsupportwin, $allwin1011, $ComputerNotSupported

Write-Host ""
Write-Host " #=======================================================================" -ForegroundColor DarkCyan
Write-Host " # [ORDINATEURS]" -ForegroundColor Cyan
Write-Host " #=======================================================================" -ForegroundColor DarkCyan
Write-Progress-Custom "Ordinateurs" "Analyse du parc informatique"

function Convert-WindowsBuildToRelease {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)][string]$BuildNumber,
        [Parameter()][ValidateSet('Win10','Win11')][string]$Branch = 'Win10'
    )
    $map10 = @{
        '10240'='1507'; '10586'='1511'; '14393'='1607'; '15063'='1703'; '16299'='1709'
        '17134'='1803'; '17763'='1809'; '18362'='1903'; '18363'='1909'
        '19041'='2004'; '19042'='20H2'; '19043'='21H1'; '19044'='21H2'; '19045'='22H2'
    }
    $map11 = @{
        '22000'='21H2'; '22621'='22H2'; '22631'='23H2'; '26100'='24H2'
    }
    if ($Branch -eq 'Win11') { return $map11[$BuildNumber] }
    else { return $map10[$BuildNumber] }
}

# OSFamilyMap est défini dans ModernActiveDirectoryEnhanced.psm1 (source unique)

# =============================================================================
# Extract-OSVersion : extraction générique de version OS
# Tente toutes les combinaisons possibles entre OperatingSystem et
# OperatingSystemVersion, peu importe comment l'AD a été rempli.
#
# Retourne la version normalisée (ex: "22.04", "14", "12") ou $null
# =============================================================================
function Extract-OSVersion {
    param(
        [string]$Raw,       # OperatingSystem
        [string]$Ver,       # OperatingSystemVersion
        [string]$Pattern    # regex de capture, ex: '\d+\.\d+' ou '\d+'
    )

    $sources = @(
        $Raw,                                    # "Ubuntu 22.04 LTS"
        $Ver,                                    # "22.04" ou "22.04.1"
        "$Raw $Ver",                             # concat
        ($Ver -replace '[^0-9\.]','')            # version nettoyée des suffixes texte
    ) | Where-Object { $_ }

    foreach ($s in $sources) {
        if ($s -match $Pattern) {
            return $matches[0]
        }
    }
    return $null
}

function Normalize-OperatingSystem {
    [CmdletBinding()]
    param(
        [string]$OperatingSystem,
        [string]$OperatingSystemVersion
    )

    $raw = ''
    if ($null -ne $OperatingSystem) { $raw = $OperatingSystem.Trim() }
    $ver = ''
    if ($null -ne $OperatingSystemVersion) { $ver = $OperatingSystemVersion.Trim() }

    if ([string]::IsNullOrWhiteSpace($raw)) {
        return [pscustomobject]@{ Vendor='Unknown'; Family='Other'; Product='Unknown'; Release=$null; Build=$null }
    }

    # Nettoyages simples
    $raw = $raw -replace '�','' -replace '\s+',' '

    # Microsoft Windows (Client & Server)
    if ($raw -match '^Windows\s+(Server\s+)?') {
        $isServer = $raw -match '^Windows\s+Server'
        $family   = if ($isServer) { 'Server' } else { 'Client' }

        # Parse build "10.0 (19045)"
        $build = $null
        if ($ver -match '^\s*\d+\.\d+\s*\((\d+)\)') { $build = $matches[1] }

        # Produit
        if ($isServer) {
        
            # 1) Editions Server IoT
            if ($raw -match 'Windows Server\s+IoT\s+(2025|20\d{2}|2019|2022)') {
                $product = "Windows Server IoT $($matches[1])"
        
            # 2) Editions Server classiques
            } elseif ($raw -match 'Windows Server\s+(2008 R2|2012 R2|2025|2022|2019|2016|2012|2008|20\d{2})') {
                $product = "Windows Server $($matches[1])"
        
            } else {
                $product = 'Windows Server'
            }
        
        } else {
            # 3) Editions Client IoT
            if ($raw -match 'Windows\s+(10|11)\s+IoT\s+Enterprise') {
                $product = "Windows $($matches[1]) IoT Enterprise"
        
            # 4) Clients classiques
            } elseif ($raw -match 'Windows 11')       { $product = 'Windows 11' }
            elseif ($raw -match 'Windows 10')         { $product = 'Windows 10' }
            elseif ($raw -match 'Windows 8\.1')       { $product = 'Windows 8.1' }
            elseif ($raw -match 'Windows 8(?!\.1)')   { $product = 'Windows 8' }
            elseif ($raw -match 'Windows 7')          { $product = 'Windows 7' }
        
            # 5) Anciens Windows XP
            elseif ($raw -match 'Windows XP( Embedded)?') {
                $product = ('Windows XP' + ($(if ($matches[1]) { ' Embedded' } else { '' })))
        
            # 6) Embedded generique
            } elseif ($raw -match 'Windows Embedded')  { $product = 'Windows Embedded' }
        
            else { $product = 'Windows (Client)' }
        }

        # Release marketing pour Win10/11 si build connu
        $release = $null
        if ($build) {
            if ($product -eq 'Windows 11') { $release = Convert-WindowsBuildToRelease -BuildNumber $build -Branch Win11 }
            elseif ($product -eq 'Windows 10') { $release = Convert-WindowsBuildToRelease -BuildNumber $build -Branch Win10 }
        }

        # Family depuis OSFamilyMap — fallback sur la détection isServer si absent (cas Windows IoT, etc.)
        $family = if ($OSFamilyMap.ContainsKey($product)) { $OSFamilyMap[$product] } elseif ($isServer) { 'Server' } else { 'Client' }
        return [pscustomobject]@{ Vendor='Microsoft'; Family=$family; Product=$product; Release=$release; Build=$build }
    }

    # Apple
    if ($raw -match '^(Mac\s*OS\s*X|macOS)') {
        # Mapping : pour macOS >= 11, clé DB = "macOS 14 Sonoma"
        # pour macOS 10.x, clé DB = "macOS 10.15 Catalina" (cycle complet)
        $macOSNames = @{
            '10.15'='Catalina'; '10.14'='Mojave'; '10.13'='High Sierra'
            '10.12'='Sierra';   '10.11'='El Capitan'
            11='Big Sur'; 12='Monterey'; 13='Ventura'; 14='Sonoma'; 15='Sequoia'
        }
        $macRelease = $null
        $allText = "$raw $ver"

        # Cas 1 : nom de version présent ("Sonoma", "Ventura", "Catalina"...)
        if ($allText -match '(Sequoia|Sonoma|Ventura|Monterey|Big\s*Sur|Catalina|Mojave|High\s*Sierra|Sierra|El\s*Capitan)') {
            $name    = $matches[1] -replace '\s+',' '
            $cycleKey = ($macOSNames.GetEnumerator() | Where-Object { $_.Value -eq $name } | Select-Object -First 1).Key
            if ($cycleKey) { $macRelease = "$cycleKey $name" }
        }

        # Cas 2 : numéro >= 11 (ex: "14.5", "macOS 13")
        if (-not $macRelease) {
            $majorStr = Extract-OSVersion -Raw $raw -Ver $ver -Pattern '(?<!\d)(1[1-9]|\d{2,})(?:\.\d+)?(?!\d)'
            if ($majorStr) {
                $major = [int]($majorStr -replace '\..*','')
                $name  = $macOSNames[$major]
                if ($name) { $macRelease = "$major $name" }
                else       { $macRelease = "$major" }
            }
        }

        # Cas 3 : macOS 10.x — garder le cycle complet "10.15" pour matcher la DB
        if (-not $macRelease) {
            $cycleStr = Extract-OSVersion -Raw $raw -Ver $ver -Pattern '10\.\d+'
            if ($cycleStr) {
                $cycle = ($cycleStr -split '\.')[0..1] -join '.'   # "10.15.7" → "10.15"
                $name  = $macOSNames[$cycle]
                $macRelease = if ($name) { "$cycle $name" } else { $cycle }
            }
        }

        $family = if ($OSFamilyMap.ContainsKey('macOS')) { $OSFamilyMap['macOS'] } else { 'Unknown' }
        return [pscustomobject]@{ Vendor='Apple'; Family=$family; Product='macOS'; Release=$macRelease; Build=$null }
    }

    # Linux — extraction générique depuis les deux champs AD
    if ($raw -match '(Ubuntu|Debian|CentOS|Red\s*Hat|RHEL|SUSE|SLES|Fedora|Linux)') {
        $prod = switch -Regex ($matches[1]) {
            'Red\s*Hat|RHEL' { 'RHEL' }
            default           { $matches[1] }
        }
        $family = if ($OSFamilyMap.ContainsKey($prod)) { $OSFamilyMap[$prod] } else { 'Unknown' }

        # Extraction de version : essaie X.Y d'abord, puis X seul
        # On exclut les versions kernel (ex: "5.15.0-91") en ignorant les valeurs < 6 en majeure
        $linuxRelease = $null

        # Pour RHEL/CentOS/Debian/Fedora, on extrait d'abord la version OS depuis le nom
        # (le noyau RHEL 8/9 = 4.x/5.x passerait le filtre $major -ge 6 a tort)
        if ($prod -in @('RHEL','CentOS','Debian','Fedora')) {
            # Chercher le numéro de version OS dans le champ OperatingSystem (ex: "Red Hat Enterprise Linux 9.2")
            if ($raw -match '(?:RHEL|Red\s*Hat[^\d]*|CentOS[^\d]*|Debian[^\d]*|Fedora[^\d]*)(\d{1,2})(?:\.\d+)?') {
                $linuxRelease = $matches[1]
            } elseif ($ver -match '^(\d{1,2})(?:\.\d+)?$') {
                # OperatingSystemVersion contient directement la version (ex: "9" ou "9.2")
                $linuxRelease = $matches[1]
            }
        }

        if (-not $linuxRelease) {
            $verXY = Extract-OSVersion -Raw $raw -Ver $ver -Pattern '\d+\.\d+'
            if ($verXY) {
                $major = [int]($verXY -split '\.')[0]
                # Ignorer les versions kernel Linux (majeure < 6, ex: "5.15.0-91")
                if ($major -ge 6) {
                    if ($prod -in @('Ubuntu','SUSE','SLES')) {
                        $linuxRelease = ($verXY -split '\.')[0..1] -join '.'
                    } else {
                        $linuxRelease = "$major"
                    }
                }
            }
            # Fallback : version majeure seule (ex: OSVersion = "22" ou "12")
            if (-not $linuxRelease) {
                $verX = Extract-OSVersion -Raw $raw -Ver $ver -Pattern '(?<!\d)\d{2}(?!\d)'
                if ($verX -and [int]$verX -ge 6) { $linuxRelease = $verX }
            }
        }

        return [pscustomobject]@{ Vendor='Linux'; Family=$family; Product=$prod; Release=$linuxRelease; Build=$null }
    }

    # Hyperviseurs — si absent de OSFamilyMap → Unknown
    if ($raw -match '(ESXi|VMware|Hyper-V|XenServer|Proxmox)') {
        $prod   = $matches[1]
        $family = if ($OSFamilyMap.ContainsKey($prod)) { $OSFamilyMap[$prod] } else { 'Unknown' }
        return [pscustomobject]@{ Vendor='Other'; Family=$family; Product=$prod; Release=$null; Build=$null }
    }

    # ChromeOS / Android — si absent de OSFamilyMap → Unknown
    if ($raw -match '(ChromeOS|Chrome OS|Android)') {
        $prod   = $matches[1] -replace ' ',''
        $family = if ($OSFamilyMap.ContainsKey($prod)) { $OSFamilyMap[$prod] } else { 'Unknown' }
        return [pscustomobject]@{ Vendor='Google'; Family=$family; Product=$prod; Release=$null; Build=$null }
    }

    # FreeBSD / Unix — si absent de OSFamilyMap → Unknown
    if ($raw -match '(FreeBSD|Unix)') {
        $prod   = $matches[1]
        $family = if ($OSFamilyMap.ContainsKey($prod)) { $OSFamilyMap[$prod] } else { 'Unknown' }
        return [pscustomobject]@{ Vendor='Other'; Family=$family; Product=$prod; Release=$null; Build=$null }
    }

    # Tout le reste → Unknown (filet de sécurité)
    return [pscustomobject]@{ Vendor='Unknown'; Family='Unknown'; Product=$raw; Release=$null; Build=$null }
}

# --- Collecte & normalisation ---
$filtercomputer = @(
    'Name','OperatingSystem','OperatingSystemVersion','ProtectedFromAccidentalDeletion',
    'lastlogondate','Created','PasswordLastSet','DistinguishedName','ipv4address',
    'userAccountControl','Enabled'
)

$ComputersProtected = 0; $ComputersNotProtected = 0
$ComputerEnabled = 0;    $ComputerDisabled    = 0
$totalcomputers = 0;     $lastcreatedpc      = 0
$endofsupportwin = 0;    $allwin1011         = 0
$ComputerNotSupported = 0
$DCCount = 0

# Collections
$ComputersTable = New-Object 'System.Collections.Generic.List[Object]'
$UnknownOSTable = New-Object 'System.Collections.Generic.List[Object]'
$ClientList     = New-Object 'System.Collections.Generic.List[Object]'
$ServerList     = New-Object 'System.Collections.Generic.List[Object]'
$DiversList     = New-Object 'System.Collections.Generic.List[Object]'

# En mode ShowSensitiveObjects, on inclut les DC (userAccountControl:=8192)
# En mode standard, on les exclut du tableau ordinateurs
$_computerLDAPFilter = if ($ShowSensitiveObjects.IsPresent) {
    "(objectCategory=computer)"
} else {
    "(!(userAccountControl:1.2.840.113556.1.4.803:=8192))"
}
Write-Progress-Custom "Ordinateurs" $(if ($ShowSensitiveObjects.IsPresent) { "DC inclus (mode ShowSensitiveObjects)" } else { "DC exclus (mode standard)" })

Get-ADComputer -LDAPFilter $_computerLDAPFilter -Properties $filtercomputer -ResultSetSize $MaxSearchObjects |
ForEach-Object {
    $totalcomputers++

    # Detecter si c'est un DC (userAccountControl bit 8192)
    $_isDC = ($_.userAccountControl -band 8192) -eq 8192

    if ($_isDC) { $DCCount++ }
    if ($_.ProtectedFromAccidentalDeletion) { $ComputersProtected++ } else { $ComputersNotProtected++ }
    if ($_.Enabled) { $ComputerEnabled++ } else { $ComputerDisabled++ }

    # Chronologie creations ($RecentObjectsDays jours)
    if ($_.Created -ge $createdlastdays) {
        $lastcreatedpc++
        $barcreated = ($_.created.ToString("yyyy/MM/dd"))
        $rec = $barcreateobject | Where-Object { $_.Date -eq $barcreated }
        if ($rec) { $rec.'Nbr_PC' += 1 }
        else {
            $obj = [pscustomobject]@{ 'Nbr_users' = 0; 'Nbr_PC' = 1; 'Date' = $barcreated }
            $barcreateobject.Add($obj)
        }
    }

    # Normalisation OS
    $norm = Normalize-OperatingSystem -OperatingSystem $_.OperatingSystem -OperatingSystemVersion $_.OperatingSystemVersion

    # Heuristique obsolescence clients Win10/11
    # BUG15 FIX : seuils séparés par OS — Win10 obsolète si build <= 19045 (22H2 = dernière),
    # Win11 part du build 22000 ; les builds Win10 (<22000) ne peuvent pas être Win11.
    if ($norm.Vendor -eq 'Microsoft' -and $norm.Family -eq 'Client' -and $norm.Product -match '^Windows (10|11)$') {
        $allwin1011++
        if ($norm.Build -as [int]) {
            $buildInt = [int]$norm.Build
            if ($norm.Product -eq 'Windows 10') {
                # Win10 : fin de support global octobre 2025 — toutes les versions sont en EoL
                # On marque comme obsolète tout build inférieur au dernier (22H2 = 19045)
                if ($buildInt -lt 19045) { $endofsupportwin++ }
            } elseif ($norm.Product -eq 'Windows 11') {
                # POINT11 FIX : seuil mis a jour — 24H2 (build 26100) est la version
                # courante de Win11 (oct 2024). Les builds anterieurs (21H2=22000,
                # 22H2=22621, 23H2=22631) sont en fin de support.
                # Ancien seuil 22621 ne couvrait ni 23H2 ni 24H2.
                if ($buildInt -lt 26100) { $endofsupportwin++ }
            }
        }
    }

    $obj = [pscustomobject]@{
        'Name'                     = $_.Name
        'Enabled'                  = $_.Enabled
        'Operating System'         = $_.OperatingSystem
        'Operating System Version' = $_.OperatingSystemVersion
        'OS_Product'               = $norm.Product
        'OS_Release'               = $norm.Release
        'OS_Build'                 = $norm.Build
        'OS_Full'                  = ''
        'EoL_Status'               = ''
        'Family'                   = $norm.Family
        'Vendor'                   = $norm.Vendor
        'IPv4Address'              = $_.IPv4Address
        'Created Date'             = $_.Created
        'Password Last Set'        = $_.PasswordLastSet
        'Last Logon Date'          = $_.LastLogonDate
        'Protect from Deletion'    = $_.ProtectedFromAccidentalDeletion
        'OU'                       = (($_.DistinguishedName -split ",") | Where-Object {$_ -like "OU=*"} | ForEach-Object {$_ -replace "OU=",""}) -join ","
        'CN'                       = $_.DistinguishedName
        'Role'                  = if ($_isDC) { 'Domain Controller' } else { 'Workstation/Server' }
        'IsDC'                  = $_isDC
    }
    $ComputersTable.Add($obj)

    switch ($norm.Family) {
        'Client'  { $ClientList.Add($obj) }
        'Server'  { $ServerList.Add($obj) }
        'Divers'  { $DiversList.Add($obj) }
        'Unknown' { $UnknownOSTable.Add([pscustomobject]@{
            Name        = $_.Name
            RawOS       = if ($_.OperatingSystem) { $_.OperatingSystem } else { '(vide)' }
            OSVersion   = if ($_.OperatingSystemVersion) { $_.OperatingSystemVersion } else { '' }
            Enabled     = $_.Enabled
            LastLogon   = $_.LastLogonDate
            OU          = $obj.OU
        }) }
        default   { $UnknownOSTable.Add([pscustomobject]@{
            Name        = $_.Name
            RawOS       = if ($_.OperatingSystem) { $_.OperatingSystem } else { '(vide)' }
            OSVersion   = if ($_.OperatingSystemVersion) { $_.OperatingSystemVersion } else { '' }
            Enabled     = $_.Enabled
            LastLogon   = $_.LastLogonDate
            OU          = $obj.OU
        }) }
    }
}

# Affichage timeline : s'il n'y a qu'un seul point on en ajoute un deuxieme
if ($barcreateobject.Count -eq 1) {
    $obj = [pscustomobject]@{ 'Nbr_users' = 0; 'Nbr_PC' = 0; 'Date' = (Get-Date).AddDays(+1) }
    $barcreateobject.Add($obj)
}

# Agregats propres
$GraphComputerOS = New-Object 'System.Collections.Generic.List[Object]'
$ComputersTable |
    Group-Object OS_Product |
    ForEach-Object { $GraphComputerOS.Add([pscustomobject]@{ Name = $_.Name; Count = $_.Count }) }

# Creation de OS_Full pour TOUS les ordinateurs
$ComputersTable | ForEach-Object {
    $full = ''
    if ($_.OS_Release) {
        # Cas normal : OS_Release rempli par Normalize-OperatingSystem
        # Couvre Windows (ex: "22H2"), macOS (ex: "14 Sonoma"), Linux (ex: "9" pour RHEL, "22.04" pour Ubuntu)
        $full = "$($_.OS_Product) $($_.OS_Release)"
    } else {
        # OS non reconnu ou sans version extractible
        $full = $_.OS_Product
    }
    $_ | Add-Member -NotePropertyName 'OS_Full' -NotePropertyValue $full.Trim() -Force
}

# Stats par OS_Full
$ClientOSStats  = $ClientList  | Group-Object OS_Full | ForEach-Object { [pscustomobject]@{ Name = $_.Name; Count = $_.Count } }
$ServerOSStats  = $ServerList  | Group-Object OS_Full | ForEach-Object { [pscustomobject]@{ Name = $_.Name; Count = $_.Count } }
$DiversOSStats  = $DiversList  | Group-Object OS_Full | ForEach-Object { [pscustomobject]@{ Name = $_.Name; Count = $_.Count } }
$UnknownOSStats = $UnknownOSTable | Group-Object RawOS | ForEach-Object { [pscustomobject]@{ Name = if ($_.Name) { $_.Name } else { '(vide)' }; Count = $_.Count } }

$ClientObsolete  = [int](@($ClientList | Where-Object { $_.Vendor -eq 'Microsoft' -and $_.OS_Product -match '^Windows (10|11)$' -and ($_.OS_Build -as [int]) -le 19042 }).Count)
$ClientSupported = $ClientList.Count - $ClientObsolete
$ServerObsolete  = [int](@($ServerList | Where-Object { $_.'Operating System' -match '2000|2003|2008|2012' }).Count)
$ServerSupported = $ServerList.Count - $ServerObsolete

$OSClass = @{}
$OSClass.Add("Total Computers", $totalcomputers)
$OSClass.Add("Clients",          $ClientList.Count)
$OSClass.Add("Servers",          $ServerList.Count)
$OSClass.Add("Divers",           $DiversList.Count)
$OSClass.Add("Domain Controllers", $DCCount)
$OSClass.Add("Unknown OS",       $UnknownOSTable.Count)
# B08 FIX : .Add() sur la List[Object] initialisee dans MAD-Helpers (evite l'ecrasement par un PSCustomObject)
$TOPComputersTable.Add([pscustomobject]$OSClass)

Write-Success "Ordinateurs collectes"
$_cTotal  = $totalcomputers
$_cClient = $ClientList.Count
$_cServer = $ServerList.Count
$_cDC = $DCCount
Write-Host " >> Total: $_cTotal | Clients: $_cClient | Serveurs: $_cServer | DC: $_cDC" -ForegroundColor DarkGray