# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

##
## ----------
## Test Note:
## ----------
## Since these tests change session and system state (constrained language and system lockdown)
## they will all use try/finally blocks instead of Pester AfterEach/AfterAll to ensure session
## and system state is restored.
## Pester AfterEach, AfterAll is not reliable when the session is constrained language or locked down.
##

Import-Module HelpersSecurity

try
{
    $defaultParamValues = $PSDefaultParameterValues.Clone()
    $PSDefaultParameterValues["it:Skip"] = !$IsWindows

    Describe "Help built-in function should not expose nested module private functions when run on locked down systems" -Tags 'Feature','RequireAdminOnWindows' {

        BeforeAll {

            $restorePSModulePath = $env:PSModulePath
            $env:PSModulePath += ";$TestDrive"

            $trustedModuleName1 = "TrustedModule$(Get-Random -Max 999)_System32"
            $trustedModulePath1 = Join-Path $TestDrive $trustedModuleName1
            New-Item -ItemType Directory $trustedModulePath1
            $trustedModuleFilePath1 = Join-Path $trustedModulePath1 ($trustedModuleName1 + ".psm1")
            $trustedModuleManifestPath1 = Join-Path $trustedModulePath1 ($trustedModuleName1 + ".psd1")

            $trustedModuleName2 = "TrustedModule$(Get-Random -Max 999)_System32"
            $trustedModulePath2 = Join-Path $TestDrive $trustedModuleName2
            New-Item -ItemType Directory $trustedModulePath2
            $trustedModuleFilePath2 = Join-Path $trustedModulePath2 ($trustedModuleName2 + ".psm1")

            $trustedModuleScript1 = @'
            function PublicFn1
            {
                NestedFn1
                PrivateFn1
            }
            function PrivateFn1
            {
                "PrivateFn1"
            }
'@
            $trustedModuleScript1 | Out-File -FilePath $trustedModuleFilePath1
            '@{{ FunctionsToExport = "PublicFn1"; ModuleVersion = "1.0"; RootModule = "{0}"; NestedModules = "{1}" }}' -f @($trustedModuleFilePath1,$trustedModuleName2) | Out-File -FilePath $trustedModuleManifestPath1

            $trustedModuleScript2 = @'
            function NestedFn1
            {
                "NestedFn1"
                "Language mode is $($ExecutionContext.SessionState.LanguageMode)"
            }
'@
            $trustedModuleScript2 | Out-File -FilePath $trustedModuleFilePath2
        }

        AfterAll {

            $env:PSModulePath = $restorePSModulePath
            if ($trustedModuleName1 -ne $null) { Remove-Module -Name $trustedModuleName1 -Force -ErrorAction Ignore }
            if ($trustedModuleName2 -ne $null) { Remove-Module -Name $trustedModuleName2 -Force -ErrorAction Ignore }
        }

        It "Verifies that private functions in trusted nested modules are not globally accessible after running the help function" {

            $isCommandAccessible = "False"
            try
            {
                Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"

                $command = @"
                Import-Module -Name $trustedModuleName1 -Force -ErrorAction Stop;
"@
                $command += @'
                $null = help -Name NestedFn1 -Category Function 2> $null;
                $result = Get-Command NestedFn1 2> $null;
                return ($result -ne $null)
'@
                $isCommandAccessible = pwsh.exe -noprofile -nologo -c $command
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode
            }

            # Verify that nested function NestedFn1 was not accessible
            $isCommandAccessible | Should -BeExactly "False"
        }
    }

    Describe "NoLanguage runspace pool session should remain in NoLanguage mode when created on a system-locked down machine" -Tags 'Feature','RequireAdminOnWindows' {

        BeforeAll {

            $configFileName = "RestrictedSessionConfig.pssc"
            $configFilePath = Join-Path $TestDrive $configFileName
            '@{ SchemaVersion = "2.0.0.0"; SessionType = "RestrictedRemoteServer"}' > $configFilePath

            $scriptModuleName = "ImportTrustedModuleForTest_System32"
            $moduleFilePath = Join-Path $TestDrive ($scriptModuleName + ".psm1")
            $template = @'
            function TestRestrictedSession
            {{
                $iss = [initialsessionstate]::CreateFromSessionConfigurationFile("{0}")
                $rsp = [runspacefactory]::CreateRunspacePool($iss)
                $rsp.Open()
                $ps = [powershell]::Create()
                $ps.RunspacePool = $rsp
                $null = $ps.AddScript("Hello")

                try
                {{
                    $ps.Invoke()
                }}
                finally
                {{
                    $ps.Dispose()
                    $rsp.Dispose()
                }}
            }}

            Export-ModuleMember -Function TestRestrictedSession
'@
            $template -f $configFilePath > $moduleFilePath
        }

        AfterAll {
            Remove-Module $scriptModuleName -Force -ErrorAction SilentlyContinue
        }

        It "Verifies that a NoLanguage runspace pool throws the expected 'script not allowed' error" {

            try
            {
                Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"

                $mod = Import-Module -Name $moduleFilePath -Force -PassThru

                # Running module function TestRestrictedSession should throw a 'script not allowed' error
                # because it runs in a 'no language' session.
                try
                {
                    & "$scriptModuleName\TestRestrictedSession"
                    throw "No Exception!"
                }
                catch
                {
                    $expectedError = $_
                }
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode
            }

            $expectedError.Exception.InnerException.ErrorRecord.FullyQualifiedErrorId | Should -BeExactly "ScriptsNotAllowed"
        }
    }

    Describe "Built-ins work within constrained language" -Tags 'Feature','RequireAdminOnWindows' {

        BeforeAll {
            $TestCasesBuiltIn = @(
                @{testName = "Verify built-in function"; scriptblock = { Get-Verb } }
                @{testName = "Verify built-in error variable"; scriptblock = { Write-Error SomeError -ErrorVariable ErrorOutput -ErrorAction SilentlyContinue; $ErrorOutput} }
            )
        }

        It "<testName>" -TestCases $TestCasesBuiltIn {

            param ($scriptblock)

            try
            {
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"

                $result = (& $scriptblock)
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode
            }

            $result.Count | Should -BeGreaterThan 0
        }
    }

    Describe "Background jobs" -Tags 'Feature','RequireAdminOnWindows' {

        Context "Background jobs in system lock down mode" {

            It "Verifies that background jobs in system lockdown mode run in constrained language" {

                try
                {
                    Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode

                    $job = Start-Job -ScriptBlock { [object]::Equals("A", "B") } | Wait-Job
                    $expectedErrorId = $job.ChildJobs[0].Error.FullyQualifiedErrorId
                    $job | Remove-Job
                }
                finally
                {
                    Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode
                }

                $expectedErrorId | Should -BeExactly "MethodInvocationNotSupportedInConstrainedLanguage"
            }
        }

        Context "Background jobs within inconsistent mode" {

            It "Verifies that background job is denied when mode is inconsistent" {

                try
                {
                    $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
                    Start-Job { [object]::Equals("A", "B") }
                    throw "No Exception!"
                }
                catch
                {
                    $expectedError = $_
                }
                finally
                {
                    Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode
                }

                $expectedError.FullyQualifiedErrorId | Should -BeExactly "CannotStartJobInconsistentLanguageMode,Microsoft.PowerShell.Commands.StartJobCommand"
            }
        }
    }

    Describe "Add-Type in constrained language" -Tags 'Feature','RequireAdminOnWindows' {

        It "Verifies Add-Type fails in constrained language mode" {

            try
            {
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
                Add-Type -TypeDefinition 'public class ConstrainedLanguageTest { public static string Hello = "HelloConstrained"; }'
                throw "No Exception!"
            }
            catch
            {
                $expectedError = $_
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode
            }

            $expectedError.FullyQualifiedErrorId | Should -BeExactly "CannotDefineNewType,Microsoft.PowerShell.Commands.AddTypeCommand"
        }

        It "Verifies Add-Type works back in full language mode again" {
            Add-Type -TypeDefinition 'public class AfterFullLanguageTest { public static string Hello = "HelloAfter"; }'
            [AfterFullLanguageTest]::Hello | Should -BeExactly "HelloAfter"
        }
    }

    Describe "New-Object in constrained language" -Tags 'Feature','RequireAdminOnWindows' {

        Context "New-Object with dotNet types" {

            It "Verifies New-Object works in constrained language of allowed string type" {

                try
                {
                    $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"

                    $resultString = New-Object System.String "Hello"
                }
                finally
                {
                    Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode
                }

                $resultString | Should -Be "Hello"
            }

            It "Verifies New-Object throws error in constrained language for disallowed IntPtr type" {

                try
                {
                    $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
                    New-Object System.IntPtr 1234
                    throw "No Exception!"
                }
                catch
                {
                    $expectedError = $_
                }
                finally
                {
                    Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode
                }

                $expectedError.FullyQualifiedErrorId | Should -BeExactly "CannotCreateTypeConstrainedLanguage,Microsoft.PowerShell.Commands.NewObjectCommand"
            }

            It "Verifies New-Object works for IntPtr type back in full language mode again" {

                New-Object System.IntPtr 1234 | Should -Be 1234
            }
        }

        Context "New-Object with COM types" {

            It "Verifies New-Object with COM types is disallowed in system lock down" {

                try
                {
                    $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
                    Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode

                    New-Object -Com ADODB.Parameter
                    throw "No Exception!"
                }
                catch
                {
                    $expectedError = $_
                }
                finally
                {
                    Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode
                }

                $expectedError.FullyQualifiedErrorId | Should -BeExactly "CannotCreateComTypeConstrainedLanguage,Microsoft.PowerShell.Commands.NewObjectCommand"
            }

            It "Verifies New-Object with COM types works back in full language mode again" {

                $result = New-Object -ComObject ADODB.Parameter
                $result.Direction | Should -Be 1
            }
        }
    }

    Describe "New-Item command on function drive in constrained language" -Tags 'Feature','RequireAdminOnWindows' {

        It "Verifies New-Item directory on function drive is not allowed in constrained language mode" {

            try
            {
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
                $null = New-Item -Path function:\SomeEvilFunction -ItemType Directory -Value SomeBadScriptBlock -ErrorAction Stop
                throw "No Exception!"
            }
            catch
            {
                $expectedError = $_
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode
            }

            $expectedError.FullyQualifiedErrorId | Should -BeExactly "NotSupported,Microsoft.PowerShell.Commands.NewItemCommand"
        }
    }

    Describe "Engine events in constrained language mode" -Tags 'Feature','RequireAdminOnWindows' {

        It "Verifies engine event in constrained language mode, its action runs as constrained" {

            try
            {
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"

                $job = Register-EngineEvent LockdownEvent -Action { [object]::Equals("A", "B") }
                $null = New-Event LockdownEvent
                Wait-Job $job
                Unregister-Event LockdownEvent
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode
            }

            $job.Error.FullyQualifiedErrorId | Should -Match "MethodInvocationNotSupportedInConstrainedLanguage"
        }
    }

    Describe "Module scope scripts in constrained language mode" -Tags 'Feature','RequireAdminOnWindows' {

        It "Verifies that while in constrained language mode script run in a module scope also runs constrained" {
            Import-Module PSDiagnostics
            $module = Get-Module PSDiagnostics

            try
            {
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
                & $module { [object]::Equals("A", "B") }
            }
            catch
            {
                $expectedError = $_
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode
                Remove-Module PSDiagnostics -Force -ErrorAction SilentlyContinue
            }

            $expectedError.FullyQualifiedErrorId | Should -BeExactly "CantInvokeCallOperatorAcrossLanguageBoundaries"
        }
    }

    Describe "Switch -file in constrained language mode" -Tags 'Feature','RequireAdminOnWindows' {

        It "Verifies that switch -file will not work in constrained language without provider" {

            [initialsessionstate] $iss = [initialsessionstate]::Create()
            $iss.LanguageMode = "ConstrainedLanguage"
            [runspace] $rs = [runspacefactory]::CreateRunspace($iss)
            $rs.Open()
            $pl = $rs.CreatePipeline("switch -file $testDrive/foo.txt { 'A' { 'B' } }")

            $e = { $pl.Invoke() } | Should -Throw -ErrorId "DriveNotFoundException"

            $rs.Dispose()
        }
    }

    Describe "Get content syntax in constrained language mode" -Tags 'Feature','RequireAdminOnWindows' {

        It "Verifies that the get content syntax returns null value in constrained language without provider" {

            $iss = [initialsessionstate]::Create()
            $iss.LanguageMode = "ConstrainedLanguage"
            $rs = [runspacefactory]::CreateRunspace($iss)
            $rs.Open()
            $pl = $rs.CreatePipeline('${' + "$testDrive/foo.txt}")

            $result = $pl.Invoke()
            $rs.Dispose()

            $result[0] | Should -BeNullOrEmpty
        }
    }

    Describe "Stream redirection in constrained language mode" -Tags 'Feature','RequireAdminOnWindows' {

        It "Verifies that stream redirection doesn't work in constrained language mode without provider" {

            $iss = [initialsessionstate]::CreateDefault2()
            $iss.Providers.Clear()
            $iss.LanguageMode = "ConstrainedLanguage"
            $rs = [runspacefactory]::CreateRunspace($iss)
            $rs.Open()
            $pl = $rs.CreatePipeline('"Hello" > c:\temp\foo.txt')

            $e = { $pl.Invoke() } | Should -Throw -ErrorId "CmdletInvocationException"

            $rs.Dispose()
        }
    }

    Describe "Invoke-Expression in constrained language mode" -Tags 'Feature','RequireAdminOnWindows' {

        BeforeAll {

            function VulnerableFunctionFromFullLanguage { Invoke-Expression $args[0] }

            $TestCasesIEX = @(
                @{testName = "Verifies direct Invoke-Expression does not bypass constrained language mode";
                  scriptblock = { Invoke-Expression '[object]::Equals("A", "B")' } }
                @{testName = "Verifies indirect Invoke-Expression does not bypass constrained language mode";
                  scriptblock = { VulnerableFunctionFromFullLanguage '[object]::Equals("A", "B")' } }
            )
        }

        It "<testName>" -TestCases $TestCasesIEX {

            param ($scriptblock)

            try
            {
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
                & $scriptblock
                throw "No Exception!"
            }
            catch
            {
                $expectedError = $_
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode
            }

            $expectedError.FullyQualifiedErrorId | Should -BeExactly "MethodInvocationNotSupportedInConstrainedLanguage,Microsoft.PowerShell.Commands.InvokeExpressionCommand"
        }
    }

    Describe "Dynamic method invocation in constrained language mode" -Tags 'Feature','RequireAdminOnWindows' {

        It "Verifies dynamic method invocation does not bypass constrained language mode" {

            try
            {
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
                & {
                    $type = [IO.Path]
                    $method = "GetRandomFileName"
                    $type::$method()
                }
                throw "No Exception!"
            }
            catch
            {
                $expectedError = $_
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode
            }

            $expectedError.FullyQualifiedErrorId | Should -BeExactly "MethodInvocationNotSupportedInConstrainedLanguage"
        }

        It "Verifies dynamic methods invocation does not bypass constrained language mode" {

            try
            {
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
                & {
                    $type = [IO.Path]
                    $methods = "GetRandomFileName","GetTempPath"
                    $type::($methods[0])()
                }
                throw "No Exception!"
            }
            catch
            {
                $expectedError = $_
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode
            }

            $expectedError.FullyQualifiedErrorId | Should -BeExactly "MethodInvocationNotSupportedInConstrainedLanguage"
        }
    }

    Describe "Conversion in constrained language mode" -Tags 'Feature','RequireAdminOnWindows' {

        It "Verifies that PowerShell cannot convert disallowed IntPtr type" {

            try
            {
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
                $result = 1234 -as [IntPtr]
                $null -eq $result | Should -BeTrue
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode
            }
        }
    }

    Describe "Variable AllScope in constrained language mode" -Tags 'Feature','RequireAdminOnWindows' {

        It "Verifies Set-Variable cannot create AllScope in constrained language" {

            try
            {
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
                Set-Variable -Name SetVariableAllScopeNotSupported -Value bar -Option AllScope
                throw "No Exception!"
            }
            catch
            {
                $expectedError = $_
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode
            }

            $expectedError.FullyQualifiedErrorId | Should -BeExactly "NotSupported,Microsoft.PowerShell.Commands.SetVariableCommand"
        }

        It "Verifies New-Variable cannot create AllScope in constrained language" {

            try
            {
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
                New-Variable -Name NewVarialbeAllScopeNotSupported -Value bar -Option AllScope
                throw "No Exception!"
            }
            catch
            {
                $expectedError = $_
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode
            }

            $expectedError.FullyQualifiedErrorId | Should -BeExactly "NotSupported,Microsoft.PowerShell.Commands.NewVariableCommand"
        }
    }

    Describe "Data section additional commands in constrained language" -Tags 'Feature','RequireAdminOnWindows' {

        function InvokeDataSectionConstrained
        {
            try
            {
                Invoke-Expression 'data foo -SupportedCommand Add-Type { Add-Type }'
                throw "No Exception!"
            }
            catch
            {
                return $_
            }
        }

        It "Verifies data section Add-Type additional command is disallowed in constrained language" {

            try
            {
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"

                $exception1 = InvokeDataSectionConstrained
                # Repeat to make sure the first time properly restored the language mode to constrained.
                $exception2 = InvokeDataSectionConstrained
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode
            }

            $exception1.FullyQualifiedErrorId | Should -Match "DataSectionAllowedCommandDisallowed"
            $exception2.FullyQualifiedErrorId | Should -Match "DataSectionAllowedCommandDisallowed"
        }

        It "Verifies data section with no-constant expression Add-Type additional command is disallowed in constrained language" {

            try
            {
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
                $addedCommand = "Add-Type"
                Invoke-Expression 'data foo -SupportedCommand $addedCommand { Add-Type }'
                throw "No Exception!"
            }
            catch
            {
                $expectedError = $_
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode
            }

            $expectedError.FullyQualifiedErrorId | Should -BeExactly "DataSectionAllowedCommandDisallowed,Microsoft.PowerShell.Commands.InvokeExpressionCommand"
        }
    }

    Describe "Add-Type in no language mode on locked down system" -Tags 'Feature','RequireAdminOnWindows' {

        It "Verifies Add-Type fails in no language mode when in system lock down" {

            # Create No-Language session, that allows Add-Type cmdlet
            $entry = [System.Management.Automation.Runspaces.SessionStateCmdletEntry]::new('Add-Type', [Microsoft.PowerShell.Commands.AddTypeCommand], $null)
            $iss = [initialsessionstate]::CreateRestricted([System.Management.Automation.SessionCapabilities]::Language)
            $iss.Commands.Add($entry)
            $rs = [runspacefactory]::CreateRunspace($iss)
            $rs.Open()

            # Try to use Add-Type in No-Language session
            $ps = [powershell]::Create($rs)
            $ps.AddCommand('Add-Type').AddParameter('TypeDefinition', 'public class C1 { }')
            $expectedError = $null
            try
            {
                Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode
                $ps.Invoke()
            }
            catch
            {
                $expectedError = $_
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode
                $rs.Dispose()
                $ps.Dispose()
            }

            $expectedError.Exception.InnerException.ErrorRecord.FullyQualifiedErrorId | Should -BeExactly 'CannotDefineNewType,Microsoft.PowerShell.Commands.AddTypeCommand'
        }
    }

    Describe "Import-LocalizedData additional commands in constrained language" -Tags 'Feature','RequireAdminOnWindows' {

        It "Verifies Import-LocalizedData disallows Add-Type in constrained language" {

            try
            {
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
                & {
                    $localizedDataFileName = Join-Path $TestDrive ImportLocalizedDataAdditionalCommandsNotSupported.psd1
                    $null = New-Item -ItemType File -Path $localizedDataFileName -Force
                    Import-LocalizedData -SupportedCommand Add-Type -BaseDirectory $TestDrive -FileName ImportLocalizedDataAdditionalCommandsNotSupported
                }
            }
            catch
            {
                $expectedError = $_
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode
            }

            $expectedError.FullyQualifiedErrorId | Should -BeExactly "CannotDefineSupportedCommand,Microsoft.PowerShell.Commands.ImportLocalizedData"
        }
    }

    Describe "Where and Foreach operators should not allow unapproved types in constrained language" -Tags 'Feature','RequireAdminOnWindows' {

        BeforeAll {

            $script1 = @'
                $data = @(
                    @{
                        Node = "first"
                        Value1 = 1
                        Value2 = 2
                        first = $true
                    }
                    @{
                        Node = "second"
                        Value1 = 3
                        Value2 = 4
                        Second = $true
                    }
                    @{
                        Node = "third"
                        Value1 = 5
                        Value2 = 6
                        third = $true
                    }
                )

                $result = $data.where{$_.Node -eq "second"}
                Write-Output $result

                # Execute method in scriptblock of where operator, should throw in ConstrainedLanguage mode.
                $data.where{[system.io.path]::GetRandomFileName() -eq "Hello"}
'@

            $script2 = @'
                $data = @(
                    @{
                        Node = "first"
                        Value1 = 1
                        Value2 = 2
                        first = $true
                    }
                    @{
                        Node = "second"
                        Value1 = 3
                        Value2 = 4
                        Second = $true
                    }
                    @{
                        Node = "third"
                        Value1 = 5
                        Value2 = 6
                        third = $true
                    }
                )

                $result = $data.ForEach('value1')
                Write-Output $result

                # Execute method in scriptblock of ForEach operator, should throw in ConstrainedLanguage mode.
                $data.ForEach{[system.io.path]::GetRandomFileName().Length}
'@

            $script3 = @'
            # Method call should throw error.
            (Get-Process powershell*).ForEach('GetHashCode')
'@

            $script4 = @'
            # Where method call should throw error.
            (get-process powershell).where{$_.GetType().FullName -match "process"}
'@

            $TestCasesForeach = @(
                @{testName = "Verify where statement with invalid method call in constrained language is disallowed"; script = $script1 }
                @{testName = "Verify foreach statement with invalid method call in constrained language is disallowed"; script = $script2 }
                @{testName = "Verify foreach statement with embedded method call in constrained language is disallowed"; script = $script3 }
                @{testName = "Verify where statement with embedded method call in constrained language is disallowed"; script = $script4 }
            )
        }

        It "<testName>" -TestCases $TestCasesForeach {

            param (
                [string] $script
            )

            try
            {
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
                & {
                    # Scriptblock must be created inside constrained language.
                    $sb = [scriptblock]::Create($script)
                    & sb
                }
                throw "No Exception!"
            }
            catch
            {
                $expectedError = $_
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode
            }

            $expectedError.FullyQualifiedErrorId | Should -BeExactly "MethodInvocationNotSupportedInConstrainedLanguage"
        }
    }

    Describe "ThreadJob Constrained Language Tests" -Tags 'Feature','RequireAdminOnWindows' {

        BeforeAll {

            $sb = { $ExecutionContext.SessionState.LanguageMode }
        }

        It "ThreadJob script must run in ConstrainedLanguage mode with system lock down" {

            try
            {
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
                Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode

                $results = Start-ThreadJob -ScriptBlock { $ExecutionContext.SessionState.LanguageMode } | Wait-Job | Receive-Job
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode
            }

            $results | Should -BeExactly "ConstrainedLanguage"
        }

        It "ThreadJob script block using variable must run in ConstrainedLanguage mode with system lock down" {

            try
            {
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
                Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode

                $results = Start-ThreadJob -ScriptBlock { & $using:sb } | Wait-Job | Receive-Job
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode
            }

            $results | Should -BeExactly "ConstrainedLanguage"
        }

        It "ThreadJob script block argument variable must run in ConstrainedLanguage mode with system lock down" {

            try
            {
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
                Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode

                $results = Start-ThreadJob -ScriptBlock { param ($sb) & $sb } -ArgumentList $sb | Wait-Job | Receive-Job
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode
            }

            $results | Should -BeExactly "ConstrainedLanguage"
        }

        It "ThreadJob script block piped variable must run in ConstrainedLanguage mode with system lock down" {

            try
            {
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
                Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode

                $results = $sb | Start-ThreadJob -ScriptBlock { $input | ForEach-Object { & $_ } } | Wait-Job | Receive-Job
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode
            }

            $results | Should -BeExactly "ConstrainedLanguage"
        }
    }

    Describe "ForEach-Object -Parallel Constrained Language Tests" -Tags 'Feature','RequireAdminOnWindows' {

        It 'Foreach-Object -Parallel must run in ConstrainedLanguage mode under system lock down' {

            try
            {
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
                Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode

                $results = 1..1 | ForEach-Object -Parallel { $ExecutionContext.SessionState.LanguageMode }
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode
            }

            $results | Should -BeExactly "ConstrainedLanguage"
        }
    }

    Describe "Dot sourced script block functions from trusted script files should not run FullLanguage in ConstrainedLanguage context" -Tags 'Feature','RequireAdminOnWindows' {

        BeforeAll {

            $scriptFileName = "TrustedScriptBlockTest_System32"
            $scriptFilePath = Join-Path $TestDrive ($scriptFileName + ".ps1")
            @'
            function TrustedFn {
                Write-Output $ExecutionContext.SessionState.LanguageMode
            }
'@ | Out-File -FilePath $scriptFilePath

            $scriptModuleName = "UntrustedModuleScriptBlockTest"
            $scriptModulePath = Join-Path $TestDrive ($scriptModuleName + ".psm1")
            @'
            function RunScriptBlock {{
                $sb = (Get-Command -Name {0}).ScriptBlock

                # ScriptBlock trusted function, TrustedFn, is dot sourced into current scope
                1 | ForEach-Object $sb
                TrustedFn
            }}
'@ -f $scriptFilePath | Out-File -FilePath $scriptModulePath
        }

        AfterAll {
            Remove-Module $scriptModuleName -Force -ErrorAction SilentlyContinue
        }

        It "Verifies a scriptblock from a trusted script file does not run as trusted" {
            if (Test-IsWindowsArm64) {
                Set-ItResult -Pending -Because "https://github.com/PowerShell/PowerShell/issues/20169"
                return
            }

            $result = $null

            try
            {
                Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode
                # Wait for the lockdown mode to take effect
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"

                # Import untrusted module
                Import-Module -Name $scriptModulePath -Force

                # Run module function that dot sources TrustedFn and runs it in module scope
                $result = RunScriptBlock
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode
            }

            # Ensure scriptblock TrustedFn function ran as untrusted
            $result | Should -BeExactly "ConstrainedLanguage"
        }
    }

    Describe "Dot sourcing trusted script in ConstrainedLanguage context is allowed when importing modules" -Tags 'Feature','RequireAdminOnWindows' {

        BeforeAll {

            $importModuleName = "ToImportTrustedModuleTest_System32"
            $importModulePath = Join-Path $TestDrive ($importModuleName + ".psm1")
            $modScript = @'
            function ImportModuleFn { "ImportModuleFn: $($ExecutionContext.SessionState.LanguageMode)" }
            Export-ModuleMember -Function "ImportModuleFn"
'@ | Out-File -FilePath $importModulePath

            $scriptModuleName = "ImportTrustedModuleTest_System32"
            $scriptModulePath = Join-Path $TestDrive ($scriptModuleName + ".psm1")
            @'
            Import-Module -Name {0} -Force
            function ModuleFn {{ "ModuleFn: $($ExecutionContext.SessionState.LanguageMode)" }}
            Export-ModuleMember -Function "ModuleFn","ImportModuleFn"
'@ -f $importModulePath | Out-File -FilePath $scriptModulePath
        }

        AfterAll {
            Remove-Module $importModuleName -Force -ErrorAction SilentlyContinue
            Remove-Module $scriptModuleName -Force -ErrorAction SilentlyContinue
        }

        It "Verifies that trusted module functions run in FullLanguage" {

            $result1 = $null
            $result2 = $null

            try
            {
                Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"

                Import-Module -Name $scriptModulePath -Force


                $result1 = ModuleFn
                $result2 = ImportModuleFn
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode
            }

            $result1 | Should -BeExactly "ModuleFn: FullLanguage"
            $result2 | Should -BeExactly "ImportModuleFn: FullLanguage"
        }
    }

    Describe "PowerShell classes are not allowed in constrained language mode" -Tags 'Feature','RequireAdminOnWindows' {

        BeforeAll {

            $randomClassName = "class_$(Get-Random -Max 9999)"

            $script = "class ${randomClassName} { static [string] GetLanguageMode() { return (Get-Variable -ValueOnly -Name ExecutionContext).SessionState.LanguageMode } }"

            $modulePathName = "modulePath_$(Get-Random -Max 9999)"
            $modulePath = Join-Path $testdrive $modulePathName
            New-Item -Path $modulePath -ItemType Directory -Force

            $untrustedScriptFile = Join-Path $modulePath "T1ScriptClass.ps1"
            $script | Out-File -FilePath $untrustedScriptFile

            $untrustedScriptModule = Join-Path $modulePath "T1ScriptClass.psm1"
            $script | Out-File -FilePath $untrustedScriptModule

            $trustedScriptFile = Join-Path $modulePath "T1ScriptClass_System32.ps1"
            $script | Out-File -FilePath $trustedScriptFile

            $trustedScriptModule = Join-Path $modulePath "T1ScriptClass_System32.psm1"
            $script | Out-File -FilePath $trustedScriptModule
        }

        AfterAll {

            Remove-Module -Name T1ScriptClass_System32 -Force -ErrorAction Ignore
            Remove-Module -Name T1ScriptClass -Force -ErrorAction Ignore
        }

        It "Verifies that classes cannot be created in script running under constrained language" {

            try
            {
                Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"

                Invoke-Expression -Command $script 2>$null -ErrorAction Stop
                throw "No Error!"
            }
            catch
            {
                $expectedError = $_
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode
            }

            $expectedError.FullyQualifiedErrorId | Should -BeExactly "ClassesNotAllowedInConstrainedLanguage,Microsoft.PowerShell.Commands.InvokeExpressionCommand"
        }

        It "Verifies that classes cannot be created in script files running under constrained language" {

            try {
                $ps = [powershell]::Create("NewRunspace")
                $ps.Runspace.LanguageMode = "ConstrainedLanguage"
                $result = $ps.AddScript($untrustedScriptFile).Invoke()
                $ps.Streams.Error[0].FullyQualifiedErrorId | Should -BeExactly  "ClassesNotAllowedInConstrainedLanguage" -Because "Invoke-Command should fail in constrained language"
            }
            catch {
                $_ | Should -BeNullOrEmpty -Because "exception '$_' unexpected."
            }
            finally {
                $ps.Dispose()
            }
        }

        It "Verifies that classes cannot be created in untrusted script modules running under constrained language" {
            try {
                $ps = [powershell]::Create("NewRunspace")
                $ps.Runspace.LanguageMode = "ConstrainedLanguage"
                # importing the module whilst in constrained language makes it untrusted, even without lockdown mode
                $ps.AddCommand("Import-Module").AddParameter("Name", $untrustedScriptModule).Invoke()
                $ps.Streams.Error[0].FullyQualifiedErrorId | Should -BeExactly  "ClassesNotAllowedInConstrainedLanguage" -Because "Import-Module should fail in constrained language"
            }
            catch {
                $_ | Should -BeNullOrEmpty -Because "exception '$_' unexpected."
            }
            finally {
                $ps.Dispose()
            }
        }

        It "Verifies that classes can be created in trusted script files running under constrained language" {

            try
            {
                Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"

                { & ($trustedScriptFile) } | Should -Not -Throw
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode -RevertLockdownMode
            }
        }

        It "Verifies that classes can be created in trusted script modules running under constrained language" {

            try
            {
                Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"

                { Import-Module -Name $trustedScriptModule -ErrorAction Stop } | Should -Not -Throw
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode -RevertLockdownMode
            }
        }
    }

    Describe "Invoke-History should not run command lines in FullLanguage mode when system is locked down" -Tags 'Feature','RequireAdminOnWindows' {

        BeforeAll {

            $LanguageModeHistoryFilePath = Join-Path $TestDrive "LanguageModeHistory.XML"

            # $ExecutionContext.SessionState.LanguageMode command line history item clixml
            @'
            <Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
                <Obj RefId="0">
                <TN RefId="0">
                    <T>Microsoft.PowerShell.Commands.HistoryInfo</T>
                    <T>System.Object</T>
                </TN>
                <ToString>$ExecutionContext.SessionState.LanguageMode</ToString>
                <Props>
                    <I64 N="Id">123</I64>
                    <S N="CommandLine">$ExecutionContext.SessionState.LanguageMode</S>
                    <Obj N="ExecutionStatus" RefId="1">
                    <TN RefId="1">
                        <T>System.Management.Automation.Runspaces.PipelineState</T>
                        <T>System.Enum</T>
                        <T>System.ValueType</T>
                        <T>System.Object</T>
                    </TN>
                    <ToString>Completed</ToString>
                    <I32>4</I32>
                    </Obj>
                    <DT N="StartExecutionTime">2018-07-26T14:36:33.923608-07:00</DT>
                    <DT N="EndExecutionTime">2018-07-26T14:36:33.9266018-07:00</DT>
                </Props>
                </Obj>
            </Objs>
'@ | Out-File -FilePath $LanguageModeHistoryFilePath

            $historyItem = Import-Clixml -Path $LanguageModeHistoryFilePath
        }

        It "Verifies that Invoke-History runs command lines in ConstrainedLanguage" {

            $result = $null
            try
            {
                Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"

                # Add "$ExecutionContext.SessionState.LanguageMode" command line to history
                $historyItem | Add-History

                # Retrieve history item command and invoke
                $retrievedItem = Get-History -Count 1
                $result = $retrievedItem | Invoke-History
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode
            }

            $result | Should -BeExactly "ConstrainedLanguage"
        }
    }

    Describe "Enter-PSHostProcess cmdlet should be disabled on locked down systems" -Tags 'Feature','RequireAdminOnWindows' {

        It "Verifies that Enter-PSHostProcess is disabled with lock down policy" {

            $expectedError = $null
            try
            {
                Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode
                $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"

                Enter-PSHostProcess -Id 5555 -ErrorAction Stop
            }
            catch
            {
                $expectedError = $_
            }
            finally
            {
                Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode
            }

            $expectedError.FullyQualifiedErrorId | Should -BeExactly 'EnterPSHostProcessCmdletDisabled,Microsoft.PowerShell.Commands.EnterPSHostProcessCommand'
        }
    }

    # End Describe blocks
}
finally
{
    if ($defaultParamValues -ne $null)
    {
        $Global:PSDefaultParameterValues = $defaultParamValues
    }
}
