modules/DecommissionReadiness/DecommissionReadiness.psm1

<#
    DecommissionReadiness.psm1
    Synthesis module. Assesses "Can this server be shut off?" using all collected
    datasets. Produces the DecommissionReadiness and ScreamTestPlan datasets.
 
    IMPORTANT: Never states an absolute "safe to shut down" recommendation. Output
    is classified apparent dependency plus explicit validation needs.
#>


function Get-DiscoveryModuleMetadata {
    [pscustomobject]@{
        ModuleName                = 'DecommissionReadiness'
        DisplayName               = 'Decommission Readiness Analysis'
        Category                  = 'Synthesis'
        Version                   = '1.0.0'
        DefaultInFast             = $true
        DefaultInDeep             = $true
        RequiresAdmin             = $false
        RequiresDomainContext     = $false
        RequiresRole              = $null
        EstimatedImpact           = 'Minimal'
        CanRunAsSystem            = $true
        ProducesDatasets          = @('DecommissionReadiness','ScreamTestPlan')
        ProducesRisks             = $false
        # $false, not a placeholder: this module's per-factor ValidationNeeded text and the
        # Scream Test Plan's WhoMustValidate/RecommendedPowerOffTestWindow fields live only in
        # the DecommissionReadiness/ScreamTestPlan dataset rows - unlike RiskEngine's
        # SuggestedValidationQuestion, nothing here is ever promoted into
        # Context.FollowUpQuestions (ClientInterviewPack.psm1 only reads Finding objects), so
        # no question this module "asks" ever actually reaches the client interview pack today.
        ProducesFollowUpQuestions = $false
        SupportsDeepMode          = $true
        SupportsComplianceLens    = $false
        IsSynthesis               = $true
    }
}

function Test-DiscoveryPrerequisites {
    param([object]$Context)
    [pscustomobject]@{ ModuleName='DecommissionReadiness'; CanRun=$true; Status='Ready'; Reason=''; Limitations=@() }
}

function New-ReadinessFactor {
    param([string]$Factor, [string]$Status, [string]$Confidence, [string]$Evidence, [string]$WhyItMatters, [string]$ValidationNeeded, [bool]$IsHighWeight = $false)
    [pscustomobject]@{
        Factor=$Factor; Status=$Status; Confidence=$Confidence; Evidence=$Evidence
        WhyItMatters=$WhyItMatters; ValidationNeeded=$ValidationNeeded; IsHighWeight=$IsHighWeight
    }
}

