Tests/Certificate-LifecycleMonitor.Tests.ps1

BeforeAll {
    $ModuleRoot = Split-Path -Parent $PSScriptRoot
    Import-Module "$ModuleRoot\Certificate-LifecycleMonitor.psd1" -Force
}

Describe 'Certificate-LifecycleMonitor Module' {

    Context 'Module Loading' {

        It 'Should import without errors' {
            $module = Get-Module Certificate-LifecycleMonitor
            $module | Should -Not -BeNullOrEmpty
        }

        It 'Should export exactly 5 public functions' {
            $exported = (Get-Module Certificate-LifecycleMonitor).ExportedFunctions.Keys
            $exported.Count | Should -Be 5
        }

        It 'Should export Invoke-CertificateAudit' {
            (Get-Module Certificate-LifecycleMonitor).ExportedFunctions.Keys |
                Should -Contain 'Invoke-CertificateAudit'
        }

        It 'Should export Get-ExpiringCertificates' {
            (Get-Module Certificate-LifecycleMonitor).ExportedFunctions.Keys |
                Should -Contain 'Get-ExpiringCertificates'
        }

        It 'Should export Get-IISCertificateReport' {
            (Get-Module Certificate-LifecycleMonitor).ExportedFunctions.Keys |
                Should -Contain 'Get-IISCertificateReport'
        }

        It 'Should export Get-CertificateStoreReport' {
            (Get-Module Certificate-LifecycleMonitor).ExportedFunctions.Keys |
                Should -Contain 'Get-CertificateStoreReport'
        }

        It 'Should export Get-WeakCertificateReport' {
            (Get-Module Certificate-LifecycleMonitor).ExportedFunctions.Keys |
                Should -Contain 'Get-WeakCertificateReport'
        }
    }

    Context 'Manifest Validation' {

        It 'Should have a valid manifest' {
            $manifestPath = Join-Path (Split-Path -Parent $PSScriptRoot) 'Certificate-LifecycleMonitor.psd1'
            { Test-ModuleManifest -Path $manifestPath -ErrorAction Stop } | Should -Not -Throw
        }

        It 'Should have the correct GUID' {
            $manifest = Test-ModuleManifest -Path (Join-Path (Split-Path -Parent $PSScriptRoot) 'Certificate-LifecycleMonitor.psd1')
            $manifest.Guid.ToString() | Should -Be 'f4a0b5c3-7d69-4e9f-c034-1b5e8f0d9a67'
        }

        It 'Should require PowerShell 5.1' {
            $manifest = Test-ModuleManifest -Path (Join-Path (Split-Path -Parent $PSScriptRoot) 'Certificate-LifecycleMonitor.psd1')
            $manifest.PowerShellVersion | Should -Be '5.1'
        }

        It 'Should have a description' {
            $manifest = Test-ModuleManifest -Path (Join-Path (Split-Path -Parent $PSScriptRoot) 'Certificate-LifecycleMonitor.psd1')
            $manifest.Description | Should -Not -BeNullOrEmpty
        }

        It 'Should list the correct author' {
            $manifest = Test-ModuleManifest -Path (Join-Path (Split-Path -Parent $PSScriptRoot) 'Certificate-LifecycleMonitor.psd1')
            $manifest.Author | Should -BeLike '*Larry Roberts*'
        }
    }

    Context 'Parameter Validation' {

        It 'Get-ExpiringCertificates should have -ComputerName parameter' {
            (Get-Command Get-ExpiringCertificates).Parameters.Keys |
                Should -Contain 'ComputerName'
        }

        It 'Get-ExpiringCertificates should have -DaysUntilExpiration parameter' {
            (Get-Command Get-ExpiringCertificates).Parameters.Keys |
                Should -Contain 'DaysUntilExpiration'
        }

        It 'Get-CertificateStoreReport -StoreName should validate against known stores' {
            $param = (Get-Command Get-CertificateStoreReport).Parameters['StoreName']
            $validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
            $validateSet | Should -Not -BeNullOrEmpty
            $validateSet.ValidValues | Should -Contain 'My'
            $validateSet.ValidValues | Should -Contain 'Root'
            $validateSet.ValidValues | Should -Contain 'CA'
            $validateSet.ValidValues | Should -Contain 'TrustedPeople'
        }

        It 'Get-CertificateStoreReport -StoreLocation should validate against known locations' {
            $param = (Get-Command Get-CertificateStoreReport).Parameters['StoreLocation']
            $validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
            $validateSet | Should -Not -BeNullOrEmpty
            $validateSet.ValidValues | Should -Contain 'LocalMachine'
            $validateSet.ValidValues | Should -Contain 'CurrentUser'
        }

        It 'Invoke-CertificateAudit should have -DaysWarning parameter defaulting to 30' {
            $param = (Get-Command Invoke-CertificateAudit).Parameters['DaysWarning']
            $param | Should -Not -BeNullOrEmpty
        }

        It 'Invoke-CertificateAudit should have -SendEmail switch parameter' {
            $param = (Get-Command Invoke-CertificateAudit).Parameters['SendEmail']
            $param.SwitchParameter | Should -Be $true
        }

        It 'Invoke-CertificateAudit should have -IncludeIIS switch parameter' {
            $param = (Get-Command Invoke-CertificateAudit).Parameters['IncludeIIS']
            $param.SwitchParameter | Should -Be $true
        }

        It 'Get-WeakCertificateReport should have -MinimumKeyLength parameter' {
            (Get-Command Get-WeakCertificateReport).Parameters.Keys |
                Should -Contain 'MinimumKeyLength'
        }
    }

    Context 'Get-ExpiringCertificates -Finding Categorization' {

        BeforeAll {
            # Mock Invoke-Command is not needed for localhost -we mock the cert
            # PSDrive results by mocking Get-ChildItem inside the scriptblock.
            # Instead, we call the function against a controlled set of objects.
            $now = Get-Date

            $mockCerts = @(
                # Expired 10 days ago
                [PSCustomObject]@{
                    Subject    = 'CN=expired.contoso.com'
                    Thumbprint = 'AAAA1111BBBB2222CCCC3333DDDD4444EEEE5555'
                    Issuer     = 'CN=Contoso CA'
                    NotBefore  = $now.AddYears(-2)
                    NotAfter   = $now.AddDays(-10)
                    HasPrivateKey = $true
                    PublicKey  = $null
                    SignatureAlgorithm = [PSCustomObject]@{ FriendlyName = 'sha256RSA' }
                }
                # Critical -expires in 3 days
                [PSCustomObject]@{
                    Subject    = 'CN=critical.contoso.com'
                    Thumbprint = 'BBBB2222CCCC3333DDDD4444EEEE5555FFFF6666'
                    Issuer     = 'CN=Contoso CA'
                    NotBefore  = $now.AddYears(-1)
                    NotAfter   = $now.AddDays(3)
                    HasPrivateKey = $true
                    PublicKey  = $null
                    SignatureAlgorithm = [PSCustomObject]@{ FriendlyName = 'sha256RSA' }
                }
                # Warning -expires in 15 days
                [PSCustomObject]@{
                    Subject    = 'CN=warning.contoso.com'
                    Thumbprint = 'CCCC3333DDDD4444EEEE5555FFFF6666AAAA7777'
                    Issuer     = 'CN=Contoso CA'
                    NotBefore  = $now.AddYears(-1)
                    NotAfter   = $now.AddDays(15)
                    HasPrivateKey = $true
                    PublicKey  = $null
                    SignatureAlgorithm = [PSCustomObject]@{ FriendlyName = 'sha256RSA' }
                }
                # OK -expires in 200 days
                [PSCustomObject]@{
                    Subject    = 'CN=healthy.contoso.com'
                    Thumbprint = 'DDDD4444EEEE5555FFFF6666AAAA7777BBBB8888'
                    Issuer     = 'CN=Contoso CA'
                    NotBefore  = $now.AddYears(-1)
                    NotAfter   = $now.AddDays(200)
                    HasPrivateKey = $true
                    PublicKey  = $null
                    SignatureAlgorithm = [PSCustomObject]@{ FriendlyName = 'sha256RSA' }
                }
            )

            Mock -ModuleName Certificate-LifecycleMonitor Invoke-Command {
                param($ComputerName, $ScriptBlock, $ArgumentList)
                $StoreName           = $ArgumentList[0]
                $StoreLocation       = $ArgumentList[1]
                $DaysUntilExpiration = $ArgumentList[2]
                $warningDate = (Get-Date).AddDays($DaysUntilExpiration)
                $nowInner    = Get-Date

                $mockCerts | ForEach-Object {
                    $daysRemaining = ($_.NotAfter - $nowInner).Days
                    $finding = if ($_.NotAfter -lt $nowInner)           { 'EXPIRED'  }
                               elseif ($daysRemaining -le 7)            { 'CRITICAL' }
                               elseif ($_.NotAfter -le $warningDate)    { 'WARNING'  }
                               else                                      { 'OK'       }
                    [PSCustomObject]@{
                        Subject       = $_.Subject
                        Thumbprint    = $_.Thumbprint
                        Issuer        = $_.Issuer
                        NotBefore     = $_.NotBefore
                        NotAfter      = $_.NotAfter
                        DaysRemaining = $daysRemaining
                        Store         = "$StoreLocation\$StoreName"
                        ComputerName  = $ComputerName
                        Finding       = $finding
                    }
                }
            }
        }

        It 'Should flag expired certificates as EXPIRED' {
            $results = Get-ExpiringCertificates -ComputerName 'MOCKSERVER' -DaysUntilExpiration 30
            $expired = $results | Where-Object Subject -eq 'CN=expired.contoso.com'
            $expired.Finding | Should -Be 'EXPIRED'
        }

        It 'Should flag certificates expiring within 7 days as CRITICAL' {
            $results = Get-ExpiringCertificates -ComputerName 'MOCKSERVER' -DaysUntilExpiration 30
            $critical = $results | Where-Object Subject -eq 'CN=critical.contoso.com'
            $critical.Finding | Should -Be 'CRITICAL'
        }

        It 'Should flag certificates expiring within threshold as WARNING' {
            $results = Get-ExpiringCertificates -ComputerName 'MOCKSERVER' -DaysUntilExpiration 30
            $warning = $results | Where-Object Subject -eq 'CN=warning.contoso.com'
            $warning.Finding | Should -Be 'WARNING'
        }

        It 'Should flag healthy certificates as OK' {
            $results = Get-ExpiringCertificates -ComputerName 'MOCKSERVER' -DaysUntilExpiration 30
            $ok = $results | Where-Object Subject -eq 'CN=healthy.contoso.com'
            $ok.Finding | Should -Be 'OK'
        }

        It 'Should return correct DaysRemaining for expired cert (negative)' {
            $results = Get-ExpiringCertificates -ComputerName 'MOCKSERVER' -DaysUntilExpiration 30
            $expired = $results | Where-Object Subject -eq 'CN=expired.contoso.com'
            $expired.DaysRemaining | Should -BeLessOrEqual -1
        }

        It 'Should return all four certificates' {
            $results = Get-ExpiringCertificates -ComputerName 'MOCKSERVER' -DaysUntilExpiration 30
            $results.Count | Should -Be 4
        }
    }

    Context 'Get-WeakCertificateReport -Weakness Detection' {

        BeforeAll {
            $now = Get-Date

            Mock -ModuleName Certificate-LifecycleMonitor Invoke-Command {
                # Return pre-built weak certificate findings
                @(
                    [PSCustomObject]@{
                        Subject            = 'CN=sha1-cert.contoso.com'
                        Thumbprint         = 'SHA1AAAA1111BBBB2222CCCC3333DDDD4444'
                        KeyLength          = 2048
                        SignatureAlgorithm = 'sha1RSA'
                        SelfSigned         = $false
                        Store              = 'LocalMachine\My'
                        ComputerName       = 'MOCKSERVER'
                        Finding            = 'SHA1_SIGNATURE'
                    }
                    [PSCustomObject]@{
                        Subject            = 'CN=weak-key.contoso.com'
                        Thumbprint         = 'WEAK1111AAAA2222BBBB3333CCCC4444'
                        KeyLength          = 1024
                        SignatureAlgorithm = 'sha256RSA'
                        SelfSigned         = $false
                        Store              = 'LocalMachine\My'
                        ComputerName       = 'MOCKSERVER'
                        Finding            = 'WEAK_KEY (1024-bit < 2048-bit)'
                    }
                    [PSCustomObject]@{
                        Subject            = 'CN=self-signed.contoso.com'
                        Thumbprint         = 'SELF1111AAAA2222BBBB3333CCCC4444'
                        KeyLength          = 2048
                        SignatureAlgorithm = 'sha256RSA'
                        SelfSigned         = $true
                        Store              = 'LocalMachine\My'
                        ComputerName       = 'MOCKSERVER'
                        Finding            = 'SELF_SIGNED_IN_MY'
                    }
                    [PSCustomObject]@{
                        Subject            = 'CN=Expired Root CA'
                        Thumbprint         = 'ROOT1111AAAA2222BBBB3333CCCC4444'
                        KeyLength          = 2048
                        SignatureAlgorithm = 'sha256RSA'
                        SelfSigned         = $true
                        Store              = 'LocalMachine\Root'
                        ComputerName       = 'MOCKSERVER'
                        Finding            = 'EXPIRED_ROOT_CA'
                    }
                )
            }
        }

        It 'Should flag SHA-1 signatures' {
            $results = Get-WeakCertificateReport -ComputerName 'MOCKSERVER'
            $sha1 = $results | Where-Object Subject -eq 'CN=sha1-cert.contoso.com'
            $sha1.Finding | Should -BeLike '*SHA1*'
        }

        It 'Should flag weak key lengths' {
            $results = Get-WeakCertificateReport -ComputerName 'MOCKSERVER'
            $weak = $results | Where-Object Subject -eq 'CN=weak-key.contoso.com'
            $weak.Finding | Should -BeLike '*WEAK_KEY*'
            $weak.KeyLength | Should -Be 1024
        }

        It 'Should flag self-signed certificates in My store' {
            $results = Get-WeakCertificateReport -ComputerName 'MOCKSERVER'
            $selfSigned = $results | Where-Object Subject -eq 'CN=self-signed.contoso.com'
            $selfSigned.Finding | Should -BeLike '*SELF_SIGNED*'
            $selfSigned.SelfSigned | Should -Be $true
        }

        It 'Should flag expired root CA certificates' {
            $results = Get-WeakCertificateReport -ComputerName 'MOCKSERVER'
            $expiredRoot = $results | Where-Object Subject -eq 'CN=Expired Root CA'
            $expiredRoot.Finding | Should -BeLike '*EXPIRED_ROOT*'
        }

        It 'Should return all four weak certificate findings' {
            $results = Get-WeakCertificateReport -ComputerName 'MOCKSERVER'
            $results.Count | Should -Be 4
        }
    }
}