Public/Install-KritHardenModules.ps1

function Get-KritHardenModuleStatus {
    <#
    .SYNOPSIS
        Read-only inventory of the OSS hardening modules Krit.Hardening orchestrates.
    .NOTES
        Author: Joshua Finley - Kritical Pty Ltd
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param()
    $names = @(
        'Harden-Windows-Security-Module',  # HotCakeX - Confirm-SystemCompliance / Protect-WindowsSecurity
        'HardeningKitty',                  # scipag - Invoke-HardeningKitty STIG/CIS audit + HailMary apply
        'AuditPolicyDsc',                  # DSC AuditPolicy resource
        'SecurityPolicyDsc',               # DSC SecurityPolicy (LSA / Kerberos / user-rights)
        'PSDscResources',                  # Modern DSC base resources
        'NetworkingDsc',                   # DSC firewall + IPSec
        'PSScriptAnalyzer'                 # PS-side static analysis (anti-pattern hardening)
    )
    $rows = foreach ($n in $names) {
        $have = Get-Module -ListAvailable -Name $n -ErrorAction SilentlyContinue |
                Sort-Object Version -Descending | Select-Object -First 1
        [pscustomobject]@{
            Module    = $n
            Installed = [bool]$have
            Version   = if ($have) { $have.Version } else { $null }
            Loaded    = [bool](Get-Module -Name $n -ErrorAction SilentlyContinue)
        }
    }
    [pscustomobject]@{ Modules = @($rows); Timestamp = (Get-Date).ToUniversalTime() }
}

function Install-KritHardenModules {
    <#
    .SYNOPSIS
        Idempotent installer for the OSS hardening giants that Krit.Hardening orchestrates.

    .DESCRIPTION
        Installs (CurrentUser scope) any missing module from the canonical hardening set:
          - HotCakeX/Harden-Windows-Security-Module
          - scipag/HardeningKitty
          - AuditPolicyDsc / SecurityPolicyDsc / PSDscResources / NetworkingDsc
          - PSScriptAnalyzer
        Honours -NoInstall (CI prebake). Honours -OnlyCore (HotCakeX + HardeningKitty only;
        skip DSC family for minimal install).

    .EXAMPLE
        Install-KritHardenModules # full set
        Install-KritHardenModules -OnlyCore # just HotCakeX + HardeningKitty
        Install-KritHardenModules -NoInstall # report-only

    .NOTES
        Author: Joshua Finley - Kritical Pty Ltd
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [switch] $OnlyCore,
        [switch] $NoInstall,
        [switch] $NoBanner,
        [switch] $Quiet
    )
    if (-not $NoBanner.IsPresent -and -not $Quiet.IsPresent) {
        Write-KritHardenBanner -Title 'Install-KritHardenModules' -Compact
    }

    $core = @('Harden-Windows-Security-Module','HardeningKitty')
    $extras = @('AuditPolicyDsc','SecurityPolicyDsc','PSDscResources','NetworkingDsc','PSScriptAnalyzer')
    $targets = if ($OnlyCore.IsPresent) { $core } else { $core + $extras }

    $rows = [System.Collections.Generic.List[pscustomobject]]::new()
    foreach ($n in $targets) {
        $have = Get-Module -ListAvailable -Name $n -ErrorAction SilentlyContinue |
                Sort-Object Version -Descending | Select-Object -First 1
        if (-not $have -and -not $NoInstall.IsPresent) {
            try {
                if (-not $Quiet.IsPresent) { Write-Host ("Installing $n (CurrentUser)") -ForegroundColor DarkCyan }
                Install-Module -Name $n -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop
                $have = Get-Module -ListAvailable -Name $n -ErrorAction SilentlyContinue |
                        Sort-Object Version -Descending | Select-Object -First 1
            } catch {
                $rows.Add([pscustomobject]@{ Module=$n; Status='INSTALL-FAILED'; Version=$null; Detail=$_.Exception.Message })
                continue
            }
        }
        if (-not $have) { $rows.Add([pscustomobject]@{ Module=$n; Status='MISSING'; Version=$null; Detail='not installed' }); continue }
        $rows.Add([pscustomobject]@{ Module=$n; Status='READY'; Version=$have.Version; Detail='installed (not auto-imported)' })
    }
    if (-not $Quiet.IsPresent) {
        $rows | Format-Table -AutoSize | Out-String | Write-Host
    }
    $bad = @($rows | Where-Object { $_.Status -in @('MISSING','INSTALL-FAILED') })
    [pscustomobject]@{
        Ok       = ($bad.Count -eq 0)
        Failures = $bad.Count
        Modules  = @($rows)
    }
}