modules/Licensing/Licensing.psm1

<#
    Licensing.psm1 - licensing indicators for validation (read-only).
    Produces: Licensing. Makes NO legal licensing determinations - flags for validation.
#>


function Get-DiscoveryModuleMetadata {
    [pscustomobject]@{
        ModuleName='Licensing'; DisplayName='Licensing'; Category='Licensing'; Version='1.0.0'
        DefaultInFast=$true; DefaultInDeep=$true; RequiresAdmin=$false; RequiresDomainContext=$false
        RequiresRole=$null; EstimatedImpact='Low'; CanRunAsSystem=$true
        ProducesDatasets=@('Licensing')
        ProducesRisks=$true; ProducesFollowUpQuestions=$true; SupportsDeepMode=$true; SupportsComplianceLens=$false
    }
}

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

function Invoke-DiscoveryCollection {
    param([object]$Context)
    $rows=[System.Collections.Generic.List[object]]::new()

    # ---- Windows activation / edition (CIM; avoids slow slmgr) ----
    try {
        foreach ($p in (Invoke-CimSafe -ClassName 'SoftwareLicensingProduct' -Filter 'PartialProductKey IS NOT NULL')) {
            $status = switch ([int]$p.LicenseStatus) { 0 {'Unlicensed'} 1 {'Licensed'} 2 {'OOB Grace'} 3 {'OOT Grace'} 4 {'Non-Genuine Grace'} 5 {'Notification'} 6 {'Extended Grace'} default {"Status $($p.LicenseStatus)"} }
            $rows.Add([pscustomobject]@{ Item=$p.Name; Type='OS/Product'; Detail=$status; Evidence='SoftwareLicensingProduct'; ValidationNeeded='Confirm entitlement transfers on server replacement (OEM licenses often do not).' })
        }
    } catch { Add-Limitation -Context $Context -Module 'Licensing' -Message 'Windows activation query failed.' -Reason $_.Exception.Message | Out-Null }
    try {
        $svc = Invoke-CimSafe -ClassName 'SoftwareLicensingService' | Select-Object -First 1
        if ($svc) { $rows.Add([pscustomobject]@{ Item='Windows Activation'; Type='OS'; Detail=("KMS/MAK channel indicators; ProductKeyChannel=" + $svc.OA3xOriginalProductKeyDescription); Evidence='SoftwareLicensingService'; ValidationNeeded='Confirm OEM vs Volume vs Retail.' }) }
    } catch { }

    # ---- RDS licensing (from registry) ----
    try {
        $mode = Get-RegistryValueSafe -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\RCM\Licensing Core' -Name 'LicensingMode'
        # Only a chosen CAL mode (2 Per Device, 4 Per User) is an RDS licensing dependency. 1 (Remote
        # Administration) is the default on every server and 5 means not configured.
        if ($mode -in 2, 4) { $rows.Add([pscustomobject]@{ Item='RDS Licensing'; Type='RDS'; Detail=("LicensingMode=$mode"); Evidence='Registry'; ValidationNeeded='Confirm RDS CAL type and license server.' }) }
    } catch { }

    # ---- SQL edition (from SqlInstances dataset) ----
    try {
        if ($Context.DataSets.Contains('SqlInstances')) {
            foreach ($s in @($Context.DataSets['SqlInstances'].Rows)) {
                $rows.Add([pscustomobject]@{ Item=("SQL: " + $s.InstanceName); Type='SQL'; Detail=("Edition=" + $s.Edition + "; Version=" + $s.Version); Evidence='SqlInstances'; ValidationNeeded='Confirm SQL licensing (core vs CAL) and edition entitlement.' })
            }
        }
    } catch { }

    # ---- License managers (from Services / InstalledApplications) ----
    try {
        $lmPattern = '(?i)flex(net|lm)|lmgrd|sentinel (ldk|hasp|license)|codemeter|reprise|\bRLM\b|LMTOOLS|license server'
        $svcRows = @(if ($Context.DataSets.Contains('Services')) { @($Context.DataSets['Services'].Rows) } else { @() })
        $appRows = @(if ($Context.DataSets.Contains('InstalledApplications')) { @($Context.DataSets['InstalledApplications'].Rows) } else { @() })
        foreach ($s in @($svcRows | Where-Object { $_.DisplayName -match $lmPattern -or $_.Name -match $lmPattern })) { $rows.Add([pscustomobject]@{ Item=$s.DisplayName; Type='LicenseManager'; Detail=('Service ' + $s.Name); Evidence='Services'; ValidationNeeded='Which app depends on this, and does licensing rebind to a new host?' }) }
        foreach ($a in @($appRows | Where-Object { $_.DisplayName -match $lmPattern })) { $rows.Add([pscustomobject]@{ Item=$a.DisplayName; Type='LicenseManager'; Detail='Installed application'; Evidence='InstalledApplications'; ValidationNeeded='Vendor license validation required.' }) }
        # Office/Visio/Project
        foreach ($a in @($appRows | Where-Object { $_.DisplayName -match '(?i)Microsoft (Office|Visio|Project)\b' })) { $rows.Add([pscustomobject]@{ Item=$a.DisplayName; Type='App'; Detail=$a.DisplayVersion; Evidence='InstalledApplications'; ValidationNeeded='Confirm licensing model (volume/365/OEM).' }) }
    } catch { }

    return ,@{ Licensing=@($rows) }
}

function ConvertTo-DiscoveryDatasets {
    param([object]$Context, $RawData)
    $rows = @(if ($RawData -and $RawData.Licensing) { @($RawData.Licensing) } else { @() })
    Add-DataSet -Context $Context -Name 'Licensing' -Description 'Licensing indicators for validation (no legal determinations).' -Rows $rows -Visibility 'Both' -SourceModule 'Licensing' | Out-Null
}

function Get-DiscoveryFollowUpQuestions {
    param([object]$Context)
    try { if ($Context.DataSets.Contains('Licensing') -and @($Context.DataSets['Licensing'].Rows | Where-Object { $_.Type -eq 'LicenseManager' }).Count -gt 0) { Add-FollowUpQuestion -Context $Context -Category 'Licensing' -Module 'Licensing' -Audience 'Both' -Question 'A license manager/dongle was detected. Which application depends on it, and is vendor support available to reactivate licensing on a new server?' | Out-Null } } catch { }
}

Export-ModuleMember -Function 'Get-DiscoveryModuleMetadata','Test-DiscoveryPrerequisites','Invoke-DiscoveryCollection','ConvertTo-DiscoveryDatasets','Get-DiscoveryFollowUpQuestions'