function Build-DecommissionReadiness {
    param([object]$Context)

    $ds = { param($n) ($Context.DataSets.Contains($n) -and @($Context.DataSets[$n].Rows).Count -gt 0) }
    $rolePresent = { param($p) (Test-RoleFeaturePresent -Context $Context -Pattern $p) }
    $fieldTrue = { param($n,$f) (Test-DatasetFieldTrue -Context $Context -Dataset $n -Field $f) }

    $factors = [System.Collections.Generic.List[object]]::new()

    $isDc = (& $rolePresent '(?i)AD-Domain|ADDS') -or (& $fieldTrue 'DomainContext' 'IsDomainController')
    $factors.Add((New-ReadinessFactor -Factor 'Active Directory Domain Controller' -Status ($(if($isDc){'Present'}else{'NotDetected'})) -Confidence ($(if($isDc){'Confirmed'}else{'NotDetected'})) -Evidence ($(if($isDc){'AD DS role / DC context detected.'}else{'No DC role detected.'})) -WhyItMatters 'A DC cannot simply be powered off; roles must be transferred/demoted.' -ValidationNeeded ($(if($isDc){'Confirm redundancy and demotion plan.'}else{'Confirm this is not a hidden/secondary directory role.'})) -IsHighWeight $isDc))

    $dns = (& $rolePresent '(?i)^DNS$|DNS-Server')
    $factors.Add((New-ReadinessFactor -Factor 'DNS Server' -Status ($(if($dns){'Present'}else{'NotDetected'})) -Confidence ($(if($dns){'Confirmed'}else{'NotDetected'})) -Evidence ($(if($dns){'DNS role detected.'}else{'No DNS role detected.'})) -WhyItMatters 'DNS is an infrastructure dependency for auth and app resolution.' -ValidationNeeded 'Confirm client/forwarder cutover plan.' -IsHighWeight $dns))

    $dhcp = (& $rolePresent '(?i)^DHCP$|DHCP-Server')
    $factors.Add((New-ReadinessFactor -Factor 'DHCP Server' -Status ($(if($dhcp){'Present'}else{'NotDetected'})) -Confidence ($(if($dhcp){'Confirmed'}else{'NotDetected'})) -Evidence ($(if($dhcp){'DHCP role detected.'}else{'No DHCP role detected.'})) -WhyItMatters 'Scopes/reservations/options must be migrated and re-authorized.' -ValidationNeeded 'Confirm DHCP failover / migration plan.' -IsHighWeight $dhcp))

    $shares = (& $fieldTrue 'SmbShares' 'IsUserShare')
    $factors.Add((New-ReadinessFactor -Factor 'File shares' -Status ($(if($shares){'Present'}else{'NotDetected'})) -Confidence ($(if($shares){'Confirmed'}else{'NotDetected'})) -Evidence ($(if($shares){'Non-administrative SMB shares detected.'}else{'No user shares detected.'})) -WhyItMatters 'Users/apps may depend on server name and UNC paths.' -ValidationNeeded 'Confirm who uses shares and how they are referenced.' -IsHighWeight $shares))

    $print = (& $fieldTrue 'Printers' 'Shared')
    $factors.Add((New-ReadinessFactor -Factor 'Print queues' -Status ($(if($print){'Present'}else{'NotDetected'})) -Confidence ($(if($print){'Confirmed'}else{'NotDetected'})) -Evidence ($(if($print){'Shared printers detected.'}else{'No shared printers detected.'})) -WhyItMatters 'Print server retirement affects users and label workflows.' -ValidationNeeded 'Confirm printer mapping and driver availability.' -IsHighWeight $print))

    $sql = (& $ds 'SqlInstances') -or (& $ds 'OtherDatabaseEngines')
    $factors.Add((New-ReadinessFactor -Factor 'SQL / database' -Status ($(if($sql){'Present'}else{'NotDetected'})) -Confidence ($(if($sql){'Confirmed'}else{'NotDetected'})) -Evidence ($(if($sql){'Database engine detected.'}else{'No database engine detected.'})) -WhyItMatters 'Databases usually back critical applications.' -ValidationNeeded 'Confirm which apps depend on the database(s).' -IsHighWeight $sql))

    $iis = (& $ds 'IisSites')
    $factors.Add((New-ReadinessFactor -Factor 'IIS / web apps' -Status ($(if($iis){'Present'}else{'NotDetected'})) -Confidence ($(if($iis){'Confirmed'}else{'NotDetected'})) -Evidence ($(if($iis){'IIS sites detected.'}else{'No IIS sites detected.'})) -WhyItMatters 'Web apps carry binding/cert/host-header dependencies.' -ValidationNeeded 'Confirm web app owners and dependencies.' -IsHighWeight $iis))

    $hv = (& $ds 'HyperVVMs')
    $factors.Add((New-ReadinessFactor -Factor 'Hyper-V VMs' -Status ($(if($hv){'Present'}else{'NotDetected'})) -Confidence ($(if($hv){'Confirmed'}else{'NotDetected'})) -Evidence ($(if($hv){'Guest VMs detected.'}else{'No guest VMs detected.'})) -WhyItMatters 'Host retirement affects all guest workloads.' -ValidationNeeded 'Confirm VM inventory, storage, and backup coverage.' -IsHighWeight $hv))

    $rds = (& $ds 'RdsDiscovery')
    $factors.Add((New-ReadinessFactor -Factor 'RDS' -Status ($(if($rds){'Present'}else{'NotDetected'})) -Confidence ($(if($rds){'Confirmed'}else{'NotDetected'})) -Evidence ($(if($rds){'RDS role detected.'}else{'No RDS role detected.'})) -WhyItMatters 'RDS has licensing, cert, and profile dependencies.' -ValidationNeeded 'Confirm licensing and user session impact.' -IsHighWeight $rds))

    $nps = (& $ds 'NpsRadiusDiscovery')
    $factors.Add((New-ReadinessFactor -Factor 'NPS / RADIUS / MFA' -Status ($(if($nps){'Present'}else{'NotDetected'})) -Confidence ($(if($nps){'Confirmed'}else{'NotDetected'})) -Evidence ($(if($nps){'NPS/RADIUS detected.'}else{'No NPS/RADIUS detected.'})) -WhyItMatters 'Authenticates network/VPN/Wi-Fi access; may integrate MFA.' -ValidationNeeded 'Confirm which devices authenticate here.' -IsHighWeight $nps))

    $ca = (& $rolePresent '(?i)AD-Certificate|ADCS')
    $factors.Add((New-ReadinessFactor -Factor 'CA / PKI' -Status ($(if($ca){'Present'}else{'NotDetected'})) -Confidence ($(if($ca){'Confirmed'}else{'NotDetected'})) -Evidence ($(if($ca){'AD CS role detected.'}else{'No CA role detected.'})) -WhyItMatters 'A CA issues/validates certificates domain-wide.' -ValidationNeeded 'Confirm CA usage and migration/retirement plan.' -IsHighWeight $ca))

    $backup = (& $ds 'BackupDiscovery') -or (& $ds 'VendorAgents')
    $factors.Add((New-ReadinessFactor -Factor 'Backup / monitoring agents' -Status ($(if($backup){'Present'}else{'NotDetected'})) -Confidence ($(if($backup){'Likely'}else{'NotDetected'})) -Evidence ($(if($backup){'Backup/monitoring agent indicators detected.'}else{'No backup/monitoring agents detected.'})) -WhyItMatters 'Agents must be transitioned/cleaned up and recovery validated.' -ValidationNeeded 'Confirm backup currency and agent handling.' -IsHighWeight $false))

    $ports = (& $ds 'ListeningPorts')
    $factors.Add((New-ReadinessFactor -Factor 'Active listening ports' -Status ($(if($ports){'Present'}else{'NotDetected'})) -Confidence ($(if($ports){'Confirmed'}else{'NotDetected'})) -Evidence ($(if($ports){("{0} listening port record(s)." -f @($Context.DataSets['ListeningPorts'].Rows).Count)}else{'No listening ports collected.'})) -WhyItMatters 'Listening ports imply other systems may connect in.' -ValidationNeeded 'Confirm which clients connect to this server.' -IsHighWeight $false))

    $tasks = (& $ds 'ScheduledTasks')
    $factors.Add((New-ReadinessFactor -Factor 'Scheduled tasks' -Status ($(if($tasks){'Present'}else{'NotDetected'})) -Confidence ($(if($tasks){'Confirmed'}else{'NotDetected'})) -Evidence ($(if($tasks){'Scheduled tasks detected.'}else{'No scheduled tasks detected.'})) -WhyItMatters 'Automations may need recreation on a replacement server.' -ValidationNeeded 'Confirm which tasks are still required.' -IsHighWeight $false))

    $nonMs = (& $fieldTrue 'Services' 'IsNonMicrosoftAutoStart')
    $factors.Add((New-ReadinessFactor -Factor 'Non-Microsoft services' -Status ($(if($nonMs){'Present'}else{'NotDetected'})) -Confidence ($(if($nonMs){'Confirmed'}else{'NotDetected'})) -Evidence ($(if($nonMs){'Non-Microsoft auto-start services detected.'}else{'No non-Microsoft auto-start services detected.'})) -WhyItMatters 'Indicates installed apps/agents in use.' -ValidationNeeded 'Confirm ownership of non-Microsoft services.' -IsHighWeight $false))

    $config = (& $ds 'ConfigDependencyHints')
    $factors.Add((New-ReadinessFactor -Factor 'Config file dependencies' -Status ($(if($config){'Present'}else{'Unknown'})) -Confidence ($(if($config){'Likely'}else{'Unknown'})) -Evidence ($(if($config){'Hardcoded dependencies found in config files.'}else{'Config scan not run or found nothing.'})) -WhyItMatters 'Hardcoded dependencies are common hidden blockers.' -ValidationNeeded 'Run/enable config dependency scan and validate hits.' -IsHighWeight $false))

    $lic = (& $ds 'Licensing')
    $factors.Add((New-ReadinessFactor -Factor 'License services / apps' -Status ($(if($lic){'Present'}else{'Unknown'})) -Confidence ($(if($lic){'Likely'}else{'Unknown'})) -Evidence ($(if($lic){'Licensing indicators detected.'}else{'No licensing indicators collected.'})) -WhyItMatters 'License managers/dongles may bind to this host.' -ValidationNeeded 'Confirm license reactivation requirements.' -IsHighWeight $false))

    $unknowns = (@($Context.Unknowns).Count -gt 0)
    $factors.Add((New-ReadinessFactor -Factor 'Unknowns that matter' -Status ($(if($unknowns){'Present'}else{'NotDetected'})) -Confidence 'Unknown' -Evidence ($(if($unknowns){("{0} unknown(s) recorded." -f @($Context.Unknowns).Count)}else{'No unknowns recorded.'})) -WhyItMatters 'Unresolved unknowns increase decommission risk.' -ValidationNeeded 'Resolve unknowns before any power-off decision.' -IsHighWeight $false))

    Add-DataSet -Context $Context -Name 'DecommissionReadiness' -Description 'Per-factor apparent-dependency assessment for decommission readiness.' -Rows @($factors) -Visibility 'Both' -SourceModule 'DecommissionReadiness' | Out-Null

    # Overall classification (indicator only, never absolute).
    $highWeightPresent = @($factors | Where-Object { $_.IsHighWeight -and $_.Status -eq 'Present' }).Count
    $anyPresent = @($factors | Where-Object { $_.Status -eq 'Present' }).Count
    $classification = if ($highWeightPresent -gt 0) { 'High apparent dependency' }
                      elseif ($anyPresent -ge 3) { 'Medium apparent dependency' }
                      elseif ($anyPresent -gt 0) { 'Low apparent dependency' }
                      else { 'Manual validation required' }
    $Context.Paths['_DecommissionClassification'] = $classification  # stash for report
    return $classification
}

