ansible-core 2.19.0b4__py3-none-any.whl → 2.19.0b5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. ansible/_internal/__init__.py +1 -1
  2. ansible/_internal/_collection_proxy.py +1 -1
  3. ansible/_internal/_errors/_alarm_timeout.py +66 -0
  4. ansible/_internal/_errors/_captured.py +25 -30
  5. ansible/_internal/_errors/_error_factory.py +89 -0
  6. ansible/_internal/_errors/_error_utils.py +240 -0
  7. ansible/_internal/_errors/_task_timeout.py +28 -0
  8. ansible/_internal/_event_formatting.py +127 -0
  9. ansible/_internal/_json/__init__.py +5 -5
  10. ansible/_internal/_json/_profiles/_cache_persistence.py +2 -0
  11. ansible/_internal/_json/_profiles/_inventory_legacy.py +1 -1
  12. ansible/_internal/_json/_profiles/_legacy.py +3 -11
  13. ansible/_internal/_ssh/__init__.py +0 -0
  14. ansible/_internal/_ssh/_agent_launch.py +91 -0
  15. ansible/{utils → _internal/_ssh}/_ssh_agent.py +55 -93
  16. ansible/_internal/_templating/__init__.py +5 -3
  17. ansible/_internal/_templating/_datatag.py +2 -1
  18. ansible/_internal/_templating/_engine.py +3 -4
  19. ansible/_internal/_templating/_jinja_bits.py +21 -16
  20. ansible/_internal/_templating/_jinja_common.py +18 -27
  21. ansible/_internal/_templating/_jinja_plugins.py +31 -3
  22. ansible/_internal/_templating/_lazy_containers.py +5 -5
  23. ansible/_internal/_templating/_transform.py +20 -19
  24. ansible/_internal/_templating/_utils.py +1 -1
  25. ansible/_internal/_yaml/_dumper.py +1 -1
  26. ansible/_internal/_yaml/_errors.py +7 -7
  27. ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/true_type.py +1 -1
  28. ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/unmask.py +1 -1
  29. ansible/cli/__init__.py +5 -82
  30. ansible/cli/arguments/option_helpers.py +2 -3
  31. ansible/cli/doc.py +84 -28
  32. ansible/cli/inventory.py +1 -1
  33. ansible/compat/importlib_resources.py +9 -12
  34. ansible/config/base.yml +22 -0
  35. ansible/errors/__init__.py +96 -49
  36. ansible/executor/module_common.py +8 -10
  37. ansible/executor/powershell/async_watchdog.ps1 +2 -2
  38. ansible/executor/powershell/async_wrapper.ps1 +3 -3
  39. ansible/executor/powershell/become_wrapper.ps1 +20 -2
  40. ansible/executor/powershell/bootstrap_wrapper.ps1 +28 -6
  41. ansible/executor/powershell/coverage_wrapper.ps1 +15 -6
  42. ansible/executor/powershell/exec_wrapper.ps1 +219 -6
  43. ansible/executor/powershell/module_manifest.py +52 -0
  44. ansible/executor/powershell/module_wrapper.ps1 +47 -21
  45. ansible/executor/powershell/powershell_expand_user.ps1 +20 -0
  46. ansible/executor/powershell/powershell_mkdtemp.ps1 +17 -0
  47. ansible/executor/process/worker.py +38 -113
  48. ansible/executor/task_executor.py +26 -61
  49. ansible/executor/task_result.py +2 -4
  50. ansible/galaxy/collection/__init__.py +1 -4
  51. ansible/inventory/manager.py +1 -1
  52. ansible/module_utils/_internal/__init__.py +0 -3
  53. ansible/module_utils/_internal/_ambient_context.py +3 -3
  54. ansible/module_utils/_internal/_ansiballz.py +4 -2
  55. ansible/module_utils/_internal/_datatag/__init__.py +20 -14
  56. ansible/module_utils/_internal/_datatag/_tags.py +2 -2
  57. ansible/module_utils/_internal/_deprecator.py +66 -48
  58. ansible/module_utils/_internal/_errors.py +88 -17
  59. ansible/module_utils/_internal/_event_utils.py +61 -0
  60. ansible/module_utils/_internal/_json/_profiles/__init__.py +21 -4
  61. ansible/module_utils/_internal/_json/_profiles/_module_legacy_c2m.py +2 -0
  62. ansible/module_utils/_internal/_json/_profiles/_module_legacy_m2c.py +2 -0
  63. ansible/module_utils/_internal/_json/_profiles/_tagless.py +3 -1
  64. ansible/module_utils/{common/messages.py → _internal/_messages.py} +28 -47
  65. ansible/module_utils/_internal/_patches/_dataclass_annotation_patch.py +1 -3
  66. ansible/module_utils/_internal/_plugin_info.py +1 -1
  67. ansible/module_utils/_internal/_stack.py +22 -0
  68. ansible/module_utils/_internal/_text_utils.py +6 -0
  69. ansible/module_utils/_internal/_traceback.py +11 -8
  70. ansible/module_utils/ansible_release.py +1 -1
  71. ansible/module_utils/basic.py +49 -15
  72. ansible/module_utils/common/arg_spec.py +2 -2
  73. ansible/module_utils/common/collections.py +6 -0
  74. ansible/module_utils/common/json.py +2 -2
  75. ansible/module_utils/common/text/converters.py +3 -3
  76. ansible/module_utils/common/validation.py +1 -1
  77. ansible/module_utils/common/warnings.py +80 -23
  78. ansible/module_utils/common/yaml.py +1 -1
  79. ansible/module_utils/datatag.py +5 -2
  80. ansible/module_utils/facts/system/distribution.py +16 -3
  81. ansible/module_utils/facts/virtual/linux.py +1 -1
  82. ansible/module_utils/service.py +2 -9
  83. ansible/modules/apt_repository.py +7 -29
  84. ansible/modules/async_status.py +13 -11
  85. ansible/modules/async_wrapper.py +5 -5
  86. ansible/modules/dnf5.py +14 -22
  87. ansible/modules/hostname.py +0 -1
  88. ansible/modules/service.py +3 -9
  89. ansible/parsing/ajson.py +3 -5
  90. ansible/parsing/dataloader.py +4 -4
  91. ansible/parsing/mod_args.py +1 -1
  92. ansible/parsing/plugin_docs.py +2 -2
  93. ansible/parsing/utils/yaml.py +3 -3
  94. ansible/parsing/vault/__init__.py +4 -4
  95. ansible/playbook/playbook_include.py +1 -1
  96. ansible/playbook/taggable.py +0 -3
  97. ansible/plugins/__init__.py +0 -25
  98. ansible/plugins/action/__init__.py +8 -31
  99. ansible/plugins/action/add_host.py +1 -1
  100. ansible/plugins/action/assemble.py +8 -16
  101. ansible/plugins/action/async_status.py +7 -2
  102. ansible/plugins/action/copy.py +8 -7
  103. ansible/plugins/action/gather_facts.py +8 -8
  104. ansible/plugins/action/package.py +5 -8
  105. ansible/plugins/action/script.py +8 -15
  106. ansible/plugins/action/service.py +3 -7
  107. ansible/plugins/action/template.py +3 -8
  108. ansible/plugins/action/unarchive.py +5 -15
  109. ansible/plugins/action/uri.py +9 -20
  110. ansible/plugins/callback/__init__.py +4 -6
  111. ansible/plugins/callback/junit.py +4 -2
  112. ansible/plugins/connection/local.py +2 -2
  113. ansible/plugins/connection/ssh.py +17 -9
  114. ansible/plugins/connection/winrm.py +5 -2
  115. ansible/plugins/doc_fragments/constructed.py +2 -2
  116. ansible/plugins/filter/core.py +13 -6
  117. ansible/plugins/filter/encryption.py +4 -4
  118. ansible/plugins/inventory/__init__.py +11 -10
  119. ansible/plugins/inventory/script.py +1 -1
  120. ansible/plugins/list.py +69 -16
  121. ansible/plugins/loader.py +7 -7
  122. ansible/plugins/lookup/csvfile.py +16 -71
  123. ansible/plugins/lookup/first_found.py +2 -1
  124. ansible/plugins/shell/__init__.py +56 -2
  125. ansible/plugins/shell/powershell.py +66 -9
  126. ansible/plugins/shell/sh.py +9 -5
  127. ansible/plugins/test/core.py +21 -15
  128. ansible/plugins/test/finished.yml +1 -1
  129. ansible/plugins/test/uri.py +2 -5
  130. ansible/release.py +1 -1
  131. ansible/template/__init__.py +30 -2
  132. ansible/utils/display.py +103 -128
  133. ansible/utils/hashing.py +0 -1
  134. ansible/utils/listify.py +6 -4
  135. ansible/utils/unsafe_proxy.py +1 -1
  136. ansible/vars/hostvars.py +1 -1
  137. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/METADATA +1 -1
  138. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/RECORD +162 -151
  139. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/WHEEL +1 -1
  140. ansible_test/_data/completion/docker.txt +3 -3
  141. ansible_test/_data/completion/remote.txt +1 -0
  142. ansible_test/_data/requirements/sanity.ansible-doc.txt +1 -1
  143. ansible_test/_data/requirements/sanity.changelog.txt +2 -2
  144. ansible_test/_data/requirements/sanity.pep8.txt +1 -1
  145. ansible_test/_data/requirements/sanity.pylint.txt +4 -4
  146. ansible_test/_data/requirements/sanity.yamllint.txt +1 -1
  147. ansible_test/_internal/util.py +20 -0
  148. ansible_test/_util/controller/sanity/pylint/config/ansible-test-target.cfg +1 -0
  149. ansible_test/_util/controller/sanity/pylint/config/ansible-test.cfg +1 -0
  150. ansible_test/_util/controller/sanity/pylint/config/code-smell.cfg +1 -0
  151. ansible_test/_util/controller/sanity/pylint/config/collection.cfg +1 -0
  152. ansible_test/_util/controller/sanity/pylint/config/default.cfg +1 -0
  153. ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +61 -7
  154. ansible_test/_util/target/setup/bootstrap.sh +31 -0
  155. ansible/_internal/_errors/_utils.py +0 -310
  156. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/entry_points.txt +0 -0
  157. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/licenses/COPYING +0 -0
  158. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/licenses/licenses/Apache-License.txt +0 -0
  159. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
  160. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/licenses/licenses/MIT-license.txt +0 -0
  161. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/licenses/licenses/PSF-license.txt +0 -0
  162. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
  163. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/top_level.txt +0 -0
