Public/Invoke-LGPolicyCheck.ps1

function Invoke-LGPolicyCheck {
    <#
    .SYNOPSIS
        Checks installed software against the policy rules in lg-policy.json.
    .EXAMPLE
        Invoke-LGPolicyCheck -PolicyPath .\lg-policy.json
    .EXAMPLE
        $sw = Get-LGInstalledSoftware
        Invoke-LGPolicyCheck -PolicyPath .\lg-policy.json -SoftwareCache $sw
    #>

    [CmdletBinding()]
    param(
        [string]$PolicyPath   = '.\lg-policy.json',
        [PSCustomObject[]]$SoftwareCache = $null
    )

    $L   = Get-LGEffectiveStrings
    $cfg = Get-LGEffectiveConfig
    Write-LGHeader $L['policySection']

    if (-not (Test-Path $PolicyPath)) {
        Write-Warning "Policy file not found: $PolicyPath"
        return @()
    }
    try {
        $policy = Get-Content $PolicyPath -Raw | ConvertFrom-Json
    } catch {
        Write-Warning "Could not read policy: $($_.Exception.Message)"
        return @()
    }

    $swRows = if ($SoftwareCache) {
        $SoftwareCache
    } else {
        Get-LGSoftwareRegistryRows -WarnDays $cfg.WarnDaysBeforeExpiry |
            ForEach-Object { [PSCustomObject]@{ Name=$_.Name; Version=$_.Version; Publisher=$_.Publisher } }
    }

    $findings = [System.Collections.Generic.List[PSCustomObject]]::new()

    foreach ($sw in $swRows) {
        # Whitelist
        $whitelisted = $false
        if ($cfg.Whitelist -and $cfg.Whitelist.Count -gt 0) {
            foreach ($wl in $cfg.Whitelist) {
                if ($sw.Name -like "*$wl*") { $whitelisted = $true; break }
            }
        }
        if ($whitelisted) {
            $findings.Add([PSCustomObject]@{
                Module       = 'PolicyCheck'; RuleId = 'WL'; Category = 'Whitelist'
                Name         = $sw.Name; Version = $sw.Version; Publisher = $sw.Publisher
                PolicyStatus = 'ALLOWED'; Status = 'OK'; Detail = $L['whitelist']
                Alternative  = ''; Reference = ''; Severity = 'LOW'
            })
            continue
        }

        # Rule matching. Multiple policy rules can match the same row (for example
        # MIT license + missing attribution); choose the highest-risk outcome.
        $ruleMatches = [System.Collections.Generic.List[PSCustomObject]]::new()
        foreach ($rule in $policy.rules) {
            $matchFieldProp = $rule.PSObject.Properties['matchField']
            $matchField = if ($matchFieldProp -and $matchFieldProp.Value) { $matchFieldProp.Value } else { 'name' }
            
            $fieldValue = switch ($matchField) {
                'license' {
                    $prop = $sw.PSObject.Properties['License']
                    if ($prop) { $prop.Value } else { '' }
                }
                'name' {
                    $sw.Name
                }
                default {
                    $prop = $sw.PSObject.Properties[$matchField]
                    if ($prop) { $prop.Value } else { '' }
                }
            }

            if (-not $fieldValue) { continue }

            $match = switch ($rule.matchType) {
                'contains'   { $fieldValue -like "*$($rule.pattern)*" }
                'startsWith' { $fieldValue -like "$($rule.pattern)*"  }
                'exact'      { $fieldValue -eq $rule.pattern           }
                'regex'      { $fieldValue -match $rule.pattern        }
                default      { $false }
            }
            if ($match) {
                $priority = switch ($rule.status) { 'PROHIBITED' { 3 } 'REQUIRES_LICENSE' { 2 } 'ALLOWED' { 1 } default { 0 } }
                $ruleMatches.Add([PSCustomObject]@{ Rule = $rule; Priority = $priority })
            }
        }

        if ($ruleMatches.Count -gt 0) {
            $rule = ($ruleMatches | Sort-Object Priority -Descending | Select-Object -First 1).Rule
            $statusMap = @{ 'PROHIBITED'='EXPIRED'; 'REQUIRES_LICENSE'='WARN'; 'ALLOWED'='OK' }
            $sevProp   = $rule.PSObject.Properties['severity']
            $severity  = if ($sevProp -and $sevProp.Value) { $sevProp.Value } else {
                switch ($rule.status) { 'PROHIBITED' { 'HIGH' } 'REQUIRES_LICENSE' { 'MEDIUM' } default { 'LOW' } }
            }
            $altProp   = $rule.PSObject.Properties['alternative']
            $refProp   = $rule.PSObject.Properties['referenceUrl']
            $swDetailProp = $sw.PSObject.Properties['Detail']
            $swDetail = if ($swDetailProp) { $swDetailProp.Value } else { '' }
            $detailVal = if ($swDetail) { "$($rule.reason) | $swDetail" } else { $rule.reason }

            $findings.Add([PSCustomObject]@{
                Module       = 'PolicyCheck'; RuleId = $rule.id; Category = $rule.category
                Name         = $sw.Name; Version = $sw.Version; Publisher = $sw.Publisher
                PolicyStatus = $rule.status; Status = $statusMap[$rule.status]; Detail = $detailVal
                Alternative  = if ($altProp) { $altProp.Value } else { '' }
                Reference    = if ($refProp) { $refProp.Value } else { '' }
                Severity     = $severity
            })
        } else {
            $findings.Add([PSCustomObject]@{
                Module       = 'PolicyCheck'; RuleId = 'N/A'; Category = 'N/A'
                Name         = $sw.Name; Version = $sw.Version; Publisher = $sw.Publisher
                PolicyStatus = 'ALLOWED'; Status = 'OK'; Detail = $L['noRule']
                Alternative  = ''; Reference = ''; Severity = 'LOW'
            })
        }
    }

    $proh = @($findings | Where-Object { $_.PolicyStatus -eq 'PROHIBITED' })
    $lic  = @($findings | Where-Object { $_.PolicyStatus -eq 'REQUIRES_LICENSE' })
    $ok   = @($findings | Where-Object { $_.PolicyStatus -eq 'ALLOWED' })

    foreach ($f in $findings) {
        $suffix = if ($f.RuleId -notin @('N/A','WL')) { " [$($f.RuleId)]" } else { '' }
        Write-LGStatus ($f.Name + $suffix) $f.Detail $f.Status
        if ($f.Status -ne 'OK') {
            if ($f.Detail)      { Write-Host " Reason: $($f.Detail)"      -ForegroundColor DarkGray }
            if ($f.Alternative) { Write-Host " Suggestion: $($f.Alternative)" -ForegroundColor DarkCyan }
        }
    }

    Write-Host ("`n $($findings.Count) matched -- $($proh.Count) $($L['prohibited']) $($lic.Count) $($L['requiresLicense']) $($ok.Count) $($L['allowed'])") -ForegroundColor Cyan
    $findings
}