function Build-ScreamTestPlan {
    param([object]$Context)
    $functions = @(Get-LikelyServerFunctions -Context $Context)
    if ($functions.Count -eq 0) { $functions = @('Undetermined role') }
    $rows = foreach ($fn in $functions) {
        [pscustomobject]@{
            System                        = $Context.ComputerName
            DetectedFunction              = $fn
            ReadinessConcern              = 'Dependent clients/applications may rely on this function without documentation.'
            RecommendedPowerOffTestWindow = '<TBD - agree an approved low-impact maintenance window with the client>'
            RollbackRequirement           = 'Validated, recent backup or documented ability to power the server back on quickly.'
            WhoMustValidate               = '<Client business owner + application/vendor contact for this function>'
            WhatToMonitor                 = 'User reports, application/service availability, authentication, dependent connections, and error logs.'
            WhatWouldConstituteAScream    = 'Any user or system reporting loss of access, failed logins, broken app function, or missing data tied to this server.'
            MinimumEvidenceBeforeRetirement = 'No screams during the agreed observation window and confirmed backup/rollback capability.'
            Confidence                    = 'Possible'
        }
    }
    Add-DataSet -Context $Context -Name 'ScreamTestPlan' -Description 'Structured power-off (scream) test plan per detected function. Uses placeholders, not real windows.' -Rows @($rows) -Visibility 'Both' -SourceModule 'DecommissionReadiness' | Out-Null
}

function Invoke-DiscoverySynthesis {
    param([object]$Context)
    Write-SectionStatus -Title 'Decommission Readiness' -Status 'Analyzing' -Context $Context
    try { Build-DecommissionReadiness -Context $Context | Out-Null } catch { Write-Log -Level WARN -Message 'Decommission readiness build failed.' -Module 'DecommissionReadiness' -Exception $_ -Context $Context }
    try { Build-ScreamTestPlan -Context $Context } catch { Write-Log -Level WARN -Message 'Scream test plan build failed.' -Module 'DecommissionReadiness' -Exception $_ -Context $Context }
}

Export-ModuleMember -Function 'Get-DiscoveryModuleMetadata','Test-DiscoveryPrerequisites','Invoke-DiscoverySynthesis','Build-DecommissionReadiness','Build-ScreamTestPlan'