modules/Devolutions.CIEM.Checks/Tests/Unit/CheckDataNeeds.Tests.ps1

BeforeAll {
    Remove-Module Devolutions.CIEM -Force -ErrorAction SilentlyContinue
    Import-Module (Join-Path $PSScriptRoot '..' '..' '..' '..' 'Devolutions.CIEM.psd1')
}

Describe 'Check data_needs' {
    BeforeEach {
        $script:TestDatabasePath = Join-Path $TestDrive ("ciem-" + [guid]::NewGuid().ToString('N') + '.db')
        $env:CIEM_TEST_DB_PATH = $script:TestDatabasePath
        New-CIEMDatabase -Path $script:TestDatabasePath

        InModuleScope Devolutions.CIEM {
            $script:DatabasePath = $env:CIEM_TEST_DB_PATH
            $script:AuthContext = @{
                Azure = [pscustomobject]@{
                    AccountId = 'test-account'
                    AccountType = 'ServicePrincipal'
                    SubscriptionIds = @('sub1')
                }
            }
            $script:AzureAuthContext = [pscustomobject]@{
                IsConnected = $true
                TenantId = 'tenant1'
                SubscriptionIds = @('sub1')
                ARMToken = 'arm-token'
                GraphToken = 'graph-token'
                KeyVaultToken = 'kv-token'
            }
        }

        Mock -ModuleName Devolutions.CIEM Write-CIEMLog {}
    }

    It 'Seeds Azure metadata for every local script' {
        $scriptCount = @(Get-ChildItem (Join-Path $PSScriptRoot '..' '..' '..' 'Azure' 'Checks') -Filter '*.ps1').Count
        $checks = @(Get-CIEMCheck -Provider Azure)

        $checks.Count | Should -Be $scriptCount
    }

    It 'Preserves user disabled state when the catalog syncs' {
        Disable-CIEMCheck -CheckId 'entra_security_defaults_enabled'
        InModuleScope Devolutions.CIEM {
            Sync-CIEMCheckCatalog -Provider Azure
        }
        (Get-CIEMCheck -CheckId 'entra_security_defaults_enabled').Disabled | Should -BeTrue
    }

    It 'Catalog-enabled checks keep required data_needs' {
        $check = Get-CIEMCheck -CheckId 'entra_security_defaults_enabled'

        @($check.DataNeeds) | Should -Contain 'entra:securitydefaults'
    }

    It 'Disabled catalog checks can omit data_needs' {
        $check = Get-CIEMCheck -CheckId 'aks_cluster_rbac_enabled'

        $check.Disabled | Should -BeTrue
        $check.DataNeeds | Should -BeNullOrEmpty
    }

    It 'Save-CIEMCheck round-trips data_needs' {
        Save-CIEMCheck -Id 'data_needs_roundtrip' `
            -Provider 'Azure' `
            -Service 'Entra' `
            -Title 'Roundtrip' `
            -Severity 'medium' `
            -CheckScript 'Test-EntraSecurityDefaultsEnabled.ps1' `
            -DataNeeds @('entra:users', 'iam:roleassignments')

        $check = Get-CIEMCheck -CheckId 'data_needs_roundtrip'

        $check | Should -Not -BeNullOrEmpty
        @($check.DataNeeds) | Should -HaveCount 2
        $check.DataNeeds[0] | Should -Be 'entra:users'
        $check.DataNeeds[1] | Should -Be 'iam:roleassignments'
    }

    It 'Update-CIEMCheck persists changed title and data_needs' {
        Update-CIEMCheck -Id 'entra_security_defaults_enabled' `
            -Title 'changed title' `
            -DataNeeds @('entra:users', 'iam:roleassignments')

        $check = Get-CIEMCheck -CheckId 'entra_security_defaults_enabled'

        $check.Title | Should -Be 'changed title'
        @($check.DataNeeds) | Should -Be @('entra:users', 'iam:roleassignments')
    }

    It 'Empty data_needs array throws at registration' {
        {
            Save-CIEMCheck -Id 'empty_data_needs' `
                -Provider 'Azure' `
                -Service 'Entra' `
                -Title 'Empty' `
                -Severity 'medium' `
                -CheckScript 'Test-EntraSecurityDefaultsEnabled.ps1' `
                -DataNeeds @()
        } | Should -Throw '*must declare at least one data need*'
    }

    It 'Missing data_needs throws at scan planning' {
        Mock -ModuleName Devolutions.CIEM Sync-CIEMCheckCatalog {}

        Save-CIEMCheck -Id 'scan_missing_data_needs' `
            -Provider 'Azure' `
            -Service 'Entra' `
            -Title 'Missing' `
            -Severity 'medium' `
            -CheckScript 'Test-EntraSecurityDefaultsEnabled.ps1'

        InModuleScope Devolutions.CIEM {
            { Invoke-CIEMScan -Provider Azure -CheckId 'scan_missing_data_needs' } | Should -Throw '*missing data_needs*'
        }
    }

    It 'Fresh Azure scans no longer fail on missing metadata rows' {
        Mock -ModuleName Devolutions.CIEM Get-CIEMAzureDiscoveryRun {
            [pscustomobject]@{ Id = 1; Status = 'Completed' }
        }
        Mock -ModuleName Devolutions.CIEM Get-CIEMAzureEntraResource { @() }
        Mock -ModuleName Devolutions.CIEM Get-CIEMAzureArmResource { @() }
        Mock -ModuleName Devolutions.CIEM Get-CIEMAzureResourceRelationship { @() }
        Mock -ModuleName Devolutions.CIEM Invoke-CIEMParallelForEach { @() }

        InModuleScope Devolutions.CIEM {
            { Invoke-CIEMScan -Provider Azure | Out-Null } | Should -Not -Throw
        }
    }

    It 'Planner unions data needs and fetches each slice once' {
        Mock -ModuleName Devolutions.CIEM Sync-CIEMCheckCatalog {}
        Mock -ModuleName Devolutions.CIEM Get-CIEMAzureDiscoveryRun {
            [pscustomobject]@{ Id = 1; Status = 'Completed' }
        }
        Mock -ModuleName Devolutions.CIEM Get-CIEMAzureEntraResource { @() }
        Mock -ModuleName Devolutions.CIEM Get-CIEMAzureArmResource { @() }
        Mock -ModuleName Devolutions.CIEM Invoke-CIEMParallelForEach { @() }

        Save-CIEMCheck -Id 'union_a' `
            -Provider 'Azure' `
            -Service 'Entra' `
            -Title 'Union A' `
            -Severity 'medium' `
            -CheckScript 'Test-EntraSecurityDefaultsEnabled.ps1' `
            -DataNeeds @('entra:users', 'entra:authorizationpolicy')

        Save-CIEMCheck -Id 'union_b' `
            -Provider 'Azure' `
            -Service 'Entra' `
            -Title 'Union B' `
            -Severity 'medium' `
            -CheckScript 'Test-EntraPolicyGuestUserAccessRestriction.ps1' `
            -DataNeeds @('entra:authorizationpolicy')

        Save-CIEMCheck -Id 'union_c' `
            -Provider 'Azure' `
            -Service 'IAM' `
            -Title 'Union C' `
            -Severity 'medium' `
            -CheckScript 'Test-IamSubscriptionRolesOwnerCustomNotCreated.ps1' `
            -DataNeeds @('iam:roleassignments')

        InModuleScope Devolutions.CIEM {
            Invoke-CIEMScan -Provider Azure -CheckId @(
                'union_a',
                'union_b',
                'union_c'
            ) | Out-Null
        }

        Assert-MockCalled Get-CIEMAzureEntraResource -ModuleName Devolutions.CIEM -Times 1 -Exactly -ParameterFilter { $Type -eq 'user' }
        Assert-MockCalled Get-CIEMAzureEntraResource -ModuleName Devolutions.CIEM -Times 1 -Exactly -ParameterFilter { $Type -eq 'authorizationPolicy' }
        Assert-MockCalled Get-CIEMAzureArmResource -ModuleName Devolutions.CIEM -Times 1 -Exactly -ParameterFilter { $Type -eq 'microsoft.authorization/roleassignments' }
    }

    It 'Unknown data need throws at planning' {
        Mock -ModuleName Devolutions.CIEM Sync-CIEMCheckCatalog {}
        Mock -ModuleName Devolutions.CIEM Get-CIEMAzureDiscoveryRun {
            [pscustomobject]@{ Id = 1; Status = 'Completed' }
        }

        Save-CIEMCheck -Id 'unknown_need' `
            -Provider 'Azure' `
            -Service 'Entra' `
            -Title 'Unknown Need' `
            -Severity 'medium' `
            -CheckScript 'Test-EntraSecurityDefaultsEnabled.ps1' `
            -DataNeeds @('entra:bogus')

        InModuleScope Devolutions.CIEM {
            { Invoke-CIEMScan -Provider Azure -CheckId 'unknown_need' } | Should -Throw "*Unknown data need 'entra:bogus'*"
        }
    }

    It 'Data needs must use lowercase canonical form' {
        Mock -ModuleName Devolutions.CIEM Sync-CIEMCheckCatalog {}

        Save-CIEMCheck -Id 'noncanonical_need' `
            -Provider 'Azure' `
            -Service 'Entra' `
            -Title 'Noncanonical Need' `
            -Severity 'medium' `
            -CheckScript 'Test-EntraSecurityDefaultsEnabled.ps1' `
            -DataNeeds @('Entra:Users')

        InModuleScope Devolutions.CIEM {
            { Invoke-CIEMScan -Provider Azure -CheckId 'noncanonical_need' } | Should -Throw "*declares non-canonical data need 'Entra:Users'*"
        }
    }

    It 'Targeted accessor uses the declared resource type filter' {
        Mock -ModuleName Devolutions.CIEM Sync-CIEMCheckCatalog {}
        Mock -ModuleName Devolutions.CIEM Get-CIEMAzureDiscoveryRun {
            [pscustomobject]@{ Id = 1; Status = 'Completed' }
        }
        Mock -ModuleName Devolutions.CIEM Get-CIEMAzureEntraResource { @() }
        Mock -ModuleName Devolutions.CIEM Invoke-CIEMParallelForEach { @() }

        Save-CIEMCheck -Id 'targeted_accessor' `
            -Provider 'Azure' `
            -Service 'Entra' `
            -Title 'Targeted Accessor' `
            -Severity 'medium' `
            -CheckScript 'Test-EntraSecurityDefaultsEnabled.ps1' `
            -DataNeeds @('entra:users')

        InModuleScope Devolutions.CIEM {
            Invoke-CIEMScan -Provider Azure -CheckId 'targeted_accessor' | Out-Null
        }

        Assert-MockCalled Get-CIEMAzureEntraResource -ModuleName Devolutions.CIEM -Times 1 -Exactly -ParameterFilter { $Type -eq 'user' }
        Assert-MockCalled Get-CIEMAzureEntraResource -ModuleName Devolutions.CIEM -Times 0 -Exactly -ParameterFilter { $Type -ne 'user' }
    }
}