Tests/NTFS-PermissionAudit.Tests.ps1

#Requires -Modules Pester

<#
.SYNOPSIS
    Pester tests for the NTFS-PermissionAudit module.
 
.DESCRIPTION
    Validates module loading, exported functions, parameter validation, and core logic
    using mocked cmdlets. No Active Directory or file system access required.
#>


BeforeAll {
    # Import the module from the project root
    $modulePath = Split-Path -Path $PSScriptRoot -Parent
    Import-Module "$modulePath\NTFS-PermissionAudit.psd1" -Force
}

Describe 'Module: NTFS-PermissionAudit' {

    Context 'Module Loading' {

        It 'Should import the module without errors' {
            $module = Get-Module -Name 'NTFS-PermissionAudit'
            $module | Should -Not -BeNullOrEmpty
        }

        It 'Should export exactly 5 public functions' {
            $module = Get-Module -Name 'NTFS-PermissionAudit'
            $module.ExportedFunctions.Count | Should -Be 5
        }

        It 'Should export Invoke-PermissionAudit' {
            Get-Command -Module 'NTFS-PermissionAudit' -Name 'Invoke-PermissionAudit' | Should -Not -BeNullOrEmpty
        }

        It 'Should export Get-DirectUserACEs' {
            Get-Command -Module 'NTFS-PermissionAudit' -Name 'Get-DirectUserACEs' | Should -Not -BeNullOrEmpty
        }

        It 'Should export Get-BrokenInheritance' {
            Get-Command -Module 'NTFS-PermissionAudit' -Name 'Get-BrokenInheritance' | Should -Not -BeNullOrEmpty
        }

        It 'Should export Get-NestedGroupReport' {
            Get-Command -Module 'NTFS-PermissionAudit' -Name 'Get-NestedGroupReport' | Should -Not -BeNullOrEmpty
        }

        It 'Should export Get-SharePermissionReport' {
            Get-Command -Module 'NTFS-PermissionAudit' -Name 'Get-SharePermissionReport' | Should -Not -BeNullOrEmpty
        }

        It 'Should NOT export New-HtmlDashboard (private function)' {
            $cmd = Get-Command -Module 'NTFS-PermissionAudit' -Name 'New-HtmlDashboard' -ErrorAction SilentlyContinue
            $cmd | Should -BeNullOrEmpty
        }
    }

    Context 'Manifest Validation' {

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

        It 'Should have the correct GUID' {
            $manifest = Test-ModuleManifest -Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'NTFS-PermissionAudit.psd1')
            $manifest.GUID.ToString() | Should -Be 'a1b2c3d4-8e7f-4a5b-9c6d-2e3f4a5b6c7d'
        }

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

        It 'Should list the correct tags' {
            $manifest = Test-ModuleManifest -Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'NTFS-PermissionAudit.psd1')
            $manifest.PrivateData.PSData.Tags | Should -Contain 'NTFS'
            $manifest.PrivateData.PSData.Tags | Should -Contain 'SOC2'
            $manifest.PrivateData.PSData.Tags | Should -Contain 'HIPAA'
        }
    }

    Context 'Parameter Validation' {

        It 'Get-DirectUserACEs: Path should be mandatory' {
            (Get-Command Get-DirectUserACEs).Parameters['Path'].Attributes |
                Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] } |
                ForEach-Object { $_.Mandatory } | Should -Contain $true
        }

        It 'Get-BrokenInheritance: Path should be mandatory' {
            (Get-Command Get-BrokenInheritance).Parameters['Path'].Attributes |
                Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] } |
                ForEach-Object { $_.Mandatory } | Should -Contain $true
        }

        It 'Get-NestedGroupReport: Path should be mandatory' {
            (Get-Command Get-NestedGroupReport).Parameters['Path'].Attributes |
                Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] } |
                ForEach-Object { $_.Mandatory } | Should -Contain $true
        }

        It 'Invoke-PermissionAudit: Path should be mandatory' {
            (Get-Command Invoke-PermissionAudit).Parameters['Path'].Attributes |
                Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] } |
                ForEach-Object { $_.Mandatory } | Should -Contain $true
        }

        It 'Get-DirectUserACEs: MaxDepth should default to 3' {
            (Get-Command Get-DirectUserACEs).Parameters['MaxDepth'].DefaultValue | Should -Be 3
        }

        It 'Get-BrokenInheritance: MaxDepth should default to 3' {
            (Get-Command Get-BrokenInheritance).Parameters['MaxDepth'].DefaultValue | Should -Be 3
        }

        It 'Get-NestedGroupReport: MaxNestingDepth should default to 3' {
            (Get-Command Get-NestedGroupReport).Parameters['MaxNestingDepth'].DefaultValue | Should -Be 3
        }

        It 'Invoke-PermissionAudit: MaxDepth should default to 3' {
            (Get-Command Invoke-PermissionAudit).Parameters['MaxDepth'].DefaultValue | Should -Be 3
        }

        It 'Get-SharePermissionReport: ComputerName should default to localhost' {
            (Get-Command Get-SharePermissionReport).Parameters['ComputerName'].DefaultValue | Should -Contain 'localhost'
        }
    }

    Context 'Get-DirectUserACEs - Mock Tests' {

        BeforeAll {
            # Create a mock ACL with both user and group entries
            $mockUserIdentity = [PSCustomObject]@{
                Value = 'CONTOSO\jsmith'
            }
            $mockGroupIdentity = [PSCustomObject]@{
                Value = 'CONTOSO\FinanceTeam'
            }
            $mockOrphanedIdentity = [PSCustomObject]@{
                Value = 'S-1-5-21-1234567890-987654321-1122334455-1001'
            }

            $mockAce_User = [PSCustomObject]@{
                IdentityReference = $mockUserIdentity
                AccessControlType = [PSCustomObject]@{ ToString = { 'Allow' } }
                FileSystemRights  = [PSCustomObject]@{ ToString = { 'Modify, Synchronize' } }
                IsInherited       = $false
            }
            $mockAce_Group = [PSCustomObject]@{
                IdentityReference = $mockGroupIdentity
                AccessControlType = [PSCustomObject]@{ ToString = { 'Allow' } }
                FileSystemRights  = [PSCustomObject]@{ ToString = { 'ReadAndExecute, Synchronize' } }
                IsInherited       = $true
            }
            $mockAce_Orphaned = [PSCustomObject]@{
                IdentityReference = $mockOrphanedIdentity
                AccessControlType = [PSCustomObject]@{ ToString = { 'Allow' } }
                FileSystemRights  = [PSCustomObject]@{ ToString = { 'FullControl' } }
                IsInherited       = $false
            }

            $mockAcl = [PSCustomObject]@{
                Access                = @($mockAce_User, $mockAce_Group, $mockAce_Orphaned)
                AreAccessRulesProtected = $false
            }
        }

        It 'Should flag user accounts as DIRECT USER ACE' {
            Mock Test-Path { $true } -ModuleName 'NTFS-PermissionAudit'
            Mock Get-ChildItem { @() } -ModuleName 'NTFS-PermissionAudit'
            Mock Get-Acl { $mockAcl } -ModuleName 'NTFS-PermissionAudit'

            # Mock AD lookups: jsmith is a user, FinanceTeam is a group
            Mock Get-ADGroup { throw 'Not a group' } -ModuleName 'NTFS-PermissionAudit' -ParameterFilter { $Identity -eq 'jsmith' }
            Mock Get-ADUser  { [PSCustomObject]@{ SamAccountName = 'jsmith' } } -ModuleName 'NTFS-PermissionAudit' -ParameterFilter { $Identity -eq 'jsmith' }
            Mock Get-ADGroup { [PSCustomObject]@{ SamAccountName = 'FinanceTeam' } } -ModuleName 'NTFS-PermissionAudit' -ParameterFilter { $Identity -eq 'FinanceTeam' }

            $results = Get-DirectUserACEs -Path 'C:\TestPath'

            $userEntry = $results | Where-Object { $_.Identity -eq 'CONTOSO\jsmith' }
            $userEntry.Finding | Should -Be 'DIRECT USER ACE'
            $userEntry.ObjectType | Should -Be 'User'
        }

        It 'Should mark group accounts as OK' {
            Mock Test-Path { $true } -ModuleName 'NTFS-PermissionAudit'
            Mock Get-ChildItem { @() } -ModuleName 'NTFS-PermissionAudit'
            Mock Get-Acl { $mockAcl } -ModuleName 'NTFS-PermissionAudit'
            Mock Get-ADGroup { throw 'Not a group' } -ModuleName 'NTFS-PermissionAudit' -ParameterFilter { $Identity -eq 'jsmith' }
            Mock Get-ADUser  { [PSCustomObject]@{ SamAccountName = 'jsmith' } } -ModuleName 'NTFS-PermissionAudit' -ParameterFilter { $Identity -eq 'jsmith' }
            Mock Get-ADGroup { [PSCustomObject]@{ SamAccountName = 'FinanceTeam' } } -ModuleName 'NTFS-PermissionAudit' -ParameterFilter { $Identity -eq 'FinanceTeam' }

            $results = Get-DirectUserACEs -Path 'C:\TestPath'

            $groupEntry = $results | Where-Object { $_.Identity -eq 'CONTOSO\FinanceTeam' }
            $groupEntry.Finding | Should -Be 'OK'
            $groupEntry.ObjectType | Should -Be 'Group'
        }

        It 'Should detect orphaned SIDs as user accounts' {
            Mock Test-Path { $true } -ModuleName 'NTFS-PermissionAudit'
            Mock Get-ChildItem { @() } -ModuleName 'NTFS-PermissionAudit'
            Mock Get-Acl { $mockAcl } -ModuleName 'NTFS-PermissionAudit'
            Mock Get-ADGroup { throw 'Not found' } -ModuleName 'NTFS-PermissionAudit' -ParameterFilter { $Identity -eq 'jsmith' }
            Mock Get-ADUser  { [PSCustomObject]@{ SamAccountName = 'jsmith' } } -ModuleName 'NTFS-PermissionAudit' -ParameterFilter { $Identity -eq 'jsmith' }
            Mock Get-ADGroup { [PSCustomObject]@{ SamAccountName = 'FinanceTeam' } } -ModuleName 'NTFS-PermissionAudit' -ParameterFilter { $Identity -eq 'FinanceTeam' }
            Mock Get-ADGroup { throw 'Not found' } -ModuleName 'NTFS-PermissionAudit' -ParameterFilter { $Identity -eq 'S-1-5-21-1234567890-987654321-1122334455-1001' }
            Mock Get-ADUser  { throw 'Not found' } -ModuleName 'NTFS-PermissionAudit' -ParameterFilter { $Identity -eq 'S-1-5-21-1234567890-987654321-1122334455-1001' }

            $results = Get-DirectUserACEs -Path 'C:\TestPath'

            $orphanedEntry = $results | Where-Object { $_.Identity -like 'S-1-5-21-*' }
            $orphanedEntry.ObjectType | Should -Be 'User'
            $orphanedEntry.Finding | Should -Be 'DIRECT USER ACE'
        }
    }

    Context 'Get-BrokenInheritance - Mock Tests' {

        It 'Should detect folders with disabled inheritance' {
            $mockAcl_Broken = [PSCustomObject]@{
                Access                  = @(
                    [PSCustomObject]@{ IdentityReference = [PSCustomObject]@{ Value = 'CONTOSO\FinanceTeam' } }
                    [PSCustomObject]@{ IdentityReference = [PSCustomObject]@{ Value = 'BUILTIN\Administrators' } }
                )
                AreAccessRulesProtected = $true
            }

            Mock Test-Path { $true } -ModuleName 'NTFS-PermissionAudit'
            Mock Get-ChildItem { @() } -ModuleName 'NTFS-PermissionAudit'
            Mock Get-Acl { $mockAcl_Broken } -ModuleName 'NTFS-PermissionAudit'

            $results = Get-BrokenInheritance -Path 'C:\TestPath'

            $results[0].InheritanceEnabled | Should -Be $false
            $results[0].Finding | Should -BeLike 'INHERITANCE DISABLED*'
            $results[0].ACECount | Should -Be 2
        }

        It 'Should report OK for folders with enabled inheritance' {
            $mockAcl_OK = [PSCustomObject]@{
                Access                  = @(
                    [PSCustomObject]@{ IdentityReference = [PSCustomObject]@{ Value = 'CONTOSO\Domain Users' } }
                )
                AreAccessRulesProtected = $false
            }

            Mock Test-Path { $true } -ModuleName 'NTFS-PermissionAudit'
            Mock Get-ChildItem { @() } -ModuleName 'NTFS-PermissionAudit'
            Mock Get-Acl { $mockAcl_OK } -ModuleName 'NTFS-PermissionAudit'

            $results = Get-BrokenInheritance -Path 'C:\TestPath'

            $results[0].InheritanceEnabled | Should -Be $true
            $results[0].Finding | Should -Be 'OK'
        }

        It 'Should return correct unique identity count' {
            $mockAcl_Multi = [PSCustomObject]@{
                Access = @(
                    [PSCustomObject]@{ IdentityReference = [PSCustomObject]@{ Value = 'CONTOSO\GroupA' } }
                    [PSCustomObject]@{ IdentityReference = [PSCustomObject]@{ Value = 'CONTOSO\GroupB' } }
                    [PSCustomObject]@{ IdentityReference = [PSCustomObject]@{ Value = 'CONTOSO\GroupA' } }
                )
                AreAccessRulesProtected = $true
            }

            Mock Test-Path { $true } -ModuleName 'NTFS-PermissionAudit'
            Mock Get-ChildItem { @() } -ModuleName 'NTFS-PermissionAudit'
            Mock Get-Acl { $mockAcl_Multi } -ModuleName 'NTFS-PermissionAudit'

            $results = Get-BrokenInheritance -Path 'C:\TestPath'

            $results[0].UniqueIdentities | Should -Be 2
            $results[0].ACECount | Should -Be 3
        }
    }

    Context 'Get-SharePermissionReport - Mock Tests' {

        BeforeAll {
            $mockShares = @(
                [PSCustomObject]@{ Name = 'Finance';  Path = 'D:\Shares\Finance' }
                [PSCustomObject]@{ Name = 'Public';   Path = 'D:\Shares\Public' }
                [PSCustomObject]@{ Name = 'ADMIN$';   Path = 'C:\Windows' }
            )

            $mockAccess_Finance = @(
                [PSCustomObject]@{
                    AccountName       = 'CONTOSO\FinanceTeam'
                    AccessRight       = 'Change'
                    AccessControlType = 'Allow'
                }
            )

            $mockAccess_Public = @(
                [PSCustomObject]@{
                    AccountName       = 'Everyone'
                    AccessRight       = 'Full'
                    AccessControlType = 'Allow'
                }
            )

            $mockAccess_Admin = @(
                [PSCustomObject]@{
                    AccountName       = 'BUILTIN\Administrators'
                    AccessRight       = 'Full'
                    AccessControlType = 'Allow'
                }
            )
        }

        It 'Should detect Everyone with Full Control' {
            Mock Get-SmbShare { $mockShares } -ModuleName 'NTFS-PermissionAudit'
            Mock Get-SmbShareAccess {
                param($Name)
                switch ($Name) {
                    'Finance' { $mockAccess_Finance }
                    'Public'  { $mockAccess_Public }
                    'ADMIN$'  { $mockAccess_Admin }
                }
            } -ModuleName 'NTFS-PermissionAudit'

            $results = Get-SharePermissionReport -ComputerName 'localhost'

            $everyoneEntry = $results | Where-Object { $_.AccountName -eq 'Everyone' -and $_.ShareName -eq 'Public' }
            $everyoneEntry.Finding | Should -BeLike '*EVERYONE FULL CONTROL*'
        }

        It 'Should exclude default admin shares when -ExcludeDefault is specified' {
            Mock Get-SmbShare { $mockShares } -ModuleName 'NTFS-PermissionAudit'
            Mock Get-SmbShareAccess {
                param($Name)
                switch ($Name) {
                    'Finance' { $mockAccess_Finance }
                    'Public'  { $mockAccess_Public }
                    'ADMIN$'  { $mockAccess_Admin }
                }
            } -ModuleName 'NTFS-PermissionAudit'

            $results = Get-SharePermissionReport -ComputerName 'localhost' -ExcludeDefault

            $adminShare = $results | Where-Object { $_.ShareName -eq 'ADMIN$' }
            $adminShare | Should -BeNullOrEmpty
        }

        It 'Should flag shares with no explicit deny' {
            Mock Get-SmbShare { @([PSCustomObject]@{ Name = 'Data'; Path = 'D:\Data' }) } -ModuleName 'NTFS-PermissionAudit'
            Mock Get-SmbShareAccess {
                @(
                    [PSCustomObject]@{
                        AccountName       = 'CONTOSO\DataTeam'
                        AccessRight       = 'Change'
                        AccessControlType = 'Allow'
                    }
                )
            } -ModuleName 'NTFS-PermissionAudit'

            $results = Get-SharePermissionReport -ComputerName 'localhost'

            $results[0].Finding | Should -BeLike '*NO EXPLICIT DENY*'
        }
    }
}