Skip to content

Commit

Permalink
Support for PowerShell CLM (#4923)
Browse files Browse the repository at this point in the history
* Moving Vsts commands from CLI to dedicated PS script file

---------

Co-authored-by: Tim Brigham <[email protected]>
Co-authored-by: Denis Nikulin <[email protected]>
  • Loading branch information
3 people authored Aug 8, 2024
1 parent 18448fa commit f9260a0
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 1 deletion.
6 changes: 6 additions & 0 deletions src/Agent.Sdk/Knob/AgentKnobs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -731,5 +731,11 @@ public class AgentKnobs
"Show warning message on the OS which is not supported by .NET 8",
new PipelineFeatureSource("Net8UnsupportedOsWarning"),
new BuiltInDefaultKnobSource("true"));

public static readonly Knob UsePSScriptWrapper = new Knob(
nameof(UsePSScriptWrapper),
"Use PowerShell script wrapper to handle PowerShell ConstrainedLanguage mode.",
new PipelineFeatureSource("UsePSScriptWrapper"),
new BuiltInDefaultKnobSource("false"));
}
}
17 changes: 16 additions & 1 deletion src/Agent.Worker/Handlers/PowerShell3Handler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,26 @@ public async Task RunAsync()
ArgUtil.File(moduleFile, nameof(moduleFile));

// Craft the args to pass to PowerShell.exe.
string powerShellExeArgs = StringUtil.Format(
string powerShellExeArgs = string.Empty;

if (AgentKnobs.UsePSScriptWrapper.GetValue(ExecutionContext).AsBoolean())
{
powerShellExeArgs = StringUtil.Format(
@"-NoLogo -Sta -NoProfile -ExecutionPolicy Unrestricted -Command ""{3}"" -VstsSdkPath {0} -DebugOption {1} -ScriptBlockString ""{2}""",
StepHost.ResolvePathForStepHost(moduleFile).Replace("'", "''"), // nested within a single-quoted string module file name arg #0
ExecutionContext.Variables.System_Debug == true ? "Continue" : "SilentlyContinue", // system debug status variable arg #1
StepHost.ResolvePathForStepHost(scriptFile).Replace("'", "''''"), // nested within a single-quoted string within a single-quoted string arg #2
Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Bin), "powershell", "Start-AzpTask.ps1") // path to wrapper script arg #3
); // nested within a single-quoted string within a single-quoted string
}
else
{
powerShellExeArgs = StringUtil.Format(
@"-NoLogo -Sta -NoProfile -NonInteractive -ExecutionPolicy Unrestricted -Command "". ([scriptblock]::Create('if ([Console]::InputEncoding -is [Text.UTF8Encoding] -and [Console]::InputEncoding.GetPreamble().Length -ne 0) {{ [Console]::InputEncoding = New-Object Text.UTF8Encoding $false }} if (!$PSHOME) {{ $null = Get-Item -LiteralPath ''variable:PSHOME'' }} else {{ Import-Module -Name ([System.IO.Path]::Combine($PSHOME, ''Modules\Microsoft.PowerShell.Management\Microsoft.PowerShell.Management.psd1'')) ; Import-Module -Name ([System.IO.Path]::Combine($PSHOME, ''Modules\Microsoft.PowerShell.Utility\Microsoft.PowerShell.Utility.psd1'')) }}')) 2>&1 | ForEach-Object {{ Write-Verbose $_.Exception.Message -Verbose }} ; Import-Module -Name '{0}' -ArgumentList @{{ NonInteractive = $true }} -ErrorAction Stop ; $VerbosePreference = '{1}' ; $DebugPreference = '{1}' ; Invoke-VstsTaskScript -ScriptBlock ([scriptblock]::Create('. ''{2}'''))""",
StepHost.ResolvePathForStepHost(moduleFile).Replace("'", "''"), // nested within a single-quoted string
ExecutionContext.Variables.System_Debug == true ? "Continue" : "SilentlyContinue",
StepHost.ResolvePathForStepHost(scriptFile).Replace("'", "''''")); // nested within a single-quoted string within a single-quoted string
}

// Resolve powershell.exe.
string powerShellExe = "powershell.exe";
Expand Down
84 changes: 84 additions & 0 deletions src/Misc/layoutbin/powershell/Start-AzpTask.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<#
A PowerShell script that is used to invoke a VSTS task script. This script is used by the VSTS task runner to invoke the task script.
This script replaces some legacy stuff in PowerShell3Handler.cs and turns it into a dedicated signed script.
since it is parameterized it can be signed and trusted for WDAC and CLM.
#>

param (
[Parameter(mandatory = $true)]
[string]$VstsSdkPath,

[Parameter(mandatory = $true)]
[string]$DebugOption,

[Parameter(mandatory = $true)]
[string]$ScriptBlockString

)

function Get-ClmStatus {
# This is new functionality to detect if we are running in a constrained language mode.
# This is only used to display debug data if the device is in CLM mode by default.

# Create a temp file and add the command which not allowed in constrained language mode.
$tempFileGuid = New-Guid | Select-Object -Expand Guid
$tempFile = "$($env:AGENT_TEMPDIRECTORY)\$($tempFileGuid).ps1"

Write-Output '$null = New-Object -TypeName System.Collections.ArrayList' | Out-File -FilePath $tempFile

try {
. $tempFile
$status = "FullLanguage"
}
catch [System.Management.Automation.PSNotSupportedException] {
$status = "ConstrainedLanguage"
}

Remove-Item $tempFile
return $status
}

$VerbosePreference = $DebugOption
$DebugPreference = $DebugOption

if (!$PSHOME) {
Write-Error -Message "The execution cannot be continued since the PSHOME variable is not defined." -ErrorAction Stop
}

# Check if the device is in CLM mode by default.
$clmResults = Get-ClmStatus
Write-Verbose "PowerShell Language mode: $($clmResults)"

if ([Console]::InputEncoding -is [Text.UTF8Encoding] -and [Console]::InputEncoding.GetPreamble().Length -ne 0) {
[Console]::InputEncoding = New-Object Text.UTF8Encoding $false
}

Import-Module -Name ([System.IO.Path]::Combine($PSHOME, 'Modules\Microsoft.PowerShell.Management\Microsoft.PowerShell.Management.psd1'))
Import-Module -Name ([System.IO.Path]::Combine($PSHOME, 'Modules\Microsoft.PowerShell.Utility\Microsoft.PowerShell.Utility.psd1'))

$importSplat = @{
Name = $VstsSdkPath
ErrorAction = 'Stop'
}

# Import the module and catch any errors
try {
Import-Module @importSplat
}
catch {
Write-Verbose $_.Exception.Message -Verbose
throw $_.Exception
}

# Now create the task and hand of to the task script
try {
Invoke-VstsTaskScript -ScriptBlock ([scriptblock]::Create( $ScriptBlockString ))
}
# We want to add improved error handling here - if the error is "xxx\powershell.ps1 is not recognized as the name of a cmdlet, function, script file, or operable program"
#
catch {
Write-Verbose "Invoke-VstsTaskScript -ScriptBlock ([scriptblock]::Create( $ScriptBlockString ))"
Write-Verbose $_.Exception.Message -Verbose
throw $_.Exception
}
#

0 comments on commit f9260a0

Please sign in to comment.