Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.3.3] - 2026-08-31

### Fixed
- **Domain Admin prerequisite false-fails under the PowerShell 7 Windows PowerShell compatibility shim (issue #47)**: when `Test-TierModelPrerequisites` ran under PowerShell 7 with an RSAT ActiveDirectory module that is not Core-native (for example on a Windows Server 2016 host, or any host whose AD module edition forces the fallback), PowerShell loaded the module through the Windows PowerShell compatibility shim (WinPSCompatSession) and returned **deserialized** objects — SIDs came back as strings, so the `Get-ADGroupMember | Where-Object { $_.SID -eq $currentUser.User }` membership check compared a deserialized SID against a live `SecurityIdentifier` and never matched. The prerequisite reported *"Domain Admin membership required for deployment operations"* even for a genuine Domain Admin. The ActiveDirectory module is now imported with `-SkipEditionCheck` so PowerShell 7 loads it in-process (native objects, no deserialization), and a fail-fast guard detects the compatibility-shim condition (domain SID returned as a string) and stops with clear remediation — preventing a silent broken deployment in which SID resolution would have written empty principals into URA and GPO restricted-groups policy. Lab-validated: full `-FullDeployment -IncludeMsa -IncludeGmsa -IncludeDmsa -IncludeWinLaps -EnableAuditing` deploy (707 actions, 0 errors, all SIDs resolving) followed by a full audit (418 checks, COMPLIANT, 0 drift).

## [1.3.2] - 2026-08-24

### Added
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,18 @@ To get started with TierModel, please refer to our comprehensive documentation:

## 🧪 Testing & Quality Assurance

**Current Test Status: ✅ 1,652 passing / 0 failures (100%)** *(Last run: 2026-08-24)*
**Current Test Status: ✅ 1,653 passing / 0 failures (100%)** *(Last run: 2026-08-31)*

| Test Suite | Test Files | Test Cases | Status | Coverage |
|------------|-----------|------------|--------|----------|
| **Unit Tests** | 22 files | 1,338 tests | ✅ All pass | **88.93%** |
| **Unit Tests** | 22 files | 1,339 tests | ✅ All pass | **88.93%** |
| **Integration Tests** | 7 files | 314 tests | ✅ All pass | **88.93%** |
| **Manual Integration Tests** | 1 file | 335 tests | ✅ 100% Pass | **N/A** |
| **Total** | **29 files** | **1,987 tests** | ✅ **100% passing** | **88.93%** |
| **Total** | **29 files** | **1,988 tests** | ✅ **100% passing** | **88.93%** |

### Test Coverage Highlights
- ✅ **71/71** production files have comprehensive test coverage
- ✅ **1,652 / 1,652** automated test cases passing — 0 failures (Pester 5.9.0, 2026-08-24)
- ✅ **1,653 / 1,653** automated test cases passing — 0 failures (Pester 5.9.0, 2026-08-31)
- ✅ **88.93%** overall Pester-measured command coverage (all files above 80% CI gate)
- ✅ **New canonical-ACL resilience (#41):** OU deploy uses a phased verify-and-remediate loop — after each disable-inheritance write, `New-TierModelOu` reads back the DACL and auto-sorts if non-canonical (via `Repair-TierModelCanonicalAcl`); lab-validated at 7/7 OUs corrected per deploy under an inherited-Deny condition. New public cmdlet `Repair-TierModelCanonicalAcl` (**95.40%** coverage) available for standalone use. `Audit-TierModel.ps1` now reports Case 1 (non-canonical domain root, deployment blocker) and Case 2 (non-canonical Tier OU, pre-fix artifact) as structured drift findings.
- ✅ **New in v1.3.0:** `-EnableAuditing` domain audit rule (SACL) support — 4 new cmdlets (`Get-TierModelAuditRule` **100%**, `Get-TierModelAuditRuleFd` **97.73%**, `Test-TierModelAuditRule` **98.41%**, `New-TierModelAuditRule` **84.14%**) covered by `Unit.AuditRuleOperations.Tests.ps1` (47 unit tests) plus `-EnableAuditing` integration tests in Audit and Deploy orchestrators; `Audit-TierModel.ps1` **77.16%**, `Deploy-TierModel.ps1` **81.53%**
Expand Down
232 changes: 0 additions & 232 deletions cspell.json

This file was deleted.

8 changes: 5 additions & 3 deletions docs/test-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@

> **How coverage is measured:** Pester v5's built-in `CodeCoverage` feature instruments each production file and tracks which lines are executed during the full test suite run (`Invoke-AllTests.ps1`). To re-run: `cd TierModel; $c = New-PesterConfiguration; $c.Run.Path = './tests'; $c.CodeCoverage.Enabled = $true; $c.CodeCoverage.Path = @('./modules/TierModel/public/*.ps1','./modules/TierModel/TierModel.psm1','./Audit-TierModel.ps1','./Deploy-TierModel.ps1'); Invoke-Pester -Configuration $c`

**Last measured:** 2026-08-24 (1,652 total tests: **1,652 passing / 0 failures — 100%** — Pester 5.9.0 pinned) | **Overall: 88.93%** | **Target: 95%** | **CI gate: 80%**
**Last measured:** 2026-08-31 (1,653 total tests: **1,653 passing / 0 failures — 100%** — Pester 5.9.0 pinned) | **Overall: 88.93%** | **Target: 95%** | **CI gate: 80%**

> ✅ **v1.3.3 measured (2026-08-31 — 100% pass rate):** Domain Admin prerequisite false-fail under the PowerShell 7 Windows PowerShell compatibility shim (issue #47) — `Test-TierModelPrerequisites.ps1` now imports the ActiveDirectory module with `-SkipEditionCheck` (native in-process load, no deserialized objects) and adds a shim-detection guard that fails fast when AD returns deserialized (string) SIDs, preventing a silent broken URA/GPO deployment. `Test-TierModelPrerequisites.ps1` **85.12% → 85.40%** (421/493; +12 analyzed commands for the guard, all covered). New compat-shim unit test in `Unit.Prerequisites.Tests.ps1` (+1 unit test; automated suite 1,652 → 1,653). Module version **1.3.2 → 1.3.3**. Overall aggregate unchanged at ~88.93% (12 new covered commands out of ~15k); all module-scope files remain above the 80% CI gate.

> ✅ **v1.3.2 measured (2026-08-24 — 100% pass rate):** Human-readable deployment duration output (issue #34) — new `Format-TierModelDuration.ps1` **100%** (11/11 commands) provides the four-tier `<1ms` / `Xms` / `Xs` / `Xm Ys` console format now used at the nine `Deploy-TierModel.ps1` `Duration:` sites (`Write-TierModelLog` lines stay raw ms). New test file `Unit.FormatDuration.Tests.ps1` (**23** unit tests, incl. banker's-rounding regression guards at 90000 / 119999 / 120000 ms), wiring-proof assertions added to `Integration.Deploy.Tests.ps1` (mock `2254 → "Duration: 2s"`, all-converged `→ "<1ms"`, aggregate `140000 → "2m 20s"`), and a manifest verb-allowlist + export assertion in `Unit.ModuleManifest.Tests.ps1`. Module version **1.3.1 → 1.3.2**. Overall aggregate 88.98% → **88.93%** (full suite 1,627 → 1,652 tests; `Deploy-TierModel.ps1` 81.53% → 81.62% with the nine formatter call-sites); all module-scope files remain above the 80% CI gate.

Expand All @@ -29,7 +31,7 @@
|------|-------|-------|
| ✅ 100% | `Write-TierModelLog`, `Resolve-*` (4), `Test-TierModelOuExists`, `Test-TierModelGPO`, `Test-TierModelGroup`, `Test-TierModelGPOLink`, `Get-TierModelGroup`, `Test-TierModelAdmx`, `Get-TierModelGPOLink`, `Get-TierModelAuditRule`, `Format-TierModelDuration` | 14 |
| 🟡 90-99% | MSA/gMSA/dMSA ops (12 files), `Resolve-TierModelPrincipalSid`, `Resolve-DomainSpecificGuid`, `Copy/Get-TierModelAdmx`, `Get-TierModelUser`, `Test-TierModelGPOAudit`, `Get-TierModelConfig`, `New-TierModelGpo`, `Get-TierModelGpoFd`, `Get-TierModelGpo`, `Test-TierModelGPOContent`, `Get-TierModelGpoLinkFd`, `Get-TierModelOuAcl`, `Get-TierModelGroupFd`, `Get-TierModelOuAclFd`, `New-TierModelOuAcl`, `New-TierModelWinLapsAcl`, `Test-TierModelWinLapsDecryptor`, `Test-TierModelCanonicalAcl`, `Test-TierModelAuditRule`, `Get-TierModelAuditRuleFd`, `Repair-TierModelCanonicalAcl` | 35 |
| 🟠 80-89% | `Audit-TierModel` (77.16% — ByServer paths exempt), `Deploy-TierModel`, `New-Tier*` (incl. `New-TierModelOu` 84.86%), `New-TierModelAuditRule`, `Get-TierModel*Fd` variants, `Import-TierModelGpo`, `Test-TierModelOuAcl`, `TierModel.psm1`, `Get-TierModelWinLapsAcl`, `Get-TierModelWinLapsAclFd`, `Test-TierModelPrerequisites` (85.12%), `Test-TierModelWinLapsAcl` | 20 |
| 🟠 80-89% | `Audit-TierModel` (77.16% — ByServer paths exempt), `Deploy-TierModel`, `New-Tier*` (incl. `New-TierModelOu` 84.86%), `New-TierModelAuditRule`, `Get-TierModel*Fd` variants, `Import-TierModelGpo`, `Test-TierModelOuAcl`, `TierModel.psm1`, `Get-TierModelWinLapsAcl`, `Get-TierModelWinLapsAclFd`, `Test-TierModelPrerequisites` (85.40%), `Test-TierModelWinLapsAcl` | 20 |
| 🔴 50-79% | `Test-TierModelOu.ps1` (79.4%) | 1 |
| 🚨 0-25% | **No critical gaps!** 🎉 | 0 |

Expand All @@ -41,7 +43,7 @@
| `modules/TierModel/public/Import-TierModelGpo.ps1` | 89 | 21 | 110 | 🟠 80.9% ✦ |
| `Deploy-TierModel.ps1` | 1854 | 420 | 2274 | 🟠 81.53% ✦ |
| `modules/TierModel/public/Get-TierModelWinLapsAcl.ps1` | — | — | 543 | 🟠 81.6% |
| `modules/TierModel/public/Test-TierModelPrerequisites.ps1` | 409 | 72 | 481 | 🟠 85.12% ✦ |
| `modules/TierModel/public/Test-TierModelPrerequisites.ps1` | 421 | 72 | 493 | 🟠 85.40% ✦ |
| `modules/TierModel/TierModel.psm1` | 581 | 122 | 703 | 🟠 82.65% ✦ |
| `modules/TierModel/public/New-TierModelGptTmplContent.ps1` | 85 | 17 | 102 | 🟠 83.3% |
| `modules/TierModel/public/Get-TierModelWinLapsAclFd.ps1` | 383 | 76 | 459 | 🟠 83.4% |
Expand Down
2 changes: 1 addition & 1 deletion modules/TierModel/TierModel.psd1
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@{
RootModule = 'TierModel.psm1'
ModuleVersion = '1.3.2'
ModuleVersion = '1.3.3'
GUID = 'b6a7c9f8-5e5d-4c7a-9b9e-2e2e9a4f6d10'
Author = 'TierModel Team'
CompanyName = 'Enterprise AD'
Expand Down
23 changes: 21 additions & 2 deletions modules/TierModel/public/Test-TierModelPrerequisites.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ function Test-TierModelPrerequisites {
# If module was found but not loaded, try to import it to verify it works
if (-not $loadedModule -and $installedModule) {
try {
Import-Module $moduleName -ErrorAction Stop -Verbose:$false | Out-Null
Import-Module $moduleName -ErrorAction Stop -Verbose:$false -SkipEditionCheck | Out-Null
try {
Write-TierModelLog -Level Debug -Message "Successfully imported $moduleName module for validation" | Out-Null
} catch {
Expand All @@ -310,8 +310,27 @@ function Test-TierModelPrerequisites {

# Check domain admin membership regardless of elevation for testing scenarios
try {
Import-Module ActiveDirectory -ErrorAction SilentlyContinue -Verbose:$false | Out-Null
Import-Module ActiveDirectory -ErrorAction SilentlyContinue -Verbose:$false -SkipEditionCheck | Out-Null
$adShimDetected = $false
if (Get-Module ActiveDirectory) {
# Guard: under PowerShell 7, an RSAT ActiveDirectory module that is not Core-native
# loads through the Windows PowerShell compatibility shim (WinPSCompatSession) and
# returns DESERIALIZED objects - SIDs come back as strings, so .SID.Value / .objectSid.Value
# resolve empty. That silently breaks SID resolution deployment-wide (URA and GPO
# restricted-groups principals resolve to nothing). Detect it by probing whether the
# domain SID comes back as a plain string (deserialized) instead of a SecurityIdentifier,
# and fail fast rather than deploy broken policy.
try {
$adDomainProbe = Get-ADDomain -Server $PreferredDc -ErrorAction Stop
if ($adDomainProbe.DomainSID -is [string]) { $adShimDetected = $true }
} catch { }
}
if ($adShimDetected) {
$result.Valid = $false
$null = $result.Errors.Add("ActiveDirectory module is loaded through the Windows PowerShell compatibility shim (deserialized objects); SID resolution would break URA and GPO deployment.")
$null = $result.Remediation.Add("Run the deployment from a host with a PowerShell 7-native RSAT ActiveDirectory module (Windows 11 / Windows Server 2022 or later), or run under Windows PowerShell 5.1.")
}
elseif (Get-Module ActiveDirectory) {
Comment on lines +328 to +333
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$domainAdmins = Get-ADGroup -Identity "Domain Admins" -Server $PreferredDc -ErrorAction SilentlyContinue

Expand Down
2 changes: 1 addition & 1 deletion tests/Integration.Module.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Describe 'TierModel Module Integration Tests' -Tag 'Integration', 'Module' {
}

It 'Module version is correct' {
$script:LoadedModule.Version.ToString() | Should -Be '1.3.2'
$script:LoadedModule.Version.ToString() | Should -Be '1.3.3'
}

It 'Module loads all public function files' {
Expand Down
4 changes: 2 additions & 2 deletions tests/Unit.ModuleManifest.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ Describe 'TierModel Module Manifest' -Tag 'Unit', 'Manifest' {
$script:Manifest.ModuleVersion | Should -Match '^\d+\.\d+\.\d+$'
}

It 'Has current version 1.3.2' {
$script:Manifest.ModuleVersion | Should -Be '1.3.2'
It 'Has current version 1.3.3' {
$script:Manifest.ModuleVersion | Should -Be '1.3.3'
}

It 'Has valid GUID' {
Expand Down
14 changes: 14 additions & 0 deletions tests/Unit.Prerequisites.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,20 @@ Describe "TierModel Prerequisites Tests" -Tag 'Unit','Prereq' {
# With Get-ADGroup returning null from BeforeEach, expect this remediation message
$result.Remediation | Should -Contain "Ensure Domain Admins group exists and user is member of Domain Admins group"
}

It "Should fail fast when the AD module is loaded via the WinPS compatibility shim (deserialized objects)" -Tag 'Negative','DomainAdmin','CompatShim' {
# PowerShell 7 loads a non-Core-native RSAT AD module through the Windows PowerShell
# compatibility shim, which deserializes results so DomainSID comes back as a plain
# string instead of a SecurityIdentifier. The prerequisite check must detect this and
# fail fast rather than let deployment proceed and resolve empty SIDs into URA/GPO policy.
Mock Get-ADDomain { return @{ DNSRoot = 'test.contoso.com'; NetBIOSName = 'TEST'; DomainSID = 'S-1-5-21-1111111111-2222222222-3333333333' } } -ModuleName TierModel

$result = Test-TierModelPrerequisites -PreferredDc 'MockDC.test.local' -DependenciesPath $validDepsFile

$result.Valid | Should -Be $false
($result.Errors -join ' ') | Should -Match 'Windows PowerShell compatibility shim'
($result.Remediation -join ' ') | Should -Match 'PowerShell 7-native RSAT'
}
}

Context "Host OS Language Enforcement" -Tag 'Language','Prereq' {
Expand Down