@@ -18,10 +18,32 @@ foreach ($obj in $code.params.PSObject.Properties) {
18
18
  $splat[$obj.Name] = $obj.Value
19
19
  }
20
20
 
21
- $cmd = [System.Management.Automation.Language.Parser]::ParseInput(
22
- $code.script,
23
- "$($code.name).ps1", # Name is used in stack traces.
24
- [ref]$null,
25
- [ref]$null).GetScriptBlock()
21
+ $filePath = $null
22
+ try {
23
+ $cmd = if ($ExecutionContext.SessionState.LanguageMode -eq 'FullLanguage') {
24
+ # In FLM we can just invoke the code as a scriptblock without touching the
25
+ # disk.
26
+ [System.Management.Automation.Language.Parser]::ParseInput(
27
+ $code.script,
28
+ "$($code.name).ps1", # Name is used in stack traces.
29
+ [ref]$null,
30
+ [ref]$null).GetScriptBlock()
31
+ }
32
+ else {
33
+ # CLM needs to execute code from a file for it to run in FLM when trusted.
34
+ # Set-Item on 5.1 doesn't have a way to use UTF-8 without a BOM but luckily
35
+ # New-Item does that by default for both 5.1 and 7. We need to ensure we
36
+ # use UTF-8 without BOM so the signature is correct.
37
+ $filePath = Join-Path -Path $env:TEMP -ChildPath "$($code.name)-$(New-Guid).ps1"
38
+ $null = New-Item -Path $filePath -Value $code.script -ItemType File -Force
39
+
40
+ $filePath
41
+ }
26
42
 
27
- $input | & $cmd @splat
43
+ $input | & $cmd @splat
44
+ }
45
+ finally {
46
+ if ($filePath -and (Test-Path -LiteralPath $filePath)) {
47
+ Remove-Item -LiteralPath $filePath -Force
48
+ }
49
+ }
@@ -28,8 +28,12 @@ $Host.Runspace.Debugger.SetDebugMode([DebugModes]::RemoteScript)
28
28
  Function New-CoverageBreakpointsForScriptBlock {
29
29
  Param (
30
30
  [Parameter(Mandatory)]
31
- [ScriptBlock]
32
- $ScriptBlock,
31
+ [string]
32
+ $ScriptName,
33
+
34
+ [Parameter(Mandatory)]
35
+ [ScriptBlockAst]
36
+ $ScriptBlockAst,
33
37
 
34
38
  [Parameter(Mandatory)]
35
39
  [String]
@@ -39,7 +43,7 @@ Function New-CoverageBreakpointsForScriptBlock {
39
43
  $predicate = {
40
44
  $args[0] -is [CommandBaseAst]
41
45
  }
42
- $scriptCmds = $ScriptBlock.Ast.FindAll($predicate, $true)
46
+ $scriptCmds = $ScriptBlockAst.FindAll($predicate, $true)
43
47
 
44
48
  # Create an object that tracks the Ansible path of the file and the breakpoints that have been set in it
45
49
  $info = [PSCustomObject]@{
@@ -68,7 +72,7 @@ Function New-CoverageBreakpointsForScriptBlock {
68
72
  }
69
73
 
70
74
  # Action is explicitly $null as it will slow down the runtime quite dramatically.
71
- $b = $lineCtor.Invoke(@($ScriptBlock.File, $cmd.Extent.StartLineNumber, $cmd.Extent.StartColumnNumber, $null))
75
+ $b = $lineCtor.Invoke(@($ScriptName, $cmd.Extent.StartLineNumber, $cmd.Extent.StartColumnNumber, $null))
72
76
  $info.Breakpoints.Add($b)
73
77
  }
74
78
 
@@ -102,11 +106,16 @@ try {
102
106
  $coveragePathFilter = $PathFilter.Split(":", [StringSplitOptions]::RemoveEmptyEntries)
103
107
  $breakpointInfo = @(
104
108
  foreach ($scriptName in @($ModuleName; $actionParams.PowerShellModules)) {
105
- $scriptInfo = Get-AnsibleScript -Name $scriptName -IncludeScriptBlock
109
+ # We don't use -IncludeScriptBlock as the script might be untrusted
110
+ # and will run under CLM. While we recreate the ScriptBlock here it
111
+ # is only to get the AST that contains the statements and their
112
+ # line numbers to create the breakpoint info for.
113
+ $scriptInfo = Get-AnsibleScript -Name $scriptName
106
114
 
107
115
  if (Compare-PathFilterPattern -Patterns $coveragePathFilter -Path $scriptInfo.Path) {
108
116
  $covParams = @{
109
- ScriptBlock = $scriptInfo.ScriptBlock
117
+ ScriptName = $scriptInfo.Name
118
+ ScriptBlockAst = [ScriptBlock]::Create($scriptInfo.Script).Ast
110
119
  AnsiblePath = $scriptInfo.Path
111
120
  }
112
121
  New-CoverageBreakpointsForScriptBlock @covParams
@@ -9,6 +9,7 @@ using namespace System.Linq
9
9
  using namespace System.Management.Automation
10
10
  using namespace System.Management.Automation.Language
11
11
  using namespace System.Management.Automation.Security
12
+ using namespace System.Reflection
12
13
  using namespace System.Security.Cryptography
13
14
  using namespace System.Text
14
15
 
@@ -53,6 +54,10 @@ begin {
53
54
  $ErrorActionPreference = "Stop"
54
55
  $ProgressPreference = "SilentlyContinue"
55
56
 
57
+ if ($PSCommandPath -and (Test-Path -LiteralPath $PSCommandPath)) {
58
+ Remove-Item -LiteralPath $PSCommandPath -Force
59
+ }
60
+
56
61
  # Try and set the console encoding to UTF-8 allowing Ansible to read the
57
62
  # output of the wrapper as UTF-8 bytes.
58
63
  try {
@@ -89,6 +94,9 @@ begin {
89
94
  }
90
95
 
91
96
  # $Script:AnsibleManifest = @{} # Defined in process/end.
97
+ $Script:AnsibleShouldConstrain = [SystemPolicy]::GetSystemLockdownPolicy() -eq 'Enforce'
98
+ $Script:AnsibleTrustedHashList = [HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
99
+ $Script:AnsibleUnsupportedHashList = [HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
92
100
  $Script:AnsibleWrapperWarnings = [List[string]]::new()
93
101
  $Script:AnsibleTempPath = @(
94
102
  # Wrapper defined tmpdir
@@ -110,6 +118,8 @@ begin {
110
118
  $false
111
119
  }
112
120
  } | Select-Object -First 1
121
+ $Script:AnsibleTempScripts = [List[string]]::new()
122
+ $Script:AnsibleClrFacadeSet = $false
113
123
 
114
124
  Function Convert-JsonObject {
115
125
  param(
@@ -147,7 +157,11 @@ begin {
147
157
 
148
158
  [Parameter()]
149
159
  [switch]
150
- $IncludeScriptBlock
160
+ $IncludeScriptBlock,
161
+
162
+ [Parameter()]
163
+ [switch]
164
+ $SkipHashCheck
151
165
  )
152
166
 
153
167
  if (-not $Script:AnsibleManifest.scripts.Contains($Name)) {
@@ -172,11 +186,93 @@ begin {
172
186
  [ref]$null).GetScriptBlock()
173
187
  }
174
188
 
175
- [PSCustomObject]@{
189
+ $outputValue = [PSCustomObject]@{
176
190
  Name = $Name
177
191
  Script = $scriptContents
178
192
  Path = $scriptInfo.path
179
193
  ScriptBlock = $sbk
194
+ ShouldConstrain = $false
195
+ }
196
+
197
+ if (-not $Script:AnsibleShouldConstrain) {
198
+ $outputValue
199
+ return
200
+ }
201
+
202
+ if (-not $SkipHashCheck) {
203
+ $sha256 = [SHA256]::Create()
204
+ $scriptHash = [BitConverter]::ToString($sha256.ComputeHash($scriptBytes)).Replace("-", "")
205
+ $sha256.Dispose()
206
+
207
+ if ($Script:AnsibleUnsupportedHashList.Contains($scriptHash)) {
208
+ $err = [ErrorRecord]::new(
209
+ [Exception]::new("Provided script for '$Name' is marked as unsupported in CLM mode."),
210
+ "ScriptUnsupported",
211
+ [ErrorCategory]::SecurityError,
212
+ $Name)
213
+ $PSCmdlet.ThrowTerminatingError($err)
214
+ }
215
+ elseif ($Script:AnsibleTrustedHashList.Contains($scriptHash)) {
216
+ $outputValue
217
+ return
218
+ }
219
+ }
220
+
221
+ # If we have reached here we are running in a locked down environment
222
+ # and the script is not trusted in the signed hashlists. Check if it
223
+ # contains the authenticode signature and verify that using PowerShell.
224
+ # [SystemPolicy]::GetFilePolicyEnforcement(...) is a new API but only
225
+ # present in Server 2025+ so we need to rely on the known behaviour of
226
+ # Get-Command to fail with CommandNotFoundException if the script is
227
+ # not allowed to run.
228
+ $outputValue.ShouldConstrain = $true
229
+ if ($scriptContents -like "*`r`n# SIG # Begin signature block`r`n*") {
230
+ Set-WinPSDefaultFileEncoding
231
+
232
+ # If the script is manually signed we need to ensure the signature
233
+ # is valid and trusted by the OS policy.
234
+ # We must use '.ps1' so the ExternalScript WDAC check will apply.
235
+ $tmpFile = [Path]::Combine($Script:AnsibleTempPath, "ansible-tmp-$([Guid]::NewGuid()).ps1")
236
+ try {
237
+ [File]::WriteAllBytes($tmpFile, $scriptBytes)
238
+ $cmd = Get-Command -Name $tmpFile -CommandType ExternalScript -ErrorAction Stop
239
+
240
+ # Get-Command caches the file contents after loading which we
241
+ # use to verify it was not modified before the signature check.
242
+ $expectedScript = $cmd.OriginalEncoding.GetString($scriptBytes)
243
+ if ($expectedScript -ne $cmd.ScriptContents) {
244
+ $err = [ErrorRecord]::new(
245
+ [Exception]::new("Script has been modified during signature check."),
246
+ "ScriptModifiedTrusted",
247
+ [ErrorCategory]::SecurityError,
248
+ $Name)
249
+ $PSCmdlet.ThrowTerminatingError($err)
250
+ }
251
+
252
+ $outputValue.ShouldConstrain = $false
253
+ }
254
+ catch [CommandNotFoundException] {
255
+ $null = $null # No-op but satisfies the linter.
256
+ }
257
+ finally {
258
+ if (Test-Path -LiteralPath $tmpFile) {
259
+ Remove-Item -LiteralPath $tmpFile -Force
260
+ }
261
+ }
262
+ }
263
+
264
+ if ($outputValue.ShouldConstrain -and $IncludeScriptBlock) {
265
+ # If the script is untrusted and a scriptblock was requested we
266
+ # error out as the sbk would have run in FLM.
267
+ $err = [ErrorRecord]::new(
268
+ [Exception]::new("Provided script for '$Name' is not trusted to run."),
269
+ "ScriptNotTrusted",
270
+ [ErrorCategory]::SecurityError,
271
+ $Name)
272
+ $PSCmdlet.ThrowTerminatingError($err)
273
+ }
274
+ else {
275
+ $outputValue
180
276
  }
181
277
  }
182
278
 
@@ -223,7 +319,7 @@ begin {
223
319
  $IncludeScriptBlock
224
320
  )
225
321
 
226
- $sbk = Get-AnsibleScript -Name exec_wrapper.ps1 -IncludeScriptBlock:$IncludeScriptBlock
322
+ $scriptInfo = Get-AnsibleScript -Name exec_wrapper.ps1 -IncludeScriptBlock:$IncludeScriptBlock
227
323
  $params = @{
228
324
  # TempPath may contain env vars that change based on the runtime
229
325
  # environment. Ensure we use that and not the $script:AnsibleTempPath
@@ -244,8 +340,7 @@ begin {
244
340
  }
245
341
 
246
342
  [PSCustomObject]@{
247
- Script = $sbk.Script
248
- ScriptBlock = $sbk.ScriptBlock
343
+ ScriptInfo = $scriptInfo
249
344
  Parameters = $params
250
345
  InputData = $inputData
251
346
  }
@@ -279,11 +374,16 @@ begin {
279
374
 
280
375
  $isBasicUtil = $false
281
376
  $csharpModules = foreach ($moduleName in $Name) {
282
- (Get-AnsibleScript -Name $moduleName).Script
377
+ $scriptInfo = Get-AnsibleScript -Name $moduleName
283
378
 
379
+ if ($scriptInfo.ShouldConstrain) {
380
+ throw "C# module util '$Name' is not trusted and cannot be loaded."
381
+ }
284
382
  if ($moduleName -eq 'Ansible.Basic.cs') {
285
383
  $isBasicUtil = $true
286
384
  }
385
+
386
+ $scriptInfo.Script
287
387
  }
288
388
 
289
389
  $fakeModule = [PSCustomObject]@{
@@ -303,6 +403,112 @@ begin {
303
403
  }
304
404
  }
305
405
 
406
+ Function Import-SignedHashList {
407
+ [CmdletBinding()]
408
+ param (
409
+ [Parameter(Mandatory, ValueFromPipeline)]
410
+ [string]
411
+ $Name
412
+ )
413
+
414
+ process {
415
+ try {
416
+ # We skip the hash check to ensure we verify based on the
417
+ # authenticode signature and not whether it's trusted by an
418
+ # existing signed hash list.
419
+ $scriptInfo = Get-AnsibleScript -Name $Name -SkipHashCheck
420
+ if ($scriptInfo.ShouldConstrain) {
421
+ throw "script is not signed or not trusted to run."
422
+ }
423
+
424
+ $hashListAst = [Parser]::ParseInput(
425
+ $scriptInfo.Script,
426
+ $Name,
427
+ [ref]$null,
428
+ [ref]$null)
429
+ $manifestAst = $hashListAst.Find({ $args[0] -is [HashtableAst] }, $false)
430
+ if ($null -eq $manifestAst) {
431
+ throw "expecting a single hashtable in the signed manifest."
432
+ }
433
+
434
+ $out = $manifestAst.SafeGetValue()
435
+ if (-not $out.Contains('Version')) {
436
+ throw "expecting hash list to contain 'Version' key."
437
+ }
438
+ if ($out.Version -ne 1) {
439
+ throw "unsupported hash list Version $($out.Version), expecting 1."
440
+ }
441
+
442
+ if (-not $out.Contains('HashList')) {
443
+ throw "expecting hash list to contain 'HashList' key."
444
+ }
445
+
446
+ $out.HashList | ForEach-Object {
447
+ if ($_ -isnot [hashtable] -or -not $_.ContainsKey('Hash') -or $_.Hash -isnot [string] -or $_.Hash.Length -ne 64) {
448
+ throw "expecting hash list to contain hashtable with Hash key with a value of a SHA256 strings."
449
+ }
450
+
451
+ if ($_.Mode -eq 'Trusted') {
452
+ $null = $Script:AnsibleTrustedHashList.Add($_.Hash)
453
+ }
454
+ elseif ($_.Mode -eq 'Unsupported') {
455
+ # Allows us to provide a better error when trying to run
456
+ # something in CLM that is marked as unsupported.
457
+ $null = $Script:AnsibleUnsupportedHashList.Add($_.Hash)
458
+ }
459
+ else {
460
+ throw "expecting hash list entry for $($_.Hash) to contain a mode of 'Trusted' or 'Unsupported' but got '$($_.Mode)'."
461
+ }
462
+ }
463
+ }
464
+ catch {
465
+ $_.ErrorDetails = [ErrorDetails]::new("Failed to process signed manifest '$Name': $_")
466
+ $PSCmdlet.WriteError($_)
467
+ }
468
+ }
469
+ }
470
+
471
+ Function New-TempAnsibleFile {
472
+ [OutputType([string])]
473
+ [CmdletBinding()]
474
+ param (
475
+ [Parameter(Mandatory)]
476
+ [string]
477
+ $FileName,
478
+
479
+ [Parameter(Mandatory)]
480
+ [string]
481
+ $Content
482
+ )
483
+
484
+ $name = [Path]::GetFileNameWithoutExtension($FileName)
485
+ $ext = [Path]::GetExtension($FileName)
486
+ $newName = "$($name)-$([Guid]::NewGuid())$ext"
487
+
488
+ $path = Join-Path -Path $Script:AnsibleTempPath $newName
489
+ Set-WinPSDefaultFileEncoding
490
+ [File]::WriteAllText($path, $Content, [UTF8Encoding]::new($false))
491
+
492
+ $path
493
+ }
494
+
495
+ Function Set-WinPSDefaultFileEncoding {
496
+ [CmdletBinding()]
497
+ param ()
498
+
499
+ # WinPS defaults to the locale encoding when loading a script from the
500
+ # file path but in Ansible we expect it to always be UTF-8 without a
501
+ # BOM. This lazily sets an internal field so pwsh reads it as UTF-8.
502
+ # If we don't do this then scripts saved as UTF-8 on the Ansible
503
+ # controller will not run as expected.
504
+ if ($PSVersionTable.PSVersion -lt '6.0' -and -not $Script:AnsibleClrFacadeSet) {
505
+ $clrFacade = [PSObject].Assembly.GetType('System.Management.Automation.ClrFacade')
506
+ $defaultEncodingField = $clrFacade.GetField('_defaultEncoding', [BindingFlags]'NonPublic, Static')
507
+ $defaultEncodingField.SetValue($null, [UTF8Encoding]::new($false))
508
+ $Script:AnsibleClrFacadeSet = $true
509
+ }
510
+ }
511
+
306
512
  Function Write-AnsibleErrorJson {
307
513
  [CmdletBinding()]
308
514
  param (
@@ -414,6 +620,10 @@ begin {
414
620
  $Script:AnsibleManifest = $Manifest
415
621
  }
416
622
 
623
+ if ($Script:AnsibleShouldConstrain) {
624
+ $Script:AnsibleManifest.signed_hashlist | Import-SignedHashList
625
+ }
626
+
417
627
  $actionInfo = Get-NextAnsibleAction
418
628
  $actionParams = $actionInfo.Parameters
419
629
 
@@ -500,5 +710,8 @@ end {
500
710
  }
501
711
  finally {
502
712
  $actionPipeline.Dispose()
713
+ if ($Script:AnsibleTempScripts) {
714
+ Remove-Item -LiteralPath $Script:AnsibleTempScripts -Force -ErrorAction Ignore
715
+ }
503
716
  }
504
717
  }
@@ -30,6 +30,7 @@ from ansible.plugins.loader import ps_module_utils_loader
30
30
  class _ExecManifest:
31
31
  scripts: dict[str, _ScriptInfo] = dataclasses.field(default_factory=dict)
32
32
  actions: list[_ManifestAction] = dataclasses.field(default_factory=list)
33
+ signed_hashlist: list[str] = dataclasses.field(default_factory=list)
33
34
 
34
35
 
35
36
  @dataclasses.dataclass(frozen=True, kw_only=True)
@@ -54,6 +55,11 @@ class PSModuleDepFinder(object):
54
55
  def __init__(self) -> None:
55
56
  # This is also used by validate-modules to get a module's required utils in base and a collection.
56
57
  self.scripts: dict[str, _ScriptInfo] = {}
58
+ self.signed_hashlist: set[str] = set()
59
+
60
+ if builtin_hashlist := _get_powershell_signed_hashlist():
61
+ self.signed_hashlist.add(builtin_hashlist.path)
62
+ self.scripts[builtin_hashlist.path] = builtin_hashlist
57
63
 
58
64
  self._util_deps: dict[str, set[str]] = {}
59
65
 
@@ -119,6 +125,15 @@ class PSModuleDepFinder(object):
119
125
  lines = module_data.split(b'\n')
120
126
  module_utils: set[tuple[str, str, bool]] = set()
121
127
 
128
+ if fqn and fqn.startswith("ansible_collections."):
129
+ submodules = fqn.split('.')
130
+ collection_name = '.'.join(submodules[:3])
131
+
132
+ collection_hashlist = _get_powershell_signed_hashlist(collection_name)
133
+ if collection_hashlist and collection_hashlist.path not in self.signed_hashlist:
134
+ self.signed_hashlist.add(collection_hashlist.path)
135
+ self.scripts[collection_hashlist.path] = collection_hashlist
136
+
122
137
  if powershell:
123
138
  checks = [
124
139
  # PS module contains '#Requires -Module Ansible.ModuleUtils.*'
@@ -315,6 +330,10 @@ def _bootstrap_powershell_script(
315
330
  )
316
331
  )
317
332
 
333
+ if hashlist := _get_powershell_signed_hashlist():
334
+ exec_manifest.signed_hashlist.append(hashlist.path)
335
+ exec_manifest.scripts[hashlist.path] = hashlist
336
+
318
337
  bootstrap_wrapper = _get_powershell_script("bootstrap_wrapper.ps1")
319
338
  bootstrap_input = _get_bootstrap_input(exec_manifest)
320
339
  if has_input:
@@ -339,6 +358,14 @@ def _get_powershell_script(
339
358
  if code is None:
340
359
  raise AnsibleFileNotFound(f"Could not find powershell script '{package_name}.{name}'")
341
360
 
361
+ try:
362
+ sig_data = pkgutil.get_data(package_name, f"{name}.authenticode")
363
+ except FileNotFoundError:
364
+ sig_data = None
365
+
366
+ if sig_data:
367
+ code = code + b"\r\n" + b"\r\n".join(sig_data.splitlines()) + b"\r\n"
368
+
342
369
  return code
343
370
 
344
371
 
@@ -501,6 +528,7 @@ def _create_powershell_wrapper(
501
528
  exec_manifest = _ExecManifest(
502
529
  scripts=finder.scripts,
503
530
  actions=actions,
531
+ signed_hashlist=list(finder.signed_hashlist),
504
532
  )
505
533
 
506
534
  return _get_bootstrap_input(
@@ -551,3 +579,27 @@ def _prepare_module_args(module_args: dict[str, t.Any], profile: str) -> dict[st
551
579
  encoder = get_module_encoder(profile, Direction.CONTROLLER_TO_MODULE)
552
580
 
553
581
  return json.loads(json.dumps(module_args, cls=encoder))
582
+
583
+
584
+ def _get_powershell_signed_hashlist(
585
+ collection: str | None = None,
586
+ ) -> _ScriptInfo | None:
587
+ """Gets the signed hashlist script stored in either the Ansible package or for
588
+ the collection specified.
589
+
590
+ :param collection: The collection namespace to get the signed hashlist for or None for the builtin.
591
+ :return: The _ScriptInfo payload of the signed hashlist script if found, None if not.
592
+ """
593
+ resource = 'ansible.config' if collection is None else f"{collection}.meta"
594
+ signature_file = 'powershell_signatures.psd1'
595
+
596
+ try:
597
+ sig_data = pkgutil.get_data(resource, signature_file)
598
+ except FileNotFoundError:
599
+ sig_data = None
600
+
601
+ if sig_data:
602
+ resource_path = f"{resource}.{signature_file}"
603
+ return _ScriptInfo(content=sig_data, path=resource_path)
604
+
605
+ return None
@@ -97,6 +97,12 @@ $ps = [PowerShell]::Create()
97
97
  if ($ForModule) {
98
98
  $ps.Runspace.SessionStateProxy.SetVariable("ErrorActionPreference", "Stop")
99
99
  }
100
+ else {
101
+ # For script files we want to ensure we load it as UTF-8. We don't set this
102
+ # for modules as they are loaded from memory whereas a script is loaded
103
+ # from disk as part of the script being run than by us.
104
+ Set-WinPSDefaultFileEncoding
105
+ }
100
106
 
101
107
  foreach ($variable in $Variables) {
102
108
  $null = $ps.AddCommand("Set-Variable").AddParameters($variable).AddStatement()
@@ -112,12 +118,31 @@ foreach ($env in $Environment.GetEnumerator()) {
112
118
  $null = $ps.AddScript('Function Write-Host($msg) { Write-Output -InputObject $msg }').AddStatement()
113
119
 
114
120
  $scriptInfo = Get-AnsibleScript -Name $Script
121
+ if ($scriptInfo.ShouldConstrain) {
122
+ # Fail if there are any module utils, in the future we may allow unsigned
123
+ # PowerShell utils in CLM but for now we don't.
124
+ if ($PowerShellModules -or $CSharpModules) {
125
+ throw "Cannot run untrusted PowerShell script '$Script' in ConstrainedLanguage mode with module util imports."
126
+ }
115
127
 
116
- if ($PowerShellModules) {
117
- foreach ($utilName in $PowerShellModules) {
118
- $utilInfo = Get-AnsibleScript -Name $utilName
128
+ # If the module is marked as needing to be constrained then we set the
129
+ # language mode to ConstrainedLanguage so that when parsed inside the
130
+ # Runspace it will run in CLM. We need to run it from a filepath as in
131
+ # CLM we cannot call the methods needed to create the ScriptBlock and we
132
+ # need to be in CLM to downgrade the language mode.
133
+ $null = $ps.AddScript('$ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"').AddStatement()
134
+ $scriptPath = New-TempAnsibleFile -FileName $Script -Content $scriptInfo.Script
135
+ $null = $ps.AddCommand($scriptPath, $false).AddStatement()
136
+ }
137
+ else {
138
+ if ($PowerShellModules) {
139
+ foreach ($utilName in $PowerShellModules) {
140
+ $utilInfo = Get-AnsibleScript -Name $utilName
141
+ if ($utilInfo.ShouldConstrain) {
142
+ throw "PowerShell module util '$utilName' is not trusted and cannot be loaded."
143
+ }
119
144
 
120
- $null = $ps.AddScript(@'
145
+ $null = $ps.AddScript(@'
121
146
  param ($Name, $Script)
122
147
 
123
148
  $moduleName = [System.IO.Path]::GetFileNameWithoutExtension($Name)
@@ -130,32 +155,33 @@ $sbk = [System.Management.Automation.Language.Parser]::ParseInput(
130
155
  New-Module -Name $moduleName -ScriptBlock $sbk |
131
156
  Import-Module -WarningAction SilentlyContinue -Scope Global
132
157
  '@, $true)
133
- $null = $ps.AddParameters(
134
- @{
135
- Name = $utilName
136
- Script = $utilInfo.Script
137
- }
138
- ).AddStatement()
158
+ $null = $ps.AddParameters(
159
+ @{
160
+ Name = $utilName
161
+ Script = $utilInfo.Script
162
+ }
163
+ ).AddStatement()
164
+ }
139
165
  }
140
- }
141
166
 
142
- if ($CSharpModules) {
143
- # C# utils are process wide so just load them here.
144
- Import-CSharpUtil -Name $CSharpModules
145
- }
167
+ if ($CSharpModules) {
168
+ # C# utils are process wide so just load them here.
169
+ Import-CSharpUtil -Name $CSharpModules
170
+ }
146
171
 
147
- # We invoke it through a command with useLocalScope $false to
148
- # ensure the code runs with it's own $script: scope. It also
149
- # cleans up the StackTrace on errors by not showing the stub
150
- # execution line and starts immediately at the module "cmd".
151
- $null = $ps.AddScript(@'
172
+ # We invoke it through a command with useLocalScope $false to
173
+ # ensure the code runs with it's own $script: scope. It also
174
+ # cleans up the StackTrace on errors by not showing the stub
175
+ # execution line and starts immediately at the module "cmd".
176
+ $null = $ps.AddScript(@'
152
177
  ${function:<AnsibleModule>} = [System.Management.Automation.Language.Parser]::ParseInput(
153
178
  $args[0],
154
179
  $args[1],
155
180
  [ref]$null,
156
181
  [ref]$null).GetScriptBlock()
157
182
  '@).AddArgument($scriptInfo.Script).AddArgument($Script).AddStatement()
158
- $null = $ps.AddCommand('<AnsibleModule>', $false).AddStatement()
183
+ $null = $ps.AddCommand('<AnsibleModule>', $false).AddStatement()
184
+ }
159
185
 
160
186
  if ($Breakpoints) {
161
187
  $ps.Runspace.Debugger.SetBreakpoints($Breakpoints)
@@ -0,0 +1,20 @@
1
+ # (c) 2025 Ansible Project
2
+ # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
3
+
4
+ [CmdletBinding()]
5
+ param (
6
+ [Parameter(Mandatory)]
7
+ [string]
8
+ $Path
9
+ )
10
+
11
+ $userProfile = [Environment]::GetFolderPath([Environment+SpecialFolder]::UserProfile)
12
+ if ($Path -eq '~') {
13
+ $userProfile
14
+ }
15
+ elseif ($Path.StartsWith(('~\'))) {
16
+ Join-Path -Path $userProfile -ChildPath $Path.Substring(2)
17
+ }
18
+ else {
19
+ $Path
20
+ }
@@ -0,0 +1,17 @@
1
+ # (c) 2025 Ansible Project
2
+ # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
3
+
4
+ [CmdletBinding()]
5
+ param (
6
+ [Parameter(Mandatory)]
7
+ [string]
8
+ $Directory,
9
+
10
+ [Parameter(Mandatory)]
11
+ [string]
12
+ $Name
13
+ )
14
+
15
+ $path = [Environment]::ExpandEnvironmentVariables($Directory)
16
+ $tmp = New-Item -Path $path -Name $Name -ItemType Directory
17
+ $tmp.FullName